Skip to content

Commit e0879bd

Browse files
committed
Add CP/CPS profile lints
1 parent c8420e9 commit e0879bd

16 files changed

Lines changed: 3226 additions & 147 deletions

linter/lints/common.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const (
2727
var (
2828
CPSV20Date = time.Date(2017, time.April, 13, 0, 0, 0, 0, time.UTC)
2929
CPSV33Date = time.Date(2021, time.June, 8, 0, 0, 0, 0, time.UTC)
30+
CPSV62Date = time.Date(2026, time.August, 1, 0, 0, 0, 0, time.UTC) // TKTK: Speculative!
3031
MozillaPolicy281Date = time.Date(2023, time.February, 15, 0, 0, 0, 0, time.UTC)
3132
)
3233

linter/lints/cpcps/helpers.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package cpcps
2+
3+
// This file contains constants and parsing utilities shared by the lints which
4+
// enforce the certificate profiles found in Section 7.1 of our CP/CPS. Only
5+
// mechanical helpers (extracting bytes, computing hashes, constructing
6+
// results) live here: every actual profile check is written out inline in the
7+
// lint which enforces it, so that each lint can be read top-to-bottom against
8+
// the text of the CP/CPS, and so that the profiles can diverge independently.
9+
10+
import (
11+
"encoding/pem"
12+
"fmt"
13+
14+
"github.com/zmap/zcrypto/encoding/asn1"
15+
"github.com/zmap/zcrypto/x509"
16+
"github.com/zmap/zcrypto/x509/pkix"
17+
"github.com/zmap/zlint/v3/lint"
18+
"golang.org/x/crypto/cryptobyte"
19+
cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1"
20+
)
21+
22+
var (
23+
// The AlgorithmIdentifier encodings specified by Section 7.1.3.2 of the
24+
// Baseline Requirements, which Section 7.1.3.2 of our CP/CPS incorporates
25+
// by reference.
26+
// https://github.com/letsencrypt/cp-cps/blob/6adcd83ff21e9571a39339048364edd6ba34ed39/CP-CPS.md?plain=1#L1115-L1117
27+
brSignatureAlgorithmIdentifiers = map[string]bool{
28+
// sha256WithRSAEncryption
29+
"300d06092a864886f70d01010b0500": true,
30+
// sha384WithRSAEncryption
31+
"300d06092a864886f70d01010c0500": true,
32+
// sha512WithRSAEncryption
33+
"300d06092a864886f70d01010d0500": true,
34+
// ecdsa-with-SHA256
35+
"300a06082a8648ce3d040302": true,
36+
// ecdsa-with-SHA384
37+
"300a06082a8648ce3d040303": true,
38+
// ecdsa-with-SHA512
39+
"300a06082a8648ce3d040304": true,
40+
}
41+
42+
// The SubjectPublicKeyInfo AlgorithmIdentifier encodings specified by
43+
// Section 7.1.3.1 of the Baseline Requirements, which Section 7.1.3.1 of
44+
// our CP/CPS incorporates by reference.
45+
// https://github.com/letsencrypt/cp-cps/blob/6adcd83ff21e9571a39339048364edd6ba34ed39/CP-CPS.md?plain=1#L1111-L1113
46+
spkiAlgorithmRSA = "300d06092a864886f70d0101010500"
47+
spkiAlgorithmP256 = "301306072a8648ce3d020106082a8648ce3d030107"
48+
spkiAlgorithmP384 = "301006072a8648ce3d020106052b81040022"
49+
spkiAlgorithmP521 = "301006072a8648ce3d020106052b81040023"
50+
51+
// Extension OIDs used by the profile lints.
52+
authorityInformationAccessOID = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 1}
53+
authorityKeyIdentifierOID = asn1.ObjectIdentifier{2, 5, 29, 35}
54+
basicConstraintsOID = asn1.ObjectIdentifier{2, 5, 29, 19}
55+
certificatePoliciesOID = asn1.ObjectIdentifier{2, 5, 29, 32}
56+
crlDistributionPointsOID = asn1.ObjectIdentifier{2, 5, 29, 31}
57+
extKeyUsageOID = asn1.ObjectIdentifier{2, 5, 29, 37}
58+
keyUsageOID = asn1.ObjectIdentifier{2, 5, 29, 15}
59+
subjectAltNameOID = asn1.ObjectIdentifier{2, 5, 29, 17}
60+
subjectKeyIdentifierOID = asn1.ObjectIdentifier{2, 5, 29, 14}
61+
sctListOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 2}
62+
63+
// The Baseline Requirements Domain Validated Reserved Policy Identifier.
64+
domainValidatedOID = asn1.ObjectIdentifier{2, 23, 140, 1, 2, 1}
65+
66+
// Subject attribute type OIDs.
67+
commonNameOID = asn1.ObjectIdentifier{2, 5, 4, 3}
68+
)
69+
70+
// Keys within the shared configuration stanza. These must match the toml
71+
// tags on IssuingCAConfig's fields, and are exported so that the linter
72+
// package can emit configuration using these keys.
73+
const (
74+
// GlobalConfigNamespace is the name of the TOML stanza from which
75+
// IssuingCAConfig is deserialized. It must match the namespace of zlint's
76+
// lint.Global higher-scoped configuration, which IssuingCAConfig embeds.
77+
GlobalConfigNamespace = "Global"
78+
// IssuerCertificateConfigKey configures the Issuing CA's certificate.
79+
IssuerCertificateConfigKey = "issuer_certificate"
80+
// ExistingCertificateConfigKey configures the pre-existing certificate of
81+
// a CA being cross-signed.
82+
ExistingCertificateConfigKey = "existing_certificate"
83+
)
84+
85+
// globalNamespace aliases zlint's lint.Global so that IssuingCAConfig can
86+
// embed it under an unexported field name. The promoted (unexported)
87+
// namespace method is what routes deserialization of IssuingCAConfig to the
88+
// shared [Global] stanza, via zlint's "higher-scoped configuration"
89+
// mechanism; the unexported field name makes zlint's reflection-based config
90+
// resolver skip the embedded field itself, which it could not deserialize.
91+
type globalNamespace = lint.Global //nolint:unused // Used in SharedConfig.
92+
93+
// SharedConfig is the lint configuration shared by every CP/CPS profile
94+
// lint. Rather than each lint carrying an identical stanza of its own, all of
95+
// them declare a pointer to this struct, which zlint fills from the single
96+
// shared [Global] stanza of the lint configuration.
97+
type SharedConfig struct {
98+
globalNamespace //nolint:unused // Used by zlint, not by us.
99+
// IssuerCertificatePEM must hold the PEM encoding of the Issuing CA's
100+
// certificate, so that the profile rows requiring byte-for-byte
101+
// correspondence with the Issuing CA can be enforced. If it is not
102+
// configured, the CP/CPS profile lints fail.
103+
IssuerCertificatePEM string `toml:"issuer_certificate" comment:"The PEM encoding of the Issuing CA's certificate."`
104+
// ExistingCertificatePEM must hold the PEM encoding of the existing CA
105+
// Certificate upon which a cross-certificate confers a second issuance
106+
// path. It is read only by the cross-certified subordinate CA profile
107+
// lint, and only the ceremony tool ever configures it, because only the
108+
// ceremony tool issues cross-certificates.
109+
ExistingCertificatePEM string `toml:"existing_certificate" comment:"The PEM encoding of the existing CA Certificate being cross-signed."`
110+
}
111+
112+
// issuerPEM returns the configured Issuing CA certificate PEM, or the empty
113+
// string if the receiver was never configured.
114+
func (c *SharedConfig) issuerPEM() string {
115+
if c == nil {
116+
return ""
117+
}
118+
return c.IssuerCertificatePEM
119+
}
120+
121+
// existingPEM returns the configured existing CA certificate PEM, or the
122+
// empty string if the receiver was never configured.
123+
func (c *SharedConfig) existingPEM() string {
124+
if c == nil {
125+
return ""
126+
}
127+
return c.ExistingCertificatePEM
128+
}
129+
130+
// errResult is a convenience constructor for a failing lint result.
131+
func errResult(details string) *lint.LintResult {
132+
return &lint.LintResult{Status: lint.Error, Details: details}
133+
}
134+
135+
// fatalResult is a convenience constructor for a fatal lint result, used when
136+
// the lint's own configuration is unusable.
137+
func fatalResult(details string) *lint.LintResult {
138+
return &lint.LintResult{Status: lint.Fatal, Details: details}
139+
}
140+
141+
// getOuterSignatureAlgorithm returns the DER bytes (including tag and length)
142+
// of the signatureAlgorithm field of the outer Certificate sequence.
143+
func getOuterSignatureAlgorithm(der []byte) ([]byte, error) {
144+
input := cryptobyte.String(der)
145+
var certificate cryptobyte.String
146+
if !input.ReadASN1(&certificate, cryptobyte_asn1.SEQUENCE) {
147+
return nil, fmt.Errorf("failed to parse certificate")
148+
}
149+
if !certificate.SkipASN1(cryptobyte_asn1.SEQUENCE) {
150+
return nil, fmt.Errorf("failed to parse tbsCertificate")
151+
}
152+
var signatureAlgorithm cryptobyte.String
153+
if !certificate.ReadASN1Element(&signatureAlgorithm, cryptobyte_asn1.SEQUENCE) {
154+
return nil, fmt.Errorf("failed to parse signatureAlgorithm")
155+
}
156+
return signatureAlgorithm, nil
157+
}
158+
159+
// getExtension returns the extension with the given OID, or nil if absent.
160+
func getExtension(c *x509.Certificate, oid asn1.ObjectIdentifier) *pkix.Extension {
161+
for _, ext := range c.Extensions {
162+
if ext.Id.Equal(oid) {
163+
return &pkix.Extension{Id: ext.Id, Critical: ext.Critical, Value: ext.Value}
164+
}
165+
}
166+
return nil
167+
}
168+
169+
// parseConfiguredCertificate parses a PEM certificate provided via lint
170+
// configuration. It returns a nil certificate and nil error if the
171+
// configuration string is empty; lints which require the certificate must
172+
// treat that as a failure.
173+
func parseConfiguredCertificate(pemBytes string) (*x509.Certificate, error) {
174+
if pemBytes == "" {
175+
return nil, nil
176+
}
177+
block, _ := pem.Decode([]byte(pemBytes))
178+
if block == nil || block.Type != "CERTIFICATE" {
179+
return nil, fmt.Errorf("failed to decode configured PEM certificate")
180+
}
181+
cert, err := x509.ParseCertificate(block.Bytes)
182+
if err != nil {
183+
return nil, fmt.Errorf("failed to parse configured certificate: %w", err)
184+
}
185+
return cert, nil
186+
}

0 commit comments

Comments
 (0)