M6: signing, timestamping and DSS writes carry no Password-field values (#3926)

CodeRabbit on #3943: sign, sign_with_timestamp, timestamp_document and dss::embed
each write a new incremental file from editor.cos, outside the writable() boundary
saves use, so a typed Password value was signed into the file. Each now writes from
the writable() copy. The new test signs a form with a typed value and checks the
signed file holds the masked appearance but not the value; the three TSA/DSS paths
need a timestamp server or validation data, and use the same one-line change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mtlcl410
2026-10-23 14:38:29 +08:00
co-authored by Claude Opus 5.5
parent 048cf14d31
commit 64c5fd8ef0
2 changed files with 26 additions and 4 deletions
+9 -4
View File
@@ -5104,7 +5104,9 @@ impl Session {
if opts.date.is_empty() {
opts.date = self.signing_date();
}
Ok(Arc::new(pdfcraft_sign::sign(&editor.cos, id, &opts)?))
// A typed Password-field value is not signed into the file (#3926), as it isn't saved.
let cos = writable(&editor.cos)?;
Ok(Arc::new(pdfcraft_sign::sign(&cos, id, &opts)?))
}
/// [`Session::sign`], embedding an RFC 3161 signature timestamp (PAdES B-T) produced by
@@ -5121,7 +5123,8 @@ impl Session {
if opts.date.is_empty() {
opts.date = self.signing_date();
}
let signed = Arc::new(pdfcraft_sign::sign_with_timestamp(&editor.cos, id, &opts, tsa)?);
let cos = writable(&editor.cos)?;
let signed = Arc::new(pdfcraft_sign::sign_with_timestamp(&cos, id, &opts, tsa)?);
// Checked like a save before anyone writes it (#2166).
self.check_save(doc, &signed)?;
Ok(signed)
@@ -5133,7 +5136,8 @@ impl Session {
let d = self.get(doc).ok_or(EditError::NoDocument)?;
let editor = d.editor.as_ref().ok_or_else(|| EditError::ReadOnly(d.read_only_reason.clone().unwrap_or_default()))?;
let date = if date.is_empty() { self.signing_date() } else { date };
let stamped = Arc::new(pdfcraft_sign::timestamp_document(&editor.cos, tsa, &date)?);
let cos = writable(&editor.cos)?;
let stamped = Arc::new(pdfcraft_sign::timestamp_document(&cos, tsa, &date)?);
self.check_save(doc, &stamped)?;
Ok(stamped)
}
@@ -5143,7 +5147,8 @@ impl Session {
pub fn embed_ltv(&self, doc: DocId, evidence: &pdfcraft_sign::dss::Evidence) -> Result<Arc<Vec<u8>>, EditError> {
let d = self.get(doc).ok_or(EditError::NoDocument)?;
let editor = d.editor.as_ref().ok_or_else(|| EditError::ReadOnly(d.read_only_reason.clone().unwrap_or_default()))?;
let embedded = Arc::new(pdfcraft_sign::dss::embed(&editor.cos, evidence)?);
let cos = writable(&editor.cos)?;
let embedded = Arc::new(pdfcraft_sign::dss::embed(&cos, evidence)?);
self.check_save(doc, &embedded)?;
Ok(embedded)
}
+17
View File
@@ -2081,6 +2081,23 @@ fn password_field_values_are_kept_in_memory_but_written_to_no_file() {
assert!(t.get(reopened).unwrap().form.iter().find(|f| f.name == "name").unwrap().value.iter().all(|v| v.is_empty()));
}
/// #3926 / #3943 review: signing writes the signed file from the same copy a save does, so a
/// typed Password-field value is not signed into the file either.
#[test]
fn a_signed_file_carries_no_password_field_value() {
let p12 = std::fs::read(concat!(env!("CARGO_MANIFEST_DIR"), "/../sign/tests/data/ec-p256.p12")).unwrap();
let digital_id = sign::pkcs12::open(&p12, "test").unwrap();
let fixture = String::from_utf8(form_fixture()).unwrap().replace("/T (name)", "/T (name) /Ff 8192");
// After the test certificate's start date (2026-10-02).
let mut s = Session::new().with_clock(|| 1_800_000_000);
let id = s.open("form.pdf", None, Arc::new(fixture.into_bytes()), None).unwrap();
s.apply(id, Edit::SetFieldValue { name: "name".into(), value: FieldValue::Text("hunter2".into()) }).unwrap();
assert!(appears_in_pdf(&s.get(id).unwrap().bytes, "hunter2"), "the working bytes hold it");
let signed = s.sign(id, &digital_id, SignOptions { rect: None, ..SignOptions::default() }).unwrap();
assert!(!appears_in_pdf(&signed, "hunter2"), "the signed file");
assert!(appears_in_pdf(&signed, "*******"), "the masked appearance is signed in");
}
/// A value its field's font can't draw is kept, and the edit that drew it reports it in
/// `edit_notices` (the interface shows it, tools return it): not a later unrelated edit, not a
/// failed one, and not a value that can be drawn. The form (an inline AcroForm here) asks other