]> Cypherpunks repositories - gostls13.git/commitdiff
crypto/x509: add RevocationList and CreateRevocationList
authorRoland Shoemaker <roland@letsencrypt.org>
Fri, 20 Mar 2020 20:28:43 +0000 (20:28 +0000)
committerFilippo Valsorda <filippo@golang.org>
Mon, 23 Mar 2020 17:23:03 +0000 (17:23 +0000)
The existing Certificate.CreateCRL method generates non-conformant CRLs and
as such cannot be used for implementations that require standards
compliance. This change implements a new top level method, CreateCRL, which
generates compliant CRLs, and offers an extensible API if any
extensions/fields need to be supported in the future.

Here is an example Issuer/CRL generated using this change:
-----BEGIN CERTIFICATE-----
MIIBNjCB3aADAgECAgEWMAoGCCqGSM49BAMCMBIxEDAOBgNVBAMTB3Rlc3Rpbmcw
IhgPMDAwMTAxMDEwMDAwMDBaGA8wMDAxMDEwMTAwMDAwMFowEjEQMA4GA1UEAxMH
dGVzdGluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLHrudbSM36sn1VBrmm/
OfQTyEsI4tIUV1VmneOKHL9ENBGCiec4GhQm2SGnDT/sZy2bB3c3yozh/roS6cZJ
UZqjIDAeMA4GA1UdDwEB/wQEAwIBAjAMBgNVHQ4EBQQDAQIDMAoGCCqGSM49BAMC
A0gAMEUCIQCoAYN6CGZPgd5Sw5a1rd5VexciT5MCxTfXj+ZfJNfoiAIgQVCTB8AE
Nm2xset7+HOgtQYlKNw/rGd8cFcv5Y9aUzo=
-----END CERTIFICATE-----
-----BEGIN X509 CRL-----
MIHWMH0CAQEwCgYIKoZIzj0EAwIwEjEQMA4GA1UEAxMHdGVzdGluZxgPMDAwMTAx
MDIwMDAwMDBaGA8wMDAxMDEwMzAwMDAwMFowFjAUAgECGA8wMDAxMDEwMTAxMDAw
MFqgHjAcMA4GA1UdIwQHMAWAAwECAzAKBgNVHRQEAwIBBTAKBggqhkjOPQQDAgNJ
ADBGAiEAjqfj/IG4ys5WkjrbTNpDbr+saHGO/NujLJotlLL9KzgCIQDm8VZPzj0f
NYEQgAW4nsiUzlvEUCoHMw0141VCZXv67A==
-----END X509 CRL-----

Fixes #35428

Change-Id: Id96b6f47698d0bed39d586b46bd12374ee6ff88f
GitHub-Last-Rev: c83a6017164e71df3989fe57322b3b4869a09f37
GitHub-Pull-Request: golang/go#36945
Reviewed-on: https://go-review.googlesource.com/c/go/+/217298
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Run-TryBot: Filippo Valsorda <filippo@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>

src/crypto/x509/x509.go
src/crypto/x509/x509_test.go

