github.com/letsencrypt/boulder@v0.20251208.0/linter/lints/cpcps/lint_crl_has_idp.go (about)

     1  package cpcps
     2  
     3  import (
     4  	"net/url"
     5  
     6  	"github.com/zmap/zcrypto/encoding/asn1"
     7  	"github.com/zmap/zcrypto/x509"
     8  	"github.com/zmap/zlint/v3/lint"
     9  	"golang.org/x/crypto/cryptobyte"
    10  	cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1"
    11  
    12  	"github.com/letsencrypt/boulder/linter/lints"
    13  )
    14  
    15  type crlHasIDP struct{}
    16  
    17  /************************************************
    18  Various root programs (and the BRs, after Ballot SC-063 passes) require that
    19  sharded/partitioned CRLs have a specifically-encoded Issuing Distribution Point
    20  extension. Since there's no way to tell from the CRL itself whether or not it
    21  is sharded, we apply this lint universally to all CRLs, but as part of the Let's
    22  Encrypt-specific suite of lints.
    23  ************************************************/
    24  
    25  func init() {
    26  	lint.RegisterRevocationListLint(&lint.RevocationListLint{
    27  		LintMetadata: lint.LintMetadata{
    28  			Name:          "e_crl_has_idp",
    29  			Description:   "Let's Encrypt CRLs must have the Issuing Distribution Point extension with appropriate contents",
    30  			Citation:      "",
    31  			Source:        lints.LetsEncryptCPS,
    32  			EffectiveDate: lints.CPSV33Date,
    33  		},
    34  		Lint: NewCrlHasIDP,
    35  	})
    36  }
    37  
    38  func NewCrlHasIDP() lint.RevocationListLintInterface {
    39  	return &crlHasIDP{}
    40  }
    41  
    42  func (l *crlHasIDP) CheckApplies(c *x509.RevocationList) bool {
    43  	return true
    44  }
    45  
    46  func (l *crlHasIDP) Execute(c *x509.RevocationList) *lint.LintResult {
    47  	/*
    48  		Let's Encrypt issues CRLs for two distinct purposes:
    49  		   1) CRLs containing subscriber certificates created by the
    50  		      crl-updater. These CRLs must have only the distributionPoint and
    51  		      onlyContainsUserCerts fields set.
    52  		   2) CRLs containing subordinate CA certificates created by the
    53  		      ceremony tool. These CRLs must only have the onlyContainsCACerts
    54  		      field set.
    55  	*/
    56  
    57  	idpOID := asn1.ObjectIdentifier{2, 5, 29, 28} // id-ce-issuingDistributionPoint
    58  	idpe := lints.GetExtWithOID(c.Extensions, idpOID)
    59  	if idpe == nil {
    60  		return &lint.LintResult{
    61  			Status:  lint.Warn,
    62  			Details: "CRL missing IssuingDistributionPoint",
    63  		}
    64  	}
    65  	if !idpe.Critical {
    66  		return &lint.LintResult{
    67  			Status:  lint.Error,
    68  			Details: "IssuingDistributionPoint MUST be critical",
    69  		}
    70  	}
    71  
    72  	// Step inside the outer issuingDistributionPoint sequence to get access to
    73  	// its constituent fields: distributionPoint [0],
    74  	// onlyContainsUserCerts [1], and onlyContainsCACerts [2].
    75  	idpv := cryptobyte.String(idpe.Value)
    76  	if !idpv.ReadASN1(&idpv, cryptobyte_asn1.SEQUENCE) {
    77  		return &lint.LintResult{
    78  			Status:  lint.Warn,
    79  			Details: "Failed to read issuingDistributionPoint",
    80  		}
    81  	}
    82  
    83  	var dpName cryptobyte.String
    84  	var distributionPointExists bool
    85  	distributionPointTag := cryptobyte_asn1.Tag(0).ContextSpecific().Constructed()
    86  	if !idpv.ReadOptionalASN1(&dpName, &distributionPointExists, distributionPointTag) {
    87  		return &lint.LintResult{
    88  			Status:  lint.Warn,
    89  			Details: "Failed to read IssuingDistributionPoint distributionPoint",
    90  		}
    91  	}
    92  
    93  	idp := lints.NewIssuingDistributionPoint()
    94  	if distributionPointExists {
    95  		lintErr := parseDistributionPointName(&dpName, idp)
    96  		if lintErr != nil {
    97  			return lintErr
    98  		}
    99  	}
   100  
   101  	onlyContainsUserCertsTag := cryptobyte_asn1.Tag(1).ContextSpecific()
   102  	if !lints.ReadOptionalASN1BooleanWithTag(&idpv, &idp.OnlyContainsUserCerts, onlyContainsUserCertsTag, false) {
   103  		return &lint.LintResult{
   104  			Status:  lint.Error,
   105  			Details: "Failed to read IssuingDistributionPoint onlyContainsUserCerts",
   106  		}
   107  	}
   108  
   109  	onlyContainsCACertsTag := cryptobyte_asn1.Tag(2).ContextSpecific()
   110  	if !lints.ReadOptionalASN1BooleanWithTag(&idpv, &idp.OnlyContainsCACerts, onlyContainsCACertsTag, false) {
   111  		return &lint.LintResult{
   112  			Status:  lint.Error,
   113  			Details: "Failed to read IssuingDistributionPoint onlyContainsCACerts",
   114  		}
   115  	}
   116  
   117  	if !idpv.Empty() {
   118  		return &lint.LintResult{
   119  			Status:  lint.Error,
   120  			Details: "Unexpected IssuingDistributionPoint fields were found",
   121  		}
   122  	}
   123  
   124  	if idp.OnlyContainsUserCerts && idp.OnlyContainsCACerts {
   125  		return &lint.LintResult{
   126  			Status:  lint.Error,
   127  			Details: "IssuingDistributionPoint should not have both onlyContainsUserCerts: TRUE and onlyContainsCACerts: TRUE",
   128  		}
   129  	} else if idp.OnlyContainsUserCerts {
   130  		if len(idp.DistributionPointURIs) == 0 {
   131  			return &lint.LintResult{
   132  				Status:  lint.Error,
   133  				Details: "User certificate CRLs MUST have at least one DistributionPointName FullName",
   134  			}
   135  		}
   136  	} else if idp.OnlyContainsCACerts {
   137  		if len(idp.DistributionPointURIs) != 0 {
   138  			return &lint.LintResult{
   139  				Status:  lint.Error,
   140  				Details: "CA certificate CRLs SHOULD NOT have a DistributionPointName FullName",
   141  			}
   142  		}
   143  	} else {
   144  		return &lint.LintResult{
   145  			Status:  lint.Error,
   146  			Details: "Neither onlyContainsUserCerts nor onlyContainsCACerts was set",
   147  		}
   148  	}
   149  
   150  	return &lint.LintResult{Status: lint.Pass}
   151  }
   152  
   153  // parseDistributionPointName examines the provided distributionPointName
   154  // and updates idp with the URI if it is found. The distribution point name is
   155  // checked for validity and returns a non-nil LintResult if there were any
   156  // problems.
   157  func parseDistributionPointName(distributionPointName *cryptobyte.String, idp *lints.IssuingDistributionPoint) *lint.LintResult {
   158  	fullNameTag := cryptobyte_asn1.Tag(0).ContextSpecific().Constructed()
   159  	if !distributionPointName.ReadASN1(distributionPointName, fullNameTag) {
   160  		return &lint.LintResult{
   161  			Status:  lint.Error,
   162  			Details: "Failed to read IssuingDistributionPoint distributionPoint fullName",
   163  		}
   164  	}
   165  
   166  	for !distributionPointName.Empty() {
   167  		var uriBytes []byte
   168  		uriTag := cryptobyte_asn1.Tag(6).ContextSpecific()
   169  		if !distributionPointName.ReadASN1Bytes(&uriBytes, uriTag) {
   170  			return &lint.LintResult{
   171  				Status:  lint.Error,
   172  				Details: "Failed to read IssuingDistributionPoint URI",
   173  			}
   174  		}
   175  		uri, err := url.Parse(string(uriBytes))
   176  		if err != nil {
   177  			return &lint.LintResult{
   178  				Status:  lint.Error,
   179  				Details: "Failed to parse IssuingDistributionPoint URI",
   180  			}
   181  		}
   182  		if uri.Scheme != "http" {
   183  			return &lint.LintResult{
   184  				Status:  lint.Error,
   185  				Details: "IssuingDistributionPoint URI MUST use http scheme",
   186  			}
   187  		}
   188  		idp.DistributionPointURIs = append(idp.DistributionPointURIs, uri)
   189  	}
   190  	if len(idp.DistributionPointURIs) == 0 {
   191  		return &lint.LintResult{
   192  			Status:  lint.Error,
   193  			Details: "IssuingDistributionPoint FullName URI MUST be present",
   194  		}
   195  	} else if len(idp.DistributionPointURIs) > 1 {
   196  		return &lint.LintResult{
   197  			Status:  lint.Notice,
   198  			Details: "IssuingDistributionPoint unexpectedly has more than one FullName",
   199  		}
   200  	}
   201  
   202  	return nil
   203  }