diff --git a/x509util/certificate.go b/x509util/certificate.go index 3909d83f..a01022d4 100644 --- a/x509util/certificate.go +++ b/x509util/certificate.go @@ -1,6 +1,9 @@ package x509util import ( + "crypto" + "crypto/rand" + "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/json" @@ -75,6 +78,11 @@ func (c *Certificate) GetCertificate() *x509.Certificate { cert.IPAddresses = c.IPAddresses cert.URIs = c.URIs + // SANs slice. + for _, san := range c.SANs { + san.Set(cert) + } + // Subject. c.Subject.Set(cert) @@ -105,6 +113,41 @@ func (c *Certificate) GetCertificate() *x509.Certificate { return cert } +func CreateCertificate(template, parent *x509.Certificate, pub crypto.PublicKey, signer crypto.Signer) (*x509.Certificate, error) { + var err error + // Complete certificate. + if template.SerialNumber == nil { + if template.SerialNumber, err = generateSerialNumber(); err != nil { + return nil, err + } + } + if template.SubjectKeyId == nil { + if template.SubjectKeyId, err = generateSubjectKeyID(pub); err != nil { + return nil, err + } + } + + // Remove KeyEncipherment and DataEncipherment for non-rsa keys. + // See: + // https://github.com/golang/go/issues/36499 + // https://tools.ietf.org/html/draft-ietf-lamps-5480-ku-clarifications-02 + if _, ok := pub.(*rsa.PublicKey); !ok { + template.KeyUsage &= ^x509.KeyUsageKeyEncipherment + template.KeyUsage &= ^x509.KeyUsageDataEncipherment + } + + // Sign certificate + asn1Data, err := x509.CreateCertificate(rand.Reader, template, parent, pub, signer) + if err != nil { + return nil, errors.Wrap(err, "error creating certificate") + } + cert, err := x509.ParseCertificate(asn1Data) + if err != nil { + return nil, errors.Wrap(err, "error parsing certificate") + } + return cert, nil +} + // Name is the JSON representation of X.501 type Name, used in the X.509 subject // and issuer fields. type Name struct { diff --git a/x509util/utils.go b/x509util/utils.go new file mode 100644 index 00000000..6268107e --- /dev/null +++ b/x509util/utils.go @@ -0,0 +1,67 @@ +package x509util + +import ( + "crypto" + "crypto/rand" + "crypto/sha1" + "crypto/x509" + "math/big" + "net" + "net/url" + + "github.com/pkg/errors" + "github.com/smallstep/cli/crypto/x509util" +) + +// SplitSANs splits a slice of Subject Alternative Names into slices of +// IP Addresses and DNS Names. If an element is not an IP address, then it +// is bucketed as a DNS Name. +func SplitSANs(sans []string) (dnsNames []string, ips []net.IP, emails []string, uris []*url.URL) { + return x509util.SplitSANs(sans) +} + +func CreateSANs(sans []string) []SubjectAlternativeName { + dnsNames, ips, emails, uris := SplitSANs(sans) + sanTypes := make([]SubjectAlternativeName, 0, len(sans)) + for _, v := range dnsNames { + sanTypes = append(sanTypes, SubjectAlternativeName{Type: "dns", Value: v}) + } + for _, v := range ips { + sanTypes = append(sanTypes, SubjectAlternativeName{Type: "ip", Value: v.String()}) + } + for _, v := range emails { + sanTypes = append(sanTypes, SubjectAlternativeName{Type: "email", Value: v}) + } + for _, v := range uris { + sanTypes = append(sanTypes, SubjectAlternativeName{Type: "uri", Value: v.String()}) + } + return sanTypes +} + +func CreateTemplateData(commonName string, sans []string) TemplateData { + return TemplateData{ + SubjectKey: Subject{ + CommonName: commonName, + }, + SANsKey: CreateSANs(sans), + } +} + +// generateSerialNumber returns a random serial number. +func generateSerialNumber() (*big.Int, error) { + limit := new(big.Int).Lsh(big.NewInt(1), 128) + sn, err := rand.Int(rand.Reader, limit) + if err != nil { + return nil, errors.Wrap(err, "error generating serial number") + } + return sn, nil +} + +func generateSubjectKeyID(pub crypto.PublicKey) ([]byte, error) { + b, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return nil, errors.Wrap(err, "error marshaling public key") + } + hash := sha1.Sum(b) + return hash[:], nil +}