index a8bef2a90dd154f481cc5bee1821d1fe8421c9b7..2bf6d379eb322558485c15fc9c6ca6740e14f1f4 100644 (file)
@@ -1633,6 +1633,7 @@ var (
        oidExtensionNameConstraints       = []int{2, 5, 29, 30}
        oidExtensionCRLDistributionPoints = []int{2, 5, 29, 31}
        oidExtensionAuthorityInfoAccess   = []int{1, 3, 6, 1, 5, 5, 7, 1, 1}
+       oidExtensionCRLNumber             = []int{2, 5, 29, 20}
 )
 
 var (
@@ -2202,6 +2203,9 @@ func ParseDERCRL(derBytes []byte) (*pkix.CertificateList, error) {
 
 // CreateCRL returns a DER encoded CRL, signed by this Certificate, that
 // contains the given list of revoked certificates.
+//
+// Note: this method does not generate an RFC 5280 conformant X.509 v2 CRL.
+// To generate a standards compliant CRL, use CreateRevocationList instead.
 func (c *Certificate) CreateCRL(rand io.Reader, priv interface{}, revokedCerts []pkix.RevokedCertificate, now, expiry time.Time) (crlBytes []byte, err error) {
        key, ok := priv.(crypto.Signer)
        if !ok {
@@ -2649,3 +2653,141 @@ func parseCertificateRequest(in *certificateRequest) (*CertificateRequest, error
 func (c *CertificateRequest) CheckSignature() error {
        return checkSignature(c.SignatureAlgorithm, c.RawTBSCertificateRequest, c.Signature, c.PublicKey)
 }
+
+// RevocationList contains the fields used to create an X.509 v2 Certificate
+// Revocation list with CreateRevocationList.
+type RevocationList struct {
+       // SignatureAlgorithm is used to determine the signature algorithm to be
+       // used when signing the CRL. If 0 the default algorithm for the signing
+       // key will be used.
+       SignatureAlgorithm SignatureAlgorithm
+
+       // RevokedCertificates is used to populate the revokedCertificates
+       // sequence in the CRL, it may be empty. RevokedCertificates may be nil,
+       // in which case an empty CRL will be created.
+       RevokedCertificates []pkix.RevokedCertificate
+
+       // Number is used to populate the X.509 v2 cRLNumber extension in the CRL,
+       // which should be a monotonically increasing sequence number for a given
+       // CRL scope and CRL issuer.
+       Number *big.Int
+       // ThisUpdate is used to populate the thisUpdate field in the CRL, which
+       // indicates the issuance date of the CRL.
+       ThisUpdate time.Time
+       // NextUpdate is used to populate the nextUpdate field in the CRL, which
+       // indicates the date by which the next CRL will be issued. NextUpdate
+       // must be greater than ThisUpdate.
+       NextUpdate time.Time
+       // ExtraExtensions contains any additional extensions to add directly to
+       // the CRL.
+       ExtraExtensions []pkix.Extension
+}
+
+// CreateRevocationList creates a new X.509 v2 Certificate Revocation List,
+// according to RFC 5280, based on template.
+//
+// The CRL is signed by priv which should be the private key associated with
+// the public key in the issuer certificate.
+//
+// The issuer may not be nil, and the crlSign bit must be set in KeyUsage in
+// order to use it as a CRL issuer.
+//
+// The issuer distinguished name CRL field and authority key identifier
+// extension are populated using the issuer certificate. issuer must have
+// SubjectKeyId set.
+func CreateRevocationList(rand io.Reader, template *RevocationList, issuer *Certificate, priv crypto.Signer) ([]byte, error) {
+       if template == nil {
+               return nil, errors.New("x509: template can not be nil")
+       }
+       if issuer == nil {
+               return nil, errors.New("x509: issuer can not be nil")
+       }
+       if (issuer.KeyUsage & KeyUsageCRLSign) == 0 {
+               return nil, errors.New("x509: issuer must have the crlSign key usage bit set")
+       }
+       if len(issuer.SubjectKeyId) == 0 {
+               return nil, errors.New("x509: issuer certificate doesn't contain a subject key identifier")
+       }
+       if template.NextUpdate.Before(template.ThisUpdate) {
+               return nil, errors.New("x509: template.ThisUpdate is after template.NextUpdate")
+       }
+       if template.Number == nil {
+               return nil, errors.New("x509: template contains nil Number field")
+       }
+
+       hashFunc, signatureAlgorithm, err := signingParamsForPublicKey(priv.Public(), template.SignatureAlgorithm)
+       if err != nil {
+               return nil, err
+       }
+
+       // Force revocation times to UTC per RFC 5280.
+       revokedCertsUTC := make([]pkix.RevokedCertificate, len(template.RevokedCertificates))
+       for i, rc := range template.RevokedCertificates {
+               rc.RevocationTime = rc.RevocationTime.UTC()
+               revokedCertsUTC[i] = rc
+       }
+
+       aki, err := asn1.Marshal(authKeyId{Id: issuer.SubjectKeyId})
+       if err != nil {
+               return nil, err
+       }
+       crlNum, err := asn1.Marshal(template.Number)
+       if err != nil {
+               return nil, err
+       }
+
+       tbsCertList := pkix.TBSCertificateList{
+               Version:    1, // v2
+               Signature:  signatureAlgorithm,
+               Issuer:     issuer.Subject.ToRDNSequence(),
+               ThisUpdate: template.ThisUpdate.UTC(),
+               NextUpdate: template.NextUpdate.UTC(),
+               Extensions: []pkix.Extension{
+                       {
+                               Id:    oidExtensionAuthorityKeyId,
+                               Value: aki,
+                       },
+                       {
+                               Id:    oidExtensionCRLNumber,
+                               Value: crlNum,
+                       },
+               },
+       }
+       if len(revokedCertsUTC) > 0 {
+               tbsCertList.RevokedCertificates = revokedCertsUTC
+       }
+
+       if len(template.ExtraExtensions) > 0 {
+               tbsCertList.Extensions = append(tbsCertList.Extensions, template.ExtraExtensions...)
+       }
+
+       tbsCertListContents, err := asn1.Marshal(tbsCertList)
+       if err != nil {
+               return nil, err
+       }
+
+       input := tbsCertListContents
+       if hashFunc != 0 {
+               h := hashFunc.New()
+               h.Write(tbsCertListContents)
+               input = h.Sum(nil)
+       }
+       var signerOpts crypto.SignerOpts = hashFunc
+       if template.SignatureAlgorithm.isRSAPSS() {
+               signerOpts = &rsa.PSSOptions{
+                       SaltLength: rsa.PSSSaltLengthEqualsHash,
+                       Hash:       hashFunc,
+               }
+       }
+
+       signature, err := priv.Sign(rand, input, signerOpts)
+       if err != nil {
+               return nil, err
+       }
+
+       return asn1.Marshal(pkix.CertificateList{
+               TBSCertList:        tbsCertList,
+               SignatureAlgorithm: signatureAlgorithm,
+               SignatureValue:     asn1.BitString{Bytes: signature, BitLength: len(signature) * 8},
+       })
+}
index 9e15b8adbfe068b0ef1ae9ac24e3cc341da6e4f7..0c6747d28df6cafd7a9983cb757f0c5c9188de73 100644 (file)
@@ -6,6 +6,7 @@ package x509
 
 import (
        "bytes"
+       "crypto"
        "crypto/dsa"
        "crypto/ecdsa"
        "crypto/ed25519"
@@ -2293,3 +2294,290 @@ func TestPKCS1MismatchKeyFormat(t *testing.T) {
                }
        }
 }
+
+func TestCreateRevocationList(t *testing.T) {
+       ec256Priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+       if err != nil {
+               t.Fatalf("Failed to generate ECDSA P256 key: %s", err)
+       }
+       _, ed25519Priv, err := ed25519.GenerateKey(rand.Reader)
+       if err != nil {
+               t.Fatalf("Failed to generate Ed25519 key: %s", err)
+       }
+       tests := []struct {
+               name          string
+               key           crypto.Signer
+               issuer        *Certificate
+               template      *RevocationList
+               expectedError string
+       }{
+               {
+                       name:          "nil template",
+                       key:           ec256Priv,
+                       issuer:        nil,
+                       template:      nil,
+                       expectedError: "x509: template can not be nil",
+               },
+               {
+                       name:          "nil issuer",
+                       key:           ec256Priv,
+                       issuer:        nil,
+                       template:      &RevocationList{},
+                       expectedError: "x509: issuer can not be nil",
+               },
+               {
+                       name: "issuer doesn't have crlSign key usage bit set",
+                       key:  ec256Priv,
+                       issuer: &Certificate{
+                               KeyUsage: KeyUsageCertSign,
+                       },
+                       template:      &RevocationList{},
+                       expectedError: "x509: issuer must have the crlSign key usage bit set",
+               },
+               {
+                       name: "issuer missing SubjectKeyId",
+                       key:  ec256Priv,
+                       issuer: &Certificate{
+                               KeyUsage: KeyUsageCRLSign,
+                       },
+                       template:      &RevocationList{},
+                       expectedError: "x509: issuer certificate doesn't contain a subject key identifier",
+               },
+               {
+                       name: "nextUpdate before thisUpdate",
+                       key:  ec256Priv,
+                       issuer: &Certificate{
+                               KeyUsage: KeyUsageCRLSign,
+                               Subject: pkix.Name{
+                                       CommonName: "testing",
+                               },
+                               SubjectKeyId: []byte{1, 2, 3},
+                       },
+                       template: &RevocationList{
+                               ThisUpdate: time.Time{}.Add(time.Hour),
+                               NextUpdate: time.Time{},
+                       },
+                       expectedError: "x509: template.ThisUpdate is after template.NextUpdate",
+               },
+               {
+                       name: "nil Number",
+                       key:  ec256Priv,
+                       issuer: &Certificate{
+                               KeyUsage: KeyUsageCRLSign,
+                               Subject: pkix.Name{
+                                       CommonName: "testing",
+                               },
+                               SubjectKeyId: []byte{1, 2, 3},
+                       },
+                       template: &RevocationList{
+                               ThisUpdate: time.Time{}.Add(time.Hour * 24),
+                               NextUpdate: time.Time{}.Add(time.Hour * 48),
+                       },
+                       expectedError: "x509: template contains nil Number field",
+               },
+               {
+                       name: "invalid signature algorithm",
+                       key:  ec256Priv,
+                       issuer: &Certificate{
+                               KeyUsage: KeyUsageCRLSign,
+                               Subject: pkix.Name{
+                                       CommonName: "testing",
+                               },
+                               SubjectKeyId: []byte{1, 2, 3},
+                       },
+                       template: &RevocationList{
+                               SignatureAlgorithm: SHA256WithRSA,
+                               RevokedCertificates: []pkix.RevokedCertificate{
+                                       {
+                                               SerialNumber:   big.NewInt(2),
+                                               RevocationTime: time.Time{}.Add(time.Hour),
+                                       },
+                               },
+                               Number:     big.NewInt(5),
+                               ThisUpdate: time.Time{}.Add(time.Hour * 24),
+                               NextUpdate: time.Time{}.Add(time.Hour * 48),
+                       },
+                       expectedError: "x509: requested SignatureAlgorithm does not match private key type",
+               },
+               {
+                       name: "valid",
+                       key:  ec256Priv,
+                       issuer: &Certificate{
+                               KeyUsage: KeyUsageCRLSign,
+                               Subject: pkix.Name{
+                                       CommonName: "testing",
+                               },
+                               SubjectKeyId: []byte{1, 2, 3},
+                       },
+                       template: &RevocationList{
+                               RevokedCertificates: []pkix.RevokedCertificate{
+                                       {
+                                               SerialNumber:   big.NewInt(2),
+                                               RevocationTime: time.Time{}.Add(time.Hour),
+                                       },
+                               },
+                               Number:     big.NewInt(5),
+                               ThisUpdate: time.Time{}.Add(time.Hour * 24),
+                               NextUpdate: time.Time{}.Add(time.Hour * 48),
+                       },
+               },
+               {
+                       name: "valid, Ed25519 key",
+                       key:  ed25519Priv,
+                       issuer: &Certificate{
+                               KeyUsage: KeyUsageCRLSign,
+                               Subject: pkix.Name{
+                                       CommonName: "testing",
+                               },
+                               SubjectKeyId: []byte{1, 2, 3},
+                       },
+                       template: &RevocationList{
+                               RevokedCertificates: []pkix.RevokedCertificate{
+                                       {
+                                               SerialNumber:   big.NewInt(2),
+                                               RevocationTime: time.Time{}.Add(time.Hour),
+                                       },
+                               },
+                               Number:     big.NewInt(5),
+                               ThisUpdate: time.Time{}.Add(time.Hour * 24),
+                               NextUpdate: time.Time{}.Add(time.Hour * 48),
+                       },
+               },
+               {
+                       name: "valid, non-default signature algorithm",
+                       key:  ec256Priv,
+                       issuer: &Certificate{
+                               KeyUsage: KeyUsageCRLSign,
+                               Subject: pkix.Name{
+                                       CommonName: "testing",
+                               },
+                               SubjectKeyId: []byte{1, 2, 3},
+                       },
+                       template: &RevocationList{
+                               SignatureAlgorithm: ECDSAWithSHA512,
+                               RevokedCertificates: []pkix.RevokedCertificate{
+                                       {
+                                               SerialNumber:   big.NewInt(2),
+                                               RevocationTime: time.Time{}.Add(time.Hour),
+                                       },
+                               },
+                               Number:     big.NewInt(5),
+                               ThisUpdate: time.Time{}.Add(time.Hour * 24),
+                               NextUpdate: time.Time{}.Add(time.Hour * 48),
+                       },
+               },
+               {
+                       name: "valid, extra extension",
+                       key:  ec256Priv,
+                       issuer: &Certificate{
+                               KeyUsage: KeyUsageCRLSign,
+                               Subject: pkix.Name{
+                                       CommonName: "testing",
+                               },
+                               SubjectKeyId: []byte{1, 2, 3},
+                       },
+                       template: &RevocationList{
+                               RevokedCertificates: []pkix.RevokedCertificate{
+                                       {
+                                               SerialNumber:   big.NewInt(2),
+                                               RevocationTime: time.Time{}.Add(time.Hour),
+                                       },
+                               },
+                               Number:     big.NewInt(5),
+                               ThisUpdate: time.Time{}.Add(time.Hour * 24),
+                               NextUpdate: time.Time{}.Add(time.Hour * 48),
+                               ExtraExtensions: []pkix.Extension{
+                                       {
+                                               Id:    []int{2, 5, 29, 99},
+                                               Value: []byte{5, 0},
+                                       },
+                               },
+                       },
+               },
+               {
+                       name: "valid, empty list",
+                       key:  ec256Priv,
+                       issuer: &Certificate{
+                               KeyUsage: KeyUsageCRLSign,
+                               Subject: pkix.Name{
+                                       CommonName: "testing",
+                               },
+                               SubjectKeyId: []byte{1, 2, 3},
+                       },
+                       template: &RevocationList{
+                               Number:     big.NewInt(5),
+                               ThisUpdate: time.Time{}.Add(time.Hour * 24),
+                               NextUpdate: time.Time{}.Add(time.Hour * 48),
+                       },
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       crl, err := CreateRevocationList(rand.Reader, tc.template, tc.issuer, tc.key)
+                       if err != nil && tc.expectedError == "" {
+                               t.Fatalf("CreateRevocationList failed unexpectedly: %s", err)
+                       } else if err != nil && tc.expectedError != err.Error() {
+                               t.Fatalf("CreateRevocationList failed unexpectedly, wanted: %s, got: %s", tc.expectedError, err)
+                       } else if err == nil && tc.expectedError != "" {
+                               t.Fatalf("CreateRevocationList didn't fail, expected: %s", tc.expectedError)
+                       }
+                       if tc.expectedError != "" {
+                               return
+                       }
+
+                       parsedCRL, err := ParseDERCRL(crl)
+                       if err != nil {
+                               t.Fatalf("Failed to parse generated CRL: %s", err)
+                       }
+
+                       if tc.template.SignatureAlgorithm != UnknownSignatureAlgorithm &&
+                               parsedCRL.SignatureAlgorithm.Algorithm.Equal(signatureAlgorithmDetails[tc.template.SignatureAlgorithm].oid) {
+                               t.Fatalf("SignatureAlgorithm mismatch: got %v; want %v.", parsedCRL.SignatureAlgorithm,
+                                       tc.template.SignatureAlgorithm)
+                       }
+
+                       if !reflect.DeepEqual(parsedCRL.TBSCertList.RevokedCertificates, tc.template.RevokedCertificates) {
+                               t.Fatalf("RevokedCertificates mismatch: got %v; want %v.",
+                                       parsedCRL.TBSCertList.RevokedCertificates, tc.template.RevokedCertificates)
+                       }
+
+                       if len(parsedCRL.TBSCertList.Extensions) != 2+len(tc.template.ExtraExtensions) {
+                               t.Fatalf("Generated CRL has wrong number of extensions, wanted: %d, got: %d", 2+len(tc.template.ExtraExtensions), len(parsedCRL.TBSCertList.Extensions))
+                       }
+                       expectedAKI, err := asn1.Marshal(authKeyId{Id: tc.issuer.SubjectKeyId})
+                       if err != nil {
+                               t.Fatalf("asn1.Marshal failed: %s", err)
+                       }
+                       akiExt := pkix.Extension{
+                               Id:    oidExtensionAuthorityKeyId,
+                               Value: expectedAKI,
+                       }
+                       if !reflect.DeepEqual(parsedCRL.TBSCertList.Extensions[0], akiExt) {
+                               t.Fatalf("Unexpected first extension: got %v, want %v",
+                                       parsedCRL.TBSCertList.Extensions[0], akiExt)
+                       }
+                       expectedNum, err := asn1.Marshal(tc.template.Number)
+                       if err != nil {
+                               t.Fatalf("asn1.Marshal failed: %s", err)
+                       }
+                       crlExt := pkix.Extension{
+                               Id:    oidExtensionCRLNumber,
+                               Value: expectedNum,
+                       }
+                       if !reflect.DeepEqual(parsedCRL.TBSCertList.Extensions[1], crlExt) {
+                               t.Fatalf("Unexpected second extension: got %v, want %v",
+                                       parsedCRL.TBSCertList.Extensions[1], crlExt)
+                       }
+                       if len(parsedCRL.TBSCertList.Extensions[2:]) == 0 && len(tc.template.ExtraExtensions) == 0 {
+                               // If we don't have anything to check return early so we don't
+                               // hit a [] != nil false positive below.
+                               return
+                       }
+                       if !reflect.DeepEqual(parsedCRL.TBSCertList.Extensions[2:], tc.template.ExtraExtensions) {
+                               t.Fatalf("Extensions mismatch: got %v; want %v.",
+                                       parsedCRL.TBSCertList.Extensions[2:], tc.template.ExtraExtensions)
+                       }
+               })
+       }
+}