diff --git a/internal/types/rfc6962/rfc6962.go b/internal/types/rfc6962/rfc6962.go index 3aaec7c5..dfa58f70 100644 --- a/internal/types/rfc6962/rfc6962.go +++ b/internal/types/rfc6962/rfc6962.go @@ -43,8 +43,10 @@ const ( TreeNodePrefix = byte(0x01) ) -// Defined in RFC 6962 s3.1. +// Defined or referenced in RFC 6962 s3.1. var ( + OIDExtAuthorityKeyId = asn1.ObjectIdentifier{2, 5, 29, 35} + OIDExtKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 37} OIDExtensionCTPoison = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3} OIDExtKeyUsageCertificateTransparency = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 4} ) diff --git a/internal/x509util/ct.go b/internal/x509util/ct.go index d97f4123..a278656b 100644 --- a/internal/x509util/ct.go +++ b/internal/x509util/ct.go @@ -25,13 +25,9 @@ import ( "github.com/transparency-dev/tessera/ctonly" "github.com/transparency-dev/tesseract/internal/types/rfc6962" -) -var ( - oidExtensionAuthorityKeyId = asn1.ObjectIdentifier{2, 5, 29, 35} - // These extensions are defined in RFC 6962 s3.1. - oidExtensionCTPoison = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3} - oidExtensionKeyUsageCertificateTransparency = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 4} + "golang.org/x/crypto/cryptobyte" + cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1" ) type tbsCertificate struct { @@ -110,7 +106,7 @@ func removeExtension(tbsData []byte, oid asn1.ObjectIdentifier) ([]byte, error) // - The precert's AuthorityKeyId is changed to the AuthorityKeyId of the // intermediate. func BuildPrecertTBS(tbsData []byte, preIssuer *x509.Certificate) ([]byte, error) { - data, err := removeExtension(tbsData, oidExtensionCTPoison) + data, err := removeExtension(tbsData, rfc6962.OIDExtensionCTPoison) if err != nil { return nil, err } @@ -133,28 +129,20 @@ func BuildPrecertTBS(tbsData []byte, preIssuer *x509.Certificate) ([]byte, error // to that of the preIssuer. var issuerKeyID []byte for _, ext := range preIssuer.Extensions { - if ext.Id.Equal(oidExtensionAuthorityKeyId) { + if ext.Id.Equal(rfc6962.OIDExtAuthorityKeyId) { issuerKeyID = ext.Value break } } - // The x509 package does not parse CT EKU, so look for it in - // extensions directly. - seenCTEKU := false - for _, ext := range preIssuer.Extensions { - if ext.Id.Equal(oidExtensionKeyUsageCertificateTransparency) { - seenCTEKU = true - break - } - } - if !seenCTEKU { + // TODO(phbnf): is this check really necessary? + if !isPreIssuer(preIssuer) { return nil, fmt.Errorf("issuer does not have CertificateTransparency extended key usage") } keyAt := -1 for i, ext := range tbs.Extensions { - if ext.Id.Equal(oidExtensionAuthorityKeyId) { + if ext.Id.Equal(rfc6962.OIDExtAuthorityKeyId) { keyAt = i break } @@ -169,7 +157,7 @@ func BuildPrecertTBS(tbsData []byte, preIssuer *x509.Certificate) ([]byte, error } else if issuerKeyID != nil { // PreCert did not have an auth-key-id, but the preIssuer does, so add it at the end. authKeyIDExt := pkix.Extension{ - Id: oidExtensionAuthorityKeyId, + Id: rfc6962.OIDExtAuthorityKeyId, Critical: false, Value: issuerKeyID, } @@ -198,7 +186,6 @@ func RemoveCTPoison(tbsData []byte) ([]byte, error) { // EntryFromChain generates an Entry from a chain and timestamp. // copied from certificate-transparency-go/serialization.go -// TODO(phboneff): add tests func EntryFromChain(chain []*x509.Certificate, isPrecert bool, timestamp uint64) (*ctonly.Entry, error) { leaf := ctonly.Entry{ IsPrecert: isPrecert, @@ -256,14 +243,35 @@ func EntryFromChain(chain []*x509.Certificate, isPrecert bool, timestamp uint64) return &leaf, nil } -// isPreIssuer indicates whether a certificate is a pre-cert issuer with the specific -// certificate transparency extended key usage. +// isPreIssuer indicates if a certificate is a precertificate signing cert. +// +// From RFC6962 s3.1, these certs should contain: +// (CA:true, Extended Key Usage: Certificate Transparency, OID 1.3.6.1.4.1.11129.2.4.4) func isPreIssuer(cert *x509.Certificate) bool { - // Look for the extension in the Extensions field and not ExtKeyUsage - // since crypto/x509 does not recognize this extension as an ExtKeyUsage. + if !cert.IsCA { + return false + } + // Look for the extension in the Extensions field and not in ExtKeyUsage + // since crypto/x509 does not recognize this extension as such. + // We cannot reliably check in UnknownExtKeyUsage either, since it might + // one day disappear from UnknownExtKeyUsage and make it to ExtKeyUsage. + // Given that ExtKeyUsage does not contain OIDs, we would not be able to + // detect such a move. for _, ext := range cert.Extensions { - if rfc6962.OIDExtKeyUsageCertificateTransparency.Equal(ext.Id) { - return true + if rfc6962.OIDExtKeyUsage.Equal(ext.Id) { + der := cryptobyte.String(ext.Value) + if !der.ReadASN1(&der, cryptobyte_asn1.SEQUENCE) { + continue + } + for !der.Empty() { + var eku asn1.ObjectIdentifier + if !der.ReadASN1ObjectIdentifier(&eku) { + continue + } + if rfc6962.OIDExtKeyUsageCertificateTransparency.Equal(eku) { + return true + } + } } } return false diff --git a/internal/x509util/ct_test.go b/internal/x509util/ct_test.go index fb1867a2..9c5a6b6f 100644 --- a/internal/x509util/ct_test.go +++ b/internal/x509util/ct_test.go @@ -17,6 +17,7 @@ import ( "bytes" "crypto/rand" "crypto/rsa" + "crypto/sha256" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" @@ -27,6 +28,11 @@ import ( "strings" "testing" "time" + + "github.com/transparency-dev/tessera/ctonly" + "github.com/transparency-dev/tesseract/internal/types/rfc6962" + "golang.org/x/crypto/cryptobyte" + cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1" ) var pemPrivateKey = testingKey(` @@ -47,6 +53,24 @@ wg/HcAJWY60xZTJDFN+Qfx8ZQvBEin6c2/h+zZi5IVY= -----END RSA TESTING KEY----- `) +// Use this method to marshal EKUs as Extensions and include them +// in certificate templates. +// Do not use templates ExtKeyUsage or UnknownExtKeyUsage to avoid +// breakages if crypto/x509 ever considers the CT EKU as a ExtKeyUsage +// rather than an UnknownExtKeyUsage. +func ekuExtWithOIDs(ekus []asn1.ObjectIdentifier) pkix.Extension { + bb := []byte{} + b := cryptobyte.NewBuilder(bb) + b.AddASN1(cryptobyte_asn1.SEQUENCE, func(b *cryptobyte.Builder) { + for _, oid := range ekus { + b.AddASN1ObjectIdentifier(oid) + } + }) + return pkix.Extension{Id: rfc6962.OIDExtKeyUsage, Value: b.BytesOrPanic()} +} + +var preIssuerEKUExt = ekuExtWithOIDs([]asn1.ObjectIdentifier{rfc6962.OIDExtKeyUsageCertificateTransparency}) + var testPrivateKey *rsa.PrivateKey func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") } @@ -74,9 +98,8 @@ func makeCert(t *testing.T, template, issuer *x509.Certificate) *x509.Certificat } func TestBuildPrecertTBS(t *testing.T) { - poisonExt := pkix.Extension{Id: oidExtensionCTPoison, Critical: true, Value: asn1.NullBytes} + poisonExt := pkix.Extension{Id: rfc6962.OIDExtensionCTPoison, Critical: true, Value: asn1.NullBytes} // TODO(phboneff): check Critical and value are ok. - ctExt := pkix.Extension{Id: oidExtensionKeyUsageCertificateTransparency} preIssuerKeyID := []byte{0x19, 0x09, 0x19, 0x70} issuerKeyID := []byte{0x07, 0x07, 0x20, 0x07} preCertTemplate := x509.Certificate{ @@ -90,24 +113,28 @@ func TestBuildPrecertTBS(t *testing.T) { AuthorityKeyId: preIssuerKeyID, } preIssuerTemplate := x509.Certificate{ - Version: 3, - SerialNumber: big.NewInt(1234), - Issuer: pkix.Name{CommonName: "real Issuer"}, - Subject: pkix.Name{CommonName: "precert Issuer"}, - NotBefore: time.Now(), - NotAfter: time.Now().Add(3 * time.Hour), - ExtraExtensions: []pkix.Extension{ctExt}, - AuthorityKeyId: issuerKeyID, - SubjectKeyId: preIssuerKeyID, + Version: 3, + SerialNumber: big.NewInt(1234), + Issuer: pkix.Name{CommonName: "real Issuer"}, + Subject: pkix.Name{CommonName: "precert Issuer"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(3 * time.Hour), + ExtraExtensions: []pkix.Extension{preIssuerEKUExt}, + AuthorityKeyId: issuerKeyID, + SubjectKeyId: preIssuerKeyID, + IsCA: true, + BasicConstraintsValid: true, } actualIssuerTemplate := x509.Certificate{ - Version: 3, - SerialNumber: big.NewInt(12345), - Issuer: pkix.Name{CommonName: "real Issuer"}, - Subject: pkix.Name{CommonName: "real Issuer"}, - NotBefore: time.Now(), - NotAfter: time.Now().Add(3 * time.Hour), - SubjectKeyId: issuerKeyID, + Version: 3, + SerialNumber: big.NewInt(12345), + Issuer: pkix.Name{CommonName: "real Issuer"}, + Subject: pkix.Name{CommonName: "real Issuer"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(3 * time.Hour), + SubjectKeyId: issuerKeyID, + IsCA: true, + BasicConstraintsValid: true, } preCertWithAKI := makeCert(t, &preCertTemplate, &preIssuerTemplate) preIssuerWithAKI := makeCert(t, &preIssuerTemplate, &actualIssuerTemplate) @@ -191,7 +218,7 @@ func TestBuildPrecertTBS(t *testing.T) { } var gotAKI []byte for _, ext := range tbs.Extensions { - if ext.Id.Equal(oidExtensionAuthorityKeyId) { + if ext.Id.Equal(rfc6962.OIDExtAuthorityKeyId) { gotAKI = ext.Value break } @@ -210,6 +237,311 @@ func TestBuildPrecertTBS(t *testing.T) { } } +func TestEntryFromChain(t *testing.T) { + // Setup certs + // Issuers + rootTemplate := x509.Certificate{ + Version: 3, + SerialNumber: big.NewInt(1), + Issuer: pkix.Name{CommonName: "root"}, + Subject: pkix.Name{CommonName: "root"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(3 * time.Hour), + IsCA: true, + BasicConstraintsValid: true, + } + rootCert := makeCert(t, &rootTemplate, &rootTemplate) + + intermediateTemplate := x509.Certificate{ + Version: 3, + SerialNumber: big.NewInt(2), + Issuer: rootCert.Subject, + Subject: pkix.Name{CommonName: "intermediate"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(3 * time.Hour), + IsCA: true, + BasicConstraintsValid: true, + } + intermediateCert := makeCert(t, &intermediateTemplate, rootCert) + + poisonExt := pkix.Extension{Id: rfc6962.OIDExtensionCTPoison, Critical: true, Value: asn1.NullBytes} + preIssuerTemplate := x509.Certificate{ + Version: 3, + SerialNumber: big.NewInt(1234), + Issuer: intermediateCert.Subject, + Subject: pkix.Name{CommonName: "precert signing certificate"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(3 * time.Hour), + IsCA: true, + BasicConstraintsValid: true, + ExtraExtensions: []pkix.Extension{preIssuerEKUExt}, + } + preIssuerCert := makeCert(t, &preIssuerTemplate, intermediateCert) + preIssuerKeyHash := sha256.Sum256(preIssuerCert.RawSubjectPublicKeyInfo) + + // Regular chain + certTemplate := x509.Certificate{ + Version: 3, + SerialNumber: big.NewInt(3), + Issuer: intermediateCert.Subject, + Subject: pkix.Name{CommonName: "cert subjet"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(3 * time.Hour), + } + cert := makeCert(t, &certTemplate, intermediateCert) + + // Precert chain with pre-issuer + preCertPreIssuerTemplate := x509.Certificate{ + Version: 3, + SerialNumber: big.NewInt(123), + Issuer: preIssuerCert.Subject, + Subject: pkix.Name{CommonName: "precert subject with pre-issuer"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(3 * time.Hour), + ExtraExtensions: []pkix.Extension{poisonExt}, + } + preCertPreIssuer := makeCert(t, &preCertPreIssuerTemplate, preIssuerCert) + + // Precert without pre-issuer + preCertNoPreIssuerTemplate := x509.Certificate{ + Version: 3, + SerialNumber: big.NewInt(456), + Issuer: intermediateCert.Subject, + Subject: pkix.Name{CommonName: "precert subject no pre-issuer"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(3 * time.Hour), + ExtraExtensions: []pkix.Extension{poisonExt}, + } + preCertNoPreIssuer := makeCert(t, &preCertNoPreIssuerTemplate, intermediateCert) + + timestamp := uint64(time.Now().UnixMilli()) + + defangedTBS, err := BuildPrecertTBS(preCertNoPreIssuer.RawTBSCertificate, nil) + if err != nil { + t.Fatalf("BuildPrecertTBS(no-pre-issuer): %v", err) + } + issuerKeyHash := sha256.Sum256(intermediateCert.RawSubjectPublicKeyInfo) + + defangedTBSWithPreIssuer, err := BuildPrecertTBS(preCertPreIssuer.RawTBSCertificate, preIssuerCert) + if err != nil { + t.Fatalf("BuildPrecertTBS(with-pre-issuer): %v", err) + } + + tests := []struct { + name string + chain []*x509.Certificate + isPrecert bool + wantErr bool + wantEntry *ctonly.Entry + }{ + { + name: "regular-cert", + chain: []*x509.Certificate{cert, intermediateCert, rootCert}, + isPrecert: false, + wantEntry: &ctonly.Entry{ + IsPrecert: false, + Timestamp: timestamp, + Certificate: cert.Raw, + FingerprintsChain: [][32]byte{ + sha256.Sum256(intermediateCert.Raw), + sha256.Sum256(rootCert.Raw), + }, + }, + }, + { + name: "precert-no-pre-issuer", + chain: []*x509.Certificate{preCertNoPreIssuer, intermediateCert, rootCert}, + isPrecert: true, + wantEntry: &ctonly.Entry{ + IsPrecert: true, + Timestamp: timestamp, + Precertificate: preCertNoPreIssuer.Raw, + Certificate: defangedTBS, + IssuerKeyHash: issuerKeyHash[:], + FingerprintsChain: [][32]byte{ + sha256.Sum256(intermediateCert.Raw), + sha256.Sum256(rootCert.Raw), + }, + }, + }, + { + name: "precert-with-pre-issuer", + chain: []*x509.Certificate{preCertPreIssuer, preIssuerCert, intermediateCert, rootCert}, + isPrecert: true, + wantEntry: &ctonly.Entry{ + IsPrecert: true, + Timestamp: timestamp, + Precertificate: preCertPreIssuer.Raw, + Certificate: defangedTBSWithPreIssuer, + IssuerKeyHash: preIssuerKeyHash[:], + FingerprintsChain: [][32]byte{ + sha256.Sum256(preIssuerCert.Raw), + sha256.Sum256(intermediateCert.Raw), + sha256.Sum256(rootCert.Raw), + }, + }, + }, + { + name: "precert-no-issuer", + chain: []*x509.Certificate{preCertPreIssuer}, + isPrecert: true, + wantErr: true, + }, + { + name: "precert-pre-issuer-no-final-issuer", + chain: []*x509.Certificate{preCertPreIssuer, preIssuerCert}, + isPrecert: true, + wantErr: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := EntryFromChain(test.chain, test.isPrecert, timestamp) + if err != nil { + if !test.wantErr { + t.Errorf("EntryFromChain() got error %v, want nil", err) + } + return + } + if test.wantErr { + t.Error("EntryFromChain() got no error, want error") + } + + if !reflect.DeepEqual(got, test.wantEntry) { + t.Errorf("EntryFromChain() got %+v, want %+v", got, test.wantEntry) + } + }) + } +} + +func TestIsPreIssuer(t *testing.T) { + // Create a self-signed issuer for our test certs + issuerTemplate := x509.Certificate{ + Version: 3, + SerialNumber: big.NewInt(1), + Issuer: pkix.Name{CommonName: "issuer"}, + Subject: pkix.Name{CommonName: "issuer"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + BasicConstraintsValid: true, + } + issuerCert := makeCert(t, &issuerTemplate, &issuerTemplate) + + otherEKUsExt := ekuExtWithOIDs([]asn1.ObjectIdentifier{ + asn1.ObjectIdentifier{2, 5, 29, 37, 0}, // anyExtendedKeyUsage + }) + + preIssuerExtEKUWithOthersEKUs := ekuExtWithOIDs([]asn1.ObjectIdentifier{ + asn1.ObjectIdentifier{2, 5, 29, 37, 0}, // anyExtendedKeyUsage + rfc6962.OIDExtKeyUsageCertificateTransparency, + }) + + tests := []struct { + name string + cert *x509.Certificate + want bool + }{ + { + name: "valid", + cert: makeCert(t, &x509.Certificate{ + Version: 3, + SerialNumber: big.NewInt(2), + Issuer: issuerCert.Subject, + Subject: pkix.Name{CommonName: "valid pre-issuer"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + BasicConstraintsValid: true, + ExtraExtensions: []pkix.Extension{preIssuerEKUExt}, + }, issuerCert), + want: true, + }, + { + name: "not-ca", + cert: makeCert(t, &x509.Certificate{ + Version: 3, + SerialNumber: big.NewInt(3), + Issuer: issuerCert.Subject, + Subject: pkix.Name{CommonName: "not a ca"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + IsCA: false, + BasicConstraintsValid: true, + ExtraExtensions: []pkix.Extension{preIssuerEKUExt}, + }, issuerCert), + want: false, + }, + { + name: "no-eku", + cert: makeCert(t, &x509.Certificate{ + Version: 3, + SerialNumber: big.NewInt(4), + Issuer: issuerCert.Subject, + Subject: pkix.Name{CommonName: "no eku"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + BasicConstraintsValid: true, + }, issuerCert), + want: false, + }, + { + name: "invalid-eku", + cert: makeCert(t, &x509.Certificate{ + Version: 3, + SerialNumber: big.NewInt(5), + Issuer: issuerCert.Subject, + Subject: pkix.Name{CommonName: "malformed eku"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + BasicConstraintsValid: true, + ExtraExtensions: []pkix.Extension{otherEKUsExt}, + }, issuerCert), + want: false, + }, + { + name: "valid-with-others", + cert: makeCert(t, &x509.Certificate{ + Version: 3, + SerialNumber: big.NewInt(6), + Issuer: issuerCert.Subject, + Subject: pkix.Name{CommonName: "valid pre-issuer with other ekus"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + BasicConstraintsValid: true, + ExtraExtensions: []pkix.Extension{preIssuerExtEKUWithOthersEKUs}, + }, issuerCert), + want: true, + }, + { + name: "no-is-ca", + cert: makeCert(t, &x509.Certificate{ + Version: 3, + SerialNumber: big.NewInt(7), + Issuer: issuerCert.Subject, + Subject: pkix.Name{CommonName: "regular cert"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + BasicConstraintsValid: true, + ExtraExtensions: []pkix.Extension{preIssuerEKUExt}, + }, issuerCert), + want: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := isPreIssuer(test.cert); got != test.want { + t.Errorf("isPreIssuer() = %v, want %v", got, test.want) + } + }) + } +} + const ( tbsNoPoison = "30820245a003020102020842822a5b866fbfeb300d06092a864886f70d01010b" + "05003071310b3009060355040613024742310f300d060355040813064c6f6e64" +