|
| 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