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 }