mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-02 06:28:49 -06:00
fix(sign): require the time-stamping purpose for trusted timestamps
A timestamp counted as trusted time whenever its signer's certificate chained to a trusted certificate, whatever the certificate was issued for. Any key from a trusted CA could then vouch for an arbitrary time. Trusted time, for a document timestamp and for a signature's embedded one, now also needs a certificate whose extended key usage includes timeStamping, whose key usage allows signing, and which has no critical extension PdfCraft doesn't process (RFC 3161 §2.3). The test timestamp authority gets such a certificate.
This commit is contained in:
+16
-7
@@ -401,6 +401,7 @@ pub const WORDINGS: &[&str] = &[
|
||||
"The timestamp token could not be read ({}).",
|
||||
"The document has been altered or corrupted since the timestamp was applied.",
|
||||
"The timestamp authority's certificate was not valid at the time of timestamping.",
|
||||
"The timestamp authority's certificate is not issued for time stamping.",
|
||||
"This document has not been modified since this timestamp was applied.",
|
||||
"The document has been modified since this timestamp was applied, but the changes are permitted ({}).",
|
||||
"The document has been altered since this timestamp was applied in ways it does not permit ({}).",
|
||||
@@ -771,13 +772,16 @@ fn validate_into(doc: &Document, bytes: &[u8], trust: &TrustStore, v: &Dict, inf
|
||||
// own claimed time, as without a timestamp.
|
||||
let mut pool = crate::timestamp::token_certs(raw);
|
||||
pool.extend(trust.certs.iter().cloned());
|
||||
let trusted_tsa = t
|
||||
.signer_certificate()
|
||||
.is_some_and(|c| c.valid_at(t.gen_time) && build_chain(&c, &pool, Some(t.gen_time)).iter().any(|x| trust.trusts(x)));
|
||||
if trusted_tsa {
|
||||
let tsa = t.signer_certificate();
|
||||
let trusted_tsa = tsa
|
||||
.as_ref()
|
||||
.is_some_and(|c| c.valid_at(t.gen_time) && build_chain(c, &pool, Some(t.gen_time)).iter().any(|x| trust.trusts(x)));
|
||||
if !trusted_tsa {
|
||||
unverified_time = Some(t.gen_time);
|
||||
} else if tsa.as_ref().is_some_and(Certificate::may_timestamp) {
|
||||
info.timestamp_time = Some(t.gen_time);
|
||||
} else {
|
||||
unverified_time = Some(t.gen_time);
|
||||
info.details.push(Detail::plain("The timestamp authority's certificate is not issued for time stamping."));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1085,6 +1089,10 @@ fn validate_doc_timestamp(doc: &Document, bytes: &[u8], v: &Dict, info: &mut Sig
|
||||
info.status = Status::Unknown;
|
||||
info.details.push(Detail::plain("The timestamp authority's certificate was not valid at the time of timestamping."));
|
||||
}
|
||||
if !cert.may_timestamp() {
|
||||
info.status = Status::Unknown;
|
||||
info.details.push(Detail::plain("The timestamp authority's certificate is not issued for time stamping."));
|
||||
}
|
||||
}
|
||||
info.modification = if covered == bytes.len() || bytes[covered..].iter().all(|b| b.is_ascii_whitespace() || *b == 0) {
|
||||
Modification::None
|
||||
@@ -1112,9 +1120,10 @@ fn validate_doc_timestamp(doc: &Document, bytes: &[u8], v: &Dict, info: &mut Sig
|
||||
pool.extend(trust.certs.iter().cloned());
|
||||
// A trusted authority whose certificate was not valid at the time it stamped proves
|
||||
// nothing either (its detail is already recorded above): never upgrade that to Valid.
|
||||
let in_validity = token.signer_certificate().is_some_and(|c| c.valid_at(token.gen_time));
|
||||
// Nor does one whose certificate is not issued for time stamping.
|
||||
let may_stamp = token.signer_certificate().is_some_and(|c| c.valid_at(token.gen_time) && c.may_timestamp());
|
||||
let trusted_tsa = token.signer_certificate().is_some_and(|c| build_chain(&c, &pool, Some(token.gen_time)).iter().any(|x| trust.trusts(x)));
|
||||
if trusted_tsa && in_validity {
|
||||
if trusted_tsa && may_stamp {
|
||||
info.status = Status::Valid;
|
||||
info.details.push(Detail::plain("The timestamp token is valid and its authority is trusted."));
|
||||
} else if trusted_tsa {
|
||||
|
||||
@@ -165,6 +165,9 @@ const PROCESSED_EXTENSIONS: [&str; 11] = [
|
||||
"1.3.6.1.5.5.7.1.3",
|
||||
];
|
||||
|
||||
/// id-kp-timeStamping (RFC 5280 §4.2.1.12), required of a timestamp authority (RFC 3161 §2.3).
|
||||
const EKU_TIME_STAMPING: &str = "1.3.6.1.5.5.7.3.8";
|
||||
|
||||
/// Extended key usages that allow signing documents: anyExtendedKeyUsage, emailProtection,
|
||||
/// codeSigning, documentSigning (RFC 9336), Adobe Authentic Documents and Microsoft document
|
||||
/// signing.
|
||||
@@ -384,6 +387,18 @@ impl Certificate {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this certificate may stamp trusted time (RFC 3161 §2.3): its extended key usage
|
||||
/// includes timeStamping, its key usage (if any) allows digital signatures or
|
||||
/// non-repudiation, and it has no critical extension PdfCraft doesn't process. A certificate
|
||||
/// issued for another purpose proves who signed, not when.
|
||||
pub fn may_timestamp(&self) -> bool {
|
||||
/// keyUsage bits 0 (digitalSignature) and 1 (nonRepudiation).
|
||||
const SIGNING_BITS: u16 = 0b11;
|
||||
self.extended_key_usage.as_ref().is_some_and(|e| e.iter().any(|o| o == EKU_TIME_STAMPING))
|
||||
&& self.key_usage.is_none_or(|u| u & SIGNING_BITS != 0)
|
||||
&& self.unknown_critical.is_empty()
|
||||
}
|
||||
|
||||
/// [`signing_problem`](Self::signing_problem) as a clause that follows the certificate's
|
||||
/// name ("it is not issued for signing documents (…)"), for lists of digital IDs.
|
||||
pub fn signing_problem_clause(&self) -> Option<String> {
|
||||
|
||||
@@ -384,6 +384,38 @@ struct TestTsa {
|
||||
time: Time,
|
||||
}
|
||||
|
||||
/// A timestamp authority's digital ID: a self-signed certificate valid from October 2026 for
|
||||
/// ten years, whose critical extended key usage is time stamping alone, as RFC 3161 requires.
|
||||
fn tsa_id() -> DigitalId {
|
||||
tsa_id_with(&["1.3.6.1.5.5.7.3.8"])
|
||||
}
|
||||
|
||||
/// [`tsa_id`] with these extended key usages.
|
||||
fn tsa_id_with(ekus: &[&str]) -> DigitalId {
|
||||
let key = pdfcraft_sign::PrivateKey::generate_p256().unwrap();
|
||||
let name = Name::build("Test TSA", "", "PdfCraft Tests", "", "US");
|
||||
let alg = key.preferred_digest();
|
||||
let sig_alg = key.signature_algorithm(alg);
|
||||
let from = Time { year: 2026, month: 10, day: 2, hour: 0, minute: 0, second: 0 };
|
||||
let until = Time { year: 2036, ..from };
|
||||
let oids: Vec<Vec<u8>> = ekus.iter().map(|o| der::oid(o)).collect();
|
||||
let oids: Vec<&[u8]> = oids.iter().map(Vec::as_slice).collect();
|
||||
let eku = der::seq(&[&der::oid("2.5.29.37"), &der::boolean(true), &der::octets(&der::seq(&oids))]);
|
||||
let tbs = der::seq(&[
|
||||
&der::explicit(0, &der::int(2)),
|
||||
&der::uint(&[0x31]),
|
||||
&sig_alg,
|
||||
&name.raw,
|
||||
&der::seq(&[&from.encode(), &until.encode()]),
|
||||
&name.raw,
|
||||
&key.public_key().spki(),
|
||||
&der::explicit(3, &der::seq(&[&eku])),
|
||||
]);
|
||||
let signature = key.sign(alg, &tbs).unwrap();
|
||||
let certificate = Certificate::parse(&der::seq(&[&tbs, &sig_alg, &der::bit_string(&signature)])).unwrap();
|
||||
DigitalId { key, certificate, chain: Vec::new(), friendly_name: None }
|
||||
}
|
||||
|
||||
impl TimestampAuthority for TestTsa {
|
||||
fn timestamp(&self, request: &[u8]) -> Result<Vec<u8>, SignError> {
|
||||
let q = pdfcraft_sign::timestamp::parse_request(request)?;
|
||||
@@ -404,7 +436,7 @@ impl TimestampAuthority for TestTsa {
|
||||
fn signing_with_a_timestamp_embeds_a_verified_rfc3161_token() {
|
||||
let id = pkcs12::open(&data("ec-p256.p12"), "test").unwrap();
|
||||
let tsa = TestTsa {
|
||||
id: pkcs12::open(&data("rsa-aes.p12"), "test").unwrap(),
|
||||
id: tsa_id(),
|
||||
time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 },
|
||||
};
|
||||
let signed = pdfcraft_sign::sign_with_timestamp(&open(&fixture()), &id, &opts(), &tsa).unwrap();
|
||||
@@ -443,7 +475,7 @@ fn a_malformed_timestamp_response_fails_signing_without_a_file() {
|
||||
#[test]
|
||||
fn a_document_timestamp_covers_the_file_and_validates() {
|
||||
let tsa = TestTsa {
|
||||
id: pkcs12::open(&data("rsa-aes.p12"), "test").unwrap(),
|
||||
id: tsa_id(),
|
||||
time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 },
|
||||
};
|
||||
let stamped = pdfcraft_sign::timestamp_document(&open(&fixture()), &tsa, "D:20261006120000Z").unwrap();
|
||||
@@ -480,7 +512,7 @@ fn a_document_timestamp_covers_the_file_and_validates() {
|
||||
#[test]
|
||||
fn cached_discovery_still_validates_current_timestamp_bytes_ranges_and_trust() {
|
||||
let tsa = TestTsa {
|
||||
id: pkcs12::open(&data("rsa-aes.p12"), "test").unwrap(),
|
||||
id: tsa_id(),
|
||||
time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 },
|
||||
};
|
||||
let mut source = open(&fixture());
|
||||
@@ -559,7 +591,7 @@ fn sign_then_ltv_then_timestamp_makes_a_b_lta_file() {
|
||||
use pdfcraft_sign::dss::{self, Evidence};
|
||||
let id = pkcs12::open(&data("ec-p256.p12"), "test").unwrap();
|
||||
let tsa = TestTsa {
|
||||
id: pkcs12::open(&data("rsa-aes.p12"), "test").unwrap(),
|
||||
id: tsa_id(),
|
||||
time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 },
|
||||
};
|
||||
let signed = pdfcraft_sign::sign(&open(&fixture()), &id, &opts()).unwrap();
|
||||
@@ -1532,7 +1564,7 @@ fn an_untrusted_timestamp_does_not_set_the_validation_time() {
|
||||
// The TSA claims 2027 (inside the signer's validity) but is not trusted; the signer's own
|
||||
// clock says 2040, after the certificate expired.
|
||||
let tsa = TestTsa {
|
||||
id: pkcs12::open(&data("rsa-aes.p12"), "test").unwrap(),
|
||||
id: tsa_id(),
|
||||
time: Time { year: 2027, month: 1, day: 1, hour: 0, minute: 0, second: 0 },
|
||||
};
|
||||
let late = SignOptions { date: "D:20400101120000Z".into(), ..opts() };
|
||||
@@ -1754,7 +1786,7 @@ fn fixture_timestamped_in_a_field(tsa: &TestTsa) -> Vec<u8> {
|
||||
#[test]
|
||||
fn a_document_timestamp_held_by_a_signature_field_is_checked_as_a_timestamp() {
|
||||
let tsa = TestTsa {
|
||||
id: pkcs12::open(&data("rsa-aes.p12"), "test").unwrap(),
|
||||
id: tsa_id(),
|
||||
time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 },
|
||||
};
|
||||
let stamped = fixture_timestamped_in_a_field(&tsa);
|
||||
@@ -1786,7 +1818,7 @@ fn a_document_timestamp_held_by_a_signature_field_is_checked_as_a_timestamp() {
|
||||
#[test]
|
||||
fn a_field_timestamp_from_a_trusted_tsa_outside_its_validity_is_not_valid() {
|
||||
let tsa = TestTsa {
|
||||
id: pkcs12::open(&data("rsa-aes.p12"), "test").unwrap(),
|
||||
id: tsa_id(),
|
||||
// Before the TSA certificate's validity (it starts in October 2026).
|
||||
time: Time { year: 2020, month: 1, day: 1, hour: 0, minute: 0, second: 0 },
|
||||
};
|
||||
@@ -1802,7 +1834,7 @@ fn a_field_timestamp_from_a_trusted_tsa_outside_its_validity_is_not_valid() {
|
||||
#[test]
|
||||
fn a_standalone_timestamp_typed_sig_by_an_older_writer_is_still_a_timestamp() {
|
||||
let tsa = TestTsa {
|
||||
id: pkcs12::open(&data("rsa-aes.p12"), "test").unwrap(),
|
||||
id: tsa_id(),
|
||||
time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 },
|
||||
};
|
||||
let stamped = pdfcraft_sign::timestamp_document(&open(&fixture()), &tsa, "D:20261006120000Z").unwrap();
|
||||
@@ -2036,7 +2068,7 @@ fn signing_waits_for_a_pending_security_change_to_be_saved() {
|
||||
#[test]
|
||||
fn encrypted_documents_take_a_document_timestamp() {
|
||||
let tsa = TestTsa {
|
||||
id: pkcs12::open(&data("rsa-aes.p12"), "test").unwrap(),
|
||||
id: tsa_id(),
|
||||
time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 },
|
||||
};
|
||||
let base = protected(pdfcraft_cos::Algorithm::Aes128, FILL_AND_SIGN_ONLY);
|
||||
@@ -2495,3 +2527,23 @@ fn switching_the_trailer_to_another_signed_catalog_is_not_permitted() {
|
||||
assert!(matches!(s.modification, Modification::Disallowed(_)), "{:?}", s.modification);
|
||||
assert_eq!(s.status, Status::Invalid, "{:?}", s.details);
|
||||
}
|
||||
|
||||
/// Only a certificate issued for time stamping makes trusted time. A trusted certificate issued
|
||||
/// for something else (here e-mail) stamps nothing trusted: neither a document timestamp nor
|
||||
/// a signature's embedded one.
|
||||
#[test]
|
||||
fn a_timestamp_from_a_certificate_not_issued_for_time_stamping_is_not_trusted_time() {
|
||||
let tsa = TestTsa { id: tsa_id_with(&["1.3.6.1.5.5.7.3.4"]), time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 } };
|
||||
let stamped = pdfcraft_sign::timestamp_document(&open(&fixture()), &tsa, "D:20261006120000Z").unwrap();
|
||||
let trust = TrustStore { certs: vec![tsa.id.certificate.clone()] };
|
||||
let s = signatures(&open(&stamped), &stamped, &trust).into_iter().find(|s| s.doc_timestamp).unwrap();
|
||||
assert_eq!(s.status, Status::Unknown, "{:?}", s.details);
|
||||
assert!(s.details.iter().any(|d| d.message.contains("not issued for time stamping")), "{:?}", s.details);
|
||||
let id = pkcs12::open(&data("ec-p256.p12"), "test").unwrap();
|
||||
let signed = pdfcraft_sign::sign_with_timestamp(&open(&fixture()), &id, &opts(), &tsa).unwrap();
|
||||
let trust = TrustStore { certs: vec![id.certificate.clone(), tsa.id.certificate.clone()] };
|
||||
let s = signatures(&open(&signed), &signed, &trust).into_iter().find(|s| s.signed).unwrap();
|
||||
assert!(s.timestamp);
|
||||
assert_eq!(s.timestamp_time, None, "{:?}", s.details);
|
||||
assert!(s.details.iter().any(|d| d.message.contains("not issued for time stamping")), "{:?}", s.details);
|
||||
}
|
||||
|
||||
@@ -2379,6 +2379,7 @@ action wizard Start Iniciar
|
||||
The timestamp token could not be read ({}). No se ha podido leer el sello de tiempo ({}).
|
||||
The document has been altered or corrupted since the timestamp was applied. El documento se ha alterado o se ha dañado desde que se aplicó el sello de tiempo.
|
||||
The timestamp authority's certificate was not valid at the time of timestamping. El certificado de la autoridad de sellado de tiempo no era válido en el momento del sellado.
|
||||
The timestamp authority's certificate is not issued for time stamping. El certificado de la autoridad de sellado de tiempo no está emitido para sellar el tiempo.
|
||||
This document has not been modified since this timestamp was applied. Este documento no se ha modificado desde que se aplicó este sello de tiempo.
|
||||
The document has been modified since this timestamp was applied, but the changes are permitted ({}). El documento se ha modificado desde que se aplicó este sello de tiempo, pero son cambios que permite ({}).
|
||||
The document has been altered since this timestamp was applied in ways it does not permit ({}). El documento se ha alterado desde que se aplicó este sello de tiempo de maneras que no permite ({}).
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 60.
|
@@ -2523,6 +2523,7 @@ animation effects Off Wyłączone
|
||||
The timestamp token could not be read ({}). Nie udało się odczytać tokenu znacznika czasu ({}).
|
||||
The document has been altered or corrupted since the timestamp was applied. Dokument został zmieniony lub uszkodzony po dodaniu znacznika czasu.
|
||||
The timestamp authority's certificate was not valid at the time of timestamping. Certyfikat urzędu znacznika czasu nie był ważny w chwili oznaczania czasem.
|
||||
The timestamp authority's certificate is not issued for time stamping. Certyfikat urzędu znakowania czasem nie jest wystawiony do znakowania czasem.
|
||||
This document has not been modified since this timestamp was applied. Dokument nie był modyfikowany od dodania tego znacznika czasu.
|
||||
The document has been modified since this timestamp was applied, but the changes are permitted ({}). Dokument zmodyfikowano po dodaniu tego znacznika czasu, ale zmiany są dozwolone ({}).
|
||||
The document has been altered since this timestamp was applied in ways it does not permit ({}). Dokument zmieniono po dodaniu tego znacznika czasu w sposób, na który znacznik nie pozwala ({}).
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
Reference in New Issue
Block a user