Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
45fdbfb
Factor out GenerateSKID for use across multiple packages
aarongable Jul 17, 2026
1895147
Add infrastructure to configure lints with issuer and pre-existing cert
aarongable Jul 18, 2026
a753a99
Add CP/CPS profile lints
aarongable Jul 18, 2026
2b658da
Add SKID test vector
aarongable Jul 22, 2026
333527d
Update validity periods
aarongable Jul 22, 2026
a0d755a
Simplify Issuer/SubjectUniqueId checks
aarongable Jul 22, 2026
a58b193
Add negative validity checks
aarongable Jul 22, 2026
c922353
Add tighter checks for http URIs
aarongable Jul 22, 2026
34534dc
Restore old lints with added IneffectiveDates
aarongable Jul 22, 2026
dec0d23
Add checks for Section 6.1.6 key quality
aarongable Jul 22, 2026
f33920e
Update checks for serial length
aarongable Jul 24, 2026
c29ea74
Rewrite comments to be mechanically verifiable
aarongable Jul 24, 2026
085dcaa
Review comments
aarongable Jul 24, 2026
e009c95
Merge branch main into cps-lints
aarongable Jul 24, 2026
eb93aea
Merge branch main into cps-lints
aarongable Jul 28, 2026
c87bf39
Move GenerateSKID into Core for sharing across ca, ceremony, and lints
aarongable Jul 28, 2026
06a2433
Create scaffolding for configuring lints with issuers
aarongable Jul 28, 2026
3655bad
Fix go.mod
aarongable Jul 29, 2026
5ba49a4
Merge branch configurable-lints into cps-lints
aarongable Jul 29, 2026
5e66c94
Fix accidental removal of cpcps import
aarongable Jul 29, 2026
cd88aee
Review comments
aarongable Jul 29, 2026
8586435
Add meta-lint to ensure each cert matches exactly one lint
aarongable Jul 29, 2026
5dfa0ae
Improve cross-cert subject public key checks
aarongable Jul 29, 2026
7c5bba2
Check for pathlen on roots
aarongable Jul 29, 2026
b37e302
Add hacky check for self-signed certs' SKID
aarongable Jul 29, 2026
fa749d9
Replace custom OIDs with zlint's existing OIDs
aarongable Jul 29, 2026
7132b86
Move speculative effective date into the past so the new lints run
aarongable Jul 29, 2026
7766eb6
cert-checker: configure issuers for CP/CPS lints
aarongable Jul 29, 2026
79cd79c
Review comments
aarongable Jul 30, 2026
149bff3
Merge branch cert-checker-lints into cps-lints
aarongable Jul 30, 2026
cf818af
Final cleanups
aarongable Jul 30, 2026
67228e9
Merge branch main into cps-lints
aarongable Jul 30, 2026
6bb1085
Fixup MTC unit tests
aarongable Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 19 additions & 10 deletions cmd/cert-checker/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/letsencrypt/boulder/goodkey"
"github.com/letsencrypt/boulder/goodkey/sagoodkey"
"github.com/letsencrypt/boulder/identifier"
"github.com/letsencrypt/boulder/issuance"
"github.com/letsencrypt/boulder/linter"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics"
Expand Down Expand Up @@ -559,13 +560,9 @@ func TestIgnoredLint(t *testing.T) {

err = loglist.InitLintList("../../test/ct-test-srv/log_list.json", false)
test.AssertNotError(t, err, "failed to load ct log list")
testKey, _ := rsa.GenerateKey(rand.Reader, 2048)
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, nil, linter.Config{}, blog.NewMock())
serial := big.NewInt(1337)

x509OID, err := x509.OIDFromInts([]uint64{1, 2, 3})
test.AssertNotError(t, err, "failed to create x509.OID")

