github.com/letsencrypt/boulder@v0.20251208.0/issuance/crl_test.go (about) 1 package issuance 2 3 import ( 4 "crypto/x509" 5 "errors" 6 "math/big" 7 "testing" 8 "time" 9 10 "github.com/jmhodges/clock" 11 "github.com/zmap/zlint/v3/lint" 12 "golang.org/x/crypto/cryptobyte" 13 cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1" 14 15 "github.com/letsencrypt/boulder/config" 16 "github.com/letsencrypt/boulder/crl/idp" 17 "github.com/letsencrypt/boulder/test" 18 ) 19 20 func TestNewCRLProfile(t *testing.T) { 21 t.Parallel() 22 tests := []struct { 23 name string 24 config CRLProfileConfig 25 expected *CRLProfile 26 expectedErr string 27 }{ 28 { 29 name: "validity too long", 30 config: CRLProfileConfig{ValidityInterval: config.Duration{Duration: 30 * 24 * time.Hour}}, 31 expected: nil, 32 expectedErr: "lifetime cannot be more than 10 days", 33 }, 34 { 35 name: "validity too short", 36 config: CRLProfileConfig{ValidityInterval: config.Duration{Duration: 0}}, 37 expected: nil, 38 expectedErr: "lifetime must be positive", 39 }, 40 { 41 name: "negative backdate", 42 config: CRLProfileConfig{ 43 ValidityInterval: config.Duration{Duration: 7 * 24 * time.Hour}, 44 MaxBackdate: config.Duration{Duration: -time.Hour}, 45 }, 46 expected: nil, 47 expectedErr: "backdate must be non-negative", 48 }, 49 { 50 name: "happy path", 51 config: CRLProfileConfig{ 52 ValidityInterval: config.Duration{Duration: 7 * 24 * time.Hour}, 53 MaxBackdate: config.Duration{Duration: time.Hour}, 54 }, 55 expected: &CRLProfile{ 56 validityInterval: 7 * 24 * time.Hour, 57 maxBackdate: time.Hour, 58 }, 59 expectedErr: "", 60 }, 61 } 62 for _, tc := range tests { 63 t.Run(tc.name, func(t *testing.T) { 64 t.Parallel() 65 actual, err := NewCRLProfile(tc.config) 66 if err != nil { 67 if tc.expectedErr == "" { 68 t.Errorf("NewCRLProfile expected success but got %q", err) 69 return 70 } 71 test.AssertContains(t, err.Error(), tc.expectedErr) 72 } else { 73 if tc.expectedErr != "" { 74 t.Errorf("NewCRLProfile succeeded but expected error %q", tc.expectedErr) 75 return 76 } 77 test.AssertEquals(t, actual.validityInterval, tc.expected.validityInterval) 78 test.AssertEquals(t, actual.maxBackdate, tc.expected.maxBackdate) 79 test.AssertNotNil(t, actual.lints, "lint registry should be populated") 80 } 81 }) 82 } 83 } 84 85 func TestIssueCRL(t *testing.T) { 86 clk := clock.NewFake() 87 clk.Set(time.Now()) 88 89 issuer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, clk) 90 test.AssertNotError(t, err, "creating test issuer") 91 92 defaultProfile := CRLProfile{ 93 validityInterval: 7 * 24 * time.Hour, 94 maxBackdate: 1 * time.Hour, 95 lints: lint.GlobalRegistry(), 96 } 97 98 defaultRequest := CRLRequest{ 99 Number: big.NewInt(123), 100 Shard: 100, 101 ThisUpdate: clk.Now().Add(-time.Second), 102 Entries: []x509.RevocationListEntry{ 103 { 104 SerialNumber: big.NewInt(987), 105 RevocationTime: clk.Now().Add(-24 * time.Hour), 106 ReasonCode: 1, 107 }, 108 }, 109 } 110 111 req := defaultRequest 112 req.ThisUpdate = clk.Now().Add(-24 * time.Hour) 113 _, err = issuer.IssueCRL(&defaultProfile, &req) 114 test.AssertError(t, err, "too old crl issuance should fail") 115 test.AssertContains(t, err.Error(), "ThisUpdate is too far in the past") 116 117 req = defaultRequest 118 req.ThisUpdate = clk.Now().Add(time.Second) 119 _, err = issuer.IssueCRL(&defaultProfile, &req) 120 test.AssertError(t, err, "future crl issuance should fail") 121 test.AssertContains(t, err.Error(), "ThisUpdate is in the future") 122 123 req = defaultRequest 124 req.Entries = append(req.Entries, x509.RevocationListEntry{ 125 SerialNumber: big.NewInt(876), 126 RevocationTime: clk.Now().Add(-24 * time.Hour), 127 ReasonCode: 6, 128 }) 129 _, err = issuer.IssueCRL(&defaultProfile, &req) 130 test.AssertError(t, err, "invalid reason code should result in lint failure") 131 test.AssertContains(t, err.Error(), "Reason code not included in BR") 132 133 req = defaultRequest 134 res, err := issuer.IssueCRL(&defaultProfile, &req) 135 test.AssertNotError(t, err, "crl issuance should have succeeded") 136 parsedRes, err := x509.ParseRevocationList(res) 137 test.AssertNotError(t, err, "parsing test crl") 138 test.AssertEquals(t, parsedRes.Issuer.CommonName, issuer.Cert.Subject.CommonName) 139 test.AssertDeepEquals(t, parsedRes.Number, big.NewInt(123)) 140 expectUpdate := req.ThisUpdate.Add(-time.Second).Add(defaultProfile.validityInterval).Truncate(time.Second).UTC() 141 test.AssertEquals(t, parsedRes.NextUpdate, expectUpdate) 142 test.AssertEquals(t, len(parsedRes.Extensions), 3) 143 found, err := revokedCertificatesFieldExists(res) 144 test.AssertNotError(t, err, "Should have been able to parse CRL") 145 test.Assert(t, found, "Expected the revokedCertificates field to exist") 146 147 idps, err := idp.GetIDPURIs(parsedRes.Extensions) 148 test.AssertNotError(t, err, "getting IDP URIs from test CRL") 149 test.AssertEquals(t, len(idps), 1) 150 test.AssertEquals(t, idps[0], "http://crl-url.example.org/100.crl") 151 152 req = defaultRequest 153 crlURLBase := issuer.crlURLBase 154 issuer.crlURLBase = "" 155 _, err = issuer.IssueCRL(&defaultProfile, &req) 156 test.AssertError(t, err, "crl issuance with no IDP should fail") 157 test.AssertContains(t, err.Error(), "must contain an issuingDistributionPoint") 158 issuer.crlURLBase = crlURLBase 159 160 // A CRL with no entries must not have the revokedCertificates field 161 req = defaultRequest 162 req.Entries = []x509.RevocationListEntry{} 163 res, err = issuer.IssueCRL(&defaultProfile, &req) 164 test.AssertNotError(t, err, "issuing crl with no entries") 165 parsedRes, err = x509.ParseRevocationList(res) 166 test.AssertNotError(t, err, "parsing test crl") 167 test.AssertEquals(t, parsedRes.Issuer.CommonName, issuer.Cert.Subject.CommonName) 168 test.AssertDeepEquals(t, parsedRes.Number, big.NewInt(123)) 169 test.AssertEquals(t, len(parsedRes.RevokedCertificateEntries), 0) 170 found, err = revokedCertificatesFieldExists(res) 171 test.AssertNotError(t, err, "Should have been able to parse CRL") 172 test.Assert(t, !found, "Violation of RFC 5280 Section 5.1.2.6") 173 } 174 175 // revokedCertificatesFieldExists is a modified version of 176 // x509.ParseRevocationList that takes a given sequence of bytes representing a 177 // CRL and parses away layers until the optional `revokedCertificates` field of 178 // a TBSCertList is found. It returns a boolean indicating whether the field was 179 // found or an error if there was an issue processing a CRL. 180 // 181 // https://datatracker.ietf.org/doc/html/rfc5280#section-5.1.2.6 182 // 183 // When there are no revoked certificates, the revoked certificates list 184 // MUST be absent. 185 // 186 // https://datatracker.ietf.org/doc/html/rfc5280#appendix-A.1 page 118 187 // 188 // CertificateList ::= SEQUENCE { 189 // tbsCertList TBSCertList 190 // .. 191 // } 192 // 193 // TBSCertList ::= SEQUENCE { 194 // .. 195 // revokedCertificates SEQUENCE OF SEQUENCE { 196 // .. 197 // } OPTIONAL, 198 // } 199 func revokedCertificatesFieldExists(der []byte) (bool, error) { 200 input := cryptobyte.String(der) 201 202 // Extract the CertificateList 203 if !input.ReadASN1(&input, cryptobyte_asn1.SEQUENCE) { 204 return false, errors.New("malformed crl") 205 } 206 207 var tbs cryptobyte.String 208 // Extract the TBSCertList from the CertificateList 209 if !input.ReadASN1(&tbs, cryptobyte_asn1.SEQUENCE) { 210 return false, errors.New("malformed tbs crl") 211 } 212 213 // Skip optional version 214 tbs.SkipOptionalASN1(cryptobyte_asn1.INTEGER) 215 216 // Skip the signature 217 tbs.SkipASN1(cryptobyte_asn1.SEQUENCE) 218 219 // Skip the issuer 220 tbs.SkipASN1(cryptobyte_asn1.SEQUENCE) 221 222 // SkipOptionalASN1 is identical to SkipASN1 except that it also does a 223 // peek. We'll handle the non-optional thisUpdate with these double peeks 224 // because there's no harm doing so. 225 skipTime := func(s *cryptobyte.String) { 226 switch { 227 case s.PeekASN1Tag(cryptobyte_asn1.UTCTime): 228 s.SkipOptionalASN1(cryptobyte_asn1.UTCTime) 229 case s.PeekASN1Tag(cryptobyte_asn1.GeneralizedTime): 230 s.SkipOptionalASN1(cryptobyte_asn1.GeneralizedTime) 231 } 232 } 233 234 // Skip thisUpdate 235 skipTime(&tbs) 236 237 // Skip optional nextUpdate 238 skipTime(&tbs) 239 240 // Finally, the field which we care about: revokedCertificates. This will 241 // not trigger on the next field `crlExtensions` because that has 242 // context-specific tag [0] and EXPLICIT encoding, not `SEQUENCE` and is 243 // therefore a safe place to end this venture. 244 if tbs.PeekASN1Tag(cryptobyte_asn1.SEQUENCE) { 245 return true, nil 246 } 247 248 return false, nil 249 }