Skip to content

Commit ce0d2e1

Browse files
xyzzyzdjc
authored andcommitted
Require CRL number extension
Reject CRLs that lack id-ce-cRLNumber during parsing and retain the parsed CRL number.
1 parent c9514d7 commit ce0d2e1

4 files changed

Lines changed: 93 additions & 31 deletions

File tree

src/crl/types.rs

Lines changed: 82 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ pub struct OwnedCertRevocationList {
175175
signed_data: OwnedSignedData,
176176

177177
next_update: UnixTime,
178+
179+
crl_number: Vec<u8>,
178180
}
179181

180182
#[cfg(feature = "alloc")]
@@ -225,6 +227,8 @@ pub struct BorrowedCertRevocationList<'a> {
225227
revoked_certs: untrusted::Input<'a>,
226228

227229
next_update: UnixTime,
230+
231+
crl_number: untrusted::Input<'a>,
228232
}
229233

230234
impl<'a> BorrowedCertRevocationList<'a> {
@@ -263,6 +267,7 @@ impl<'a> BorrowedCertRevocationList<'a> {
263267
.map(|idp| idp.as_slice_less_safe().to_vec()),
264268
revoked_certs,
265269
next_update: self.next_update,
270+
crl_number: self.crl_number.as_slice_less_safe().to_vec(),
266271
})
267272
}
268273

@@ -273,22 +278,26 @@ impl<'a> BorrowedCertRevocationList<'a> {
273278
match id {
274279
// id-ce-cRLNumber 2.5.29.20 - RFC 5280 §5.2.3
275280
Standard(20) => {
281+
// Duplicate cRLNumber extensions are invalid.
282+
if !self.crl_number.as_slice_less_safe().is_empty() {
283+
return Err(Error::ExtensionValueInvalid);
284+
}
285+
276286
// RFC 5280 §5.2.3:
277287
// CRL verifiers MUST be able to handle CRLNumber values
278288
// up to 20 octets. Conforming CRL issuers MUST NOT use CRLNumber
279289
// values longer than 20 octets.
280290
//
281-
extension.value.read_all(Error::InvalidCrlNumber, |der| {
282-
let crl_number = der::nonnegative_integer(der)
283-
.map_err(|_| Error::InvalidCrlNumber)?
284-
.as_slice_less_safe();
285-
if crl_number.len() <= 20 {
291+
self.crl_number = extension.value.read_all(Error::InvalidCrlNumber, |der| {
292+
let crl_number =
293+
der::nonnegative_integer(der).map_err(|_| Error::InvalidCrlNumber)?;
294+
if crl_number.as_slice_less_safe().len() <= 20 {
286295
Ok(crl_number)
287296
} else {
288297
Err(Error::InvalidCrlNumber)
289298
}
290299
})?;
291-
// We enforce the cRLNumber is sensible, but don't retain the value for use.
300+
292301
Ok(())
293302
}
294303

@@ -408,6 +417,7 @@ impl<'a> FromDer<'a> for BorrowedCertRevocationList<'a> {
408417
revoked_certs,
409418
issuing_distribution_point: None,
410419
next_update,
420+
crl_number: untrusted::Input::from(&[]),
411421
};
412422

413423
// RFC 5280 §5.1.2.7:
@@ -441,6 +451,13 @@ impl<'a> FromDer<'a> for BorrowedCertRevocationList<'a> {
441451
},
442452
)?;
443453

454+
// From RFC 5280, Section 5.2.3:
455+
// CRL issuers conforming to this profile MUST include this extension in all
456+
// CRLs and MUST mark this extension as non-critical.
457+
if crl.crl_number.as_slice_less_safe().is_empty() {
458+
return Err(Error::MissingCrlNumber);
459+
}
460+
444461
Ok(crl)
445462
})?;
446463

@@ -473,6 +490,7 @@ impl core::hash::Hash for BorrowedCertRevocationList<'_> {
473490
issuing_distribution_point,
474491
revoked_certs,
475492
next_update,
493+
crl_number,
476494
} = self;
477495
signed_data.hash(state);
478496
issuer.as_slice_less_safe().hash(state);
@@ -481,6 +499,7 @@ impl core::hash::Hash for BorrowedCertRevocationList<'_> {
481499
.hash(state);
482500
revoked_certs.as_slice_less_safe().hash(state);
483501
next_update.hash(state);
502+
crl_number.as_slice_less_safe().hash(state);
484503
}
485504
}
486505

@@ -1313,21 +1332,67 @@ mod tests {
13131332
assert!(OwnedCertRevocationList::from_der(crl).is_ok())
13141333
}
13151334

