Merge PR #1962: M9: read DocMDP permissions from the signed revision (#1788)

This commit is contained in:
aiongg
2026-10-22 01:46:29 +00:00
2 changed files with 45 additions and 13 deletions
+28 -13
View File
@@ -684,6 +684,19 @@ fn is_doc_timestamp(d: &Dict) -> bool {
d.name(b"SubFilter") == Some(b"ETSI.RFC3161") && matches!(d.name(b"Type"), Some(b"DocTimeStamp" | b"Sig"))
}
/// The DocMDP permission level (`/P`, 1–3) of signature value `v`, or `None` for an approval
/// signature.
fn docmdp(doc: &Document, v: &Dict) -> Option<u8> {
v.get(b"Reference")
.map(|r| doc.resolve(r))
.and_then(|r| r.as_array().cloned())
.into_iter()
.flatten()
.filter_map(|r| doc.resolve(&r).as_dict().cloned())
.find(|r| r.name(b"TransformMethod") == Some(b"DocMDP"))
.map(|r| r.get(b"TransformParams").map(|p| doc.resolve(p)).and_then(|p| p.as_dict().and_then(|p| p.int(b"P"))).unwrap_or(2).clamp(1, 3) as u8)
}
fn validate_into(doc: &Document, bytes: &[u8], trust: &TrustStore, v: &Dict, info: &mut SignatureInfo, cache: &DigestCache) {
// A document timestamp can also be the value of a signature field (Documenso writes
// `Timestamp_1` that way). Its `/Contents` is a bare RFC 3161 token, not a signature over the
@@ -701,17 +714,7 @@ fn validate_into(doc: &Document, bytes: &[u8], trust: &TrustStore, v: &Dict, inf
info.contact = text(doc, v, b"ContactInfo");
info.sub_filter = v.name(b"SubFilter").map(|s| String::from_utf8_lossy(s).into_owned());
info.signer = text(doc, v, b"Name");
info.certify = v
.get(b"Reference")
.map(|r| doc.resolve(r))
.and_then(|r| r.as_array().cloned())
.into_iter()
.flatten()
.filter_map(|r| doc.resolve(&r).as_dict().cloned())
.find(|r| r.name(b"TransformMethod") == Some(b"DocMDP"))
.map(|r| {
r.get(b"TransformParams").map(|p| doc.resolve(p)).and_then(|p| p.as_dict().and_then(|p| p.int(b"P"))).unwrap_or(2).clamp(1, 3) as u8
});
info.certify = docmdp(doc, v);
let invalid = |info: &mut SignatureInfo, why: Detail| {
info.status = Status::Invalid;
info.details.push(why);
@@ -889,7 +892,18 @@ fn finish_validation(
info.modification = if covered == bytes.len() || bytes[covered..].iter().all(|b| b.is_ascii_whitespace() || *b == 0) {
Modification::None
} else {
classify_changes(doc, cache.revision(doc, bytes, covered), info.certify)
// The permissions are the signed revision's: a later one can rewrite `/P` (#1788). This
// signature is the one whose byte range ends that revision.
let old = cache.revision(doc, bytes, covered);
if let Some(old) = &old
&& let Some(v) = sig_fields(old)
.iter()
.filter_map(|f| f.signature(old))
.find(|v| nums(old, v, b"ByteRange").is_some_and(|b| b.len() == 4 && b[2] + b[3] == covered as f64))
{
info.certify = docmdp(old, &v);
}
classify_changes(doc, old, info.certify)
};
let mut problems = false;
// The changes are handed over as the words they are, not stitched into the sentence, so an
@@ -1639,7 +1653,8 @@ fn change_kind(doc: &Document, old: &Document, d: &Dict, before: Option<&Dict>)
// change is only a brand-new object, or one that was already a signature object when signed.
let signature_ok = before.is_none_or(is_signature_dict);
match ty {
Some(b"Sig" | b"DocTimeStamp" | b"SigRef" | b"TransformParams") if signature_ok => return kind::SIGNATURE,
// Only a new signature value or policy: rewriting a signed one is not a signature (#1788).
Some(b"Sig" | b"DocTimeStamp" | b"SigRef" | b"TransformParams") if before.is_none() => return kind::SIGNATURE,
Some(b"Catalog") => {
let keys: &[&[u8]] = &[b"AcroForm", b"DSS", b"Perms", b"Metadata", b"NeedsRendering"];
return match before {
+17
View File
@@ -238,6 +238,23 @@ fn later_changes_are_classified_under_the_signature_permissions() {
// Certified allowing comments: fine.
let certified3 = pdfcraft_sign::sign(&open(&fixture()), &id, &SignOptions { certify: Some(3), ..opts() }).unwrap();
assert_eq!(check(&edit_after(&certified3, add_comment)).status, Status::Valid);
// #1788: a later revision relaxing the signed P 1 to P 3 does not permit the comment.
let relaxed = edit_after(&certified, |doc| {
let catalog = doc.get(doc.root().unwrap()).as_dict().cloned().unwrap();
let sig = doc.resolve(catalog.get(b"Perms").unwrap()).as_dict().and_then(|p| p.get(b"DocMDP").and_then(Object::as_ref)).unwrap();
doc.update_dict(sig, |v| {
if let Some(Object::Array(r)) = v.get_mut(b"Reference")
&& let Some(Object::Dict(sr)) = r.first_mut()
&& let Some(Object::Dict(tp)) = sr.get_mut(b"TransformParams")
{
tp.set(b"P".to_vec(), Object::Int(3));
}
})
.unwrap();
add_comment(doc);
});
let s = check(&relaxed);
assert_eq!((s.certify, s.status), (Some(1), Status::Invalid), "{:?}", s.modification);
}
#[test]