]> Cypherpunks repositories - gostls13.git/commitdiff
crypto/x509: Add certificate signature request (CSR) support.
authorKyle Isom <kyle@gokyle.net>
Thu, 13 Feb 2014 17:54:04 +0000 (12:54 -0500)
committerAdam Langley <agl@golang.org>
Thu, 13 Feb 2014 17:54:04 +0000 (12:54 -0500)
This change adds support for parsing and serialisation of PKCS #10,
certificate signature requests.

LGTM=agl
R=golang-codereviews, agl
CC=agl, golang-codereviews, nick
https://golang.org/cl/49830048

src/pkg/crypto/x509/pkix/pkix.go
src/pkg/crypto/x509/x509.go
src/pkg/crypto/x509/x509_test.go
src/pkg/encoding/asn1/asn1.go
src/pkg/encoding/asn1/asn1_test.go

index 5034946f7103d38af2d6ba3d57ab1c47fbf5f7b2..58c1e54d1004cc5b6a1ee40d1e2c1c45cb76d93c 100644 (file)
@@ -30,6 +30,13 @@ type AttributeTypeAndValue struct {
        Value interface{}
 }
 
+// AttributeTypeAndValueSET represents a set of ASN.1 sequences of
+// AttributeTypeAndValue sequences from RFC 2986 (PKCS #10).
+type AttributeTypeAndValueSET struct {
+       Type  asn1.ObjectIdentifier
+       Value [][]AttributeTypeAndValue `asn1:"set"`
+}
+
 // Extension represents the ASN.1 structure of the same name. See RFC
 // 5280, section 4.2.
 type Extension struct {
index 2a55fb1e5581c30c2e59dba2d0a3690be9233ead..3570e02359e804bce6b3c77350e467ef749b4dff 100644 (file)
@@ -790,6 +790,58 @@ func parsePublicKey(algo PublicKeyAlgorithm, keyData *publicKeyInfo) (interface{
        }
 }
 
+func parseSANExtension(value []byte) (dnsNames, emailAddresses []string, ipAddresses []net.IP, err error) {
+       // RFC 5280, 4.2.1.6
+
+       // SubjectAltName ::= GeneralNames
+       //
+       // GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
+       //
+       // GeneralName ::= CHOICE {
+       //      otherName                       [0]     OtherName,
+       //      rfc822Name                      [1]     IA5String,
+       //      dNSName                         [2]     IA5String,
+       //      x400Address                     [3]     ORAddress,
+       //      directoryName                   [4]     Name,
+       //      ediPartyName                    [5]     EDIPartyName,
+       //      uniformResourceIdentifier       [6]     IA5String,
+       //      iPAddress                       [7]     OCTET STRING,
+       //      registeredID                    [8]     OBJECT IDENTIFIER }
+       var seq asn1.RawValue
+       if _, err = asn1.Unmarshal(value, &seq); err != nil {
+               return
+       }
+       if !seq.IsCompound || seq.Tag != 16 || seq.Class != 0 {
+               err = asn1.StructuralError{Msg: "bad SAN sequence"}
+               return
+       }
+
+       rest := seq.Bytes
+       for len(rest) > 0 {
+               var v asn1.RawValue
+               rest, err = asn1.Unmarshal(rest, &v)
+               if err != nil {
+                       return
+               }
+               switch v.Tag {
+               case 1:
+                       emailAddresses = append(emailAddresses, string(v.Bytes))
+               case 2:
+                       dnsNames = append(dnsNames, string(v.Bytes))
+               case 7:
+                       switch len(v.Bytes) {
+                       case net.IPv4len, net.IPv6len:
+                               ipAddresses = append(ipAddresses, v.Bytes)
+                       default:
+                               err = errors.New("x509: certificate contained IP address of length " + strconv.Itoa(len(v.Bytes)))
+                               return
+                       }
+               }
+       }
+
+       return
+}
+
 func parseCertificate(in *certificate) (*Certificate, error) {
        out := new(Certificate)
        out.Raw = in.Raw
@@ -863,58 +915,12 @@ func parseCertificate(in *certificate) (*Certificate, error) {
                                        continue
                                }
                        case 17:
-                               // RFC 5280, 4.2.1.6
-
-                               // SubjectAltName ::= GeneralNames
-                               //
-                               // GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
-                               //
-                               // GeneralName ::= CHOICE {
-                               //      otherName                       [0]     OtherName,
-                               //      rfc822Name                      [1]     IA5String,
-                               //      dNSName                         [2]     IA5String,
-                               //      x400Address                     [3]     ORAddress,
-                               //      directoryName                   [4]     Name,
-                               //      ediPartyName                    [5]     EDIPartyName,
-                               //      uniformResourceIdentifier       [6]     IA5String,
-                               //      iPAddress                       [7]     OCTET STRING,
-                               //      registeredID                    [8]     OBJECT IDENTIFIER }
-                               var seq asn1.RawValue
-                               _, err := asn1.Unmarshal(e.Value, &seq)
+                               out.DNSNames, out.EmailAddresses, out.IPAddresses, err = parseSANExtension(e.Value)
                                if err != nil {
                                        return nil, err
                                }
-                               if !seq.IsCompound || seq.Tag != 16 || seq.Class != 0 {
-                                       return nil, asn1.StructuralError{Msg: "bad SAN sequence"}
-                               }
-
-                               parsedName := false
 
-                               rest := seq.Bytes
-                               for len(rest) > 0 {
-                                       var v asn1.RawValue
-                                       rest, err = asn1.Unmarshal(rest, &v)
-                                       if err != nil {
-                                               return nil, err
-                                       }
-                                       switch v.Tag {
-                                       case 1:
-                                               out.EmailAddresses = append(out.EmailAddresses, string(v.Bytes))
-                                               parsedName = true
-                                       case 2:
-                                               out.DNSNames = append(out.DNSNames, string(v.Bytes))
-                                               parsedName = true
-                                       case 7:
-                                               switch len(v.Bytes) {
-                                               case net.IPv4len, net.IPv6len:
-                                                       out.IPAddresses = append(out.IPAddresses, v.Bytes)
-                                               default:
-                                                       return nil, errors.New("x509: certificate contained IP address of length " + strconv.Itoa(len(v.Bytes)))
-                                               }
-                                       }
-                               }
-
-                               if parsedName {
+                               if len(out.DNSNames) > 0 || len(out.EmailAddresses) > 0 || len(out.IPAddresses) > 0 {
                                        continue
                                }
                                // If we didn't parse any of the names then we
@@ -1151,6 +1157,27 @@ func oidInExtensions(oid asn1.ObjectIdentifier, extensions []pkix.Extension) boo
        return false
 }
 
+// marshalSANs marshals a list of addresses into a the contents of an X.509
+// SubjectAlternativeName extension.
+func marshalSANs(dnsNames, emailAddresses []string, ipAddresses []net.IP) (derBytes []byte, err error) {
+       var rawValues []asn1.RawValue
+       for _, name := range dnsNames {
+               rawValues = append(rawValues, asn1.RawValue{Tag: 2, Class: 2, Bytes: []byte(name)})
+       }
+       for _, email := range emailAddresses {
+               rawValues = append(rawValues, asn1.RawValue{Tag: 1, Class: 2, Bytes: []byte(email)})
+       }
+       for _, rawIP := range ipAddresses {
+               // If possible, we always want to encode IPv4 addresses in 4 bytes.
+               ip := rawIP.To4()
+               if ip == nil {
+                       ip = rawIP
+               }
+               rawValues = append(rawValues, asn1.RawValue{Tag: 7, Class: 2, Bytes: ip})
+       }
+       return asn1.Marshal(rawValues)
+}
+
 func buildExtensions(template *Certificate) (ret []pkix.Extension, err error) {
        ret = make([]pkix.Extension, 10 /* maximum number of elements. */)
        n := 0
@@ -1252,22 +1279,7 @@ func buildExtensions(template *Certificate) (ret []pkix.Extension, err error) {
        if (len(template.DNSNames) > 0 || len(template.EmailAddresses) > 0 || len(template.IPAddresses) > 0) &&
                !oidInExtensions(oidExtensionSubjectAltName, template.ExtraExtensions) {
                ret[n].Id = oidExtensionSubjectAltName
-               var rawValues []asn1.RawValue
-               for _, name := range template.DNSNames {
-                       rawValues = append(rawValues, asn1.RawValue{Tag: 2, Class: 2, Bytes: []byte(name)})
-               }
-               for _, email := range template.EmailAddresses {
-                       rawValues = append(rawValues, asn1.RawValue{Tag: 1, Class: 2, Bytes: []byte(email)})
-               }
-               for _, rawIP := range template.IPAddresses {
-                       // If possible, we always want to encode IPv4 addresses in 4 bytes.
-                       ip := rawIP.To4()
-                       if ip == nil {
-                               ip = rawIP
-                       }
-                       rawValues = append(rawValues, asn1.RawValue{Tag: 7, Class: 2, Bytes: ip})
-               }
-               ret[n].Value, err = asn1.Marshal(rawValues)
+               ret[n].Value, err = marshalSANs(template.DNSNames, template.EmailAddresses, template.IPAddresses)
                if err != nil {
                        return
                }
@@ -1342,75 +1354,94 @@ func subjectBytes(cert *Certificate) ([]byte, error) {
        return asn1.Marshal(cert.Subject.ToRDNSequence())
 }
 
-// CreateCertificate creates a new certificate based on a template. The
-// following members of template are used: SerialNumber, Subject, NotBefore,
-// NotAfter, KeyUsage, ExtKeyUsage, UnknownExtKeyUsage, BasicConstraintsValid,
-// IsCA, MaxPathLen, SubjectKeyId, DNSNames, PermittedDNSDomainsCritical,
-// PermittedDNSDomains, SignatureAlgorithm.
-//
-// The certificate is signed by parent. If parent is equal to template then the
-// certificate is self-signed. The parameter pub is the public key of the
-// signee and priv is the private key of the signer.
-//
-// The returned slice is the certificate in DER encoding.
-//
-// The only supported key types are RSA and ECDSA (*rsa.PublicKey or
-// *ecdsa.PublicKey for pub, *rsa.PrivateKey or *ecdsa.PrivateKey for priv).
-func CreateCertificate(rand io.Reader, template, parent *Certificate, pub interface{}, priv interface{}) (cert []byte, err error) {
-       var publicKeyBytes []byte
-       var publicKeyAlgorithm pkix.AlgorithmIdentifier
-
-       if publicKeyBytes, publicKeyAlgorithm, err = marshalPublicKey(pub); err != nil {
-               return nil, err
-       }
-
-       var signatureAlgorithm pkix.AlgorithmIdentifier
-       var hashFunc crypto.Hash
-       var privType PublicKeyAlgorithm
+// signingParamsForPrivateKey returns the parameters to use for signing with
+// priv. If requestedSigAlgo is not zero then it overrides the default
+// signature algorithm.
+func signingParamsForPrivateKey(priv interface{}, requestedSigAlgo SignatureAlgorithm) (hashFunc crypto.Hash, sigAlgo pkix.AlgorithmIdentifier, err error) {
+       var pubType PublicKeyAlgorithm
 
        switch priv := priv.(type) {
        case *rsa.PrivateKey:
-               privType = RSA
-               signatureAlgorithm.Algorithm = oidSignatureSHA256WithRSA
+               pubType = RSA
+               sigAlgo.Algorithm = oidSignatureSHA256WithRSA
                hashFunc = crypto.SHA256
+
        case *ecdsa.PrivateKey:
-               privType = ECDSA
+               pubType = ECDSA
 
                switch priv.Curve {
                case elliptic.P224(), elliptic.P256():
                        hashFunc = crypto.SHA256
-                       signatureAlgorithm.Algorithm = oidSignatureECDSAWithSHA256
+                       sigAlgo.Algorithm = oidSignatureECDSAWithSHA256
                case elliptic.P384():
                        hashFunc = crypto.SHA384
-                       signatureAlgorithm.Algorithm = oidSignatureECDSAWithSHA384
+                       sigAlgo.Algorithm = oidSignatureECDSAWithSHA384
                case elliptic.P521():
                        hashFunc = crypto.SHA512
-                       signatureAlgorithm.Algorithm = oidSignatureECDSAWithSHA512
+                       sigAlgo.Algorithm = oidSignatureECDSAWithSHA512
                default:
-                       return nil, errors.New("x509: unknown elliptic curve")
+                       err = errors.New("x509: unknown elliptic curve")
                }
+
        default:
-               return nil, errors.New("x509: only RSA and ECDSA private keys supported")
+               err = errors.New("x509: only RSA and ECDSA private keys supported")
        }
 
-       if template.SignatureAlgorithm != 0 {
-               found := false
-               for _, details := range signatureAlgorithmDetails {
-                       if details.algo == template.SignatureAlgorithm {
-                               if details.pubKeyAlgo != privType {
-                                       return nil, errors.New("x509: requested SignatureAlgorithm does not match private key type")
-                               }
-                               signatureAlgorithm.Algorithm, hashFunc = details.oid, details.hash
-                               if hashFunc == 0 {
-                                       return nil, errors.New("x509: cannot sign with hash function requested")
-                               }
-                               found = true
-                               break
+       if err != nil {
+               return
+       }
+
+       if requestedSigAlgo == 0 {
+               return
+       }
+
+       found := false
+       for _, details := range signatureAlgorithmDetails {
+               if details.algo == requestedSigAlgo {
+                       if details.pubKeyAlgo != pubType {
+                               err = errors.New("x509: requested SignatureAlgorithm does not match private key type")
+                               return
                        }
+                       sigAlgo.Algorithm, hashFunc = details.oid, details.hash
+                       if hashFunc == 0 {
+                               err = errors.New("x509: cannot sign with hash function requested")
+                               return
+                       }
+                       found = true
+                       break
                }
-               if !found {
-                       return nil, errors.New("x509: unknown SignatureAlgorithm")
-               }
+       }
+
+       if !found {
+               err = errors.New("x509: unknown SignatureAlgorithm")
+       }
+
+       return
+}
+
+// CreateCertificate creates a new certificate based on a template. The
+// following members of template are used: SerialNumber, Subject, NotBefore,
+// NotAfter, KeyUsage, ExtKeyUsage, UnknownExtKeyUsage, BasicConstraintsValid,
+// IsCA, MaxPathLen, SubjectKeyId, DNSNames, PermittedDNSDomainsCritical,
+// PermittedDNSDomains, SignatureAlgorithm.
+//
+// The certificate is signed by parent. If parent is equal to template then the
+// certificate is self-signed. The parameter pub is the public key of the
+// signee and priv is the private key of the signer.
+//
+// The returned slice is the certificate in DER encoding.
+//
+// The only supported key types are RSA and ECDSA (*rsa.PublicKey or
+// *ecdsa.PublicKey for pub, *rsa.PrivateKey or *ecdsa.PrivateKey for priv).
+func CreateCertificate(rand io.Reader, template, parent *Certificate, pub interface{}, priv interface{}) (cert []byte, err error) {
+       hashFunc, signatureAlgorithm, err := signingParamsForPrivateKey(priv, template.SignatureAlgorithm)
+       if err != nil {
+               return nil, err
+       }
+
+       publicKeyBytes, publicKeyAlgorithm, err := marshalPublicKey(pub)
+       if err != nil {
+               return nil, err
        }
 
        if err != nil {
@@ -1559,3 +1590,313 @@ func (c *Certificate) CreateCRL(rand io.Reader, priv interface{}, revokedCerts [
                SignatureValue: asn1.BitString{Bytes: signature, BitLength: len(signature) * 8},
        })
 }
+
+// CertificateRequest represents a PKCS #10, certificate signature request.
+type CertificateRequest struct {
+       Raw                      []byte // Complete ASN.1 DER content (CSR, signature algorithm and signature).
+       RawTBSCertificateRequest []byte // Certificate request info part of raw ASN.1 DER content.
+       RawSubjectPublicKeyInfo  []byte // DER encoded SubjectPublicKeyInfo.
+       RawSubject               []byte // DER encoded Subject.
+
+       Version            int
+       Signature          []byte
+       SignatureAlgorithm SignatureAlgorithm
+
+       PublicKeyAlgorithm PublicKeyAlgorithm
+       PublicKey          interface{}
+
+       Subject pkix.Name
+
+       // Attributes is a collection of attributes providing
+       // additional information about the subject of the certificate.
+       // See RFC 2986 section 4.1.
+       Attributes []pkix.AttributeTypeAndValueSET
+
+       // Extensions contains raw X.509 extensions. When parsing CSRs, this
+       // can be used to extract extensions that are not parsed by this
+       // package.
+       Extensions []pkix.Extension
+
+       // ExtraExtensions contains extensions to be copied, raw, into any
+       // marshaled CSR. Values override any extensions that would otherwise
+       // be produced based on the other fields but are overridden by any
+       // extensions specified in Attributes.
+       //
+       // The ExtraExtensions field is not populated when parsing CSRs, see
+       // Extensions.
+       ExtraExtensions []pkix.Extension
+
+       // Subject Alternate Name values.
+       DNSNames       []string
+       EmailAddresses []string
+       IPAddresses    []net.IP
+}
+
+// These structures reflect the ASN.1 structure of X.509 certificate
+// signature requests (see RFC 2986):
+
+type tbsCertificateRequest struct {
+       Raw        asn1.RawContent
+       Version    int
+       Subject    asn1.RawValue
+       PublicKey  publicKeyInfo
+       Attributes []pkix.AttributeTypeAndValueSET `asn1:"tag:0"`
+}
+
+type certificateRequest struct {
+       Raw                asn1.RawContent
+       TBSCSR             tbsCertificateRequest
+       SignatureAlgorithm pkix.AlgorithmIdentifier
+       SignatureValue     asn1.BitString
+}
+
+// oidExtensionRequest is a PKCS#9 OBJECT IDENTIFIER that indicates requested
+// extensions in a CSR.
+var oidExtensionRequest = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 14}
+
+// CreateCertificateRequest creates a new certificate based on a template. The
+// following members of template are used: Subject, Attributes,
+// SignatureAlgorithm, Extension, DNSNames, EmailAddresses, and IPAddresses.
+// The private key is the private key of the signer.
+//
+// The returned slice is the certificate request in DER encoding.
+//
+// The only supported key types are RSA (*rsa.PrivateKey) and ECDSA
+// (*ecdsa.PrivateKey).
+func CreateCertificateRequest(rand io.Reader, template *CertificateRequest, priv interface{}) (csr []byte, err error) {
+       hashFunc, sigAlgo, err := signingParamsForPrivateKey(priv, template.SignatureAlgorithm)
+       if err != nil {
+               return nil, err
+       }
+
+       var publicKeyBytes []byte
+       var publicKeyAlgorithm pkix.AlgorithmIdentifier
+
+       switch priv := priv.(type) {
+       case *rsa.PrivateKey:
+               publicKeyBytes, publicKeyAlgorithm, err = marshalPublicKey(&priv.PublicKey)
+       case *ecdsa.PrivateKey:
+               publicKeyBytes, publicKeyAlgorithm, err = marshalPublicKey(&priv.PublicKey)
+       default:
+               panic("internal error")
+       }
+
+       if err != nil {
+               return nil, err
+       }
+
+       var extensions []pkix.Extension
+
+       if (len(template.DNSNames) > 0 || len(template.EmailAddresses) > 0 || len(template.IPAddresses) > 0) &&
+               !oidInExtensions(oidExtensionSubjectAltName, template.ExtraExtensions) {
+               sanBytes, err := marshalSANs(template.DNSNames, template.EmailAddresses, template.IPAddresses)
+               if err != nil {
+                       return nil, err
+               }
+
+               extensions = append(extensions, pkix.Extension{
+                       Id:    oidExtensionSubjectAltName,
+                       Value: sanBytes,
+               })
+       }
+
+       extensions = append(extensions, template.ExtraExtensions...)
+
+       var attributes []pkix.AttributeTypeAndValueSET
+       attributes = append(attributes, template.Attributes...)
+
+       if len(extensions) > 0 {
+               // specifiedExtensions contains all the extensions that we
+               // found specified via template.Attributes.
+               specifiedExtensions := make(map[string]bool)
+
+               for _, atvSet := range template.Attributes {
+                       if !atvSet.Type.Equal(oidExtensionRequest) {
+                               continue
+                       }
+
+                       for _, atvs := range atvSet.Value {
+                               for _, atv := range atvs {
+                                       specifiedExtensions[atv.Type.String()] = true
+                               }
+                       }
+               }
+
+               atvs := make([]pkix.AttributeTypeAndValue, 0, len(extensions))
+               for _, e := range extensions {
+                       if specifiedExtensions[e.Id.String()] {
+                               // Attributes already contained a value for
+                               // this extension and it takes priority.
+                               continue
+                       }
+
+                       atvs = append(atvs, pkix.AttributeTypeAndValue{
+                               // There is no place for the critical flag in a CSR.
+                               Type:  e.Id,
+                               Value: e.Value,
+                       })
+               }
+
+               // Append the extensions to an existing attribute if possible.
+               appended := false
+               for _, atvSet := range attributes {
+                       if !atvSet.Type.Equal(oidExtensionRequest) || len(atvSet.Value) == 0 {
+                               continue
+                       }
+
+                       atvSet.Value[0] = append(atvSet.Value[0], atvs...)
+                       appended = true
+                       break
+               }
+
+               // Otherwise, add a new attribute for the extensions.
+               if !appended {
+                       attributes = append(attributes, pkix.AttributeTypeAndValueSET{
+                               Type: oidExtensionRequest,
+                               Value: [][]pkix.AttributeTypeAndValue{
+                                       atvs,
+                               },
+                       })
+               }
+       }
+
+       asn1Subject := template.RawSubject
+       if len(asn1Subject) == 0 {
+               asn1Subject, err = asn1.Marshal(template.Subject.ToRDNSequence())
+               if err != nil {
+                       return
+               }
+       }
+
+       tbsCSR := tbsCertificateRequest{
+               Version: 0, // PKCS #10, RFC 2986
+               Subject: asn1.RawValue{FullBytes: asn1Subject},
+               PublicKey: publicKeyInfo{
+                       Algorithm: publicKeyAlgorithm,
+                       PublicKey: asn1.BitString{
+                               Bytes:     publicKeyBytes,
+                               BitLength: len(publicKeyBytes) * 8,
+                       },
+               },
+               Attributes: attributes,
+       }
+
+       tbsCSRContents, err := asn1.Marshal(tbsCSR)
+       if err != nil {
+               return
+       }
+       tbsCSR.Raw = tbsCSRContents
+
+       h := hashFunc.New()
+       h.Write(tbsCSRContents)
+       digest := h.Sum(nil)
+
+       var signature []byte
+       switch priv := priv.(type) {
+       case *rsa.PrivateKey:
+               signature, err = rsa.SignPKCS1v15(rand, priv, hashFunc, digest)
+       case *ecdsa.PrivateKey:
+               var r, s *big.Int
+               if r, s, err = ecdsa.Sign(rand, priv, digest); err == nil {
+                       signature, err = asn1.Marshal(ecdsaSignature{r, s})
+               }
+       default:
+               panic("internal error")
+       }
+
+       if err != nil {
+               return
+       }
+
+       return asn1.Marshal(certificateRequest{
+               TBSCSR:             tbsCSR,
+               SignatureAlgorithm: sigAlgo,
+               SignatureValue: asn1.BitString{
+                       Bytes:     signature,
+                       BitLength: len(signature) * 8,
+               },
+       })
+}
+
+// ParseCertificateRequest parses a single certificate request from the
+// given ASN.1 DER data.
+func ParseCertificateRequest(asn1Data []byte) (*CertificateRequest, error) {
+       var csr certificateRequest
+
+       rest, err := asn1.Unmarshal(asn1Data, &csr)
+       if err != nil {
+               return nil, err
+       } else if len(rest) != 0 {
+               return nil, asn1.SyntaxError{Msg: "trailing data"}
+       }
+
+       return parseCertificateRequest(&csr)
+}
+
+func parseCertificateRequest(in *certificateRequest) (*CertificateRequest, error) {
+       out := &CertificateRequest{
+               Raw: in.Raw,
+               RawTBSCertificateRequest: in.TBSCSR.Raw,
+               RawSubjectPublicKeyInfo:  in.TBSCSR.PublicKey.Raw,
+               RawSubject:               in.TBSCSR.Subject.FullBytes,
+
+               Signature:          in.SignatureValue.RightAlign(),
+               SignatureAlgorithm: getSignatureAlgorithmFromOID(in.SignatureAlgorithm.Algorithm),
+
+               PublicKeyAlgorithm: getPublicKeyAlgorithmFromOID(in.TBSCSR.PublicKey.Algorithm.Algorithm),
+
+               Version:    in.TBSCSR.Version,
+               Attributes: in.TBSCSR.Attributes,
+       }
+
+       var err error
+       out.PublicKey, err = parsePublicKey(out.PublicKeyAlgorithm, &in.TBSCSR.PublicKey)
+       if err != nil {
+               return nil, err
+       }
+
+       var subject pkix.RDNSequence
+       if _, err := asn1.Unmarshal(in.TBSCSR.Subject.FullBytes, &subject); err != nil {
+               return nil, err
+       }
+
+       out.Subject.FillFromRDNSequence(&subject)
+
+       var extensions []pkix.AttributeTypeAndValue
+
+       for _, atvSet := range in.TBSCSR.Attributes {
+               if !atvSet.Type.Equal(oidExtensionRequest) {
+                       continue
+               }
+
+               for _, atvs := range atvSet.Value {
+                       extensions = append(extensions, atvs...)
+               }
+       }
+
+       out.Extensions = make([]pkix.Extension, 0, len(extensions))
+
+       for _, e := range extensions {
+               value, ok := e.Value.([]byte)
+               if !ok {
+                       return nil, errors.New("x509: extension attribute contained non-OCTET STRING data")
+               }
+
+               out.Extensions = append(out.Extensions, pkix.Extension{
+                       Id:    e.Type,
+                       Value: value,
+               })
+
+               if len(e.Type) == 4 && e.Type[0] == 2 && e.Type[1] == 5 && e.Type[2] == 29 {
+                       switch e.Type[3] {
+                       case 17:
+                               out.DNSNames, out.EmailAddresses, out.IPAddresses, err = parseSANExtension(value)
+                               if err != nil {
+                                       return nil, err
+                               }
+                       }
+               }
+       }
+
+       return out, nil
+}
index 9d727f0fa7c94bad2ce4c2ac0c6e0ec91a6982c2..4b4b9d7c7686edb562395c9de35f9dd7f9bd0e4a 100644 (file)
@@ -679,11 +679,11 @@ func TestCRLCreation(t *testing.T) {
 
 func fromBase64(in string) []byte {
        out := make([]byte, base64.StdEncoding.DecodedLen(len(in)))
-       _, err := base64.StdEncoding.Decode(out, []byte(in))
+       n, err := base64.StdEncoding.Decode(out, []byte(in))
        if err != nil {
                panic("failed to base64 decode")
        }
-       return out
+       return out[:n]
 }
 
 func TestParseDERCRL(t *testing.T) {
@@ -735,3 +735,213 @@ func TestImports(t *testing.T) {
 const derCRLBase64 = "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"
 
 const pemCRLBase64 = "LS0tLS1CRUdJTiBYNTA5IENSTC0tLS0tDQpNSUlCOWpDQ0FWOENBUUV3RFFZSktvWklodmNOQVFFRkJRQXdiREVhTUJnR0ExVUVDaE1SVWxOQklGTmxZM1Z5DQphWFI1SUVsdVl5NHhIakFjQmdOVkJBTVRGVkpUUVNCUWRXSnNhV01nVW05dmRDQkRRU0IyTVRFdU1Dd0dDU3FHDQpTSWIzRFFFSkFSWWZjbk5oYTJWdmJuSnZiM1J6YVdkdVFISnpZWE5sWTNWeWFYUjVMbU52YlJjTk1URXdNakl6DQpNVGt5T0RNd1doY05NVEV3T0RJeU1Ua3lPRE13V2pDQmpEQktBaEVBckRxb2g5RkhKSFhUN09QZ3V1bjQrQmNODQpNRGt4TVRBeU1UUXlOekE1V2pBbU1Bb0dBMVVkRlFRRENnRUpNQmdHQTFVZEdBUVJHQTh5TURBNU1URXdNakUwDQpNalExTlZvd1BnSVJBTEd6blowOTVQQjVhQU9MUGc1N2ZNTVhEVEF5TVRBeU16RTBOVEF4TkZvd0dqQVlCZ05WDQpIUmdFRVJnUE1qQXdNakV3TWpNeE5EVXdNVFJhb0RBd0xqQWZCZ05WSFNNRUdEQVdnQlQxVERGNlVRTS9MTmVMDQpsNWx2cUhHUXEzZzltekFMQmdOVkhSUUVCQUlDQUlRd0RRWUpLb1pJaHZjTkFRRUZCUUFEZ1lFQUZVNUFzNk16DQpxNVBSc2lmYW9iUVBHaDFhSkx5QytNczVBZ2MwYld5QTNHQWR4dXI1U3BQWmVSV0NCamlQL01FSEJXSkNsQkhQDQpHUmNxNXlJZDNFakRrYUV5eFJhK2k2N0x6dmhJNmMyOUVlNks5cFNZd2ppLzdSVWhtbW5Qclh0VHhsTDBsckxyDQptUVFKNnhoRFJhNUczUUE0Q21VZHNITnZicnpnbUNZcHZWRT0NCi0tLS0tRU5EIFg1MDkgQ1JMLS0tLS0NCg0K"
+
+func TestCreateCertificateRequest(t *testing.T) {
+       random := rand.Reader
+
+       block, _ := pem.Decode([]byte(pemPrivateKey))
+       rsaPriv, err := ParsePKCS1PrivateKey(block.Bytes)
+       if err != nil {
+               t.Fatalf("Failed to parse private key: %s", err)
+       }
+
+       ecdsa256Priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+       if err != nil {
+               t.Fatalf("Failed to generate ECDSA key: %s", err)
+       }
+
+       ecdsa384Priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
+       if err != nil {
+               t.Fatalf("Failed to generate ECDSA key: %s", err)
+       }
+
+       ecdsa521Priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
+       if err != nil {
+               t.Fatalf("Failed to generate ECDSA key: %s", err)
+       }
+
+       tests := []struct {
+               name    string
+               priv    interface{}
+               sigAlgo SignatureAlgorithm
+       }{
+               {"RSA", rsaPriv, SHA1WithRSA},
+               {"ECDSA-256", ecdsa256Priv, ECDSAWithSHA1},
+               {"ECDSA-384", ecdsa384Priv, ECDSAWithSHA1},
+               {"ECDSA-521", ecdsa521Priv, ECDSAWithSHA1},
+       }
+
+       for _, test := range tests {
+               template := CertificateRequest{
+                       Subject: pkix.Name{
+                               CommonName:   "test.example.com",
+                               Organization: []string{"Σ Acme Co"},
+                       },
+                       SignatureAlgorithm: test.sigAlgo,
+                       DNSNames:           []string{"test.example.com"},
+                       EmailAddresses:     []string{"gopher@golang.org"},
+                       IPAddresses:        []net.IP{net.IPv4(127, 0, 0, 1).To4(), net.ParseIP("2001:4860:0:2001::68")},
+               }
+
+               derBytes, err := CreateCertificateRequest(random, &template, test.priv)
+               if err != nil {
+                       t.Errorf("%s: failed to create certificate request: %s", test.name, err)
+                       continue
+               }
+
+               out, err := ParseCertificateRequest(derBytes)
+               if err != nil {
+                       t.Errorf("%s: failed to create certificate request: %s", test.name, err)
+                       continue
+               }
+
+               if out.Subject.CommonName != template.Subject.CommonName {
+                       t.Errorf("%s: output subject common name and template subject common name don't match", test.name)
+               } else if len(out.Subject.Organization) != len(template.Subject.Organization) {
+                       t.Errorf("%s: output subject organisation and template subject organisation don't match", test.name)
+               } else if len(out.DNSNames) != len(template.DNSNames) {
+                       t.Errorf("%s: output DNS names and template DNS names don't match", test.name)
+               } else if len(out.EmailAddresses) != len(template.EmailAddresses) {
+                       t.Errorf("%s: output email addresses and template email addresses don't match", test.name)
+               } else if len(out.IPAddresses) != len(template.IPAddresses) {
+                       t.Errorf("%s: output IP addresses and template IP addresses names don't match", test.name)
+               }
+       }
+}
+
+func marshalAndParseCSR(t *testing.T, template *CertificateRequest) *CertificateRequest {
+       block, _ := pem.Decode([]byte(pemPrivateKey))
+       rsaPriv, err := ParsePKCS1PrivateKey(block.Bytes)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       derBytes, err := CreateCertificateRequest(rand.Reader, template, rsaPriv)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       csr, err := ParseCertificateRequest(derBytes)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       return csr
+}
+
+func TestCertificateRequestOverrides(t *testing.T) {
+       sanContents, err := marshalSANs([]string{"foo.example.com"}, nil, nil)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       template := CertificateRequest{
+               Subject: pkix.Name{
+                       CommonName:   "test.example.com",
+                       Organization: []string{"Σ Acme Co"},
+               },
+               DNSNames: []string{"test.example.com"},
+
+               // An explicit extension should override the DNSNames from the
+               // template.
+               ExtraExtensions: []pkix.Extension{
+                       pkix.Extension{
+                               Id:    oidExtensionSubjectAltName,
+                               Value: sanContents,
+                       },
+               },
+       }
+
+       csr := marshalAndParseCSR(t, &template)
+
+       if len(csr.DNSNames) != 1 || csr.DNSNames[0] != "foo.example.com" {
+               t.Errorf("Extension did not override template. Got %v\n", csr.DNSNames)
+       }
+
+       // If there is already an attribute with X.509 extensions then the
+       // extra extensions should be added to it rather than creating a CSR
+       // with two extension attributes.
+
+       template.Attributes = []pkix.AttributeTypeAndValueSET{
+               pkix.AttributeTypeAndValueSET{
+                       Type: oidExtensionRequest,
+                       Value: [][]pkix.AttributeTypeAndValue{
+                               []pkix.AttributeTypeAndValue{
+                                       pkix.AttributeTypeAndValue{
+                                               Type:  oidExtensionAuthorityInfoAccess,
+                                               Value: []byte("foo"),
+                                       },
+                               },
+                       },
+               },
+       }
+
+       csr = marshalAndParseCSR(t, &template)
+       if l := len(csr.Attributes); l != 1 {
+               t.Errorf("incorrect number of attributes: %d\n", l)
+       }
+
+       if !csr.Attributes[0].Type.Equal(oidExtensionRequest) ||
+               len(csr.Attributes[0].Value) != 1 ||
+               len(csr.Attributes[0].Value[0]) != 2 {
+               t.Errorf("bad attributes: %#v\n", csr.Attributes)
+       }
+
+       sanContents2, err := marshalSANs([]string{"foo2.example.com"}, nil, nil)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       // Extensions in Attributes should override those in ExtraExtensions.
+       template.Attributes[0].Value[0] = append(template.Attributes[0].Value[0], pkix.AttributeTypeAndValue{
+               Type:  oidExtensionSubjectAltName,
+               Value: sanContents2,
+       })
+
+       csr = marshalAndParseCSR(t, &template)
+
+       if len(csr.DNSNames) != 1 || csr.DNSNames[0] != "foo2.example.com" {
+               t.Errorf("Attributes did not override ExtraExtensions. Got %v\n", csr.DNSNames)
+       }
+}
+
+func TestParseCertificateRequest(t *testing.T) {
+       csrBytes := fromBase64(csrBase64)
+       csr, err := ParseCertificateRequest(csrBytes)
+       if err != nil {
+               t.Fatalf("failed to parse CSR: %s", err)
+       }
+
+       if len(csr.EmailAddresses) != 1 || csr.EmailAddresses[0] != "gopher@golang.org" {
+               t.Errorf("incorrect email addresses found: %v", csr.EmailAddresses)
+       }
+
+       if len(csr.DNSNames) != 1 || csr.DNSNames[0] != "test.example.com" {
+               t.Errorf("incorrect DNS names found: %v", csr.DNSNames)
+       }
+
+       if len(csr.Subject.Country) != 1 || csr.Subject.Country[0] != "AU" {
+               t.Errorf("incorrect Subject name: %v", csr.Subject)
+       }
+
+       found := false
+       for _, e := range csr.Extensions {
+               if e.Id.Equal(oidExtensionBasicConstraints) {
+                       found = true
+                       break
+               }
+       }
+       if !found {
+               t.Errorf("basic constraints extension not found in CSR")
+       }
+}
+
+// This CSR was generated with OpenSSL:
+//  openssl req -out CSR.csr -new -newkey rsa:2048 -nodes -keyout privateKey.key -config openssl.cnf
+//
+// The openssl.cnf needs to include this section:
+//   [ v3_req ]
+//   basicConstraints = CA:FALSE
+//   keyUsage = nonRepudiation, digitalSignature, keyEncipherment
+//   subjectAltName = email:gopher@golang.org,DNS:test.example.com
+const csrBase64 = "MIIC4zCCAcsCAQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOY+MVedRg2JEnyeLcSzcsMv2VcsTfkB5+Etd6hihAh6MrGezNyASMMKuQN6YhCX1icQDiQtGsDLTtheNnSXK06tAhHjAP/hGlszRJp+5+rP2M58fDBAkUBEhskbCUWwpY14jFtVuGNJ8vF8h8IeczdolvQhX9lVai9G0EUXJMliMKdjA899H0mRs9PzHyidyrXFNiZlQXfD8Kg7gETn2Ny965iyI6ujAIYSCvam6TnxRHYH2MBKyVGvsYGbPYUQJCsgdgyajEg6ekihvQY3SzO1HSAlZAd7d1QYO4VeWJ2mY6Wu3Jpmh+AmG19S9CcHqGjd0bhuAX9cpPOKgnEmqn0CAwEAAaBZMFcGCSqGSIb3DQEJDjFKMEgwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwLgYDVR0RBCcwJYERZ29waGVyQGdvbGFuZy5vcmeCEHRlc3QuZXhhbXBsZS5jb20wDQYJKoZIhvcNAQEFBQADggEBAC9+QpKfdabxwCWwf4IEe1cKjdXLS1ScSuw27a3kZzQiPV78WJMa6dB8dqhdH5BRwGZ/qsgLrO6ZHlNeIv2Ib41Ccq71ecHW/nXc94A1BzJ/bVdI9LZcmTUvR1/m1jCpN7UqQ0ml1u9VihK7Pe762hEYxuWDQzYEU0l15S/bXmqeq3eF1A59XT/2jwe5+NV0Wwf4UQlkTXsAQMsJ+KzrQafd8Qv2A49o048uRvmjeJDrXLawGVianZ7D5A6Fpd1rZh6XcjqBpmgLw41DRQWENOdzhy+HyphKRv1MlY8OLkNqpGMhu8DdgJVGoT16DGiickoEa7Z3UCPVNgdTkT9jq7U="
index b8a732e0244c85654189cd30b0e9386d42f75d31..7a3c3797c8bb8240455c133c208f912073315dc5 100644 (file)
@@ -23,6 +23,7 @@ import (
        "fmt"
        "math/big"
        "reflect"
+       "strconv"
        "time"
 )
 
@@ -197,6 +198,19 @@ func (oi ObjectIdentifier) Equal(other ObjectIdentifier) bool {
        return true
 }
 
+func (oi ObjectIdentifier) String() string {
+       var s string
+
+       for i, v := range oi {
+               if i > 0 {
+                       s += "."
+               }
+               s += strconv.Itoa(v)
+       }
+
+       return s
+}
+
 // parseObjectIdentifier parses an OBJECT IDENTIFIER from the given bytes and
 // returns it. An object identifier is a sequence of variable length integers
 // that are assigned in a hierarchy.
index 4f60b6751dfeea72fcfa4c505742a62e61415b79..b553f78e0a6fe659b0f77a541694895976e8c7cd 100644 (file)
@@ -232,6 +232,10 @@ func TestObjectIdentifier(t *testing.T) {
                        }
                }
        }
+
+       if s := ObjectIdentifier([]int{1, 2, 3, 4}).String(); s != "1.2.3.4" {
+               t.Errorf("bad ObjectIdentifier.String(). Got %s, want 1.2.3.4", s)
+       }
 }
 
 type timeTest struct {