1335+
#[test]
1336+
fn test_crl_missing_crl_number() {
1337+
assert_eq!(
1338+
BorrowedCertRevocationList::from_der(CRL_MISSING_CRL_NUMBER).err(),
1339+
Some(Error::MissingCrlNumber)
1340+
);
1341+
}
1342+
1343+
#[test]
1344+
fn test_crl_duplicate_crl_number() {
1345+
assert_eq!(
1346+
BorrowedCertRevocationList::from_der(CRL_DUPLICATE_CRL_NUMBER).err(),
1347+
Some(Error::ExtensionValueInvalid)
1348+
);
1349+
}
1350+
13161351
#[test]
13171352
fn test_crl_issuing_distribution_point_illegal_bit_string() {
1318-
let crl = &[
1319-
0x30, 0x65, 0x30, 0x50, 0x02, 0x01, 0x01, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48,
1320-
0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x30, 0x0c, 0x31, 0x0a, 0x30, 0x08,
1321-
0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x01, 0x41, 0x17, 0x0d, 0x32, 0x30, 0x30, 0x31,
1322-
0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x17, 0x0d, 0x32, 0x31, 0x30,
1323-
0x31, 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0xa0, 0x10, 0x30, 0x0e,
1324-
0x30, 0x0c, 0x06, 0x03, 0x55, 0x1d, 0x1c, 0x04, 0x05, 0x30, 0x03, 0x83, 0x01, 0x00,
1325-
0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05,
1326-
0x00, 0x03, 0x02, 0x00, 0x00,
1327-
];
13281353
assert_eq!(
1329-
BorrowedCertRevocationList::from_der(crl).err(),
1354+
BorrowedCertRevocationList::from_der(CRL_WITH_REASON_PARTITIONED_IDP).err(),
13301355
Some(Error::UnsupportedRevocationReasonsPartitioning)
13311356
);
13321357
}
1358+
1359+
const CRL_MISSING_CRL_NUMBER: &[u8] = &[
1360+
0x30, 0x81, 0xd0, 0x30, 0x79, 0x02, 0x01, 0x01, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48,
1361+
0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, 0x25, 0x31, 0x23, 0x30, 0x21, 0x06, 0x03, 0x55, 0x04,
1362+
0x03, 0x0c, 0x1a, 0x63, 0x72, 0x6c, 0x2d, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x2d,
1363+
0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x17,
1364+
0x0d, 0x32, 0x30, 0x30, 0x31, 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x17,
1365+
0x0d, 0x32, 0x31, 0x30, 0x31, 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0xa0,
1366+
0x23, 0x30, 0x21, 0x30, 0x1f, 0x06, 0x03, 0x55, 0x1d, 0x23, 0x04, 0x18, 0x30, 0x16, 0x80,
1367+
0x14, 0x40, 0x0e, 0x46, 0x72, 0x43, 0xeb, 0x8b, 0xac, 0x02, 0xdd, 0x09, 0xb6, 0xcc, 0x9d,
1368+
0x98, 0xe9, 0x5c, 0x34, 0xdc, 0xee, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d,
1369+
0x04, 0x03, 0x02, 0x03, 0x47, 0x00, 0x30, 0x44, 0x02, 0x20, 0x44, 0xdb, 0x2f, 0x86, 0xb9,
1370+
0x9e, 0xb8, 0x9d, 0x4a, 0x4b, 0x42, 0xe3, 0x37, 0x97, 0xb5, 0x7a, 0x1f, 0x50, 0x8a, 0xae,
1371+
0x8a, 0x5c, 0x55, 0x29, 0xba, 0x5a, 0x5c, 0x4d, 0x53, 0x1d, 0x44, 0xce, 0x02, 0x20, 0x10,
1372+
0x13, 0x77, 0x62, 0x16, 0x7a, 0x89, 0x69, 0x2e, 0x76, 0xed, 0xb7, 0xde, 0x62, 0x03, 0x78,
1373+
0xc9, 0x50, 0xfb, 0xe7, 0xcd, 0xb8, 0x12, 0xa3, 0x83, 0x40, 0x2f, 0xd6, 0xf0, 0x5c, 0xc1,
1374+
0xb6,
1375+
];
1376+
1377+
const CRL_DUPLICATE_CRL_NUMBER: &[u8] = &[
1378+
0x30, 0x6f, 0x30, 0x5a, 0x02, 0x01, 0x01, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
1379+
0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x30, 0x0c, 0x31, 0x0a, 0x30, 0x08, 0x06, 0x03,
1380+
0x55, 0x04, 0x03, 0x13, 0x01, 0x41, 0x17, 0x0d, 0x32, 0x30, 0x30, 0x31, 0x30, 0x31, 0x30,
1381+
0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x17, 0x0d, 0x32, 0x31, 0x30, 0x31, 0x30, 0x31, 0x30,
1382+
0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0xa0, 0x1a, 0x30, 0x18, 0x30, 0x0a, 0x06, 0x03, 0x55,
1383+
0x1d, 0x14, 0x04, 0x03, 0x02, 0x01, 0x01, 0x30, 0x0a, 0x06, 0x03, 0x55, 0x1d, 0x14, 0x04,
1384+
0x03, 0x02, 0x01, 0x01, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01,
1385+
0x01, 0x0b, 0x05, 0x00, 0x03, 0x02, 0x00, 0x00,
1386+
];
1387+
1388+
const CRL_WITH_REASON_PARTITIONED_IDP: &[u8] = &[
1389+
0x30, 0x71, 0x30, 0x5c, 0x02, 0x01, 0x01, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
1390+
0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x30, 0x0c, 0x31, 0x0a, 0x30, 0x08, 0x06, 0x03,
1391+
0x55, 0x04, 0x03, 0x13, 0x01, 0x41, 0x17, 0x0d, 0x32, 0x30, 0x30, 0x31, 0x30, 0x31, 0x30,
1392+
0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x17, 0x0d, 0x32, 0x31, 0x30, 0x31, 0x30, 0x31, 0x30,
1393+
0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0xa0, 0x1c, 0x30, 0x1a, 0x30, 0x0c, 0x06, 0x03, 0x55,
1394+
0x1d, 0x1c, 0x04, 0x05, 0x30, 0x03, 0x83, 0x01, 0x00, 0x30, 0x0a, 0x06, 0x03, 0x55, 0x1d,
1395+
0x14, 0x04, 0x03, 0x02, 0x01, 0x01, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7,
1396+
0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x03, 0x02, 0x00, 0x00,
1397+
];
13331398
}

