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:
Jacob Hoffman-Andrews 2023-06-26 15:35:06 -07:00 committed by GitHub
parent 8dcbc4c92f
commit f6a005bc25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 902 additions and 71 deletions

View File

@ -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
}

View File

@ -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")
}

222
precert/corr.go Normal file
View File

@ -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
}

339
precert/corr_test.go Normal file
View File

@ -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")
}
}

8
precert/testdata/README.md vendored Normal file
View File

@ -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.

36
precert/testdata/bad/final.pem vendored Normal file
View File

@ -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-----

30
precert/testdata/bad/precert.pem vendored Normal file
View File

@ -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-----

24
precert/testdata/good/final.pem vendored Normal file
View File

@ -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-----

20
precert/testdata/good/precert.pem vendored Normal file
View File

@ -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-----