Skip to content

Commit a0bf408

Browse files
committed
Verify usage of keyCertSign bit in keyUsage extensions
1 parent a3af81a commit a0bf408

3 files changed

Lines changed: 100 additions & 5 deletions

File tree

src/error.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ pub enum Error {
7777
/// An end-entity certificate is being used as a CA certificate.
7878
EndEntityUsedAsCa,
7979

80+
/// An end-entity certificate has the keyCertSign bit set in its KeyUsage extension.
81+
EndEntityCertHasCertSignKeyUsage,
82+
8083
/// An X.509 extension is invalid.
8184
ExtensionValueInvalid,
8285

@@ -110,6 +113,15 @@ pub enum Error {
110113
/// The signature is invalid for the given public key.
111114
InvalidSignatureForPublicKey,
112115

116+
/// An intermediate certificate was missing the keyCertSign bit in its KeyUsage extension.
117+
///
118+
/// See [RFC 5280 section 4.2.1.3] and [RFC 5280 section 6.1.4] step (n). Note that this is
119+
/// only enforced for intermediate certificates, not for trust anchors.
120+
///
121+
/// [RFC 5280 section 4.2.1.3]: https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3
122+
/// [RFC 5280 section 6.1.4]: https://datatracker.ietf.org/doc/html/rfc5280#section-6.1.4
123+
IssuerNotCertSigner,
124+
113125
/// A CRL was signed by an issuer that has a KeyUsage bitstring that does not include
114126
/// the cRLSign key usage bit.
115127
IssuerNotCrlSigner,
@@ -250,7 +262,9 @@ impl Error {
250262
Self::RequiredEkuNotFound(_) => 240,
251263
Self::NameConstraintViolation => 230,
252264
Self::PathLenConstraintViolated => 220,
265+
Self::IssuerNotCertSigner => 215,
253266
Self::CaUsedAsEndEntity | Self::EndEntityUsedAsCa => 210,
267+
Self::EndEntityCertHasCertSignKeyUsage => 205,
254268
Self::IssuerNotCrlSigner => 200,
255269

256270
// Errors related to supported features used in an invalid way.

src/verify_cert.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,9 +438,35 @@ fn check_issuer_independent_properties(
438438
})?;
439439
untrusted::read_all_optional(cert.eku, Error::BadDer, |input| check_eku(input, eku))?;
440440

441+
if let Some(key_usage) = cert.key_usage {
442+
// RFC 5280 requires the KeyUsage extension be present in CA certificates, but historically
443+
// its absence has been tolerated and treated as if all usages were asserted. We follow that
444+
// convention here and only enforce keyCertSign when a KeyUsage extension is present.
445+
check_key_usage_cert_sign(key_usage, role)?;
446+
}
447+
441448
Ok(())
442449
}
443450

451+
/// Check that issuer certificate have the keyCertSign bit set in their KeyUsage extension.
452+
///
453+
/// <https://www.rfc-editor.org/info/rfc5280/#section-4.2.1.3>
454+
/// <https://www.rfc-editor.org/info/rfc5280/#section-6.1.4> step (n)
455+
fn check_key_usage_cert_sign(key_usage: untrusted::Input<'_>, role: Role) -> Result<(), Error> {
456+
// The KeyUsage extension is a BIT STRING; keyCertSign is bit 5.
457+
const KEY_CERT_SIGN: usize = 5;
458+
let bit_string = der::expect_tag(&mut untrusted::Reader::new(key_usage), der::Tag::BitString)?;
459+
match (
460+
role,
461+
der::bit_string_flags(bit_string)?.bit_set(KEY_CERT_SIGN),
462+
) {
463+
(Role::Issuer, true) | (Role::EndEntity, false) => Ok(()),
464+
(Role::Issuer, false) => Err(Error::IssuerNotCertSigner),
465+
// Disallowed per https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.9
466+
(Role::EndEntity, true) => Err(Error::EndEntityCertHasCertSignKeyUsage),
467+
}
468+
}
469+
444470
fn check_eku(
445471
input: Option<&mut untrusted::Reader<'_>>,
446472
eku: &dyn ExtendedKeyUsageValidator,
@@ -1285,6 +1311,66 @@ mod tests {
12851311
CertifiedIssuer::signed_by(params, key, issuer).unwrap()
12861312
}
12871313

1314+
#[test]
1315+
fn intermediate_without_key_cert_sign_rejected() {
1316+
// An intermediate that carries a KeyUsage extension which does not assert keyCertSign
1317+
// must not be usable as a CA certificate during path building.
1318+
let trust_anchor = make_issuer("Trust Anchor");
1319+
let trust_anchors = &[anchor_from_trusted_cert(trust_anchor.der()).unwrap()];
1320+
1321+
let mut params = issuer_params("Intermediate");
1322+
params.key_usages = vec![rcgen::KeyUsagePurpose::CrlSign];
1323+
let key = KeyPair::generate_for(test_utils::RCGEN_SIGNATURE_ALG).unwrap();
1324+
let intermediate = CertifiedIssuer::signed_by(params, key, &trust_anchor).unwrap();
1325+
let intermediates = &[intermediate.der().clone()];
1326+
1327+
let ee = make_end_entity(&intermediate);
1328+
let ee_cert = &EndEntityCert::try_from(ee.cert.der()).unwrap();
1329+
1330+
assert!(matches!(
1331+
verify_chain(trust_anchors, intermediates, ee_cert, None, None),
1332+
Err(ControlFlow::Continue(Error::IssuerNotCertSigner))
1333+
));
1334+
}
1335+
1336+
#[test]
1337+
fn intermediate_without_key_usage_accepted() {
1338+
// An intermediate without any KeyUsage extension is treated as if all usages are asserted,
1339+
// so it remains usable as a CA certificate.
1340+
let trust_anchor = make_issuer("Trust Anchor");
1341+
let trust_anchors = &[anchor_from_trusted_cert(trust_anchor.der()).unwrap()];
1342+
1343+
let mut params = issuer_params("Intermediate");
1344+
params.key_usages = vec![];
1345+
let key = KeyPair::generate_for(test_utils::RCGEN_SIGNATURE_ALG).unwrap();
1346+
let intermediate = CertifiedIssuer::signed_by(params, key, &trust_anchor).unwrap();
1347+
let intermediates = &[intermediate.der().clone()];
1348+
1349+
let ee = make_end_entity(&intermediate);
1350+
let ee_cert = &EndEntityCert::try_from(ee.cert.der()).unwrap();
1351+
1352+
assert!(verify_chain(trust_anchors, intermediates, ee_cert, None, None).is_ok());
1353+
}
1354+
1355+
#[test]
1356+
fn trust_anchor_without_key_cert_sign_accepted() {
1357+
// The keyCertSign check applies only to intermediate certificates, not trust anchors.
1358+
// A trust anchor whose KeyUsage omits keyCertSign must still be usable.
1359+
let mut ta_params = issuer_params("Trust Anchor");
1360+
ta_params.key_usages = vec![rcgen::KeyUsagePurpose::CrlSign];
1361+
let ta_key = KeyPair::generate_for(test_utils::RCGEN_SIGNATURE_ALG).unwrap();
1362+
let trust_anchor = CertifiedIssuer::self_signed(ta_params, ta_key).unwrap();
1363+
let trust_anchors = &[anchor_from_trusted_cert(trust_anchor.der()).unwrap()];
1364+
1365+
let intermediate = make_intermediate("Intermediate", &trust_anchor);
1366+
let intermediates = &[intermediate.der().clone()];
1367+
1368+
let ee = make_end_entity(&intermediate);
1369+
let ee_cert = &EndEntityCert::try_from(ee.cert.der()).unwrap();
1370+
1371+
assert!(verify_chain(trust_anchors, intermediates, ee_cert, None, None).is_ok());
1372+
}
1373+
12881374
fn build_and_verify_degenerate_chain(
12891375
intermediate_count: usize,
12901376
trust_anchor: ChainTrustAnchor,

third-party/x509-limbo/exceptions.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,6 @@
144144
"actual": "SUCCESS",
145145
"reason": "webpki does not enforce consistency between BasicConstraints.cA and KeyUsage.keyCertSign in trust anchors"
146146
},
147-
"rfc5280::leaf-ku-keycertsign": {
148-
"expected": "FAILURE",
149-
"actual": "SUCCESS",
150-
"reason": "webpki does not reject EE certs with keyCertSign in KeyUsage"
151-
},
152147
"webpki::aki::root-with-aki-missing-keyidentifier": {
153148
"expected": "FAILURE",
154149
"actual": "SUCCESS",

0 commit comments

Comments
 (0)