src/error.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ pub enum Error {
9090
/// - it was too long
9191
InvalidCrlNumber,
9292

93+
/// A CRL number extension was missing from a CRL.
94+
MissingCrlNumber,
95+
9396
/// A iPAddress name constraint was invalid:
9497
/// - it had a sparse network mask (ie, cannot be written in CIDR form).
9598
/// - it was too long or short
@@ -254,7 +257,7 @@ impl Error {
254257
Self::InvalidCertValidity => 190,
255258
Self::InvalidNetworkMaskConstraint => 180,
256259
Self::InvalidSerialNumber => 170,
257-
Self::InvalidCrlNumber => 160,
260+
Self::InvalidCrlNumber | Self::MissingCrlNumber => 160,
258261

259262
// Errors related to unsupported features.
260263
Self::UnsupportedCrlSignatureAlgorithmForPublicKey(_)

tests/x509_limbo.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,15 +118,14 @@ fn run_validation(tc: &Testcase) -> Result<(), String> {
118118
.crls
119119
.iter()
120120
.map(|pem| {
121-
OwnedCertRevocationList::from_der(
122-
CertificateRevocationListDer::from_pem_slice(pem.as_bytes())
123-
.expect("CRL PEM parse failed")
124-
.as_ref(),
125-
)
126-
.expect("CRL DER parse failed")
127-
.into()
121+
let der = CertificateRevocationListDer::from_pem_slice(pem.as_bytes())
122+
.map_err(|e| format!("CRL PEM parse failed: {e}"))?;
123+
124+
OwnedCertRevocationList::from_der(der.as_ref())
125+
.map(Into::into)
126+
.map_err(|e| format!("CRL DER parse failed: {e}"))
128127
})
129-
.collect::<Vec<_>>();
128+
.collect::<Result<Vec<_>, _>>()?;
130129
let crls = crls.iter().collect::<Vec<_>>();
131130

132131
let builder = if !crls.is_empty() {

third-party/x509-limbo/exceptions.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,6 @@
2424
"actual": "FAILURE",
2525
"reason": "webpki intentionally rejects CA certificates in leaf position (CaUsedAsEndEntity)"
2626
},
27-
"crl::crlnumber-missing": {
28-
"expected": "FAILURE",
29-
"actual": "SUCCESS",
30-
"reason": "webpki does not enforce RFC 5280 requirement for CRLNumber extension presence"
31-
},
3227
"crl::crlnumber-critical": {
3328
"expected": "FAILURE",
3429
"actual": "SUCCESS",

0 commit comments

Comments
 (0)