Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 28 additions & 11 deletions cmd/ceremony/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,15 @@ type lintCert *x509.Certificate
// template certificate signed by a given issuer and returns a *lintCert or an
// error. The lint certificate is linted prior to being returned. The public key
// from the just issued lint certificate is checked by the GoodKey package.
func issueLintCertAndPerformLinting(tbs, issuer *x509.Certificate, subjectPubKey crypto.PublicKey, signer crypto.Signer, skipLints []string) (lintCert, error) {
bytes, err := linter.Check(tbs, subjectPubKey, issuer, signer, skipLints)
// When cross-signing, existing is the pre-existing certificate of the CA being
// cross-signed, allowing the CP/CPS profile lints to check correspondence with
// it; it is nil otherwise.
func issueLintCertAndPerformLinting(tbs, issuer *x509.Certificate, subjectPubKey crypto.PublicKey, signer crypto.Signer, existing *x509.Certificate, skipLints []string) (lintCert, error) {
lintConfig, err := linter.Config{}.WithExisting(existing)
if err != nil {
return nil, fmt.Errorf("unable to create lint config: %w", err)
}
bytes, err := linter.Check(tbs, subjectPubKey, issuer, signer, lintConfig, skipLints)
if err != nil {
return nil, fmt.Errorf("certificate failed pre-issuance lint: %w", err)
}
Expand All @@ -66,8 +73,10 @@ func issueLintCertAndPerformLinting(tbs, issuer *x509.Certificate, subjectPubKey
// postIssuanceLinting performs post-issuance linting on the raw bytes of a
// given certificate with the same set of lints as
// issueLintCertAndPerformLinting. The public key is also checked by the GoodKey
// package.
func postIssuanceLinting(fc *x509.Certificate, skipLints []string) error {
// package. The issuer and existing certificates, when non-nil, are supplied to
// the CP/CPS profile lints so that they can perform their correspondence
// checks.
func postIssuanceLinting(fc, issuer, existing *x509.Certificate, skipLints []string) error {
if fc == nil {
return fmt.Errorf("certificate was not provided")
}
Expand All @@ -77,7 +86,15 @@ func postIssuanceLinting(fc *x509.Certificate, skipLints []string) error {
// lint. This should be treated as ZLint rejecting the certificate
return fmt.Errorf("unable to parse certificate: %s", err)
}
registry, err := linter.NewRegistry(skipLints)
lintConfig, err := linter.Config{}.WithIssuer(issuer)
if err != nil {
return fmt.Errorf("unable to create lint config: %s", err)
}
lintConfig, err = lintConfig.WithExisting(existing)
if err != nil {
return fmt.Errorf("unable to create lint config: %s", err)
}
registry, err := linter.NewRegistryWithConfig(skipLints, lintConfig)
if err != nil {
return fmt.Errorf("unable to create zlint registry: %s", err)
}
Expand Down Expand Up @@ -586,15 +603,15 @@ func rootCeremony(configBytes []byte) error {
if err != nil {
return fmt.Errorf("failed to create certificate profile: %s", err)
}
lintCert, err := issueLintCertAndPerformLinting(template, template, keyInfo.key, signer, config.SkipLints)
lintCert, err := issueLintCertAndPerformLinting(template, template, keyInfo.key, signer, nil, config.SkipLints)
if err != nil {
return err
}
finalCert, err := signAndWriteCert(template, template, lintCert, keyInfo.key, signer, config.Outputs.CertificatePath)
if err != nil {
return err
}
err = postIssuanceLinting(finalCert, config.SkipLints)
err = postIssuanceLinting(finalCert, nil, nil, config.SkipLints)
if err != nil {
return err
}
Expand Down Expand Up @@ -631,7 +648,7 @@ func intermediateCeremony(configBytes []byte) error {
return fmt.Errorf("failed to create certificate profile: %s", err)
}
template.AuthorityKeyId = issuer.SubjectKeyId
lintCert, err := issueLintCertAndPerformLinting(template, issuer, pub, signer, config.SkipLints)
lintCert, err := issueLintCertAndPerformLinting(template, issuer, pub, signer, nil, config.SkipLints)
if err != nil {
return err
}
Expand All @@ -646,7 +663,7 @@ func intermediateCeremony(configBytes []byte) error {
if !bytes.Equal(lintCert.RawTBSCertificate, finalCert.RawTBSCertificate) {
return fmt.Errorf("mismatch between lintCert and finalCert RawTBSCertificate DER bytes: \"%x\" != \"%x\"", lintCert.RawTBSCertificate, finalCert.RawTBSCertificate)
}
err = postIssuanceLinting(finalCert, config.SkipLints)
err = postIssuanceLinting(finalCert, issuer, nil, config.SkipLints)
if err != nil {
return err
}
Expand Down Expand Up @@ -687,7 +704,7 @@ func crossCertCeremony(configBytes []byte) error {
return fmt.Errorf("failed to create certificate profile: %s", err)
}
template.AuthorityKeyId = issuer.SubjectKeyId
lintCert, err := issueLintCertAndPerformLinting(template, issuer, pub, signer, config.SkipLints)
lintCert, err := issueLintCertAndPerformLinting(template, issuer, pub, signer, toBeCrossSigned, config.SkipLints)
if err != nil {
return err
}
Expand Down Expand Up @@ -748,7 +765,7 @@ func crossCertCeremony(configBytes []byte) error {
if !bytes.Equal(lintCert.RawTBSCertificate, finalCert.RawTBSCertificate) {
return fmt.Errorf("mismatch between lintCert and finalCert RawTBSCertificate DER bytes: \"%x\" != \"%x\"", lintCert.RawTBSCertificate, finalCert.RawTBSCertificate)
}
err = postIssuanceLinting(finalCert, config.SkipLints)
err = postIssuanceLinting(finalCert, issuer, toBeCrossSigned, config.SkipLints)
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/ceremony/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1292,7 +1292,7 @@ func TestSignAndWriteNoLintCert(t *testing.T) {

func TestPostIssuanceLinting(t *testing.T) {
clk := clock.New()
err := postIssuanceLinting(nil, nil)
err := postIssuanceLinting(nil, nil, nil, nil)
test.AssertError(t, err, "should have failed because no certificate was provided")

testKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
Expand All @@ -1306,6 +1306,6 @@ func TestPostIssuanceLinting(t *testing.T) {
test.AssertNotError(t, err, "unable to create certificate")
parsedCert, err := x509.ParseCertificate(certDer)
test.AssertNotError(t, err, "unable to parse DER bytes")
err = postIssuanceLinting(parsedCert, nil)
err = postIssuanceLinting(parsedCert, nil, nil, nil)
test.AssertNotError(t, err, "should not have errored")
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
github.com/miekg/dns v1.1.62
github.com/miekg/pkcs11 v1.1.2
github.com/nxadm/tail v1.4.11
github.com/pelletier/go-toml v1.9.5
github.com/prometheus/client_golang v1.22.0
github.com/prometheus/client_model v0.6.1
github.com/redis/go-redis/extra/redisotel/v9 v9.5.3
Expand Down Expand Up @@ -76,7 +77,6 @@ require (
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/poy/onpar v1.1.2 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
Expand Down
22 changes: 16 additions & 6 deletions issuance/cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,14 @@ type Profile struct {

maxCertificateSize int

// lints is the registry of lints to run against certificates issued under
// this profile. It carries no lint configuration of its own: at issuance
// time it is combined with a configuration derived from lintConfig.
lints lint.Registry
// lintConfig is the in-memory contents of this profile's zlint config
// file. At issuance time it is augmented with the issuing Issuer's
// certificate via WithIssuer.
lintConfig linter.Config
}

// NewProfile converts the profile config into a usable profile.
Expand All @@ -102,11 +109,9 @@ func NewProfile(profileConfig ProfileConfig) (*Profile, error) {

lints, err := linter.NewRegistry(profileConfig.IgnoredLints)
cmd.FailOnError(err, "Failed to create zlint registry")
if profileConfig.LintConfig != "" {
lintconfig, err := lint.NewConfigFromFile(profileConfig.LintConfig)
cmd.FailOnError(err, "Failed to load zlint config file")
lints.SetConfiguration(lintconfig)
}

lintConfig, err := linter.LoadConfigFile(profileConfig.LintConfig)
cmd.FailOnError(err, "Failed to load zlint config file")

sp := &Profile{
omitCommonName: profileConfig.OmitCommonName,
Expand All @@ -118,6 +123,7 @@ func NewProfile(profileConfig ProfileConfig) (*Profile, error) {
maxValidity: profileConfig.MaxValidityPeriod.Duration,
maxCertificateSize: profileConfig.MaxCertificateSize,
lints: lints,
lintConfig: lintConfig,
}

return sp, nil
Expand Down Expand Up @@ -386,7 +392,11 @@ func (i *Issuer) Prepare(prof *Profile, req *IssuanceRequest) ([]byte, *issuance

// check that the tbsCertificate is properly formed by signing it
// with a throwaway key and then linting it using zlint
lintCertBytes, err := i.Linter.Check(template, req.PublicKey.PublicKey, prof.lints)
lintConfig, err := prof.lintConfig.WithIssuer(i.Cert.Certificate)
if err != nil {
return nil, nil, fmt.Errorf("building lint config: %w", err)
}
lintCertBytes, err := i.Linter.Check(template, req.PublicKey.PublicKey, prof.lints, lintConfig)
if err != nil {
return nil, nil, fmt.Errorf("tbsCertificate linting failed: %w", err)
}
Expand Down
121 changes: 121 additions & 0 deletions linter/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package linter

import (
"crypto/x509"
"encoding/pem"
"fmt"
"os"

"github.com/pelletier/go-toml"
"github.com/zmap/zlint/v3/lint"

"github.com/letsencrypt/boulder/linter/lints/cpcps"
)

// Config is a validated, in-memory zlint lint configuration. The zero value
// is an empty configuration.
type Config struct {
// The zlint package only accepts configuration as TOML (and re-parses it on
// every lint pass anyway), so we store the config in zlint's format.
toml string
}

// LoadConfigFile reads and validates a zlint TOML config file. An empty path
// yields an empty Config. It is an error for the file to set the shared
// configuration keys read by the CP/CPS profile lints: those are derived from
// certificates via WithIssuer and WithExisting instead.
func LoadConfigFile(path string) (Config, error) {
if path == "" {
return Config{}, nil
}
contents, err := os.ReadFile(path)
if err != nil {
return Config{}, fmt.Errorf("failed to read zlint config file: %w", err)
}
tree, err := toml.LoadBytes(contents)
if err != nil {
return Config{}, fmt.Errorf("failed to parse zlint config file %q: %w", path, err)
}
for _, key := range []string{cpcps.IssuerCertificateConfigKey, cpcps.ExistingCertificateConfigKey} {
if tree.HasPath([]string{cpcps.GlobalConfigNamespace, key}) {
return Config{}, fmt.Errorf("zlint config file %q must not set %s.%s: it is derived from the issuer certificate", path, cpcps.GlobalConfigNamespace, key)
}
}
return Config{toml: string(contents)}, nil
}

// WithIssuer returns a copy of the Config with a stanza holding the PEM of the
// issuer's certificate. This is necessary for the CP/CPS profile lints, which
// check that certain fields of the certificate being linted match the issuer. A
// nil issuer, or one with no raw DER bytes (i.e. a to-be-signed template rather
// than a real certificate, as in a self-signed root ceremony), returns the
// Config unchanged.
func (c Config) WithIssuer(issuer *x509.Certificate) (Config, error) {
if issuer == nil || len(issuer.Raw) == 0 {
return c, nil
}
issuerPEM := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: issuer.Raw}))
return c.set(cpcps.IssuerCertificateConfigKey, issuerPEM)
}

// WithExisting returns a copy of the Config with a stanza holding the PEM of
// the pre-existing certificate which is being cross-signed. This is necessary
// for the CP/CPS Cross-Certified Subordinate CA Certificate lint. It is only
// used by the ceremony tool. A nil existing certificate, or one with no raw DER
// bytes, returns the Config unchanged.
func (c Config) WithExisting(existing *x509.Certificate) (Config, error) {
if existing == nil || len(existing.Raw) == 0 {
return c, nil
}
existingPEM := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: existing.Raw}))
return c.set(cpcps.ExistingCertificateConfigKey, existingPEM)
}

// set returns a copy of the Config with the given key (inside the Global
// namespace) set to the given value.
func (c Config) set(key string, value string) (Config, error) {
tree, err := toml.Load(c.toml)
if err != nil {
return Config{}, fmt.Errorf("failed to parse zlint config: %w", err)
}
tree.SetPath([]string{cpcps.GlobalConfigNamespace, key}, value)
tomlString, err := tree.ToTomlString()
if err != nil {
return Config{}, fmt.Errorf("failed to serialize zlint config: %w", err)
}
return Config{toml: tomlString}, nil
}

// build converts the Config into the lint.Configuration that zlint consumes.
func (c Config) build() (lint.Configuration, error) {
return lint.NewConfigFromString(c.toml)
}

// configuredRegistry implements the zlint.Registry interface by embedding a
// normal Registry but replacing the GetConfiguration method with one that
// returns our own config. This allows us to easily supply different config
// objects for each lint pass without having to modify the underlying registry.
type configuredRegistry struct {
lint.Registry
config lint.Configuration
}

// GetConfiguration returns the config associated with this registry. It
// satisfies the zlint.Registry interface.
func (r configuredRegistry) GetConfiguration() lint.Configuration {
return r.config
}

// NewRegistryWithConfig is like NewRegistry, but the returned registry also
// carries the contents of the given Config.
func NewRegistryWithConfig(skipLints []string, config Config) (lint.Registry, error) {
reg, err := NewRegistry(skipLints)
if err != nil {
return nil, err
}
lintConfig, err := config.build()
if err != nil {
return nil, err
}
return configuredRegistry{reg, lintConfig}, nil
}
Loading