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  }