mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-29 12:38:58 -06:00
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:
co-authored by
Claude Opus 5.5
parent
048cf14d31
commit
64c5fd8ef0
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user