From: Roland Shoemaker Date: Fri, 20 Mar 2020 20:28:43 +0000 (+0000) Subject: crypto/x509: add RevocationList and CreateRevocationList X-Git-Tag: go1.15beta1~788 X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=5d47f870a64982173f3da8a82a6a793b83075d6a;p=gostls13.git crypto/x509: add RevocationList and CreateRevocationList 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 Run-TryBot: Filippo Valsorda TryBot-Result: Gobot Gobot --- diff --git a/src/crypto/x509/x509.go b/src/crypto/x509/x509.go index a8bef2a90d..2bf6d379eb 100644 --- a/src/crypto/x509/x509.go +++ b/src/crypto/x509/x509.go @@ -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}, + }) +} diff --git a/src/crypto/x509/x509_test.go b/src/crypto/x509/x509_test.go index 9e15b8adbf..0c6747d28d 100644 --- a/src/crypto/x509/x509_test.go +++ b/src/crypto/x509/x509_test.go @@ -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) + } + }) + } +}