From 9139a692196d11abc5fa818317d6b288c75b5bcc Mon Sep 17 00:00:00 2001 From: Aaron Gable Date: Tue, 28 Jul 2026 16:59:30 -0700 Subject: [PATCH 1/2] Create scaffolding for configuring lints with issuers --- cmd/ceremony/main.go | 39 ++++++-- cmd/ceremony/main_test.go | 4 +- issuance/cert.go | 22 ++-- linter/config.go | 121 ++++++++++++++++++++++ linter/config_test.go | 183 ++++++++++++++++++++++++++++++++++ linter/linter.go | 22 +++- linter/linter_test.go | 2 +- linter/lints/cpcps/helpers.go | 65 ++++++++++++ 8 files changed, 435 insertions(+), 23 deletions(-) create mode 100644 linter/config.go create mode 100644 linter/config_test.go create mode 100644 linter/lints/cpcps/helpers.go diff --git a/cmd/ceremony/main.go b/cmd/ceremony/main.go index d7f87b80515..0ee69aa9fb7 100644 --- a/cmd/ceremony/main.go +++ b/cmd/ceremony/main.go @@ -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) } @@ -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") } @@ -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) } @@ -586,7 +603,7 @@ 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 } @@ -594,7 +611,7 @@ func rootCeremony(configBytes []byte) error { if err != nil { return err } - err = postIssuanceLinting(finalCert, config.SkipLints) + err = postIssuanceLinting(finalCert, nil, nil, config.SkipLints) if err != nil { return err } @@ -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 } @@ -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 } @@ -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 } @@ -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 } diff --git a/cmd/ceremony/main_test.go b/cmd/ceremony/main_test.go index 697d97ec172..c6cd04364fa 100644 --- a/cmd/ceremony/main_test.go +++ b/cmd/ceremony/main_test.go @@ -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) @@ -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") } diff --git a/issuance/cert.go b/issuance/cert.go index f7e23387689..56b42c150f8 100644 --- a/issuance/cert.go +++ b/issuance/cert.go @@ -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. @@ -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, @@ -118,6 +123,7 @@ func NewProfile(profileConfig ProfileConfig) (*Profile, error) { maxValidity: profileConfig.MaxValidityPeriod.Duration, maxCertificateSize: profileConfig.MaxCertificateSize, lints: lints, + lintConfig: lintConfig, } return sp, nil @@ -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) } diff --git a/linter/config.go b/linter/config.go new file mode 100644 index 00000000000..4e49e99c7a4 --- /dev/null +++ b/linter/config.go @@ -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 +} diff --git a/linter/config_test.go b/linter/config_test.go new file mode 100644 index 00000000000..ca235ff9931 --- /dev/null +++ b/linter/config_test.go @@ -0,0 +1,183 @@ +package linter + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "os" + "path" + "strings" + "testing" + "time" + + "github.com/letsencrypt/boulder/linter/lints/cpcps" +) + +// testCert generates a minimal self-signed certificate with the given CN. +func testCert(t *testing.T, cn string) *x509.Certificate { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("generating test key: %s", err) + } + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: cn}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) + if err != nil { + t.Fatalf("creating test certificate: %s", err) + } + cert, err := x509.ParseCertificate(der) + if err != nil { + t.Fatalf("parsing test certificate: %s", err) + } + return cert +} + +func TestLoadConfigFile(t *testing.T) { + t.Parallel() + + cfg, err := LoadConfigFile("") + if err != nil { + t.Errorf("got error %q for empty path, want nil", err) + } + if cfg.toml != "" { + t.Errorf("got non-empty config %q for empty path", cfg.toml) + } + + dir := t.TempDir() + + good := path.Join(dir, "good.toml") + err = os.WriteFile(good, []byte("[w_subject_common_name_included]\nsomething = true\n"), 0644) + if err != nil { + t.Fatalf("writing config file: %s", err) + } + _, err = LoadConfigFile(good) + if err != nil { + t.Errorf("got error %q for valid config file, want nil", err) + } + + garbage := path.Join(dir, "garbage.toml") + err = os.WriteFile(garbage, []byte("this is not toml"), 0644) + if err != nil { + t.Fatalf("writing config file: %s", err) + } + _, err = LoadConfigFile(garbage) + if err == nil { + t.Error("got nil error for invalid config file, want error") + } + + // Config files may not set the shared keys read by the CP/CPS profile + // lints: their configuration is derived from the issuer certificate. + // Other keys in the shared stanza are permitted. + reserved := path.Join(dir, "reserved.toml") + err = os.WriteFile(reserved, []byte("[Global]\nissuer_certificate = \"bogus\"\n"), 0644) + if err != nil { + t.Fatalf("writing config file: %s", err) + } + _, err = LoadConfigFile(reserved) + if err == nil || !strings.Contains(err.Error(), "must not set") { + t.Errorf("got %v for config file with reserved key, want rejection", err) + } + + otherGlobal := path.Join(dir, "other-global.toml") + err = os.WriteFile(otherGlobal, []byte("[Global]\nsomething_else = true\n"), 0644) + if err != nil { + t.Fatalf("writing config file: %s", err) + } + _, err = LoadConfigFile(otherGlobal) + if err != nil { + t.Errorf("got error %q for config file with unrelated shared-stanza key, want nil", err) + } + + _, err = LoadConfigFile(path.Join(dir, "does-not-exist.toml")) + if err == nil { + t.Error("got nil error for nonexistent config file, want error") + } +} + +// TestConfigDerivation exercises the whole layering path: a config file +// loaded from disk, augmented with an issuer and an existing certificate, +// built into a single zlint Configuration in which all three are visible. +func TestConfigDerivation(t *testing.T) { + t.Parallel() + + filePath := path.Join(t.TempDir(), "zlint.toml") + err := os.WriteFile(filePath, []byte("[w_subject_common_name_included]\nsomething = true\n"), 0644) + if err != nil { + t.Fatalf("writing config file: %s", err) + } + config, err := LoadConfigFile(filePath) + if err != nil { + t.Fatalf("loading config file: %s", err) + } + + issuer := testCert(t, "issuer") + config, err = config.WithIssuer(issuer) + if err != nil { + t.Fatalf("adding issuer to config: %s", err) + } + + existing := testCert(t, "existing") + config, err = config.WithExisting(existing) + if err != nil { + t.Fatalf("adding existing certificate to config: %s", err) + } + + merged, err := config.build() + if err != nil { + t.Fatalf("building lint configuration: %s", err) + } + + var gotCross struct { + IssuerCertificatePEM string `toml:"issuer_certificate"` + ExistingCertificatePEM string `toml:"existing_certificate"` + } + err = merged.Configure(&gotCross, cpcps.GlobalConfigNamespace) + if err != nil { + t.Fatalf("deserializing issuer lint configuration: %s", err) + } + + issuerBlock, _ := pem.Decode([]byte(gotCross.IssuerCertificatePEM)) + if issuerBlock == nil { + t.Fatal("issuer_certificate did not round-trip as PEM") + } + if !issuer.Equal(mustParse(t, issuerBlock.Bytes)) { + t.Error("issuer_certificate does not match the configured issuer") + } + + existingBlock, _ := pem.Decode([]byte(gotCross.ExistingCertificatePEM)) + if existingBlock == nil { + t.Fatal("existing_certificate did not round-trip as PEM") + } + if !existing.Equal(mustParse(t, existingBlock.Bytes)) { + t.Error("existing_certificate does not match the configured existing certificate") + } + + var gotFile struct { + Something bool `toml:"something"` + } + err = merged.Configure(&gotFile, "w_subject_common_name_included") + if err != nil { + t.Fatalf("deserializing file lint configuration: %s", err) + } + if !gotFile.Something { + t.Error("config-file section is not visible in the merged configuration") + } +} + +func mustParse(t *testing.T, der []byte) *x509.Certificate { + t.Helper() + cert, err := x509.ParseCertificate(der) + if err != nil { + t.Fatalf("parsing certificate: %s", err) + } + return cert +} diff --git a/linter/linter.go b/linter/linter.go index e52aa6a3e81..48f9258715b 100644 --- a/linter/linter.go +++ b/linter/linter.go @@ -30,18 +30,23 @@ var ErrLinting = fmt.Errorf("failed lint(s)") // primary public interface of this package, but it can be inefficient; creating // a new signer and a new lint registry are expensive operations which // performance-sensitive clients may want to cache via linter.New(). -func Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey, realIssuer *x509.Certificate, realSigner crypto.Signer, skipLints []string) ([]byte, error) { +func Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey, realIssuer *x509.Certificate, realSigner crypto.Signer, config Config, skipLints []string) ([]byte, error) { linter, err := New(realIssuer, realSigner) if err != nil { return nil, err } + config, err = config.WithIssuer(realIssuer) + if err != nil { + return nil, err + } + reg, err := NewRegistry(skipLints) if err != nil { return nil, err } - lintCertBytes, err := linter.Check(tbs, subjectPubKey, reg) + lintCertBytes, err := linter.Check(tbs, subjectPubKey, reg, config) if err != nil { return nil, err } @@ -98,7 +103,18 @@ func New(realIssuer *x509.Certificate, realSigner crypto.Signer) (*Linter, error // replaced with the linter's pubkey so that it appears self-signed. It returns // an error if any lint fails. On success it also returns the DER bytes of the // linting certificate. -func (l *Linter) Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey, reg lint.Registry) ([]byte, error) { +func (l *Linter) Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey, reg lint.Registry, config Config) ([]byte, error) { + if reg == nil { + reg = lint.GlobalRegistry() + } + + lintConfig, err := config.build() + if err != nil { + return nil, err + } + + reg = configuredRegistry{reg, lintConfig} + lintPubKey := subjectPubKey selfSigned, err := core.PublicKeysEqual(subjectPubKey, l.realPubKey) if err != nil { diff --git a/linter/linter_test.go b/linter/linter_test.go index 3b35578e907..b6cfebd917d 100644 --- a/linter/linter_test.go +++ b/linter/linter_test.go @@ -97,7 +97,7 @@ func TestMakeIssuer(t *testing.T) { ee := &x509.Certificate{} - lintCertBytes, err := linter.Check(ee, eeKey.Public(), nil) + lintCertBytes, err := linter.Check(ee, eeKey.Public(), nil, Config{}) if err != nil { t.Fatal(err) } diff --git a/linter/lints/cpcps/helpers.go b/linter/lints/cpcps/helpers.go new file mode 100644 index 00000000000..f637eef7071 --- /dev/null +++ b/linter/lints/cpcps/helpers.go @@ -0,0 +1,65 @@ +package cpcps + +import ( + "github.com/zmap/zlint/v3/lint" +) + +// Keys within the shared configuration stanza. These must match the toml +// tags on SharedConfig's fields, and are exported so that the linter +// package can emit configuration using these keys. +const ( + // GlobalConfigNamespace is the name of the TOML stanza from which + // IssuingCAConfig is deserialized. It must match the namespace of zlint's + // lint.Global higher-scoped configuration, which IssuingCAConfig embeds. + GlobalConfigNamespace = "Global" + // IssuerCertificateConfigKey configures the Issuing CA's certificate. + IssuerCertificateConfigKey = "issuer_certificate" + // ExistingCertificateConfigKey configures the pre-existing certificate of + // a CA being cross-signed. + ExistingCertificateConfigKey = "existing_certificate" +) + +// globalNamespace aliases zlint's lint.Global so that IssuingCAConfig can +// embed it under an unexported field name. The promoted (unexported) +// namespace method is what routes deserialization of IssuingCAConfig to the +// shared [Global] stanza, via zlint's "higher-scoped configuration" +// mechanism; the unexported field name makes zlint's reflection-based config +// resolver skip the embedded field itself, which it could not deserialize. +type globalNamespace = lint.Global //nolint:unused // Used in SharedConfig. + +// SharedConfig is the lint configuration shared by every CP/CPS profile +// lint. Rather than each lint carrying an identical stanza of its own, all of +// them declare a pointer to this struct, which zlint fills from the single +// shared [Global] stanza of the lint configuration. +type SharedConfig struct { + globalNamespace //nolint:unused // Used by zlint, not by us. + // IssuerCertificatePEM must hold the PEM encoding of the Issuing CA's + // certificate, so that the profile rows requiring byte-for-byte + // correspondence with the Issuing CA can be enforced. If it is not + // configured, the CP/CPS profile lints fail. + IssuerCertificatePEM string `toml:"issuer_certificate" comment:"The PEM encoding of the Issuing CA's certificate."` + // ExistingCertificatePEM must hold the PEM encoding of the existing CA + // Certificate upon which a cross-certificate confers a second issuance + // path. It is read only by the cross-certified subordinate CA profile + // lint, and only the ceremony tool ever configures it, because only the + // ceremony tool issues cross-certificates. + ExistingCertificatePEM string `toml:"existing_certificate" comment:"The PEM encoding of the existing CA Certificate being cross-signed."` +} + +// issuerPEM returns the configured Issuing CA certificate PEM, or the empty +// string if the receiver was never configured. +func (c *SharedConfig) issuerPEM() string { //nolint:unused // Will be used in a followup PR. + if c == nil { + return "" + } + return c.IssuerCertificatePEM +} + +// existingPEM returns the configured existing CA certificate PEM, or the +// empty string if the receiver was never configured. +func (c *SharedConfig) existingPEM() string { //nolint:unused // Will be used in a followup PR. + if c == nil { + return "" + } + return c.ExistingCertificatePEM +} From e19eb5a646b7f2988a2234bcf9407413cc0a50d0 Mon Sep 17 00:00:00 2001 From: Aaron Gable Date: Tue, 28 Jul 2026 17:08:36 -0700 Subject: [PATCH 2/2] Fix go.mod --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index b64ab28e851..3a38c0bdbfb 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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