Add pkilint to CI via custom zlint (#7441)

Add a new "LintConfig" item to the CA's config, which can point to a
zlint configuration toml file. This allows lints to be configured, e.g.
to control the number of rounds of factorization performed by the Fermat
factorization lint.

Leverage this new config to create a new custom zlint which calls out to
a configured pkilint API endpoint. In config-next integration tests,
configure the lint to point at a new pkilint docker container.

This approach has three nice forward-looking features: we now have the
ability to configure any of our lints; it's easy to expand this
mechanism to lint CRLs when the pkilint API has support for that; and
it's easy to enable this new lint if we decide to stand up a pkilint
container in our production environment.

No production configuration changes are necessary at this time.

Fixes https://github.com/letsencrypt/boulder/issues/7430
This commit is contained in:
Aaron Gable 2024-04-30 09:29:26 -07:00 committed by GitHub
parent 9f2a27e03b
commit 939ac1be8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 324 additions and 113 deletions

View File

@ -23,6 +23,7 @@ import (
"github.com/jmhodges/clock" "github.com/jmhodges/clock"
"github.com/miekg/pkcs11" "github.com/miekg/pkcs11"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/zmap/zlint/v3/lint"
"golang.org/x/crypto/ocsp" "golang.org/x/crypto/ocsp"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
@ -125,18 +126,18 @@ func makeIssuerMaps(issuers []*issuance.Issuer) (issuerMaps, error) {
return issuerMaps{issuersByAlg, issuersByNameID}, nil return issuerMaps{issuersByAlg, issuersByNameID}, nil
} }
// makeCertificateProfilesMap processes a list of certificate issuance profile // makeCertificateProfilesMap processes a set of named certificate issuance
// configs and an option slice of zlint lint names to ignore into a set of // profile configs into a two pre-computed maps: 1) a human-readable name to the
// pre-computed maps: 1) a human-readable name to the profile and 2) a unique // profile and 2) a unique hash over contents of the profile to the profile
// hash over contents of the profile to the profile itself. It returns the maps // itself. It returns the maps or an error if a duplicate name or hash is found.
// or an error if a duplicate name or hash is found. // It also associates the given lint registry with each profile.
// //
// The unique hash is used in the case of // The unique hash is used in the case of
// - RA instructs CA1 to issue a precertificate // - RA instructs CA1 to issue a precertificate
// - CA1 returns the precertificate DER bytes and profile hash to the RA // - CA1 returns the precertificate DER bytes and profile hash to the RA
// - RA instructs CA2 to issue a final certificate, but CA2 does not contain a // - RA instructs CA2 to issue a final certificate, but CA2 does not contain a
// profile corresponding to that hash and an issuance is prevented. // profile corresponding to that hash and an issuance is prevented.
func makeCertificateProfilesMap(defaultName string, profiles map[string]issuance.ProfileConfig, ignoredLints []string) (certProfilesMaps, error) { func makeCertificateProfilesMap(defaultName string, profiles map[string]issuance.ProfileConfig, lints lint.Registry) (certProfilesMaps, error) {
if len(profiles) <= 0 { if len(profiles) <= 0 {
return certProfilesMaps{}, fmt.Errorf("must pass at least one certificate profile") return certProfilesMaps{}, fmt.Errorf("must pass at least one certificate profile")
} }
@ -151,7 +152,7 @@ func makeCertificateProfilesMap(defaultName string, profiles map[string]issuance
profileByHash := make(map[[32]byte]*certProfileWithID, len(profiles)) profileByHash := make(map[[32]byte]*certProfileWithID, len(profiles))
for name, profileConfig := range profiles { for name, profileConfig := range profiles {
profile, err := issuance.NewProfile(profileConfig, ignoredLints) profile, err := issuance.NewProfile(profileConfig, lints)
if err != nil { if err != nil {
return certProfilesMaps{}, err return certProfilesMaps{}, err
} }
@ -205,8 +206,8 @@ func NewCertificateAuthorityImpl(
pa core.PolicyAuthority, pa core.PolicyAuthority,
boulderIssuers []*issuance.Issuer, boulderIssuers []*issuance.Issuer,
defaultCertProfileName string, defaultCertProfileName string,
ignoredCertProfileLints []string,
certificateProfiles map[string]issuance.ProfileConfig, certificateProfiles map[string]issuance.ProfileConfig,
lints lint.Registry,
ecdsaAllowList *ECDSAAllowList, ecdsaAllowList *ECDSAAllowList,
certExpiry time.Duration, certExpiry time.Duration,
certBackdate time.Duration, certBackdate time.Duration,
@ -238,7 +239,7 @@ func NewCertificateAuthorityImpl(
return nil, errors.New("must have at least one issuer") return nil, errors.New("must have at least one issuer")
} }
certProfiles, err := makeCertificateProfilesMap(defaultCertProfileName, certificateProfiles, ignoredCertProfileLints) certProfiles, err := makeCertificateProfilesMap(defaultCertProfileName, certificateProfiles, lints)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -20,6 +20,7 @@ import (
ctx509 "github.com/google/certificate-transparency-go/x509" ctx509 "github.com/google/certificate-transparency-go/x509"
"github.com/jmhodges/clock" "github.com/jmhodges/clock"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/zmap/zlint/v3/lint"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
@ -31,6 +32,7 @@ import (
"github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/features"
"github.com/letsencrypt/boulder/goodkey" "github.com/letsencrypt/boulder/goodkey"
"github.com/letsencrypt/boulder/issuance" "github.com/letsencrypt/boulder/issuance"
"github.com/letsencrypt/boulder/linter"
blog "github.com/letsencrypt/boulder/log" blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/must" "github.com/letsencrypt/boulder/must"
@ -101,23 +103,23 @@ func mustRead(path string) []byte {
} }
type testCtx struct { type testCtx struct {
pa core.PolicyAuthority pa core.PolicyAuthority
ocsp *ocspImpl ocsp *ocspImpl
crl *crlImpl crl *crlImpl
defaultCertProfileName string defaultCertProfileName string
ignoredCertProfileLints []string lints lint.Registry
certProfiles map[string]issuance.ProfileConfig certProfiles map[string]issuance.ProfileConfig
certExpiry time.Duration certExpiry time.Duration
certBackdate time.Duration certBackdate time.Duration
serialPrefix int serialPrefix int
maxNames int maxNames int
boulderIssuers []*issuance.Issuer boulderIssuers []*issuance.Issuer
keyPolicy goodkey.KeyPolicy keyPolicy goodkey.KeyPolicy
fc clock.FakeClock fc clock.FakeClock
stats prometheus.Registerer stats prometheus.Registerer
signatureCount *prometheus.CounterVec signatureCount *prometheus.CounterVec
signErrorCount *prometheus.CounterVec signErrorCount *prometheus.CounterVec
logger *blog.Mock logger *blog.Mock
} }
type mockSA struct { type mockSA struct {
@ -217,6 +219,9 @@ func setup(t *testing.T) *testCtx {
Help: "A counter of signature errors labelled by error type", Help: "A counter of signature errors labelled by error type",
}, []string{"type"}) }, []string{"type"})
lints, err := linter.NewRegistry([]string{"w_subject_common_name_included"})
test.AssertNotError(t, err, "Failed to create zlint registry")
ocsp, err := NewOCSPImpl( ocsp, err := NewOCSPImpl(
boulderIssuers, boulderIssuers,
24*time.Hour, 24*time.Hour,
@ -243,23 +248,23 @@ func setup(t *testing.T) *testCtx {
test.AssertNotError(t, err, "Failed to create crl impl") test.AssertNotError(t, err, "Failed to create crl impl")
return &testCtx{ return &testCtx{
pa: pa, pa: pa,
ocsp: ocsp, ocsp: ocsp,
crl: crl, crl: crl,
defaultCertProfileName: "defaultBoulderCertificateProfile", defaultCertProfileName: "defaultBoulderCertificateProfile",
ignoredCertProfileLints: []string{"w_subject_common_name_included"}, lints: lints,
certProfiles: certProfiles, certProfiles: certProfiles,
certExpiry: 8760 * time.Hour, certExpiry: 8760 * time.Hour,
certBackdate: time.Hour, certBackdate: time.Hour,
serialPrefix: 17, serialPrefix: 17,
maxNames: 2, maxNames: 2,
boulderIssuers: boulderIssuers, boulderIssuers: boulderIssuers,
keyPolicy: keyPolicy, keyPolicy: keyPolicy,
fc: fc, fc: fc,
stats: metrics.NoopRegisterer, stats: metrics.NoopRegisterer,
signatureCount: signatureCount, signatureCount: signatureCount,
signErrorCount: signErrorCount, signErrorCount: signErrorCount,
logger: blog.NewMock(), logger: blog.NewMock(),
} }
} }
@ -390,8 +395,8 @@ func issueCertificateSubTestSetup(t *testing.T, e *ECDSAAllowList) (*certificate
testCtx.pa, testCtx.pa,
testCtx.boulderIssuers, testCtx.boulderIssuers,
testCtx.defaultCertProfileName, testCtx.defaultCertProfileName,
testCtx.ignoredCertProfileLints,
testCtx.certProfiles, testCtx.certProfiles,
testCtx.lints,
e, e,
testCtx.certExpiry, testCtx.certExpiry,
testCtx.certBackdate, testCtx.certBackdate,
@ -440,8 +445,8 @@ func TestNoIssuers(t *testing.T) {
testCtx.pa, testCtx.pa,
nil, // No issuers nil, // No issuers
testCtx.defaultCertProfileName, testCtx.defaultCertProfileName,
testCtx.ignoredCertProfileLints,
testCtx.certProfiles, testCtx.certProfiles,
testCtx.lints,
nil, nil,
testCtx.certExpiry, testCtx.certExpiry,
testCtx.certBackdate, testCtx.certBackdate,
@ -467,8 +472,8 @@ func TestMultipleIssuers(t *testing.T) {
testCtx.pa, testCtx.pa,
testCtx.boulderIssuers, testCtx.boulderIssuers,
testCtx.defaultCertProfileName, testCtx.defaultCertProfileName,
testCtx.ignoredCertProfileLints,
testCtx.certProfiles, testCtx.certProfiles,
testCtx.lints,
nil, nil,
testCtx.certExpiry, testCtx.certExpiry,
testCtx.certBackdate, testCtx.certBackdate,
@ -547,8 +552,8 @@ func TestUnpredictableIssuance(t *testing.T) {
testCtx.pa, testCtx.pa,
boulderIssuers, boulderIssuers,
testCtx.defaultCertProfileName, testCtx.defaultCertProfileName,
testCtx.ignoredCertProfileLints,
testCtx.certProfiles, testCtx.certProfiles,
testCtx.lints,
nil, nil,
testCtx.certExpiry, testCtx.certExpiry,
testCtx.certBackdate, testCtx.certBackdate,
@ -596,8 +601,8 @@ func TestUnpredictableIssuance(t *testing.T) {
func TestProfiles(t *testing.T) { func TestProfiles(t *testing.T) {
t.Parallel() t.Parallel()
ctx := setup(t) testCtx := setup(t)
test.AssertEquals(t, len(ctx.certProfiles), 2) test.AssertEquals(t, len(testCtx.certProfiles), 2)
sa := &mockSA{} sa := &mockSA{}
@ -672,10 +677,10 @@ func TestProfiles(t *testing.T) {
}, },
{ {
name: "default profiles from setup func", name: "default profiles from setup func",
profileConfigs: ctx.certProfiles, profileConfigs: testCtx.certProfiles,
expectedProfiles: []nameToHash{ expectedProfiles: []nameToHash{
{ {
name: ctx.defaultCertProfileName, name: testCtx.defaultCertProfileName,
hash: [32]byte{205, 182, 88, 236, 32, 18, 154, 120, 148, 194, 42, 215, 117, 140, 13, 169, 127, 196, 219, 67, 82, 36, 147, 67, 254, 117, 65, 112, 202, 60, 185, 9}, hash: [32]byte{205, 182, 88, 236, 32, 18, 154, 120, 148, 194, 42, 215, 117, 140, 13, 169, 127, 196, 219, 67, 82, 36, 147, 67, 254, 117, 65, 112, 202, 60, 185, 9},
}, },
{ {
@ -709,28 +714,28 @@ func TestProfiles(t *testing.T) {
tc := tc tc := tc
// This is handled by boulder-ca, not the CA package. // This is handled by boulder-ca, not the CA package.
if tc.defaultName == "" { if tc.defaultName == "" {
tc.defaultName = ctx.defaultCertProfileName tc.defaultName = testCtx.defaultCertProfileName
} }
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
tCA, err := NewCertificateAuthorityImpl( tCA, err := NewCertificateAuthorityImpl(
sa, sa,
ctx.pa, testCtx.pa,
ctx.boulderIssuers, testCtx.boulderIssuers,
tc.defaultName, tc.defaultName,
ctx.ignoredCertProfileLints,
tc.profileConfigs, tc.profileConfigs,
testCtx.lints,
nil, nil,
ctx.certExpiry, testCtx.certExpiry,
ctx.certBackdate, testCtx.certBackdate,
ctx.serialPrefix, testCtx.serialPrefix,
ctx.maxNames, testCtx.maxNames,
ctx.keyPolicy, testCtx.keyPolicy,
ctx.logger, testCtx.logger,
ctx.stats, testCtx.stats,
ctx.signatureCount, testCtx.signatureCount,
ctx.signErrorCount, testCtx.signErrorCount,
ctx.fc, testCtx.fc,
) )
if tc.expectedErrSubstr != "" { if tc.expectedErrSubstr != "" {
@ -862,8 +867,8 @@ func TestInvalidCSRs(t *testing.T) {
testCtx.pa, testCtx.pa,
testCtx.boulderIssuers, testCtx.boulderIssuers,
testCtx.defaultCertProfileName, testCtx.defaultCertProfileName,
testCtx.ignoredCertProfileLints,
testCtx.certProfiles, testCtx.certProfiles,
testCtx.lints,
nil, nil,
testCtx.certExpiry, testCtx.certExpiry,
testCtx.certBackdate, testCtx.certBackdate,
@ -903,8 +908,8 @@ func TestRejectValidityTooLong(t *testing.T) {
testCtx.pa, testCtx.pa,
testCtx.boulderIssuers, testCtx.boulderIssuers,
testCtx.defaultCertProfileName, testCtx.defaultCertProfileName,
testCtx.ignoredCertProfileLints,
testCtx.certProfiles, testCtx.certProfiles,
testCtx.lints,
nil, nil,
testCtx.certExpiry, testCtx.certExpiry,
testCtx.certBackdate, testCtx.certBackdate,
@ -1007,8 +1012,8 @@ func TestIssueCertificateForPrecertificate(t *testing.T) {
testCtx.pa, testCtx.pa,
testCtx.boulderIssuers, testCtx.boulderIssuers,
testCtx.defaultCertProfileName, testCtx.defaultCertProfileName,
testCtx.ignoredCertProfileLints,
testCtx.certProfiles, testCtx.certProfiles,
testCtx.lints,
nil, nil,
testCtx.certExpiry, testCtx.certExpiry,
testCtx.certBackdate, testCtx.certBackdate,
@ -1078,8 +1083,8 @@ func TestIssueCertificateForPrecertificateWithSpecificCertificateProfile(t *test
testCtx.pa, testCtx.pa,
testCtx.boulderIssuers, testCtx.boulderIssuers,
testCtx.defaultCertProfileName, testCtx.defaultCertProfileName,
testCtx.ignoredCertProfileLints,
testCtx.certProfiles, testCtx.certProfiles,
testCtx.lints,
nil, nil,
testCtx.certExpiry, testCtx.certExpiry,
testCtx.certBackdate, testCtx.certBackdate,
@ -1200,8 +1205,8 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
testCtx.pa, testCtx.pa,
testCtx.boulderIssuers, testCtx.boulderIssuers,
testCtx.defaultCertProfileName, testCtx.defaultCertProfileName,
testCtx.ignoredCertProfileLints,
testCtx.certProfiles, testCtx.certProfiles,
testCtx.lints,
nil, nil,
testCtx.certExpiry, testCtx.certExpiry,
testCtx.certBackdate, testCtx.certBackdate,
@ -1253,8 +1258,8 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
testCtx.pa, testCtx.pa,
testCtx.boulderIssuers, testCtx.boulderIssuers,
testCtx.defaultCertProfileName, testCtx.defaultCertProfileName,
testCtx.ignoredCertProfileLints,
testCtx.certProfiles, testCtx.certProfiles,
testCtx.lints,
nil, nil,
testCtx.certExpiry, testCtx.certExpiry,
testCtx.certBackdate, testCtx.certBackdate,

View File

@ -39,8 +39,8 @@ func TestOCSP(t *testing.T) {
testCtx.pa, testCtx.pa,
testCtx.boulderIssuers, testCtx.boulderIssuers,
testCtx.defaultCertProfileName, testCtx.defaultCertProfileName,
testCtx.ignoredCertProfileLints,
testCtx.certProfiles, testCtx.certProfiles,
testCtx.lints,
nil, nil,
testCtx.certExpiry, testCtx.certExpiry,
testCtx.certBackdate, testCtx.certBackdate,

View File

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/zmap/zlint/v3/lint"
"github.com/letsencrypt/boulder/ca" "github.com/letsencrypt/boulder/ca"
capb "github.com/letsencrypt/boulder/ca/proto" capb "github.com/letsencrypt/boulder/ca/proto"
@ -19,6 +20,7 @@ import (
"github.com/letsencrypt/boulder/goodkey/sagoodkey" "github.com/letsencrypt/boulder/goodkey/sagoodkey"
bgrpc "github.com/letsencrypt/boulder/grpc" bgrpc "github.com/letsencrypt/boulder/grpc"
"github.com/letsencrypt/boulder/issuance" "github.com/letsencrypt/boulder/issuance"
"github.com/letsencrypt/boulder/linter"
"github.com/letsencrypt/boulder/policy" "github.com/letsencrypt/boulder/policy"
sapb "github.com/letsencrypt/boulder/sa/proto" sapb "github.com/letsencrypt/boulder/sa/proto"
) )
@ -52,6 +54,7 @@ type Config struct {
// TODO(#7159): Make this required once all live configs are using it. // TODO(#7159): Make this required once all live configs are using it.
CRLProfile issuance.CRLProfileConfig `validate:"-"` CRLProfile issuance.CRLProfileConfig `validate:"-"`
Issuers []issuance.IssuerConfig `validate:"min=1,dive"` Issuers []issuance.IssuerConfig `validate:"min=1,dive"`
LintConfig string
IgnoredLints []string IgnoredLints []string
} }
@ -234,6 +237,14 @@ func main() {
c.CA.Issuance.CertProfiles[c.CA.Issuance.DefaultCertificateProfileName] = c.CA.Issuance.Profile c.CA.Issuance.CertProfiles[c.CA.Issuance.DefaultCertificateProfileName] = c.CA.Issuance.Profile
} }
lints, err := linter.NewRegistry(c.CA.Issuance.IgnoredLints)
cmd.FailOnError(err, "Failed to create zlint registry")
if c.CA.Issuance.LintConfig != "" {
lintconfig, err := lint.NewConfigFromFile(c.CA.Issuance.LintConfig)
cmd.FailOnError(err, "Failed to load zlint config file")
lints.SetConfiguration(lintconfig)
}
tlsConfig, err := c.CA.TLS.Load(scope) tlsConfig, err := c.CA.TLS.Load(scope)
cmd.FailOnError(err, "TLS config") cmd.FailOnError(err, "TLS config")
@ -295,8 +306,8 @@ func main() {
pa, pa,
issuers, issuers,
c.CA.Issuance.DefaultCertificateProfileName, c.CA.Issuance.DefaultCertificateProfileName,
c.CA.Issuance.IgnoredLints,
c.CA.Issuance.CertProfiles, c.CA.Issuance.CertProfiles,
lints,
ecdsaAllowList, ecdsaAllowList,
c.CA.Expiry.Duration, c.CA.Expiry.Duration,
c.CA.Backdate.Duration, c.CA.Backdate.Duration,

View File

@ -40,6 +40,11 @@ services:
# TODO: Remove this when ServerAddress is deprecated in favor of SRV records # TODO: Remove this when ServerAddress is deprecated in favor of SRV records
# and DNSAuthority. # and DNSAuthority.
dns: 10.55.55.10 dns: 10.55.55.10
extra_hosts:
# Allow the boulder container to be reached as "ca.example.org", so that
# we can put that name inside our integration test certs (e.g. as a crl
# url) and have it look like a publicly-accessible name.
- "ca.example.org:10.77.77.77"
ports: ports:
- 4001:4001 # ACMEv2 - 4001:4001 # ACMEv2
- 4002:4002 # OCSP - 4002:4002 # OCSP
@ -53,6 +58,7 @@ services:
- bredis_4 - bredis_4
- bconsul - bconsul
- bjaeger - bjaeger
- bpkilint
entrypoint: test/entrypoint.sh entrypoint: test/entrypoint.sh
working_dir: &boulder_working_dir /boulder working_dir: &boulder_working_dir /boulder
@ -141,6 +147,13 @@ services:
bouldernet: bouldernet:
ipv4_address: 10.77.77.17 ipv4_address: 10.77.77.17
bpkilint:
image: ghcr.io/digicert/pkilint:v0.10.1
networks:
bouldernet:
ipv4_address: 10.77.77.9
command: "gunicorn -w 8 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:80 pkilint.rest:app"
networks: networks:
# This network is primarily used for boulder services. It is also used by # This network is primarily used for boulder services. It is also used by
# challtestsrv, which is used in the integration tests. # challtestsrv, which is used in the integration tests.

View File

@ -22,7 +22,6 @@ import (
"github.com/zmap/zlint/v3/lint" "github.com/zmap/zlint/v3/lint"
"github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/config"
"github.com/letsencrypt/boulder/linter"
"github.com/letsencrypt/boulder/precert" "github.com/letsencrypt/boulder/precert"
) )
@ -58,14 +57,8 @@ type Profile struct {
lints lint.Registry lints lint.Registry
} }
// NewProfile synthesizes the profile config and issuer config into a single // NewProfile converts the profile config and lint registry into a usable profile.
// object, and checks various aspects for correctness. func NewProfile(profileConfig ProfileConfig, lints lint.Registry) (*Profile, error) {
func NewProfile(profileConfig ProfileConfig, skipLints []string) (*Profile, error) {
reg, err := linter.NewRegistry(skipLints)
if err != nil {
return nil, fmt.Errorf("creating lint registry: %w", err)
}
sp := &Profile{ sp := &Profile{
allowMustStaple: profileConfig.AllowMustStaple, allowMustStaple: profileConfig.AllowMustStaple,
allowCTPoison: profileConfig.AllowCTPoison, allowCTPoison: profileConfig.AllowCTPoison,
@ -73,7 +66,7 @@ func NewProfile(profileConfig ProfileConfig, skipLints []string) (*Profile, erro
allowCommonName: profileConfig.AllowCommonName, allowCommonName: profileConfig.AllowCommonName,
maxBackdate: profileConfig.MaxValidityBackdate.Duration, maxBackdate: profileConfig.MaxValidityBackdate.Duration,
maxValidity: profileConfig.MaxValidityPeriod.Duration, maxValidity: profileConfig.MaxValidityPeriod.Duration,
lints: reg, lints: lints,
} }
return sp, nil return sp, nil

View File

@ -27,10 +27,11 @@ var (
) )
func defaultProfile() *Profile { func defaultProfile() *Profile {
p, _ := NewProfile(defaultProfileConfig(), []string{ lints, _ := linter.NewRegistry([]string{
"w_ct_sct_policy_count_unsatisfied", "w_ct_sct_policy_count_unsatisfied",
"e_scts_from_same_operator", "e_scts_from_same_operator",
}) })
p, _ := NewProfile(defaultProfileConfig(), lints)
return p return p
} }
@ -380,11 +381,13 @@ func TestIssueCommonName(t *testing.T) {
fc := clock.NewFake() fc := clock.NewFake()
fc.Set(time.Now()) fc.Set(time.Now())
cnProfile, err := NewProfile(defaultProfileConfig(), []string{ lints, err := linter.NewRegistry([]string{
"w_subject_common_name_included", "w_subject_common_name_included",
"w_ct_sct_policy_count_unsatisfied", "w_ct_sct_policy_count_unsatisfied",
"e_scts_from_same_operator", "e_scts_from_same_operator",
}) })
test.AssertNotError(t, err, "building test lint registry")
cnProfile, err := NewProfile(defaultProfileConfig(), lints)
test.AssertNotError(t, err, "NewProfile failed") test.AssertNotError(t, err, "NewProfile failed")
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc) signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed") test.AssertNotError(t, err, "NewIssuer failed")
@ -469,7 +472,9 @@ func TestIssueSCTList(t *testing.T) {
err := loglist.InitLintList("../test/ct-test-srv/log_list.json") err := loglist.InitLintList("../test/ct-test-srv/log_list.json")
test.AssertNotError(t, err, "failed to load log list") test.AssertNotError(t, err, "failed to load log list")
enforceSCTsProfile, err := NewProfile(defaultProfileConfig(), []string{}) lints, err := linter.NewRegistry([]string{})
test.AssertNotError(t, err, "building test lint registry")
enforceSCTsProfile, err := NewProfile(defaultProfileConfig(), lints)
test.AssertNotError(t, err, "NewProfile failed") test.AssertNotError(t, err, "NewProfile failed")
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc) signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed") test.AssertNotError(t, err, "NewIssuer failed")
@ -566,7 +571,9 @@ func TestIssueBadLint(t *testing.T) {
fc := clock.NewFake() fc := clock.NewFake()
fc.Set(time.Now()) fc.Set(time.Now())
noSkipLintsProfile, err := NewProfile(defaultProfileConfig(), []string{}) lints, err := linter.NewRegistry([]string{})
test.AssertNotError(t, err, "building test lint registry")
noSkipLintsProfile, err := NewProfile(defaultProfileConfig(), lints)
test.AssertNotError(t, err, "NewProfile failed") test.AssertNotError(t, err, "NewProfile failed")
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc) signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed") test.AssertNotError(t, err, "NewIssuer failed")
@ -690,11 +697,13 @@ func TestMismatchedProfiles(t *testing.T) {
issuer1, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc) issuer1, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed") test.AssertNotError(t, err, "NewIssuer failed")
cnProfile, err := NewProfile(defaultProfileConfig(), []string{ lints, err := linter.NewRegistry([]string{
"w_subject_common_name_included", "w_subject_common_name_included",
"w_ct_sct_policy_count_unsatisfied", "w_ct_sct_policy_count_unsatisfied",
"e_scts_from_same_operator", "e_scts_from_same_operator",
}) })
test.AssertNotError(t, err, "building test lint registry")
cnProfile, err := NewProfile(defaultProfileConfig(), lints)
test.AssertNotError(t, err, "NewProfile failed") test.AssertNotError(t, err, "NewProfile failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
@ -717,10 +726,12 @@ func TestMismatchedProfiles(t *testing.T) {
// Create a new profile that differs slightly (no common name) // Create a new profile that differs slightly (no common name)
profileConfig := defaultProfileConfig() profileConfig := defaultProfileConfig()
profileConfig.AllowCommonName = false profileConfig.AllowCommonName = false
noCNProfile, err := NewProfile(profileConfig, []string{ lints, err = linter.NewRegistry([]string{
"w_ct_sct_policy_count_unsatisfied", "w_ct_sct_policy_count_unsatisfied",
"e_scts_from_same_operator", "e_scts_from_same_operator",
}) })
test.AssertNotError(t, err, "building test lint registry")
noCNProfile, err := NewProfile(profileConfig, lints)
test.AssertNotError(t, err, "NewProfile failed") test.AssertNotError(t, err, "NewProfile failed")
issuer2, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc) issuer2, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)

View File

@ -0,0 +1,156 @@
package rfc
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"slices"
"strings"
"time"
"github.com/zmap/zcrypto/x509"
"github.com/zmap/zlint/v3/lint"
"github.com/zmap/zlint/v3/util"
)
type certViaPKILint struct {
PKILintAddr string `toml:"pkilint_addr" comment:"The address where a pkilint REST API can be reached."`
PKILintTimeout time.Duration `toml:"pkilint_timeout" comment:"How long, in nanoseconds, to wait before giving up."`
IgnoreLints []string `toml:"ignore_lints" comment:"The unique Validator:Code IDs of lint findings which should be ignored."`
}
func init() {
lint.RegisterCertificateLint(&lint.CertificateLint{
LintMetadata: lint.LintMetadata{
Name: "e_pkilint_lint_cabf_serverauth_cert",
Description: "Runs pkilint's suite of cabf serverauth certificate lints",
Citation: "https://github.com/digicert/pkilint",
Source: lint.Community,
EffectiveDate: util.CABEffectiveDate,
},
Lint: NewCertValidityNotRound,
})
}
func NewCertValidityNotRound() lint.CertificateLintInterface {
return &certViaPKILint{}
}
func (l *certViaPKILint) Configure() interface{} {
return l
}
func (l *certViaPKILint) CheckApplies(c *x509.Certificate) bool {
// This lint applies to all certificates issued by Boulder, as long as it has
// been configured with an address to reach out to. If not, skip it.
return l.PKILintAddr != ""
}
type PKILintResponse struct {
Results []struct {
Validator string `json:"validator"`
NodePath string `json:"node_path"`
FindingDescriptions []struct {
Severity string `json:"severity"`
Code string `json:"code"`
Message string `json:"message,omitempty"`
} `json:"finding_descriptions"`
} `json:"results"`
Linter struct {
Name string `json:"name"`
} `json:"linter"`
}
func (l *certViaPKILint) Execute(c *x509.Certificate) *lint.LintResult {
timeout := l.PKILintTimeout
if timeout == 0 {
timeout = 100 * time.Millisecond
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
reqJSON, err := json.Marshal(struct {
B64 string `json:"b64"`
}{
B64: base64.StdEncoding.EncodeToString(c.Raw),
})
if err != nil {
return &lint.LintResult{
Status: lint.Error,
Details: fmt.Sprintf("marshalling pkilint request: %s", err),
}
}
url := fmt.Sprintf("%s/certificate/cabf-serverauth", l.PKILintAddr)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(reqJSON))
if err != nil {
return &lint.LintResult{
Status: lint.Error,
Details: fmt.Sprintf("creating pkilint request: %s", err),
}
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return &lint.LintResult{
Status: lint.Error,
Details: fmt.Sprintf("making POST request to pkilint API: %s", err),
}
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return &lint.LintResult{
Status: lint.Error,
Details: fmt.Sprintf("got status %d (%s) from pkilint API", resp.StatusCode, resp.Status),
}
}
res, err := io.ReadAll(resp.Body)
if err != nil {
return &lint.LintResult{
Status: lint.Error,
Details: fmt.Sprintf("reading response from pkilint API: %s", err),
}
}
var jsonResult PKILintResponse
err = json.Unmarshal(res, &jsonResult)
if err != nil {
return &lint.LintResult{
Status: lint.Error,
Details: fmt.Sprintf("parsing response from pkilint API: %s", err),
}
}
var findings []string
for _, validator := range jsonResult.Results {
for _, finding := range validator.FindingDescriptions {
id := fmt.Sprintf("%s:%s", validator.Validator, finding.Code)
if slices.Contains(l.IgnoreLints, id) {
continue
}
desc := fmt.Sprintf("%s from %s at %s", finding.Severity, id, validator.NodePath)
if finding.Message != "" {
desc = fmt.Sprintf("%s: %s", desc, finding.Message)
}
findings = append(findings, desc)
}
}
if len(findings) != 0 {
// Group the findings by severity, for human readers.
slices.Sort(findings)
return &lint.LintResult{
Status: lint.Error,
Details: fmt.Sprintf("got %d lint findings from pkilint API: %s", len(findings), strings.Join(findings, "; ")),
}
}
return &lint.LintResult{Status: lint.Pass}
}

View File

@ -9,6 +9,7 @@ import (
"os" "os"
"path" "path"
"regexp" "regexp"
"strings"
"time" "time"
"github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/cmd"
@ -25,6 +26,7 @@ func (srv *aiaTestSrv) handleIssuer(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return return
} }
issuerName = strings.ReplaceAll(issuerName, "-", " ")
issuer, ok := srv.issuersByName[issuerName] issuer, ok := srv.issuersByName[issuerName]
if !ok { if !ok {

View File

@ -65,9 +65,9 @@
"issuers": [ "issuers": [
{ {
"active": true, "active": true,
"issuerURL": "http://127.0.0.1:4502/int ecdsa a", "issuerURL": "http://ca.example.org:4502/int-ecdsa-a",
"ocspURL": "http://127.0.0.1:4002/", "ocspURL": "http://ca.example.org:4002/",
"crlURLBase": "http://127.0.0.1:4501/ecdsa-a/", "crlURLBase": "http://ca.example.org:4501/ecdsa-a/",
"location": { "location": {
"configFile": "/hierarchy/int-ecdsa-a.pkcs11.json", "configFile": "/hierarchy/int-ecdsa-a.pkcs11.json",
"certFile": "/hierarchy/int-ecdsa-a.cert.pem", "certFile": "/hierarchy/int-ecdsa-a.cert.pem",
@ -76,9 +76,9 @@
}, },
{ {
"active": true, "active": true,
"issuerURL": "http://127.0.0.1:4502/int ecdsa b", "issuerURL": "http://ca.example.org:4502/int-ecdsa-b",
"ocspURL": "http://127.0.0.1:4002/", "ocspURL": "http://ca.example.org:4002/",
"crlURLBase": "http://127.0.0.1:4501/ecdsa-b/", "crlURLBase": "http://ca.example.org:4501/ecdsa-b/",
"location": { "location": {
"configFile": "/hierarchy/int-ecdsa-b.pkcs11.json", "configFile": "/hierarchy/int-ecdsa-b.pkcs11.json",
"certFile": "/hierarchy/int-ecdsa-b.cert.pem", "certFile": "/hierarchy/int-ecdsa-b.cert.pem",
@ -87,9 +87,9 @@
}, },
{ {
"active": false, "active": false,
"issuerURL": "http://127.0.0.1:4502/int ecdsa c", "issuerURL": "http://ca.example.org:4502/int-ecdsa-c",
"ocspURL": "http://127.0.0.1:4002/", "ocspURL": "http://ca.example.org:4002/",
"crlURLBase": "http://127.0.0.1:4501/ecdsa-c/", "crlURLBase": "http://ca.example.org:4501/ecdsa-c/",
"location": { "location": {
"configFile": "/hierarchy/int-ecdsa-c.pkcs11.json", "configFile": "/hierarchy/int-ecdsa-c.pkcs11.json",
"certFile": "/hierarchy/int-ecdsa-c.cert.pem", "certFile": "/hierarchy/int-ecdsa-c.cert.pem",
@ -98,9 +98,9 @@
}, },
{ {
"active": true, "active": true,
"issuerURL": "http://127.0.0.1:4502/int rsa a", "issuerURL": "http://ca.example.org:4502/int-rsa-a",
"ocspURL": "http://127.0.0.1:4002/", "ocspURL": "http://ca.example.org:4002/",
"crlURLBase": "http://127.0.0.1:4501/rsa-a/", "crlURLBase": "http://ca.example.org:4501/rsa-a/",
"location": { "location": {
"configFile": "/hierarchy/int-rsa-a.pkcs11.json", "configFile": "/hierarchy/int-rsa-a.pkcs11.json",
"certFile": "/hierarchy/int-rsa-a.cert.pem", "certFile": "/hierarchy/int-rsa-a.cert.pem",
@ -109,9 +109,9 @@
}, },
{ {
"active": true, "active": true,
"issuerURL": "http://127.0.0.1:4502/int rsa b", "issuerURL": "http://ca.example.org:4502/int-rsa-b",
"ocspURL": "http://127.0.0.1:4002/", "ocspURL": "http://ca.example.org:4002/",
"crlURLBase": "http://127.0.0.1:4501/rsa-b/", "crlURLBase": "http://ca.example.org:4501/rsa-b/",
"location": { "location": {
"configFile": "/hierarchy/int-rsa-b.pkcs11.json", "configFile": "/hierarchy/int-rsa-b.pkcs11.json",
"certFile": "/hierarchy/int-rsa-b.cert.pem", "certFile": "/hierarchy/int-rsa-b.cert.pem",
@ -120,9 +120,9 @@
}, },
{ {
"active": false, "active": false,
"issuerURL": "http://127.0.0.1:4502/int rsa c", "issuerURL": "http://ca.example.org:4502/int-rsa-c",
"ocspURL": "http://127.0.0.1:4002/", "ocspURL": "http://ca.example.org:4002/",
"crlURLBase": "http://127.0.0.1:4501/rsa-c/", "crlURLBase": "http://ca.example.org:4501/rsa-c/",
"location": { "location": {
"configFile": "/hierarchy/int-rsa-c.pkcs11.json", "configFile": "/hierarchy/int-rsa-c.pkcs11.json",
"certFile": "/hierarchy/int-rsa-c.cert.pem", "certFile": "/hierarchy/int-rsa-c.cert.pem",
@ -130,6 +130,7 @@
} }
} }
], ],
"lintConfig": "test/config-next/zlint.toml",
"ignoredLints": [ "ignoredLints": [
"w_subject_common_name_included", "w_subject_common_name_included",
"w_sub_cert_aia_contains_internal_names" "w_sub_cert_aia_contains_internal_names"

View File

@ -0,0 +1,18 @@
[e_pkilint_lint_cabf_serverauth_cert]
pkilint_addr = "http://10.77.77.9"
pkilint_timeout = 200000000 # 200 milliseconds
ignore_lints = [
# We include the CN in (almost) all of our certificates, on purpose.
# See https://github.com/letsencrypt/boulder/issues/5112 for details.
"DvSubcriberAttributeAllowanceValidator:cabf.serverauth.dv.common_name_attribute_present",
# We include the SKID in all of our certs, on purpose.
# See https://github.com/letsencrypt/boulder/issues/7446 for details.
"SubscriberExtensionAllowanceValidator:cabf.serverauth.subscriber.subject_key_identifier_extension_present",
# We compute the skid using RFC7093 Method 1, on purpose.
# See https://github.com/letsencrypt/boulder/pull/7179 for details.
"SubjectKeyIdentifierValidator:pkix.subject_key_identifier_rfc7093_method_1_identified",
# We include the keyEncipherment key usage in RSA certs, on purpose.
# It is only necessary for old versions of TLS, and is included for backwards
# compatibility. We intend to remove this in the short-lived profile.
"SubscriberKeyUsageValidator:cabf.serverauth.subscriber_rsa_digitalsignature_and_keyencipherment_present",
]

View File

@ -61,8 +61,8 @@
{ {
"useForRSALeaves": false, "useForRSALeaves": false,
"useForECDSALeaves": true, "useForECDSALeaves": true,
"issuerURL": "http://127.0.0.1:4502/int ecdsa a", "issuerURL": "http://ca.example.org:4502/int-ecdsa-a",
"ocspURL": "http://127.0.0.1:4002/", "ocspURL": "http://ca.example.org:4002/",
"location": { "location": {
"configFile": "/hierarchy/int-ecdsa-a.pkcs11.json", "configFile": "/hierarchy/int-ecdsa-a.pkcs11.json",
"certFile": "/hierarchy/int-ecdsa-a.cert.pem", "certFile": "/hierarchy/int-ecdsa-a.cert.pem",
@ -72,8 +72,8 @@
{ {
"useForRSALeaves": true, "useForRSALeaves": true,
"useForECDSALeaves": true, "useForECDSALeaves": true,
"issuerURL": "http://127.0.0.1:4502/int rsa a", "issuerURL": "http://ca.example.org:4502/int-rsa-a",
"ocspURL": "http://127.0.0.1:4002/", "ocspURL": "http://ca.example.org:4002/",
"location": { "location": {
"configFile": "/hierarchy/int-rsa-a.pkcs11.json", "configFile": "/hierarchy/int-rsa-a.pkcs11.json",
"certFile": "/hierarchy/int-rsa-a.cert.pem", "certFile": "/hierarchy/int-rsa-a.cert.pem",
@ -83,8 +83,8 @@
{ {
"useForRSALeaves": false, "useForRSALeaves": false,
"useForECDSALeaves": false, "useForECDSALeaves": false,
"issuerURL": "http://127.0.0.1:4502/int rsa b", "issuerURL": "http://ca.example.org:4502/int-rsa-b",
"ocspURL": "http://127.0.0.1:4002/", "ocspURL": "http://ca.example.org:4003/",
"location": { "location": {
"configFile": "/hierarchy/int-rsa-b.pkcs11.json", "configFile": "/hierarchy/int-rsa-b.pkcs11.json",
"certFile": "/hierarchy/int-rsa-b.cert.pem", "certFile": "/hierarchy/int-rsa-b.cert.pem",