github.com/letsencrypt/boulder@v0.20251208.0/linter/linter.go (about) 1 package linter 2 3 import ( 4 "bytes" 5 "crypto" 6 "crypto/ecdsa" 7 "crypto/rand" 8 "crypto/rsa" 9 "crypto/x509" 10 "fmt" 11 "strings" 12 13 zlintx509 "github.com/zmap/zcrypto/x509" 14 "github.com/zmap/zlint/v3" 15 "github.com/zmap/zlint/v3/lint" 16 17 "github.com/letsencrypt/boulder/core" 18 19 _ "github.com/letsencrypt/boulder/linter/lints/cabf_br" 20 _ "github.com/letsencrypt/boulder/linter/lints/chrome" 21 _ "github.com/letsencrypt/boulder/linter/lints/cpcps" 22 _ "github.com/letsencrypt/boulder/linter/lints/rfc" 23 ) 24 25 var ErrLinting = fmt.Errorf("failed lint(s)") 26 27 // Check accomplishes the entire process of linting: it generates a throwaway 28 // signing key, uses that to create a linting cert, and runs a default set of 29 // lints (everything except for the ETSI and EV lints) against it. If the 30 // subjectPubKey and realSigner indicate that this is a self-signed cert, the 31 // cert will have its pubkey replaced to also be self-signed. This is the 32 // primary public interface of this package, but it can be inefficient; creating 33 // a new signer and a new lint registry are expensive operations which 34 // performance-sensitive clients may want to cache via linter.New(). 35 func Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey, realIssuer *x509.Certificate, realSigner crypto.Signer, skipLints []string) ([]byte, error) { 36 linter, err := New(realIssuer, realSigner) 37 if err != nil { 38 return nil, err 39 } 40 41 reg, err := NewRegistry(skipLints) 42 if err != nil { 43 return nil, err 44 } 45 46 lintCertBytes, err := linter.Check(tbs, subjectPubKey, reg) 47 if err != nil { 48 return nil, err 49 } 50 51 return lintCertBytes, nil 52 } 53 54 // CheckCRL is like Check, but for CRLs. 55 func CheckCRL(tbs *x509.RevocationList, realIssuer *x509.Certificate, realSigner crypto.Signer, skipLints []string) error { 56 linter, err := New(realIssuer, realSigner) 57 if err != nil { 58 return err 59 } 60 61 reg, err := NewRegistry(skipLints) 62 if err != nil { 63 return err 64 } 65 66 return linter.CheckCRL(tbs, reg) 67 } 68 69 // Linter is capable of linting a to-be-signed (TBS) certificate. It does so by 70 // signing that certificate with a throwaway private key and a fake issuer whose 71 // public key matches the throwaway private key, and then running the resulting 72 // certificate through a registry of zlint lints. 73 type Linter struct { 74 issuer *x509.Certificate 75 signer crypto.Signer 76 realPubKey crypto.PublicKey 77 } 78 79 // New constructs a Linter. It uses the provided real certificate and signer 80 // (private key) to generate a matching fake keypair and issuer cert that will 81 // be used to sign the lint certificate. It uses the provided list of lint names 82 // to skip to filter the zlint global registry to only those lints which should 83 // be run. 84 func New(realIssuer *x509.Certificate, realSigner crypto.Signer) (*Linter, error) { 85 lintSigner, err := makeSigner(realSigner) 86 if err != nil { 87 return nil, err 88 } 89 lintIssuer, err := makeIssuer(realIssuer, lintSigner) 90 if err != nil { 91 return nil, err 92 } 93 return &Linter{lintIssuer, lintSigner, realSigner.Public()}, nil 94 } 95 96 // Check signs the given TBS certificate using the Linter's fake issuer cert and 97 // private key, then runs the resulting certificate through all lints in reg. 98 // If the subjectPubKey is identical to the public key of the real signer 99 // used to create this linter, then the throwaway cert will have its pubkey 100 // replaced with the linter's pubkey so that it appears self-signed. It returns 101 // an error if any lint fails. On success it also returns the DER bytes of the 102 // linting certificate. 103 func (l Linter) Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey, reg lint.Registry) ([]byte, error) { 104 lintPubKey := subjectPubKey 105 selfSigned, err := core.PublicKeysEqual(subjectPubKey, l.realPubKey) 106 if err != nil { 107 return nil, err 108 } 109 if selfSigned { 110 lintPubKey = l.signer.Public() 111 } 112 113 lintCertBytes, cert, err := makeLintCert(tbs, lintPubKey, l.issuer, l.signer) 114 if err != nil { 115 return nil, err 116 } 117 118 lintRes := zlint.LintCertificateEx(cert, reg) 119 err = ProcessResultSet(lintRes) 120 if err != nil { 121 return nil, err 122 } 123 124 return lintCertBytes, nil 125 } 126 127 // CheckCRL signs the given RevocationList template using the Linter's fake 128 // issuer cert and private key, then runs the resulting CRL through all CRL 129 // lints in the registry. It returns an error if any check fails. 130 func (l Linter) CheckCRL(tbs *x509.RevocationList, reg lint.Registry) error { 131 crl, err := makeLintCRL(tbs, l.issuer, l.signer) 132 if err != nil { 133 return err 134 } 135 lintRes := zlint.LintRevocationListEx(crl, reg) 136 return ProcessResultSet(lintRes) 137 } 138 139 func makeSigner(realSigner crypto.Signer) (crypto.Signer, error) { 140 var lintSigner crypto.Signer 141 var err error 142 switch k := realSigner.Public().(type) { 143 case *rsa.PublicKey: 144 lintSigner, err = rsa.GenerateKey(rand.Reader, k.Size()*8) 145 if err != nil { 146 return nil, fmt.Errorf("failed to create RSA lint signer: %w", err) 147 } 148 case *ecdsa.PublicKey: 149 lintSigner, err = ecdsa.GenerateKey(k.Curve, rand.Reader) 150 if err != nil { 151 return nil, fmt.Errorf("failed to create ECDSA lint signer: %w", err) 152 } 153 default: 154 return nil, fmt.Errorf("unsupported lint signer type: %T", k) 155 } 156 return lintSigner, nil 157 } 158 159 func makeIssuer(realIssuer *x509.Certificate, lintSigner crypto.Signer) (*x509.Certificate, error) { 160 lintIssuerTBS := &x509.Certificate{ 161 // This is nearly the full list of attributes that 162 // x509.CreateCertificate() says it carries over from the template. 163 // Constructing this TBS certificate in this way ensures that the 164 // resulting lint issuer is as identical to the real issuer as we can 165 // get, without sharing a public key. 166 // 167 // We do not copy the SignatureAlgorithm field while constructing the 168 // lintIssuer because the lintIssuer is self-signed. Depending on the 169 // realIssuer, which could be either an intermediate or cross-signed 170 // intermediate, the SignatureAlgorithm of that certificate may differ 171 // from the root certificate that had signed it. 172 AuthorityKeyId: realIssuer.AuthorityKeyId, 173 BasicConstraintsValid: realIssuer.BasicConstraintsValid, 174 CRLDistributionPoints: realIssuer.CRLDistributionPoints, 175 DNSNames: realIssuer.DNSNames, 176 EmailAddresses: realIssuer.EmailAddresses, 177 ExcludedDNSDomains: realIssuer.ExcludedDNSDomains, 178 ExcludedEmailAddresses: realIssuer.ExcludedEmailAddresses, 179 ExcludedIPRanges: realIssuer.ExcludedIPRanges, 180 ExcludedURIDomains: realIssuer.ExcludedURIDomains, 181 ExtKeyUsage: realIssuer.ExtKeyUsage, 182 ExtraExtensions: realIssuer.ExtraExtensions, 183 IPAddresses: realIssuer.IPAddresses, 184 IsCA: realIssuer.IsCA, 185 IssuingCertificateURL: realIssuer.IssuingCertificateURL, 186 KeyUsage: realIssuer.KeyUsage, 187 MaxPathLen: realIssuer.MaxPathLen, 188 MaxPathLenZero: realIssuer.MaxPathLenZero, 189 NotAfter: realIssuer.NotAfter, 190 NotBefore: realIssuer.NotBefore, 191 OCSPServer: realIssuer.OCSPServer, 192 PermittedDNSDomains: realIssuer.PermittedDNSDomains, 193 PermittedDNSDomainsCritical: realIssuer.PermittedDNSDomainsCritical, 194 PermittedEmailAddresses: realIssuer.PermittedEmailAddresses, 195 PermittedIPRanges: realIssuer.PermittedIPRanges, 196 PermittedURIDomains: realIssuer.PermittedURIDomains, 197 Policies: realIssuer.Policies, 198 SerialNumber: realIssuer.SerialNumber, 199 Subject: realIssuer.Subject, 200 SubjectKeyId: realIssuer.SubjectKeyId, 201 URIs: realIssuer.URIs, 202 UnknownExtKeyUsage: realIssuer.UnknownExtKeyUsage, 203 } 204 lintIssuerBytes, err := x509.CreateCertificate(rand.Reader, lintIssuerTBS, lintIssuerTBS, lintSigner.Public(), lintSigner) 205 if err != nil { 206 return nil, fmt.Errorf("failed to create lint issuer: %w", err) 207 } 208 lintIssuer, err := x509.ParseCertificate(lintIssuerBytes) 209 if err != nil { 210 return nil, fmt.Errorf("failed to parse lint issuer: %w", err) 211 } 212 return lintIssuer, nil 213 } 214 215 // NewRegistry returns a zlint Registry with irrelevant (ETSI, EV) lints 216 // excluded. This registry also includes all custom lints defined in Boulder. 217 func NewRegistry(skipLints []string) (lint.Registry, error) { 218 reg, err := lint.GlobalRegistry().Filter(lint.FilterOptions{ 219 ExcludeNames: skipLints, 220 ExcludeSources: []lint.LintSource{ 221 // Excluded because Boulder does not issue EV certs. 222 lint.CABFEVGuidelines, 223 // Excluded because Boulder does not use the 224 // ETSI EN 319 412-5 qcStatements extension. 225 lint.EtsiEsi, 226 }, 227 }) 228 if err != nil { 229 return nil, fmt.Errorf("failed to create lint registry: %w", err) 230 } 231 return reg, nil 232 } 233 234 func makeLintCert(tbs *x509.Certificate, subjectPubKey crypto.PublicKey, issuer *x509.Certificate, signer crypto.Signer) ([]byte, *zlintx509.Certificate, error) { 235 lintCertBytes, err := x509.CreateCertificate(rand.Reader, tbs, issuer, subjectPubKey, signer) 236 if err != nil { 237 return nil, nil, fmt.Errorf("failed to create lint certificate: %w", err) 238 } 239 lintCert, err := zlintx509.ParseCertificate(lintCertBytes) 240 if err != nil { 241 return nil, nil, fmt.Errorf("failed to parse lint certificate: %w", err) 242 } 243 // RFC 5280, Sections 4.1.2.6 and 8 244 // 245 // When the subject of the certificate is a CA, the subject 246 // field MUST be encoded in the same way as it is encoded in the 247 // issuer field (Section 4.1.2.4) in all certificates issued by 248 // the subject CA. 249 if !bytes.Equal(issuer.RawSubject, lintCert.RawIssuer) { 250 return nil, nil, fmt.Errorf("mismatch between lint issuer RawSubject and lintCert.RawIssuer DER bytes: \"%x\" != \"%x\"", issuer.RawSubject, lintCert.RawIssuer) 251 } 252 253 return lintCertBytes, lintCert, nil 254 } 255 256 func ProcessResultSet(lintRes *zlint.ResultSet) error { 257 if lintRes.NoticesPresent || lintRes.WarningsPresent || lintRes.ErrorsPresent || lintRes.FatalsPresent { 258 var failedLints []string 259 for lintName, result := range lintRes.Results { 260 if result.Status > lint.Pass { 261 failedLints = append(failedLints, fmt.Sprintf("%s (%s)", lintName, result.Details)) 262 } 263 } 264 return fmt.Errorf("%w: %s", ErrLinting, strings.Join(failedLints, ", ")) 265 } 266 return nil 267 } 268 269 func makeLintCRL(tbs *x509.RevocationList, issuer *x509.Certificate, signer crypto.Signer) (*zlintx509.RevocationList, error) { 270 lintCRLBytes, err := x509.CreateRevocationList(rand.Reader, tbs, issuer, signer) 271 if err != nil { 272 return nil, err 273 } 274 lintCRL, err := zlintx509.ParseRevocationList(lintCRLBytes) 275 if err != nil { 276 return nil, err 277 } 278 return lintCRL, nil 279 }