fix(sign): don't build chains through a CA with unprocessed critical extensions

Chain building accepted an intermediate CA without looking at its critical
extensions, which were checked only on the signer's certificate. A
subordinate CA limited by critical name constraints could vouch for a
signer outside its namespace, because PdfCraft does not enforce those
constraints. A CA certificate (other than a self-signed root, whose
limits are the trust store's business) with a critical extension
PdfCraft doesn't process is refused as an issuer, and the reason is
reported.
This commit is contained in:
aiongg
2026-10-22 12:13:16 -04:00
committed by Dylan M. Taylor
parent fbada004d3
commit d9cb7c2fe7
5 changed files with 29 additions and 1 deletions
+1
View File
@@ -378,6 +378,7 @@ pub const WORDINGS: &[&str] = &[
"The signer's certificate has critical extensions PdfCraft does not recognize ({}).",
// Why a certificate that matched the issuer was not used to build the chain.
"The certificate of {} is not a CA certificate that may issue certificates, so it is not used to vouch for {}.",
"The certificate of {} has critical extensions PdfCraft does not recognize, so it is not used to vouch for {}.",
"The certificate of {} does not allow this many CA certificates below it (path length), so it is not used to vouch for {}.",
"The certificate of {} was not valid at the time of signing, so it is not used to vouch for {}.",
"The signer's identity is unknown because it has not been included in your list of trusted certificates and none of its parent certificates are trusted certificates.",
+6 -1
View File
@@ -492,7 +492,8 @@ pub fn subject_of(raw: &[u8]) -> Option<Name> {
}
/// The chain from `leaf` up through `pool`, as far as issuers can be found and may issue: each
/// issuer's key verifies the certificate below it, it is a CA allowed to sign certificates, its
/// issuer's key verifies the certificate below it, it is a CA allowed to sign certificates, it
/// has no critical extension PdfCraft doesn't process (unless it is self-signed), its
/// `pathLenConstraint` allows the CA certificates below it, and (when `at` is given) it was valid
/// then. Stops at a self-signed certificate. Certificates embedded in a document are in `pool`
/// too, so an ordinary end-entity certificate must never be accepted as an issuer.
@@ -522,6 +523,10 @@ pub fn build_chain_noted<'a>(leaf: &'a Certificate, pool: &'a [Certificate], at:
// two names where it needs them.
let why = if !c.may_issue() {
Some("The certificate of {} is not a CA certificate that may issue certificates, so it is not used to vouch for {}.")
} else if !c.unknown_critical.is_empty() && !c.is_self_signed() {
// What such an extension limits (name constraints, policy constraints) would
// go unchecked. A self-signed root's extensions are the trust store's business.
Some("The certificate of {} has critical extensions PdfCraft does not recognize, so it is not used to vouch for {}.")
} else if c.path_len.is_some_and(|n| below > n as usize) {
Some("The certificate of {} does not allow this many CA certificates below it (path length), so it is not used to vouch for {}.")
} else if at.is_some_and(|t| !c.valid_at(t)) {
+20
View File
@@ -340,3 +340,23 @@ fn an_unreadable_extended_key_usage_allows_no_purpose() {
verdict(signer_with("Good EKU", &r, &[key_usage(DIGITAL_SIGNATURE), extended_key_usage(&["2.5.29.37.0"])]), vec![], &r.cert);
assert_eq!(status, Status::Valid, "{details:?}");
}
/// A CA certificate with a critical extension PdfCraft does not enforce (here name constraints)
/// vouches for nothing: what that extension limits would go unchecked. A self-signed root's
/// extensions are the trust store's business, so such a root still anchors.
#[test]
fn an_issuer_with_an_unprocessed_critical_extension_does_not_vouch() {
let name_constraints = ext("2.5.29.30", &der::seq(&[]));
let r = root();
let constrained = issue("Name Constrained CA", Some(&r), FOREVER, &[basic_constraints(true, None), key_usage(KEY_CERT_SIGN), name_constraints.clone()]);
let l = end_entity("Outside Its Names", &constrained);
let pool = [constrained.cert.clone(), r.cert.clone()];
let (chain, why) = build_chain_noted(&l.cert, &pool, None);
assert_eq!(names(&chain), ["Outside Its Names"]);
assert!(why.is_some_and(|w| w.message.contains("critical extensions")), "the reason is reported");
let constrained_root = issue("Name Constrained Root", None, FOREVER, &[basic_constraints(true, None), key_usage(KEY_CERT_SIGN), name_constraints]);
let i = ca("Under Constrained Root", &constrained_root, None);
let l = end_entity("Leaf", &i);
let pool = [i.cert.clone(), constrained_root.cert.clone()];
assert_eq!(names(&build_chain(&l.cert, &pool, None)), ["Leaf", "Under Constrained Root", "Name Constrained Root"]);
}
+1
View File
@@ -2359,6 +2359,7 @@ action wizard Start Iniciar
The signer's certificate has critical extensions PdfCraft does not recognize ({}). El certificado del firmante tiene extensiones críticas que PdfCraft no reconoce ({}).
# Why a certificate that matched the issuer was not used to build the chain.
The certificate of {} is not a CA certificate that may issue certificates, so it is not used to vouch for {}. El certificado de {} no es un certificado de CA que pueda emitir certificados, así que no se usa para avalar a {}.
The certificate of {} has critical extensions PdfCraft does not recognize, so it is not used to vouch for {}. El certificado de {} tiene extensiones críticas que PdfCraft no reconoce, así que no se usa para avalar a {}.
The certificate of {} does not allow this many CA certificates below it (path length), so it is not used to vouch for {}. El certificado de {} no admite tantos certificados de CA por debajo (longitud de ruta), así que no se usa para avalar a {}.
The certificate of {} was not valid at the time of signing, so it is not used to vouch for {}. El certificado de {} no era válido en el momento de la firma, así que no se usa para avalar a {}.
The signer's identity is unknown because it has not been included in your list of trusted certificates and none of its parent certificates are trusted certificates. La identidad del firmante es desconocida porque no está en tu lista de certificados de confianza y ninguno de los certificados superiores es de confianza.
Can't render this file because it contains an unexpected character in line 6 and column 60.
+1
View File
@@ -2503,6 +2503,7 @@ animation effects Off Wyłączone
The signer's certificate is not issued for signing documents (its extended key usage has no document-signing purpose). Certyfikat podpisującego nie jest wystawiony do podpisywania dokumentów (jego rozszerzone użycie klucza nie obejmuje podpisywania dokumentów).
The signer's certificate has critical extensions PdfCraft does not recognize ({}). Certyfikat podpisującego ma rozszerzenia krytyczne, których PdfCraft nie rozpoznaje ({}).
The certificate of {} is not a CA certificate that may issue certificates, so it is not used to vouch for {}. Certyfikat podmiotu {} nie jest certyfikatem urzędu certyfikacji, który może wystawiać certyfikaty, dlatego nie poświadcza certyfikatu {}.
The certificate of {} has critical extensions PdfCraft does not recognize, so it is not used to vouch for {}. Certyfikat podmiotu {} ma rozszerzenia krytyczne, których PdfCraft nie rozpoznaje, dlatego nie poświadcza certyfikatu {}.
The certificate of {} does not allow this many CA certificates below it (path length), so it is not used to vouch for {}. Certyfikat podmiotu {} nie dopuszcza tylu certyfikatów urzędów certyfikacji poniżej siebie (długość ścieżki), dlatego nie poświadcza certyfikatu {}.
The certificate of {} was not valid at the time of signing, so it is not used to vouch for {}. Certyfikat podmiotu {} nie był ważny w chwili podpisania, dlatego nie poświadcza certyfikatu {}.
The signer's identity is unknown because it has not been included in your list of trusted certificates and none of its parent certificates are trusted certificates. Tożsamość podpisującego jest nieznana, ponieważ jego certyfikatu nie ma na liście zaufanych certyfikatów i żaden z certyfikatów nadrzędnych nie jest zaufany.
Can't render this file because it contains an unexpected character in line 6 and column 47.