// Create a self-signed issuer certificate to use
serial := big.NewInt(1337)
template := &x509.Certificate{
Subject: pkix.Name{
CommonName: "CPU's Cool CA",
Expand All @@ -575,27 +572,37 @@ func TestIgnoredLint(t *testing.T) {
NotAfter: time.Now().Add(testValidityDuration - time.Second),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
Policies: []x509.OID{x509OID},
BasicConstraintsValid: true,
IsCA: true,
IssuingCertificateURL: []string{"http://aia.example.org"},
SubjectKeyId: []byte("foobar"),
}

// Create a self-signed issuer certificate to use
testKey, _ := rsa.GenerateKey(rand.Reader, 2048)
issuerDer, err := x509.CreateCertificate(rand.Reader, template, template, testKey.Public(), testKey)
test.AssertNotError(t, err, "failed to create self-signed issuer cert")
issuerCert, err := x509.ParseCertificate(issuerDer)
test.AssertNotError(t, err, "failed to parse self-signed issuer cert")
issuer, err := issuance.NewCertificate(issuerCert)
test.AssertNotError(t, err, "failed to make self-signed issuer cert")

checker := newChecker(
saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations,
map[string]*issuance.Certificate{issuerCert.Subject.CommonName: issuer},
nil, linter.Config{}, blog.NewMock(),
)

// Reconfigure the template for an EE cert with a Subj. CN
serial = big.NewInt(1338)
serial, _ = big.NewInt(0).SetString("12345678901234567890123456789012", 10)
template.SerialNumber = serial
template.Subject.CommonName = "zombo.com"
template.DNSNames = []string{"zombo.com"}
template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}
template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
template.CRLDistributionPoints = []string{"http://crl.example.org"}
template.SubjectKeyId, _ = core.GenerateSKID(testKey.Public())
dvOID, _ := x509.OIDFromASN1OID(asn1.ObjectIdentifier{2, 23, 140, 1, 2, 1})
template.Policies = []x509.OID{dvOID}
template.IsCA = false

subjectCertDer, err := x509.CreateCertificate(rand.Reader, template, issuerCert, testKey.Public(), testKey)
Expand All @@ -618,6 +625,7 @@ func TestIgnoredLint(t *testing.T) {
"zlint warn: w_ext_subject_key_identifier_not_recommended_subscriber",
"zlint info: w_ct_sct_policy_count_unsatisfied Certificate had 0 embedded SCTs. Browser policy may require 2 for this certificate.",
"zlint error: e_scts_from_same_operator Certificate had too few embedded SCTs; browser policy requires 2.",
"zlint error: e_subscriber_server_certificate_matches_cps_profile signedCertificateTimestampList extension is not present",
}
slices.Sort(expectedProblems)

Expand All @@ -634,6 +642,7 @@ func TestIgnoredLint(t *testing.T) {
"w_ext_subject_key_identifier_not_recommended_subscriber",
"w_ct_sct_policy_count_unsatisfied",
"e_scts_from_same_operator",
"e_subscriber_server_certificate_matches_cps_profile",
})
test.AssertNotError(t, err, "creating test lint registry")
checker.lints = lints
Expand Down
50 changes: 27 additions & 23 deletions issuance/cert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ func TestRequestValid(t *testing.T) {
SubjectKeyId: goodSKID,
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18},
IncludeCTPoison: true,
},
},
Expand All @@ -290,7 +290,7 @@ func TestRequestValid(t *testing.T) {
SubjectKeyId: goodSKID,
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18},
sctList: []ct.SignedCertificateTimestamp{},
},
},
Expand Down Expand Up @@ -367,7 +367,7 @@ func TestIssue(t *testing.T) {
lintCertBytes, issuanceToken, err := signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: skid,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18},
DNSNames: []string{"example.com"},
IPAddresses: []net.IP{net.ParseIP("128.101.101.101"), net.ParseIP("3fff:aaa:a:c0ff:ee:a:bad:deed")},
NotBefore: fc.Now(),
Expand All @@ -392,7 +392,7 @@ func TestIssue(t *testing.T) {
// addresses back to 4 bytes. Adding .To4() both allows this test to
// succeed, and covers this requirement.
test.AssertDeepEquals(t, cert.IPAddresses, []net.IP{net.ParseIP("128.101.101.101").To4(), net.ParseIP("3fff:aaa:a:c0ff:ee:a:bad:deed")})
test.AssertByteEquals(t, cert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8, 9})
test.AssertByteEquals(t, cert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18})
test.AssertDeepEquals(t, cert.PublicKey, pk.Public())
test.AssertEquals(t, len(cert.Extensions), 10) // Constraints, KU, EKU, SKID, AKID, AIA, CRLDP, SAN, Policies, Poison
test.AssertEquals(t, cert.KeyUsage, tc.ku)
Expand Down Expand Up @@ -454,7 +454,7 @@ func TestIssueDNSNamesOnly(t *testing.T) {
_, issuanceToken, err := signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: skid,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
Expand Down Expand Up @@ -497,7 +497,7 @@ func TestIssueIPAddressesOnly(t *testing.T) {
_, issuanceToken, err := signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: skid,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18},
IPAddresses: []net.IP{net.ParseIP("128.101.101.101"), net.ParseIP("3fff:aaa:a:c0ff:ee:a:bad:deed")},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
Expand Down Expand Up @@ -544,7 +544,7 @@ func TestIssueWithCRLDP(t *testing.T) {
_, issuanceToken, err := signer.Prepare(profile, &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: skid,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
Expand All @@ -562,7 +562,7 @@ func TestIssueWithCRLDP(t *testing.T) {
t.Fatalf("x509.ParseCertificate: %s", err)
}
// Because CRL shard is calculated deterministically from serial, we know which shard will be chosen.
expectedCRLDP := []string{"http://crls.example.net/919.crl"}
expectedCRLDP := []string{"http://crls.example.net/838.crl"}
if !reflect.DeepEqual(cert.CRLDistributionPoints, expectedCRLDP) {
t.Errorf("CRLDP=%+v, want %+v", cert.CRLDistributionPoints, expectedCRLDP)
}
Expand All @@ -585,7 +585,7 @@ func TestIssueCommonName(t *testing.T) {
ir := &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: skid,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18},
DNSNames: []string{"example.com", "www.example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
Expand Down Expand Up @@ -636,6 +636,10 @@ func TestPrepareMTC(t *testing.T) {
// Ignore the warning about *not* including the SubjectKeyIdentifier extension:
// zlint has both lints (one enforcing RFC5280, the other the BRs).
"w_ext_subject_key_identifier_missing_sub_cert",
// MTCs are not (yet) subject to our CPS profiles, and will likely be
// subject to a *different* profile when we get around to issuing them
// from prod. Ignore our CPS-specific lint for now.
"e_subscriber_server_certificate_matches_cps_profile",
}
prof, err := NewProfile(pc)
if err != nil {
Expand All @@ -657,7 +661,7 @@ func TestPrepareMTC(t *testing.T) {
sctList: nil,

PublicKey: MarshalablePublicKey{pk.Public()},
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
Expand All @@ -670,7 +674,7 @@ func TestPrepareMTC(t *testing.T) {
IncludeCTPoison: true,

PublicKey: MarshalablePublicKey{pk.Public()},
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
Expand All @@ -683,7 +687,7 @@ func TestPrepareMTC(t *testing.T) {
sctList: []ct.SignedCertificateTimestamp{{SCTVersion: 1}},

PublicKey: MarshalablePublicKey{pk.Public()},
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
Expand Down Expand Up @@ -722,7 +726,7 @@ func TestIssueOmissions(t *testing.T) {
_, issuanceToken, err := signer.Prepare(prof, &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: skid,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18},
DNSNames: []string{"example.com"},
CommonName: "example.com",
IncludeCTPoison: true,
Expand Down Expand Up @@ -753,7 +757,7 @@ func TestIssueCTPoison(t *testing.T) {
_, issuanceToken, err := signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: skid,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18},
DNSNames: []string{"example.com"},
IncludeCTPoison: true,
NotBefore: fc.Now(),
Expand All @@ -766,7 +770,7 @@ func TestIssueCTPoison(t *testing.T) {
test.AssertNotError(t, err, "failed to parse certificate")
err = cert.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.AssertByteEquals(t, cert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18})
test.AssertDeepEquals(t, cert.PublicKey, pk.Public())
test.AssertEquals(t, len(cert.Extensions), 10) // Constraints, KU, EKU, SKID, AKID, AIA, CRLDP, SAN, Policies, Poison
test.AssertDeepEquals(t, cert.Extensions[9], ctPoisonExt)
Expand Down Expand Up @@ -803,7 +807,7 @@ func TestIssueSCTList(t *testing.T) {
_, issuanceToken, err := signer.Prepare(enforceSCTsProfile, &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: skid,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
Expand Down Expand Up @@ -840,7 +844,7 @@ func TestIssueSCTList(t *testing.T) {

err = finalCert.CheckSignatureFrom(issuerCert.Certificate)
test.AssertNotError(t, err, "signature validation failed")
test.AssertByteEquals(t, finalCert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8, 9})
test.AssertByteEquals(t, finalCert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18})
test.AssertDeepEquals(t, finalCert.PublicKey, pk.Public())
test.AssertEquals(t, len(finalCert.Extensions), 10) // Constraints, KU, EKU, SKID, AKID, AIA, CRLDP, SAN, Policies, Poison
test.AssertDeepEquals(t, finalCert.Extensions[9], pkix.Extension{
Expand Down Expand Up @@ -873,7 +877,7 @@ func TestIssueBadLint(t *testing.T) {
_, _, err = signer.Prepare(noSkipLintsProfile, &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: skid,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18},
DNSNames: []string{"example-com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
Expand Down Expand Up @@ -904,7 +908,7 @@ func TestIssuanceToken(t *testing.T) {
_, issuanceToken, err := signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: skid,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
Expand All @@ -921,7 +925,7 @@ func TestIssuanceToken(t *testing.T) {
_, issuanceToken, err = signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: skid,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
Expand Down Expand Up @@ -953,7 +957,7 @@ func TestInvalidProfile(t *testing.T) {
_, _, err = signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: skid,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
Expand All @@ -965,7 +969,7 @@ func TestInvalidProfile(t *testing.T) {
_, _, err = signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: skid,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
Expand Down Expand Up @@ -1004,7 +1008,7 @@ func TestMismatchedProfiles(t *testing.T) {
_, issuanceToken, err := issuer1.Prepare(cnProfile, &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: skid,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18},
CommonName: "example.com",
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
Expand Down
1 change: 1 addition & 0 deletions issuance/issuer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

func defaultProfileConfig() ProfileConfig {
return ProfileConfig{
OmitClientAuth: true,
MaxValidityPeriod: config.Duration{Duration: time.Hour},
MaxValidityBackdate: config.Duration{Duration: time.Hour},
IgnoredLints: []string{
Expand Down
25 changes: 25 additions & 0 deletions linter/linter.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto"
"crypto/rand"
"crypto/x509"
"errors"
"fmt"
"strings"

Expand Down Expand Up @@ -121,7 +122,31 @@ func (l *Linter) Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey, re
return nil, err
}
if selfSigned {
// If the cert being linted is going to be self-signed, replace the lint
// cert's public key and subjectKeyId extension with ones built from the
// fake lint signing key, so that everything lines up as it should.
lintPubKey = l.signer.Public()
if len(tbs.SubjectKeyId) != 0 {
// This is a dumb hack. Because we're replacing the SKID with a fake one
// derived from the lint key, none of the lints will actually inspect the
// real SKID. So to make up for it, do the most critical check here and
// now.
realSKID, err := core.GenerateSKID(subjectPubKey)
if err != nil {
return nil, err
}
if !bytes.Equal(tbs.SubjectKeyId, realSKID) {
return nil, errors.New("self-signed certificate's subjectKeyIdentifier was not computed from its subjectPublicKey per RFC 7093 Section 2(1)")
}

lintSKID, err := core.GenerateSKID(lintPubKey)
if err != nil {
return nil, err
}
tbsCopy := *tbs
tbsCopy.SubjectKeyId = lintSKID
tbs = &tbsCopy
}
}

lintCertBytes, cert, err := makeLintCert(tbs, lintPubKey, l.issuer, l.signer)
Expand Down
35 changes: 35 additions & 0 deletions linter/linter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import (
"math/big"
"strings"
"testing"
"time"

"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/test"
)

Expand Down Expand Up @@ -111,3 +113,36 @@ func TestMakeIssuer(t *testing.T) {
t.Errorf("linting certificate issuer: got %x, want %x", lintCert.RawIssuer, realIssuer.RawSubject)
}
}

func TestCheckSelfSignedSKID(t *testing.T) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "generating test key")

tbs := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "Self-Signed Test"},
NotBefore: time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC),
NotAfter: time.Date(2026, time.February, 1, 0, 0, 0, 0, time.UTC),
BasicConstraintsValid: true,
IsCA: true,
SubjectKeyId: []byte{1, 2, 3, 4},
}

// A self-signed tbs certificate whose subjectKeyIdentifier was not
// computed from its public key must be rejected outright: the SKID
// substitution performed by Check would otherwise hide it from the lints.
_, err = Check(tbs, key.Public(), tbs, key, Config{}, nil)
test.AssertError(t, err, "linting should have failed")
test.AssertContains(t, err.Error(), "RFC 7093")

// With the correct subjectKeyIdentifier, linting may fail for other
// reasons (this minimal certificate is far from profile-compliant), but
// not because of the subjectKeyIdentifier check.
skid, err := core.GenerateSKID(key.Public())
test.AssertNotError(t, err, "computing SKID")
tbs.SubjectKeyId = skid
_, err = Check(tbs, key.Public(), tbs, key, Config{}, nil)
if err != nil && strings.Contains(err.Error(), "RFC 7093") {
t.Errorf("linting failed on the subjectKeyIdentifier check despite a correct SKID: %s", err)
}
}
Loading
Loading