ca: check correspondence between precertificate and final linting certificate (#6953)
This introduces a small new package, `precert`, with one function `Correspond` that checks a precertificate against a final certificate to see if they correspond in the relationship described in RFC 6962. This also modifies the `issuance` package so that RequestFromPrecert generates an IssuanceRequest that keeps a reference to the precertificate's bytes. The allows `issuance.Prepare` to do a correspondence check when preparing to sign the final certificate. Note in particular that the correspondence check is done against the _linting_ version of the final certificate. This allows us to catch correspondence problems before the real, trusted signature is actually made. Fixes #6945
This commit is contained in:
parent
8dcbc4c92f
commit
f6a005bc25
|
@ -32,6 +32,7 @@ import (
|
|||
"github.com/letsencrypt/boulder/core"
|
||||
"github.com/letsencrypt/boulder/linter"
|
||||
"github.com/letsencrypt/boulder/policyasn1"
|
||||
"github.com/letsencrypt/boulder/precert"
|
||||
"github.com/letsencrypt/boulder/privatekey"
|
||||
"github.com/letsencrypt/pkcs11key/v4"
|
||||
)
|
||||
|
@ -272,11 +273,11 @@ func (p *Profile) requestValid(clk clock.Clock, req *IssuanceRequest) error {
|
|||
return errors.New("ct poison extension cannot be included")
|
||||
}
|
||||
|
||||
if !p.allowSCTList && req.SCTList != nil {
|
||||
if !p.allowSCTList && req.sctList != nil {
|
||||
return errors.New("sct list extension cannot be included")
|
||||
}
|
||||
|
||||
if req.IncludeCTPoison && req.SCTList != nil {
|
||||
if req.IncludeCTPoison && req.sctList != nil {
|
||||
return errors.New("cannot include both ct poison and sct list extensions")
|
||||
}
|
||||
|
||||
|
@ -596,7 +597,14 @@ type IssuanceRequest struct {
|
|||
|
||||
IncludeMustStaple bool
|
||||
IncludeCTPoison bool
|
||||
SCTList []ct.SignedCertificateTimestamp
|
||||
|
||||
// sctList is a list of SCTs to include in a final certificate.
|
||||
// If it is non-empty, PrecertDER must also be non-empty.
|
||||
sctList []ct.SignedCertificateTimestamp
|
||||
// precertDER is the encoded bytes of the precertificate that a
|
||||
// final certificate is expected to correspond to. If it is non-empty,
|
||||
// SCTList must also be non-empty.
|
||||
precertDER []byte
|
||||
}
|
||||
|
||||
// An issuanceToken represents an assertion that Issuer.Lint has generated
|
||||
|
@ -650,12 +658,17 @@ func (i *Issuer) Prepare(req *IssuanceRequest) ([]byte, *issuanceToken, error) {
|
|||
|
||||
if req.IncludeCTPoison {
|
||||
template.ExtraExtensions = append(template.ExtraExtensions, ctPoisonExt)
|
||||
} else if req.SCTList != nil {
|
||||
sctListExt, err := generateSCTListExt(req.SCTList)
|
||||
} else if len(req.sctList) > 0 {
|
||||
if len(req.precertDER) == 0 {
|
||||
return nil, nil, errors.New("inconsistent request contains sctList but no precertDER")
|
||||
}
|
||||
sctListExt, err := generateSCTListExt(req.sctList)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
template.ExtraExtensions = append(template.ExtraExtensions, sctListExt)
|
||||
} else {
|
||||
return nil, nil, errors.New("invalid request contains neither sctList nor precertDER")
|
||||
}
|
||||
|
||||
if req.IncludeMustStaple {
|
||||
|
@ -669,6 +682,13 @@ func (i *Issuer) Prepare(req *IssuanceRequest) ([]byte, *issuanceToken, error) {
|
|||
return nil, nil, fmt.Errorf("tbsCertificate linting failed: %w", err)
|
||||
}
|
||||
|
||||
if len(req.precertDER) > 0 {
|
||||
err = precert.Correspond(req.precertDER, lintCertBytes)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("precert does not correspond to linted final cert: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
token := &issuanceToken{sync.Mutex{}, template, req.PublicKey, i}
|
||||
return lintCertBytes, token, nil
|
||||
}
|
||||
|
@ -728,7 +748,8 @@ func RequestFromPrecert(precert *x509.Certificate, scts []ct.SignedCertificateTi
|
|||
CommonName: precert.Subject.CommonName,
|
||||
DNSNames: precert.DNSNames,
|
||||
IncludeMustStaple: ContainsMustStaple(precert.Extensions),
|
||||
SCTList: scts,
|
||||
sctList: scts,
|
||||
precertDER: precert.Raw,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -220,7 +220,7 @@ func TestRequestValid(t *testing.T) {
|
|||
},
|
||||
request: &IssuanceRequest{
|
||||
PublicKey: &ecdsa.PublicKey{},
|
||||
SCTList: []ct.SignedCertificateTimestamp{},
|
||||
sctList: []ct.SignedCertificateTimestamp{},
|
||||
},
|
||||
expectedError: "sct list extension cannot be included",
|
||||
},
|
||||
|
@ -234,7 +234,7 @@ func TestRequestValid(t *testing.T) {
|
|||
request: &IssuanceRequest{
|
||||
PublicKey: &ecdsa.PublicKey{},
|
||||
IncludeCTPoison: true,
|
||||
SCTList: []ct.SignedCertificateTimestamp{},
|
||||
sctList: []ct.SignedCertificateTimestamp{},
|
||||
},
|
||||
expectedError: "cannot include both ct poison and sct list extensions",
|
||||
},
|
||||
|
@ -553,12 +553,13 @@ func TestIssue(t *testing.T) {
|
|||
pk, err := tc.generateFunc()
|
||||
test.AssertNotError(t, err, "failed to generate test key")
|
||||
lintCertBytes, issuanceToken, err := signer.Prepare(&IssuanceRequest{
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
CommonName: "example.com",
|
||||
DNSNames: []string{"example.com"},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour - time.Second),
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
CommonName: "example.com",
|
||||
DNSNames: []string{"example.com"},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour - time.Second),
|
||||
IncludeCTPoison: true,
|
||||
})
|
||||
test.AssertNotError(t, err, "Prepare failed")
|
||||
_, err = x509.ParseCertificate(lintCertBytes)
|
||||
|
@ -573,7 +574,7 @@ func TestIssue(t *testing.T) {
|
|||
test.AssertEquals(t, cert.Subject.CommonName, "example.com")
|
||||
test.AssertByteEquals(t, cert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8, 9})
|
||||
test.AssertDeepEquals(t, cert.PublicKey, pk.Public())
|
||||
test.AssertEquals(t, len(cert.Extensions), 8) // Constraints, KU, EKU, SKID, AKID, AIA, SAN, Policies
|
||||
test.AssertEquals(t, len(cert.Extensions), 9) // Constraints, KU, EKU, SKID, AKID, AIA, SAN, Policies, Poison
|
||||
test.AssertEquals(t, cert.KeyUsage, tc.ku)
|
||||
})
|
||||
}
|
||||
|
@ -596,13 +597,14 @@ func TestIssueRSA(t *testing.T) {
|
|||
pk, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
test.AssertNotError(t, err, "failed to generate test key")
|
||||
_, issuanceToken, err := signer.Prepare(&IssuanceRequest{
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
DNSNames: []string{"example.com"},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour - time.Second),
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
DNSNames: []string{"example.com"},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour - time.Second),
|
||||
IncludeCTPoison: true,
|
||||
})
|
||||
test.AssertNotError(t, err, "failed to parse lint certificate")
|
||||
test.AssertNotError(t, err, "failed to prepare lint certificate")
|
||||
certBytes, err := signer.Issue(issuanceToken)
|
||||
test.AssertNotError(t, err, "failed to parse certificate")
|
||||
cert, err := x509.ParseCertificate(certBytes)
|
||||
|
@ -611,7 +613,7 @@ func TestIssueRSA(t *testing.T) {
|
|||
test.AssertNotError(t, err, "signature validation failed")
|
||||
test.AssertByteEquals(t, cert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8, 9})
|
||||
test.AssertDeepEquals(t, cert.PublicKey, pk.Public())
|
||||
test.AssertEquals(t, len(cert.Extensions), 8) // Constraints, KU, EKU, SKID, AKID, AIA, SAN, Policies
|
||||
test.AssertEquals(t, len(cert.Extensions), 9) // Constraints, KU, EKU, SKID, AKID, AIA, SAN, Policies, Poison
|
||||
test.AssertEquals(t, cert.KeyUsage, x509.KeyUsageDigitalSignature|x509.KeyUsageKeyEncipherment)
|
||||
}
|
||||
|
||||
|
@ -633,12 +635,13 @@ func TestIssueCommonName(t *testing.T) {
|
|||
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
test.AssertNotError(t, err, "failed to generate test key")
|
||||
ir := &IssuanceRequest{
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
CommonName: "example.com",
|
||||
DNSNames: []string{"example.com", "www.example.com"},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour - time.Second),
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
CommonName: "example.com",
|
||||
DNSNames: []string{"example.com", "www.example.com"},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour - time.Second),
|
||||
IncludeCTPoison: true,
|
||||
}
|
||||
|
||||
_, issuanceToken, err := signer.Prepare(ir)
|
||||
|
@ -701,6 +704,14 @@ func TestIssueCTPoison(t *testing.T) {
|
|||
test.AssertDeepEquals(t, cert.Extensions[8], ctPoisonExt)
|
||||
}
|
||||
|
||||
func mustDecodeB64(b string) []byte {
|
||||
out, err := base64.StdEncoding.DecodeString(b)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestIssueSCTList(t *testing.T) {
|
||||
fc := clock.NewFake()
|
||||
fc.Set(time.Now())
|
||||
|
@ -716,38 +727,49 @@ func TestIssueSCTList(t *testing.T) {
|
|||
test.AssertNotError(t, err, "NewIssuer failed")
|
||||
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
test.AssertNotError(t, err, "failed to generate test key")
|
||||
logID1, err := base64.StdEncoding.DecodeString("OJiMlNA1mMOTLd/pI7q68npCDrlsQeFaqAwasPwEvQM=")
|
||||
test.AssertNotError(t, err, "failed to decode ct log ID")
|
||||
logID2, err := base64.StdEncoding.DecodeString("UtToynGEyMkkXDMQei8Ll54oMwWHI0IieDEKs12/Td4=")
|
||||
test.AssertNotError(t, err, "failed to decode ct log ID")
|
||||
_, issuanceToken, err := signer.Prepare(&IssuanceRequest{
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
DNSNames: []string{"example.com"},
|
||||
SCTList: []ct.SignedCertificateTimestamp{
|
||||
{
|
||||
SCTVersion: ct.V1,
|
||||
LogID: ct.LogID{KeyID: *(*[32]byte)(logID1)},
|
||||
},
|
||||
{
|
||||
SCTVersion: ct.V1,
|
||||
LogID: ct.LogID{KeyID: *(*[32]byte)(logID2)},
|
||||
},
|
||||
},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour - time.Second),
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
DNSNames: []string{"example.com"},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour - time.Second),
|
||||
IncludeCTPoison: true,
|
||||
})
|
||||
test.AssertNotError(t, err, "Prepare failed")
|
||||
certBytes, err := signer.Issue(issuanceToken)
|
||||
precertBytes, err := signer.Issue(issuanceToken)
|
||||
test.AssertNotError(t, err, "Issue failed")
|
||||
cert, err := x509.ParseCertificate(certBytes)
|
||||
precert, err := x509.ParseCertificate(precertBytes)
|
||||
test.AssertNotError(t, err, "failed to parse certificate")
|
||||
err = cert.CheckSignatureFrom(issuerCert.Certificate)
|
||||
|
||||
sctList := []ct.SignedCertificateTimestamp{
|
||||
{
|
||||
SCTVersion: ct.V1,
|
||||
LogID: ct.LogID{KeyID: *(*[32]byte)(mustDecodeB64("OJiMlNA1mMOTLd/pI7q68npCDrlsQeFaqAwasPwEvQM="))},
|
||||
},
|
||||
{
|
||||
SCTVersion: ct.V1,
|
||||
LogID: ct.LogID{KeyID: *(*[32]byte)(mustDecodeB64("UtToynGEyMkkXDMQei8Ll54oMwWHI0IieDEKs12/Td4="))},
|
||||
},
|
||||
}
|
||||
|
||||
request2, err := RequestFromPrecert(precert, sctList)
|
||||
test.AssertNotError(t, err, "generating request from precert")
|
||||
|
||||
_, issuanceToken2, err := signer.Prepare(request2)
|
||||
test.AssertNotError(t, err, "preparing final cert issuance")
|
||||
|
||||
finalCertBytes, err := signer.Issue(issuanceToken2)
|
||||
test.AssertNotError(t, err, "Issue failed")
|
||||
|
||||
finalCert, err := x509.ParseCertificate(finalCertBytes)
|
||||
test.AssertNotError(t, err, "failed to parse certificate")
|
||||
|
||||
err = finalCert.CheckSignatureFrom(issuerCert.Certificate)
|
||||
test.AssertNotError(t, err, "signature validation failed")
|
||||
test.AssertByteEquals(t, cert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8, 9})
|
||||
test.AssertDeepEquals(t, cert.PublicKey, pk.Public())
|
||||
test.AssertEquals(t, len(cert.Extensions), 9) // Constraints, KU, EKU, SKID, AKID, AIA, SAN, Policies, SCT list
|
||||
test.AssertDeepEquals(t, cert.Extensions[8], pkix.Extension{
|
||||
test.AssertByteEquals(t, finalCert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8, 9})
|
||||
test.AssertDeepEquals(t, finalCert.PublicKey, pk.Public())
|
||||
test.AssertEquals(t, len(finalCert.Extensions), 9) // Constraints, KU, EKU, SKID, AKID, AIA, SAN, Policies, SCT list
|
||||
test.AssertDeepEquals(t, finalCert.Extensions[8], pkix.Extension{
|
||||
Id: sctListOID,
|
||||
Value: []byte{
|
||||
4, 100, 0, 98, 0, 47, 0, 56, 152, 140, 148, 208, 53, 152, 195, 147, 45,
|
||||
|
@ -783,6 +805,7 @@ func TestIssueMustStaple(t *testing.T) {
|
|||
IncludeMustStaple: true,
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour - time.Second),
|
||||
IncludeCTPoison: true,
|
||||
})
|
||||
test.AssertNotError(t, err, "Prepare failed")
|
||||
certBytes, err := signer.Issue(issuanceToken)
|
||||
|
@ -793,8 +816,8 @@ func TestIssueMustStaple(t *testing.T) {
|
|||
test.AssertNotError(t, err, "signature validation failed")
|
||||
test.AssertByteEquals(t, cert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8, 9})
|
||||
test.AssertDeepEquals(t, cert.PublicKey, pk.Public())
|
||||
test.AssertEquals(t, len(cert.Extensions), 9) // Constraints, KU, EKU, SKID, AKID, AIA, SAN, Policies, Must-Staple
|
||||
test.AssertDeepEquals(t, cert.Extensions[8], mustStapleExt)
|
||||
test.AssertEquals(t, len(cert.Extensions), 10) // Constraints, KU, EKU, SKID, AKID, AIA, SAN, Policies, Must-Staple, Poison
|
||||
test.AssertDeepEquals(t, cert.Extensions[9], mustStapleExt)
|
||||
}
|
||||
|
||||
func TestIssueBadLint(t *testing.T) {
|
||||
|
@ -807,11 +830,12 @@ func TestIssueBadLint(t *testing.T) {
|
|||
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
test.AssertNotError(t, err, "failed to generate test key")
|
||||
_, _, err = signer.Prepare(&IssuanceRequest{
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
DNSNames: []string{"example.com"},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour - time.Second),
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
DNSNames: []string{"example-com"},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour - time.Second),
|
||||
IncludeCTPoison: true,
|
||||
})
|
||||
test.AssertError(t, err, "Prepare didn't fail")
|
||||
test.AssertErrorIs(t, err, linter.ErrLinting)
|
||||
|
@ -890,11 +914,12 @@ func TestIssuanceToken(t *testing.T) {
|
|||
pk, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
test.AssertNotError(t, err, "failed to generate test key")
|
||||
_, issuanceToken, err := signer.Prepare(&IssuanceRequest{
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
DNSNames: []string{"example.com"},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour - time.Second),
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
DNSNames: []string{"example.com"},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour - time.Second),
|
||||
IncludeCTPoison: true,
|
||||
})
|
||||
test.AssertNotError(t, err, "expected Prepare to succeed")
|
||||
_, err = signer.Issue(issuanceToken)
|
||||
|
@ -905,11 +930,12 @@ func TestIssuanceToken(t *testing.T) {
|
|||
test.AssertContains(t, err.Error(), "issuance token already redeemed")
|
||||
|
||||
_, issuanceToken, err = signer.Prepare(&IssuanceRequest{
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
DNSNames: []string{"example.com"},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour - time.Second),
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
DNSNames: []string{"example.com"},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour - time.Second),
|
||||
IncludeCTPoison: true,
|
||||
})
|
||||
test.AssertNotError(t, err, "expected Prepare to succeed")
|
||||
|
||||
|
@ -920,3 +946,108 @@ func TestIssuanceToken(t *testing.T) {
|
|||
test.AssertError(t, err, "expected redeeming an issuance token with the wrong issuer to fail")
|
||||
test.AssertContains(t, err.Error(), "wrong issuer")
|
||||
}
|
||||
|
||||
func TestInvalidProfile(t *testing.T) {
|
||||
fc := clock.NewFake()
|
||||
fc.Set(time.Now())
|
||||
err := loglist.InitLintList("../test/ct-test-srv/log_list.json")
|
||||
test.AssertNotError(t, err, "failed to load log list")
|
||||
linter, err := linter.New(
|
||||
issuerCert.Certificate,
|
||||
issuerSigner,
|
||||
[]string{},
|
||||
)
|
||||
test.AssertNotError(t, err, "failed to create linter")
|
||||
signer, err := NewIssuer(issuerCert, issuerSigner, defaultProfile(), linter, fc)
|
||||
test.AssertNotError(t, err, "NewIssuer failed")
|
||||
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
test.AssertNotError(t, err, "failed to generate test key")
|
||||
_, _, err = signer.Prepare(&IssuanceRequest{
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
DNSNames: []string{"example.com"},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour - time.Second),
|
||||
IncludeCTPoison: true,
|
||||
precertDER: []byte{6, 6, 6},
|
||||
})
|
||||
test.AssertError(t, err, "Invalid IssuanceRequest")
|
||||
|
||||
_, _, err = signer.Prepare(&IssuanceRequest{
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
DNSNames: []string{"example.com"},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour - time.Second),
|
||||
sctList: []ct.SignedCertificateTimestamp{
|
||||
{
|
||||
SCTVersion: ct.V1,
|
||||
LogID: ct.LogID{KeyID: *(*[32]byte)(mustDecodeB64("OJiMlNA1mMOTLd/pI7q68npCDrlsQeFaqAwasPwEvQM="))},
|
||||
},
|
||||
},
|
||||
precertDER: []byte{},
|
||||
})
|
||||
test.AssertError(t, err, "Invalid IssuanceRequest")
|
||||
}
|
||||
|
||||
// Generate a precert from one profile and a final cert from another, and verify
|
||||
// that the final cert errors out when linted because the lint cert doesn't
|
||||
// corresponding with the precert.
|
||||
func TestMismatchedProfiles(t *testing.T) {
|
||||
fc := clock.NewFake()
|
||||
fc.Set(time.Now())
|
||||
err := loglist.InitLintList("../test/ct-test-srv/log_list.json")
|
||||
test.AssertNotError(t, err, "failed to load log list")
|
||||
linter, err := linter.New(
|
||||
issuerCert.Certificate,
|
||||
issuerSigner,
|
||||
[]string{},
|
||||
)
|
||||
test.AssertNotError(t, err, "failed to create linter")
|
||||
|
||||
issuer1, err := NewIssuer(issuerCert, issuerSigner, defaultProfile(), linter, fc)
|
||||
test.AssertNotError(t, err, "NewIssuer failed")
|
||||
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
test.AssertNotError(t, err, "failed to generate test key")
|
||||
_, issuanceToken, err := issuer1.Prepare(&IssuanceRequest{
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
DNSNames: []string{"example.com"},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour - time.Second),
|
||||
IncludeCTPoison: true,
|
||||
})
|
||||
test.AssertNotError(t, err, "making IssuanceRequest")
|
||||
|
||||
precertDER, err := issuer1.Issue(issuanceToken)
|
||||
test.AssertNotError(t, err, "signing precert")
|
||||
|
||||
// Create a new profile that differs slightly (one more PolicyInformation than the precert)
|
||||
profileConfig := defaultProfileConfig()
|
||||
profileConfig.Policies = append(profileConfig.Policies, PolicyInformation{OID: "1.2.3.4", Qualifiers: nil})
|
||||
p, err := NewProfile(profileConfig, defaultIssuerConfig())
|
||||
test.AssertNotError(t, err, "NewProfile failed")
|
||||
issuer2, err := NewIssuer(issuerCert, issuerSigner, p, linter, fc)
|
||||
test.AssertNotError(t, err, "NewIssuer failed")
|
||||
|
||||
sctList := []ct.SignedCertificateTimestamp{
|
||||
{
|
||||
SCTVersion: ct.V1,
|
||||
LogID: ct.LogID{KeyID: *(*[32]byte)(mustDecodeB64("OJiMlNA1mMOTLd/pI7q68npCDrlsQeFaqAwasPwEvQM="))},
|
||||
},
|
||||
{
|
||||
SCTVersion: ct.V1,
|
||||
LogID: ct.LogID{KeyID: *(*[32]byte)(mustDecodeB64("UtToynGEyMkkXDMQei8Ll54oMwWHI0IieDEKs12/Td4="))},
|
||||
},
|
||||
}
|
||||
|
||||
precert, err := x509.ParseCertificate(precertDER)
|
||||
test.AssertNotError(t, err, "parsing precert")
|
||||
|
||||
request2, err := RequestFromPrecert(precert, sctList)
|
||||
test.AssertNotError(t, err, "RequestFromPrecert")
|
||||
|
||||
_, _, err = issuer2.Prepare(request2)
|
||||
test.AssertError(t, err, "preparing final cert issuance")
|
||||
test.AssertContains(t, err.Error(), "precert does not correspond to linted final cert")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,222 @@
|
|||
package precert
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
encoding_asn1 "encoding/asn1"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
"golang.org/x/crypto/cryptobyte/asn1"
|
||||
)
|
||||
|
||||
// Correspond returns nil if the two certificates are a valid precertificate/final certificate pair.
|
||||
// Order of the arguments matters: the precertificate is first and the final certificate is second.
|
||||
// Note that RFC 6962 allows the precertificate and final certificate to have different Issuers, but
|
||||
// this function rejects such pairs.
|
||||
func Correspond(precertDER, finalDER []byte) error {
|
||||
preTBS, err := tbsDERFromCertDER(precertDER)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing precert: %w", err)
|
||||
}
|
||||
|
||||
finalTBS, err := tbsDERFromCertDER(finalDER)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing final cert: %w", err)
|
||||
}
|
||||
|
||||
// The first 7 fields of TBSCertificate must be byte-for-byte identical.
|
||||
// The next 2 fields (issuerUniqueID and subjectUniqueID) are forbidden
|
||||
// by the Baseline Requirements so we assume they are not present (if they
|
||||
// are, they will fail the next check, for extensions).
|
||||
// https://datatracker.ietf.org/doc/html/rfc5280#page-117
|
||||
// TBSCertificate ::= SEQUENCE {
|
||||
// version [0] Version DEFAULT v1,
|
||||
// serialNumber CertificateSerialNumber,
|
||||
// signature AlgorithmIdentifier,
|
||||
// issuer Name,
|
||||
// validity Validity,
|
||||
// subject Name,
|
||||
// subjectPublicKeyInfo SubjectPublicKeyInfo,
|
||||
// issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
|
||||
// -- If present, version MUST be v2 or v3
|
||||
// subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
|
||||
// -- If present, version MUST be v2 or v3
|
||||
// extensions [3] Extensions OPTIONAL
|
||||
// -- If present, version MUST be v3 -- }
|
||||
for i := 0; i < 7; i++ {
|
||||
if err := readIdenticalElement(&preTBS, &finalTBS); err != nil {
|
||||
return fmt.Errorf("checking for identical field %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// The extensions should be mostly the same, with these exceptions:
|
||||
// - The precertificate should have exactly one precertificate poison extension
|
||||
// not present in the final certificate.
|
||||
// - The final certificate should have exactly one SCTList extension not present
|
||||
// in the precertificate.
|
||||
// - As a consequence, the byte lengths of the extensions fields will not be the
|
||||
// same, so we ignore the lengths (so long as they parse)
|
||||
precertExtensionBytes, err := unwrapExtensions(preTBS)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing precert extensions: %w", err)
|
||||
}
|
||||
|
||||
finalCertExtensionBytes, err := unwrapExtensions(finalTBS)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing final cert extensions: %w", err)
|
||||
}
|
||||
|
||||
precertParser := extensionParser{bytes: precertExtensionBytes, skippableOID: poisonOID}
|
||||
finalCertParser := extensionParser{bytes: finalCertExtensionBytes, skippableOID: sctListOID}
|
||||
|
||||
for i := 0; ; i++ {
|
||||
precertExtn, err := precertParser.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
finalCertExtn, err := finalCertParser.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.Equal(precertExtn, finalCertExtn) {
|
||||
return fmt.Errorf("precert extension %d (%x) not equal to final cert extension %d (%x)",
|
||||
i+precertParser.skipped, precertExtn, i+finalCertParser.skipped, finalCertExtn)
|
||||
}
|
||||
|
||||
if precertExtn == nil && finalCertExtn == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if precertParser.skipped == 0 {
|
||||
return fmt.Errorf("no poison extension found in precert")
|
||||
}
|
||||
if precertParser.skipped > 1 {
|
||||
return fmt.Errorf("multiple poison extensions found in precert")
|
||||
}
|
||||
if finalCertParser.skipped == 0 {
|
||||
return fmt.Errorf("no SCTList extension found in final cert")
|
||||
}
|
||||
if finalCertParser.skipped > 1 {
|
||||
return fmt.Errorf("multiple SCTList extensions found in final cert")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var poisonOID = []int{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3}
|
||||
var sctListOID = []int{1, 3, 6, 1, 4, 1, 11129, 2, 4, 2}
|
||||
|
||||
// extensionParser takes a sequence of bytes representing the inner bytes of the
|
||||
// `extensions` field. Repeated calls to Next() will return all the extensions
|
||||
// except those that match the skippableOID. The skipped extensions will be
|
||||
// counted in `skipped`.
|
||||
type extensionParser struct {
|
||||
skippableOID encoding_asn1.ObjectIdentifier
|
||||
bytes cryptobyte.String
|
||||
skipped int
|
||||
}
|
||||
|
||||
// Next returns the next extension in the sequence, skipping (and counting)
|
||||
// any extension that matches the skippableOID.
|
||||
// Returns nil, nil when there are no more extensions.
|
||||
func (e *extensionParser) Next() (cryptobyte.String, error) {
|
||||
if e.bytes.Empty() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var next cryptobyte.String
|
||||
if !e.bytes.ReadASN1(&next, asn1.SEQUENCE) {
|
||||
return nil, fmt.Errorf("failed to parse extension")
|
||||
}
|
||||
|
||||
var oid encoding_asn1.ObjectIdentifier
|
||||
nextCopy := next
|
||||
if !nextCopy.ReadASN1ObjectIdentifier(&oid) {
|
||||
return nil, fmt.Errorf("failed to parse extension OID")
|
||||
}
|
||||
|
||||
if oid.Equal(e.skippableOID) {
|
||||
e.skipped++
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
return next, nil
|
||||
}
|
||||
|
||||
// unwrapExtensions takes a given a sequence of bytes representing the `extensions` field
|
||||
// of a TBSCertificate and parses away the outermost two layers, returning the inner bytes
|
||||
// of the Extensions SEQUENCE.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc5280#page-117
|
||||
//
|
||||
// TBSCertificate ::= SEQUENCE {
|
||||
// ...
|
||||
// extensions [3] Extensions OPTIONAL
|
||||
// }
|
||||
//
|
||||
// Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension
|
||||
func unwrapExtensions(field cryptobyte.String) (cryptobyte.String, error) {
|
||||
var extensions cryptobyte.String
|
||||
if !field.ReadASN1(&extensions, asn1.Tag(3).Constructed().ContextSpecific()) {
|
||||
return nil, errors.New("error reading extensions")
|
||||
}
|
||||
|
||||
var extensionsInner cryptobyte.String
|
||||
if !extensions.ReadASN1(&extensionsInner, asn1.SEQUENCE) {
|
||||
return nil, errors.New("error reading extensions inner")
|
||||
}
|
||||
|
||||
return extensionsInner, nil
|
||||
}
|
||||
|
||||
// readIdenticalElement parses a single ASN1 element and returns an error if
|
||||
// their tags are different or their contents are different.
|
||||
func readIdenticalElement(a, b *cryptobyte.String) error {
|
||||
var aInner, bInner cryptobyte.String
|
||||
var aTag, bTag asn1.Tag
|
||||
if !a.ReadAnyASN1Element(&aInner, &aTag) {
|
||||
return fmt.Errorf("failed to read element from first input")
|
||||
}
|
||||
if !b.ReadAnyASN1Element(&bInner, &bTag) {
|
||||
return fmt.Errorf("failed to read element from first input")
|
||||
}
|
||||
if aTag != bTag {
|
||||
return fmt.Errorf("tags differ: %d != %d", aTag, bTag)
|
||||
}
|
||||
if !bytes.Equal([]byte(aInner), []byte(bInner)) {
|
||||
return fmt.Errorf("elements differ: %x != %x", aInner, bInner)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tbsDERFromCertDER takes a Certificate object encoded as DER, and parses
|
||||
// away the outermost two SEQUENCEs to get the inner bytes of the TBSCertificate.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc5280#page-116
|
||||
//
|
||||
// Certificate ::= SEQUENCE {
|
||||
// tbsCertificate TBSCertificate,
|
||||
// ...
|
||||
//
|
||||
// TBSCertificate ::= SEQUENCE {
|
||||
// version [0] Version DEFAULT v1,
|
||||
// serialNumber CertificateSerialNumber,
|
||||
// ...
|
||||
func tbsDERFromCertDER(certDER []byte) (cryptobyte.String, error) {
|
||||
var inner cryptobyte.String
|
||||
input := cryptobyte.String(certDER)
|
||||
|
||||
if !input.ReadASN1(&inner, asn1.SEQUENCE) {
|
||||
return nil, fmt.Errorf("failed to read outer sequence")
|
||||
}
|
||||
|
||||
var tbsCertificate cryptobyte.String
|
||||
if !inner.ReadASN1(&tbsCertificate, asn1.SEQUENCE) {
|
||||
return nil, fmt.Errorf("failed to read tbsCertificate")
|
||||
}
|
||||
|
||||
return tbsCertificate, nil
|
||||
}
|
|
@ -0,0 +1,339 @@
|
|||
package precert
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCorrespondIncorrectArgumentOrder(t *testing.T) {
|
||||
pre, final, err := readPair("testdata/good/precert.pem", "testdata/good/final.pem")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// The final cert is in the precert position and vice versa.
|
||||
err = Correspond(final, pre)
|
||||
if err == nil {
|
||||
t.Errorf("expected failure when final and precertificates were in wrong order, got success")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorrespondGood(t *testing.T) {
|
||||
pre, final, err := readPair("testdata/good/precert.pem", "testdata/good/final.pem")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = Correspond(pre, final)
|
||||
if err != nil {
|
||||
t.Errorf("expected testdata/good/ certs to correspond, got %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorrespondBad(t *testing.T) {
|
||||
pre, final, err := readPair("testdata/bad/precert.pem", "testdata/bad/final.pem")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = Correspond(pre, final)
|
||||
if err == nil {
|
||||
t.Errorf("expected testdata/bad/ certs to not correspond, got nil error")
|
||||
}
|
||||
expected := "precert extension 7 (0603551d20040c300a3008060667810c010201) not equal to final cert extension 7 (0603551d20044530433008060667810c0102013037060b2b0601040182df130101013028302606082b06010505070201161a687474703a2f2f6370732e6c657473656e63727970742e6f7267)"
|
||||
if !strings.Contains(err.Error(), expected) {
|
||||
t.Errorf("expected error to contain %q, got %q", expected, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorrespondCompleteMismatch(t *testing.T) {
|
||||
pre, final, err := readPair("testdata/good/precert.pem", "testdata/bad/final.pem")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = Correspond(pre, final)
|
||||
if err == nil {
|
||||
t.Errorf("expected testdata/good and testdata/bad/ certs to not correspond, got nil error")
|
||||
}
|
||||
expected := "checking for identical field 1: elements differ: 021203d91c3d22b404f20df3c1631c22e1754b8d != 021203e2267b786b7e338317ddd62e764fcb3c71"
|
||||
if !strings.Contains(err.Error(), expected) {
|
||||
t.Errorf("expected error to contain %q, got %q", expected, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func readPair(a, b string) ([]byte, []byte, error) {
|
||||
aDER, err := derFromPEMFile(a)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
bDER, err := derFromPEMFile(b)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return aDER, bDER, nil
|
||||
}
|
||||
|
||||
// derFromPEMFile reads a PEM file and returns the DER-encoded bytes.
|
||||
func derFromPEMFile(filename string) ([]byte, error) {
|
||||
precertPEM, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading %s: %w", filename, err)
|
||||
}
|
||||
|
||||
precertPEMBlock, _ := pem.Decode(precertPEM)
|
||||
if precertPEMBlock == nil {
|
||||
return nil, fmt.Errorf("error PEM decoding %s", filename)
|
||||
}
|
||||
|
||||
return precertPEMBlock.Bytes, nil
|
||||
}
|
||||
|
||||
func TestMismatches(t *testing.T) {
|
||||
issuerKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// A separate issuer key, used for signing the final certificate, but
|
||||
// using the same simulated issuer certificate.
|
||||
untrustedIssuerKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
subscriberKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// By reading the crypto/x509 code, we know that Subject is the only field
|
||||
// of the issuer certificate that we need to care about for the purposes
|
||||
// of signing below.
|
||||
issuer := x509.Certificate{
|
||||
Subject: pkix.Name{
|
||||
CommonName: "Some Issuer",
|
||||
},
|
||||
}
|
||||
|
||||
precertTemplate := x509.Certificate{
|
||||
SerialNumber: big.NewInt(3141592653589793238),
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(24 * time.Hour),
|
||||
DNSNames: []string{"example.com"},
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{
|
||||
Id: poisonOID,
|
||||
Value: []byte{0x5, 0x0},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
precertDER, err := x509.CreateCertificate(rand.Reader, &precertTemplate, &issuer, &subscriberKey.PublicKey, issuerKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Sign a final certificate with the untrustedIssuerKey, first applying the
|
||||
// given modify function to the default template. Return the DER encoded bytes.
|
||||
makeFinalCert := func(modify func(c *x509.Certificate)) []byte {
|
||||
t.Helper()
|
||||
finalCertTemplate := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(3141592653589793238),
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(24 * time.Hour),
|
||||
DNSNames: []string{"example.com"},
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{
|
||||
Id: sctListOID,
|
||||
Value: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
modify(finalCertTemplate)
|
||||
|
||||
finalCertDER, err := x509.CreateCertificate(rand.Reader, finalCertTemplate,
|
||||
&issuer, &subscriberKey.PublicKey, untrustedIssuerKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return finalCertDER
|
||||
}
|
||||
|
||||
// Expect success with a matching precert and final cert
|
||||
finalCertDER := makeFinalCert(func(c *x509.Certificate) {})
|
||||
err = Correspond(precertDER, finalCertDER)
|
||||
if err != nil {
|
||||
t.Errorf("expected precert and final cert to correspond, got: %s", err)
|
||||
}
|
||||
|
||||
// Set up a precert / final cert pair where the SCTList and poison extensions are
|
||||
// not in the same position
|
||||
precertTemplate2 := x509.Certificate{
|
||||
SerialNumber: big.NewInt(3141592653589793238),
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(24 * time.Hour),
|
||||
DNSNames: []string{"example.com"},
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{
|
||||
Id: poisonOID,
|
||||
Value: []byte{0x5, 0x0},
|
||||
},
|
||||
// Arbitrary extension to make poisonOID not be the last extension
|
||||
{
|
||||
Id: []int{1, 2, 3, 4},
|
||||
Value: []byte{0x5, 0x0},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
precertDER2, err := x509.CreateCertificate(rand.Reader, &precertTemplate2, &issuer, &subscriberKey.PublicKey, issuerKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
finalCertDER = makeFinalCert(func(c *x509.Certificate) {
|
||||
c.ExtraExtensions = []pkix.Extension{
|
||||
{
|
||||
Id: []int{1, 2, 3, 4},
|
||||
Value: []byte{0x5, 0x0},
|
||||
},
|
||||
{
|
||||
Id: sctListOID,
|
||||
Value: nil,
|
||||
},
|
||||
}
|
||||
})
|
||||
err = Correspond(precertDER2, finalCertDER)
|
||||
if err != nil {
|
||||
t.Errorf("expected precert and final cert to correspond with differently positioned extensions, got: %s", err)
|
||||
}
|
||||
|
||||
// Expect failure with a mismatched Issuer
|
||||
issuer = x509.Certificate{
|
||||
Subject: pkix.Name{
|
||||
CommonName: "Some Other Issuer",
|
||||
},
|
||||
}
|
||||
|
||||
finalCertDER = makeFinalCert(func(c *x509.Certificate) {})
|
||||
err = Correspond(precertDER, finalCertDER)
|
||||
if err == nil {
|
||||
t.Errorf("expected error for mismatched issuer, got nil error")
|
||||
}
|
||||
|
||||
// Restore original issuer
|
||||
issuer = x509.Certificate{
|
||||
Subject: pkix.Name{
|
||||
CommonName: "Some Issuer",
|
||||
},
|
||||
}
|
||||
|
||||
// Expect failure with a mismatched Serial
|
||||
finalCertDER = makeFinalCert(func(c *x509.Certificate) {
|
||||
c.SerialNumber = big.NewInt(2718281828459045)
|
||||
})
|
||||
err = Correspond(precertDER, finalCertDER)
|
||||
if err == nil {
|
||||
t.Errorf("expected error for mismatched serial, got nil error")
|
||||
}
|
||||
|
||||
// Expect failure with mismatched names
|
||||
finalCertDER = makeFinalCert(func(c *x509.Certificate) {
|
||||
c.DNSNames = []string{"example.com", "www.example.com"}
|
||||
})
|
||||
|
||||
err = Correspond(precertDER, finalCertDER)
|
||||
if err == nil {
|
||||
t.Errorf("expected error for mismatched names, got nil error")
|
||||
}
|
||||
|
||||
// Expect failure with mismatched NotBefore
|
||||
finalCertDER = makeFinalCert(func(c *x509.Certificate) {
|
||||
c.NotBefore = time.Now().Add(24 * time.Hour)
|
||||
})
|
||||
|
||||
err = Correspond(precertDER, finalCertDER)
|
||||
if err == nil {
|
||||
t.Errorf("expected error for mismatched NotBefore, got nil error")
|
||||
}
|
||||
|
||||
// Expect failure with mismatched NotAfter
|
||||
finalCertDER = makeFinalCert(func(c *x509.Certificate) {
|
||||
c.NotAfter = time.Now().Add(48 * time.Hour)
|
||||
})
|
||||
err = Correspond(precertDER, finalCertDER)
|
||||
if err == nil {
|
||||
t.Errorf("expected error for mismatched NotAfter, got nil error")
|
||||
}
|
||||
|
||||
// Expect failure for mismatched extensions
|
||||
finalCertDER = makeFinalCert(func(c *x509.Certificate) {
|
||||
c.ExtraExtensions = append(c.ExtraExtensions, pkix.Extension{
|
||||
Critical: true,
|
||||
Id: []int{1, 2, 3},
|
||||
Value: []byte("hello"),
|
||||
})
|
||||
})
|
||||
|
||||
err = Correspond(precertDER, finalCertDER)
|
||||
if err == nil {
|
||||
t.Errorf("expected error for mismatched extensions, got nil error")
|
||||
}
|
||||
expectedError := "precert extension 2 () not equal to final cert extension 2 (06022a030101ff040568656c6c6f)"
|
||||
if err.Error() != expectedError {
|
||||
t.Errorf("expected error %q, got %q", expectedError, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnwrapExtensions(t *testing.T) {
|
||||
validExtensionsOuter := []byte{0xA3, 0x3, 0x30, 0x1, 0x0}
|
||||
_, err := unwrapExtensions(validExtensionsOuter)
|
||||
if err != nil {
|
||||
t.Errorf("expected success for validExtensionsOuter, got %s", err)
|
||||
}
|
||||
|
||||
invalidExtensionsOuter := []byte{0xA3, 0x99, 0x30, 0x1, 0x0}
|
||||
_, err = unwrapExtensions(invalidExtensionsOuter)
|
||||
if err == nil {
|
||||
t.Error("expected error for invalidExtensionsOuter, got none")
|
||||
}
|
||||
|
||||
invalidExtensionsInner := []byte{0xA3, 0x3, 0x30, 0x99, 0x0}
|
||||
_, err = unwrapExtensions(invalidExtensionsInner)
|
||||
if err == nil {
|
||||
t.Error("expected error for invalidExtensionsInner, got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTBSFromCertDER(t *testing.T) {
|
||||
validCertOuter := []byte{0x30, 0x3, 0x30, 0x1, 0x0}
|
||||
_, err := tbsDERFromCertDER(validCertOuter)
|
||||
if err != nil {
|
||||
t.Errorf("expected success for validCertOuter, got %s", err)
|
||||
}
|
||||
|
||||
invalidCertOuter := []byte{0x30, 0x99, 0x30, 0x1, 0x0}
|
||||
_, err = tbsDERFromCertDER(invalidCertOuter)
|
||||
if err == nil {
|
||||
t.Error("expected error for invalidCertOuter, got none")
|
||||
}
|
||||
|
||||
invalidCertInner := []byte{0x30, 0x3, 0x30, 0x99, 0x0}
|
||||
_, err = tbsDERFromCertDER(invalidCertInner)
|
||||
if err == nil {
|
||||
t.Error("expected error for invalidExtensionsInner, got none")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
The data in this directory consists of real certificates issued by Let's
|
||||
Encrypt in 2023. The ones under the `bad` directory were issued during
|
||||
the Duplicate Serial Numbers incident (https://bugzilla.mozilla.org/show_bug.cgi?id=1838667)
|
||||
and differ in the presence / absence of a second policyIdentifier in the
|
||||
Certificate Policies extension.
|
||||
|
||||
The ones under the `good` directory were issued shortly after recovery
|
||||
from the incident and represent a correct correspondence relationship.
|
|
@ -0,0 +1,36 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIGRjCCBS6gAwIBAgISA+Ime3hrfjODF93WLnZPyzxxMA0GCSqGSIb3DQEBCwUA
|
||||
MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD
|
||||
EwJSMzAeFw0yMzA2MTUxNDM2MTZaFw0yMzA5MTMxNDM2MTVaMB4xHDAaBgNVBAMM
|
||||
EyouN2FjbnIubW9uZ29kYi5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
|
||||
AoICAQCjLiLXI/mTBSEkSKVucC3NcnXGu/M2qwLIk1uenifnoNMmdJmEyp+oWFUS
|
||||
n9rIXtHw27YTlJLRRYLSIzqqujDV5PmXzFrSJ/9JrgIbNUowaVF3j9bf1+NPENEH
|
||||
81RnNGevtKUN5NoEo3fAmZaMWrGjWioNnpIsegSjvvuHeqMqC7SNrGSvtKLBiPkO
|
||||
bL5oScPYj/cHzt3RYJ17ru6xWgUDV6aqvEblrxcXvPmd/1SxB3Vkdkc+bCuSLSNM
|
||||
/NmcET0YUhWizanjodJarpYJRuW1SjGmPda0jBAQZQDPmZHCEgwTBcCEIg5J3XzA
|
||||
fFUZPPlTVgE+7Mbjd/DK7iz46D0uHOigVTZto3lPYRdRiyVFNUMAN0GLAlkaJ7Td
|
||||
0FnAxvhE74lSjI7lFqDNtiyA8ovp/JbKfPmnvfH+fQa7vEFbR5H9v4UZt0XLeI6W
|
||||
dV4pYoCwuK5mfr0NQLCy/015OAU8WF4MLM+Fyt+GG+sOk2Maz6ysAShMOvdNH7B3
|
||||
GSn65xBVgBxlPWyYpodW9SS1NSVgrgbKMg0yHzx/PdosQehyh9p6OpuTaeEi2iQg
|
||||
yTODKGHX+cmjzUx0iCG2ByC9bvMo32eZXiC+itZCaHb0FGXh+K7UcOCsvsi7NLGR
|
||||
ngVKK7u7gZmPu4UkVUBpF3jz/OK3OsudHcflZIGd6nf8w4lp0wIDAQABo4ICaDCC
|
||||
AmQwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
|
||||
AjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBREcOX3VXl7+uM7aqTQ/coniJsAAjAf
|
||||
BgNVHSMEGDAWgBQULrMXt1hWy65QCUDmH6+dixTCxjBVBggrBgEFBQcBAQRJMEcw
|
||||
IQYIKwYBBQUHMAGGFWh0dHA6Ly9yMy5vLmxlbmNyLm9yZzAiBggrBgEFBQcwAoYW
|
||||
aHR0cDovL3IzLmkubGVuY3Iub3JnLzA4BgNVHREEMTAvghgqLjdhY25yLm1lc2gu
|
||||
bW9uZ29kYi5uZXSCEyouN2FjbnIubW9uZ29kYi5uZXQwTAYDVR0gBEUwQzAIBgZn
|
||||
gQwBAgEwNwYLKwYBBAGC3xMBAQEwKDAmBggrBgEFBQcCARYaaHR0cDovL2Nwcy5s
|
||||
ZXRzZW5jcnlwdC5vcmcwggEEBgorBgEEAdZ5AgQCBIH1BIHyAPAAdgC3Pvsk35xN
|
||||
unXyOcW6WPRsXfxCz3qfNcSeHQmBJe20mQAAAYi/s0QZAAAEAwBHMEUCID4vc7PN
|
||||
WNauTkmkS7CqSwdiyOV+LYIT9g8KygWW4atTAiEA6Re4Cz7BsEMi+/U8G+r9Lmqb
|
||||
qwGXGS4mXG7RiEfeQEcAdgB6MoxU2LcttiDqOOBSHumEFnAyE4VNO9IrwTpXo1Lr
|
||||
UgAAAYi/s0RQAAAEAwBHMEUCIQD95SqDycwXGZ+JKBUVBR+hBxn4BRIQ7EPIaMTI
|
||||
/+854gIgDpJm5BFX9vKUf5tKWn9f/Fagktt5J6hPnrmURSV/egAwDQYJKoZIhvcN
|
||||
AQELBQADggEBAKWyDSRmiM9N+2AhYgRuzh3JnxtvhmEXUBEgwuFnlQyCm5ZvScvW
|
||||
Kmw2sqcj+gI2UNUxmWjq3PbIVBrTLDEgXtVN+JU6HwC4TdYPIB4LzfrWsGY7cc2a
|
||||
aY76YbWlwEyhN9niQLijZORKhZ6HLM7MI76FM7oJ9eZmvnfypjJ7E0J9ek/y7S1w
|
||||
qg5EM+QiAf03YcjSxUCyL3/+EzlYRz65diLh7Eb6gBd58rWLOa1nbgTOFsToAkBE
|
||||
7qR3HymfWysxApDN8x95jDzubbkqiyuk3dvzjn3oouN1H8NsG/xYrYmMMwnJ8xul
|
||||
1AJ31ZMxJ9hr29G122DSEaX9smAyyzWhAwM=
|
||||
-----END CERTIFICATE-----
|
|
@ -0,0 +1,30 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIFGjCCBAKgAwIBAgISA+Ime3hrfjODF93WLnZPyzxxMA0GCSqGSIb3DQEBCwUA
|
||||
MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD
|
||||
EwJSMzAeFw0yMzA2MTUxNDM2MTZaFw0yMzA5MTMxNDM2MTVaMB4xHDAaBgNVBAMM
|
||||
EyouN2FjbnIubW9uZ29kYi5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
|
||||
AoICAQCjLiLXI/mTBSEkSKVucC3NcnXGu/M2qwLIk1uenifnoNMmdJmEyp+oWFUS
|
||||
n9rIXtHw27YTlJLRRYLSIzqqujDV5PmXzFrSJ/9JrgIbNUowaVF3j9bf1+NPENEH
|
||||
81RnNGevtKUN5NoEo3fAmZaMWrGjWioNnpIsegSjvvuHeqMqC7SNrGSvtKLBiPkO
|
||||
bL5oScPYj/cHzt3RYJ17ru6xWgUDV6aqvEblrxcXvPmd/1SxB3Vkdkc+bCuSLSNM
|
||||
/NmcET0YUhWizanjodJarpYJRuW1SjGmPda0jBAQZQDPmZHCEgwTBcCEIg5J3XzA
|
||||
fFUZPPlTVgE+7Mbjd/DK7iz46D0uHOigVTZto3lPYRdRiyVFNUMAN0GLAlkaJ7Td
|
||||
0FnAxvhE74lSjI7lFqDNtiyA8ovp/JbKfPmnvfH+fQa7vEFbR5H9v4UZt0XLeI6W
|
||||
dV4pYoCwuK5mfr0NQLCy/015OAU8WF4MLM+Fyt+GG+sOk2Maz6ysAShMOvdNH7B3
|
||||
GSn65xBVgBxlPWyYpodW9SS1NSVgrgbKMg0yHzx/PdosQehyh9p6OpuTaeEi2iQg
|
||||
yTODKGHX+cmjzUx0iCG2ByC9bvMo32eZXiC+itZCaHb0FGXh+K7UcOCsvsi7NLGR
|
||||
ngVKK7u7gZmPu4UkVUBpF3jz/OK3OsudHcflZIGd6nf8w4lp0wIDAQABo4IBPDCC
|
||||
ATgwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
|
||||
AjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBREcOX3VXl7+uM7aqTQ/coniJsAAjAf
|
||||
BgNVHSMEGDAWgBQULrMXt1hWy65QCUDmH6+dixTCxjBVBggrBgEFBQcBAQRJMEcw
|
||||
IQYIKwYBBQUHMAGGFWh0dHA6Ly9yMy5vLmxlbmNyLm9yZzAiBggrBgEFBQcwAoYW
|
||||
aHR0cDovL3IzLmkubGVuY3Iub3JnLzA4BgNVHREEMTAvghgqLjdhY25yLm1lc2gu
|
||||
bW9uZ29kYi5uZXSCEyouN2FjbnIubW9uZ29kYi5uZXQwEwYDVR0gBAwwCjAIBgZn
|
||||
gQwBAgEwEwYKKwYBBAHWeQIEAwEB/wQCBQAwDQYJKoZIhvcNAQELBQADggEBALIU
|
||||
rHns6TWfT/kfJ60D9R1Ek4YGB/jVsrh2d3uiIU2hiRBBjgDkCLyKd7oXM761uXX3
|
||||
LL4H4JPegqTrZAPO88tUtzBSb3IF4yA0o1NWhE6ceLnBk9fl5TRCC8QASliApsOi
|
||||
gDgRi1VFmyFOHpHnVZdbpPucy6T+CdKXKfj4iNw+aOZcoQxJ70XECXxQbdqJ7VdY
|
||||
f0B+wtk5HZU8cuVVCj1i/iDv1zqITCzaavbz870QugiHO/8rj2ctrA07SX3Ovs4J
|
||||
GbCGuMzlpxeIFtQDWVufVbu1ZZltzPlSHFqv6mPKW9stYtt8JCjmPwNW6UdrlBtN
|
||||
gvFgkgDpz+Q6/Vu+u7g=
|
||||
-----END CERTIFICATE-----
|
|
@ -0,0 +1,24 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIE/TCCA+WgAwIBAgISA9kcPSK0BPIN88FjHCLhdUuNMA0GCSqGSIb3DQEBCwUAMDIxCzAJBgNVBAYT
|
||||
AlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJSMzAeFw0yMzA2MTUxNTAxNDRaFw0y
|
||||
MzA5MTMxNTAxNDNaMCIxIDAeBgNVBAMTF2hvdXNldHJhaW5pbmdwdXBweS5pbmZvMIIBIjANBgkqhkiG
|
||||
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr/XUbBzyFKRMJ0vYSpqw4Wy2Y2eV+vSCix5TcGNxTR9tB9EX+hNd
|
||||
C7/zlKJAGUj9ZTSfbJO27HvleVN3D5idhIFxfP2tdfAp4OxQkf4a4nqKXZzPJpTlDs2LQNjKcwszaxKY
|
||||
CMzGThieeBm7jUiWL6fuAX+sCsBIO0frJ9klq77f7NplfwJ3FcKWFyvMo71rtFZCoLt7dfgKim+SBGYn
|
||||
agfNe8mmxy4ipqvWtGzMO3cdcKdiRijMzZG1upRjhoggHI/vS2JkWP4bNoZdGCAvaxriEoBdS5K9LqHQ
|
||||
P6GurVXM5B3kuJkMBN+OmnrXxvcnWbYY6JwAO3KZ1+Vbi2ryPQIDAQABo4ICGzCCAhcwDgYDVR0PAQH/
|
||||
BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQW
|
||||
BBQmE8zNXgf+dOmQ3kFb3p4xfznLjTAfBgNVHSMEGDAWgBQULrMXt1hWy65QCUDmH6+dixTCxjBVBggr
|
||||
BgEFBQcBAQRJMEcwIQYIKwYBBQUHMAGGFWh0dHA6Ly9yMy5vLmxlbmNyLm9yZzAiBggrBgEFBQcwAoYW
|
||||
aHR0cDovL3IzLmkubGVuY3Iub3JnLzAiBgNVHREEGzAZghdob3VzZXRyYWluaW5ncHVwcHkuaW5mbzAT
|
||||
BgNVHSAEDDAKMAgGBmeBDAECATCCAQYGCisGAQQB1nkCBAIEgfcEgfQA8gB3AHoyjFTYty22IOo44FIe
|
||||
6YQWcDIThU070ivBOlejUutSAAABiL/Kk3wAAAQDAEgwRgIhAN//jI1iByfobY0b+JXWFhc5zQpKC+mI
|
||||
qXIWrWlXPgrqAiEAiArpAl0FCxvy5vv/C/t+ZOFh0OTxMc2w9rj0GlAhPrAAdwDoPtDaPvUGNTLnVyi8
|
||||
iWvJA9PL0RFr7Otp4Xd9bQa9bgAAAYi/ypP1AAAEAwBIMEYCIQC7XKe+yYzkIeu/294qGrQB/G4I8+hz
|
||||
//3HJVWFam+6KQIhAMy2iY3IITazdGhmQXGQAUPSzXt2wtm1PGHPmyNmIQnXMA0GCSqGSIb3DQEBCwUA
|
||||
A4IBAQBtrtoi4zea7CnswZc/1Ql3aV0j7nblq4gXxiMoHdoq1srZbypnqvDIFaEp5BjSccEc0D0jK4u2
|
||||
nwnFzIljjRi/HXoTBJBHKIxX/s9G/tWFgfnrRSonyN1mguyi7avfWLELrl+Or2+h1K4LZIasrlN8oJpu
|
||||
a4msgl8HXRdla9Kej7x6fYgyBOJEAcb82i7Ur4bM5OGKZObePHGK6NDsTcpdmqBAjAuKLYMtpHXpFo4/
|
||||
14X2A027hOdDBFkeNcRF2KZsbSvp78qIZsSYtjEyYBlTPWLh/aoXx2sc2vl43VaLYOlEIfuzrEKCTiqr
|
||||
D3TU5CmThOuzm/H0HeCmtlNuQlzK
|
||||
-----END CERTIFICATE-----
|
|
@ -0,0 +1,20 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIECDCCAvCgAwIBAgISA9kcPSK0BPIN88FjHCLhdUuNMA0GCSqGSIb3DQEBCwUAMDIxCzAJBgNVBAYT
|
||||
AlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJSMzAeFw0yMzA2MTUxNTAxNDRaFw0y
|
||||
MzA5MTMxNTAxNDNaMCIxIDAeBgNVBAMTF2hvdXNldHJhaW5pbmdwdXBweS5pbmZvMIIBIjANBgkqhkiG
|
||||
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr/XUbBzyFKRMJ0vYSpqw4Wy2Y2eV+vSCix5TcGNxTR9tB9EX+hNd
|
||||
C7/zlKJAGUj9ZTSfbJO27HvleVN3D5idhIFxfP2tdfAp4OxQkf4a4nqKXZzPJpTlDs2LQNjKcwszaxKY
|
||||
CMzGThieeBm7jUiWL6fuAX+sCsBIO0frJ9klq77f7NplfwJ3FcKWFyvMo71rtFZCoLt7dfgKim+SBGYn
|
||||
agfNe8mmxy4ipqvWtGzMO3cdcKdiRijMzZG1upRjhoggHI/vS2JkWP4bNoZdGCAvaxriEoBdS5K9LqHQ
|
||||
P6GurVXM5B3kuJkMBN+OmnrXxvcnWbYY6JwAO3KZ1+Vbi2ryPQIDAQABo4IBJjCCASIwDgYDVR0PAQH/
|
||||
BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQW
|
||||
BBQmE8zNXgf+dOmQ3kFb3p4xfznLjTAfBgNVHSMEGDAWgBQULrMXt1hWy65QCUDmH6+dixTCxjBVBggr
|
||||
BgEFBQcBAQRJMEcwIQYIKwYBBQUHMAGGFWh0dHA6Ly9yMy5vLmxlbmNyLm9yZzAiBggrBgEFBQcwAoYW
|
||||
aHR0cDovL3IzLmkubGVuY3Iub3JnLzAiBgNVHREEGzAZghdob3VzZXRyYWluaW5ncHVwcHkuaW5mbzAT
|
||||
BgNVHSAEDDAKMAgGBmeBDAECATATBgorBgEEAdZ5AgQDAQH/BAIFADANBgkqhkiG9w0BAQsFAAOCAQEA
|
||||
n8r5gDWJjoEEE9+hmk/61EleSVQA9SslR7deQnCrItdSOZQo877FJfWtfoRZNItcOfml9E7uYjXhzEOc
|
||||
bVRe9+VbBt1jjUUu3xLLM7RA5+2pvb+cN1LJ2ijIsnkJwSgYhudGPx+1EgKEJ2huKQTVXqu8AT6rp9Tr
|
||||
vs/3gXzqlVncXcfEb+5PjvcibCugdt9pE5BfRYBP5V2GcwOQs3zr2DShPuSPmXiLSoUxVczltfndPfM+
|
||||
WYaj5VOkvW5UNsm+IVPRlEcbHGmHwEHkBeBGHn4kvgv/14fKpEClkZ+VxgnRky6x951NDMVEJLdV9Vbs
|
||||
G04Vh0wRjRyiuTPyT5Zj3g==
|
||||
-----END CERTIFICATE-----
|
Loading…
Reference in New Issue