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/miekg/pkcs11"
"github.com/prometheus/client_golang/prometheus"
"github.com/zmap/zlint/v3/lint"
"golang.org/x/crypto/ocsp"
"google.golang.org/protobuf/types/known/timestamppb"
@ -125,18 +126,18 @@ func makeIssuerMaps(issuers []*issuance.Issuer) (issuerMaps, error) {
return issuerMaps{issuersByAlg, issuersByNameID}, nil
}
// makeCertificateProfilesMap processes a list of certificate issuance profile
// configs and an option slice of zlint lint names to ignore into a set of
// pre-computed maps: 1) a human-readable name to the profile and 2) a unique
// hash over contents of the profile to the profile itself. It returns the maps
// or an error if a duplicate name or hash is found.
// makeCertificateProfilesMap processes a set of named certificate issuance
// profile configs into a two pre-computed maps: 1) a human-readable name to the
// profile and 2) a unique hash over contents of the profile to the profile
// itself. It returns the maps 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
// - RA instructs CA1 to issue a precertificate
// - 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
// 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 {
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))
for name, profileConfig := range profiles {
profile, err := issuance.NewProfile(profileConfig, ignoredLints)
profile, err := issuance.NewProfile(profileConfig, lints)
if err != nil {
return certProfilesMaps{}, err
}
@ -205,8 +206,8 @@ func NewCertificateAuthorityImpl(
pa core.PolicyAuthority,
boulderIssuers []*issuance.Issuer,
defaultCertProfileName string,
ignoredCertProfileLints []string,
certificateProfiles map[string]issuance.ProfileConfig,
lints lint.Registry,
ecdsaAllowList *ECDSAAllowList,
certExpiry time.Duration,
certBackdate time.Duration,
@ -238,7 +239,7 @@ func NewCertificateAuthorityImpl(
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 {
return nil, err
}

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import (
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/zmap/zlint/v3/lint"
"github.com/letsencrypt/boulder/ca"
capb "github.com/letsencrypt/boulder/ca/proto"
@ -19,6 +20,7 @@ import (
"github.com/letsencrypt/boulder/goodkey/sagoodkey"
bgrpc "github.com/letsencrypt/boulder/grpc"
"github.com/letsencrypt/boulder/issuance"
"github.com/letsencrypt/boulder/linter"
"github.com/letsencrypt/boulder/policy"
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.
CRLProfile issuance.CRLProfileConfig `validate:"-"`
Issuers []issuance.IssuerConfig `validate:"min=1,dive"`
LintConfig string
IgnoredLints []string
}
@ -234,6 +237,14 @@ func main() {
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)
cmd.FailOnError(err, "TLS config")
@ -295,8 +306,8 @@ func main() {
pa,
issuers,
c.CA.Issuance.DefaultCertificateProfileName,
c.CA.Issuance.IgnoredLints,
c.CA.Issuance.CertProfiles,
lints,
ecdsaAllowList,
c.CA.Expiry.Duration,
c.CA.Backdate.Duration,

View File

@ -40,6 +40,11 @@ services:
# TODO: Remove this when ServerAddress is deprecated in favor of SRV records
# and DNSAuthority.
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:
- 4001:4001 # ACMEv2
- 4002:4002 # OCSP
@ -53,6 +58,7 @@ services:
- bredis_4
- bconsul
- bjaeger
- bpkilint
entrypoint: test/entrypoint.sh
working_dir: &boulder_working_dir /boulder
@ -141,6 +147,13 @@ services:
bouldernet:
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:
# This network is primarily used for boulder services. It is also used by
# challtestsrv, which is used in the integration tests.

View File

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

View File

@ -27,10 +27,11 @@ var (
)
func defaultProfile() *Profile {
p, _ := NewProfile(defaultProfileConfig(), []string{
lints, _ := linter.NewRegistry([]string{
"w_ct_sct_policy_count_unsatisfied",
"e_scts_from_same_operator",
})
p, _ := NewProfile(defaultProfileConfig(), lints)
return p
}
@ -380,11 +381,13 @@ func TestIssueCommonName(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
cnProfile, err := NewProfile(defaultProfileConfig(), []string{
lints, err := linter.NewRegistry([]string{
"w_subject_common_name_included",
"w_ct_sct_policy_count_unsatisfied",
"e_scts_from_same_operator",
})
test.AssertNotError(t, err, "building test lint registry")
cnProfile, err := NewProfile(defaultProfileConfig(), lints)
test.AssertNotError(t, err, "NewProfile failed")
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
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")
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")
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
@ -566,7 +571,9 @@ func TestIssueBadLint(t *testing.T) {
fc := clock.NewFake()
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")
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
@ -690,11 +697,13 @@ func TestMismatchedProfiles(t *testing.T) {
issuer1, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
cnProfile, err := NewProfile(defaultProfileConfig(), []string{
lints, err := linter.NewRegistry([]string{
"w_subject_common_name_included",
"w_ct_sct_policy_count_unsatisfied",
"e_scts_from_same_operator",
})
test.AssertNotError(t, err, "building test lint registry")
cnProfile, err := NewProfile(defaultProfileConfig(), lints)
test.AssertNotError(t, err, "NewProfile failed")
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)
profileConfig := defaultProfileConfig()
profileConfig.AllowCommonName = false
noCNProfile, err := NewProfile(profileConfig, []string{
lints, err = linter.NewRegistry([]string{
"w_ct_sct_policy_count_unsatisfied",
"e_scts_from_same_operator",
})
test.AssertNotError(t, err, "building test lint registry")
noCNProfile, err := NewProfile(profileConfig, lints)
test.AssertNotError(t, err, "NewProfile failed")
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"
"path"
"regexp"
"strings"
"time"
"github.com/letsencrypt/boulder/cmd"
@ -25,6 +26,7 @@ func (srv *aiaTestSrv) handleIssuer(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
return
}
issuerName = strings.ReplaceAll(issuerName, "-", " ")
issuer, ok := srv.issuersByName[issuerName]
if !ok {

View File

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