mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-30 21:17:55 -06:00
fix(sign): a time-stamp response must carry the requested nonce and policy
parse_response compared only the digest algorithm and imprint with the query, and the token parser dropped the TSTInfo nonce. A genuine token for the same imprint from another request or under another policy was accepted. The token keeps its nonce, and a requested nonce or policy must match.
This commit is contained in:
@@ -62,6 +62,8 @@ pub struct TimestampToken {
|
||||
pub policy: String,
|
||||
pub serial: Vec<u8>,
|
||||
pub gen_time: Time,
|
||||
/// The TSTInfo nonce, when it has one that fits a `u64` (the size requests use).
|
||||
pub nonce: Option<u64>,
|
||||
}
|
||||
|
||||
impl TimestampToken {
|
||||
@@ -140,6 +142,9 @@ pub fn parse_token(raw: &[u8]) -> Result<TimestampToken, SignError> {
|
||||
.to_vec();
|
||||
let serial = fields.get(3).ok_or_else(|| bad("TSTInfo has no serial number"))?.uint_bytes().to_vec();
|
||||
let gen_time = fields.get(4).ok_or_else(|| bad("TSTInfo has no generation time"))?.time()?;
|
||||
// The nonce is the only INTEGER after genTime (RFC 3161 §2.4.2): accuracy is a SEQUENCE and
|
||||
// ordering a BOOLEAN.
|
||||
let nonce = fields.iter().skip(5).find(|f| f.tag == tag::INTEGER).and_then(|f| f.u64().ok());
|
||||
// The signed messageDigest uses the SignerInfo's digest algorithm, which need not be the
|
||||
// imprint's (RFC 5652 §5.4).
|
||||
let content_digest = token.signer.digest.digest(&[info]);
|
||||
@@ -151,7 +156,7 @@ pub fn parse_token(raw: &[u8]) -> Result<TimestampToken, SignError> {
|
||||
if !token.verify_signature(cert, &content_digest)? {
|
||||
return Err(bad("TSA signature is invalid"));
|
||||
}
|
||||
Ok(TimestampToken { raw: raw.to_vec(), digest, imprint, policy, serial, gen_time })
|
||||
Ok(TimestampToken { raw: raw.to_vec(), digest, imprint, policy, serial, gen_time, nonce })
|
||||
}
|
||||
|
||||
/// Parse and validate a `TimeStampResp`, including the CMS signature and message imprint.
|
||||
@@ -170,6 +175,12 @@ pub fn parse_response(bytes: &[u8], query: &TimestampQuery) -> Result<TimestampT
|
||||
if token.digest != query.digest || token.imprint != query.imprint {
|
||||
return Err(bad("message imprint does not match the requested digest"));
|
||||
}
|
||||
if query.nonce.is_some() && token.nonce != query.nonce {
|
||||
return Err(bad("time-stamp token does not carry the requested nonce"));
|
||||
}
|
||||
if query.policy.as_ref().is_some_and(|p| *p != token.policy) {
|
||||
return Err(bad("time-stamp token was issued under another policy than the one requested"));
|
||||
}
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
|
||||
@@ -1580,6 +1580,28 @@ fn a_timestamp_token_may_sign_with_a_different_digest_than_its_imprint() {
|
||||
assert_eq!((token.digest, token.imprint, token.gen_time), (pdfcraft_sign::DigestAlg::Sha1, imprint, time));
|
||||
}
|
||||
|
||||
/// A token answers the request that asked for it: a requested nonce or policy must come back.
|
||||
#[test]
|
||||
fn a_timestamp_response_must_carry_the_requested_nonce_and_policy() {
|
||||
use pdfcraft_sign::timestamp::{TimestampQuery, parse_response, respond};
|
||||
let tsa = pkcs12::open(&data("rsa-aes.p12"), "test").unwrap();
|
||||
let alg = pdfcraft_sign::DigestAlg::Sha256;
|
||||
let time = Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 };
|
||||
let query = |nonce: Option<u64>, policy: Option<&str>| {
|
||||
let mut q = TimestampQuery::new(alg, alg.digest(&[b"document"])).unwrap();
|
||||
(q.nonce, q.policy) = (nonce, policy.map(str::to_string));
|
||||
q
|
||||
};
|
||||
let answer = |q: &TimestampQuery| respond(&tsa.key, &tsa.certificate, &tsa.chain, alg, q, "1.2.3.4", time, 9).unwrap();
|
||||
let with_nonce = answer(&query(Some(42), None));
|
||||
assert!(parse_response(&with_nonce, &query(Some(42), Some("1.2.3.4"))).is_ok());
|
||||
assert!(parse_response(&with_nonce, &query(None, None)).is_ok());
|
||||
assert!(parse_response(&with_nonce, &query(Some(43), None)).is_err(), "another request's nonce");
|
||||
assert!(parse_response(&with_nonce, &query(Some(42), Some("1.2.3.5"))).is_err(), "another policy");
|
||||
let without_nonce = answer(&query(None, None));
|
||||
assert!(parse_response(&without_nonce, &query(Some(42), None)).is_err(), "no nonce in the token");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn windows_store_enumeration_and_missing_identity() {
|
||||
|
||||
Reference in New Issue
Block a user