From a605c2142cec1d7c4feebb70ea6a859bf01a8422 Mon Sep 17 00:00:00 2001 From: Matthias Valvekens Date: Wed, 27 Mar 2024 20:53:48 +0100 Subject: [PATCH] Deal with sigs in encrypted docs when copying See #412 --- pyhanko/pdf_utils/writer.py | 42 +++++++++++++++ ...gned-encrypted-pubkey-with-catalog-ref.pdf | Bin 0 -> 17450 bytes pyhanko_tests/test_sign_encrypted.py | 51 ++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 pyhanko_tests/data/pdf/signed-encrypted-pubkey-with-catalog-ref.pdf diff --git a/pyhanko/pdf_utils/writer.py b/pyhanko/pdf_utils/writer.py index 3e4a014b..96061b50 100644 --- a/pyhanko/pdf_utils/writer.py +++ b/pyhanko/pdf_utils/writer.py @@ -4,6 +4,7 @@ for the original license. """ +import logging import os import typing from typing import Dict, Iterable, List, Optional, Set, Tuple, Union, cast @@ -53,6 +54,8 @@ 'copy_into_new_writer', ] +logger = logging.getLogger(__name__) + # TODO move this to content.py? def init_xobject_dictionary( @@ -1240,6 +1243,44 @@ def process_reference(self, ref: generic.Reference) -> generic.PdfObject: self.queued_references.append((ref, new_ido.reference)) return new_ido + def preprocess_signature_data(self): + # Signature /Contents is never encrypted => ensure we respect that + # (even though the import operation is guaranteed to break the signature + # there are valid use cases for stripping the encryption on such files, + # e.g. for downstream processing) + from ..sign.fields import enumerate_sig_fields + + signature_dict_refs = [ + field_value.reference + for fq_name, field_value, field_ref in enumerate_sig_fields( + self.source, filled_status=True + ) + # this is the case in all valid PDFs + if isinstance(field_value, generic.IndirectObject) + ] + if signature_dict_refs: + logger.warning( + "Source document contains filled signature fields--the copy " + "operation will invalidate them." + ) + for ref in signature_dict_refs: + sig_dict = ref.get_object() + assert isinstance(sig_dict, generic.DictionaryObject) + raw_dict = { + k: self._ingest(v) + for k, v in sig_dict.items() + if k != '/Contents' + } + raw_dict['/Contents'] = generic.ByteStringObject( + sig_dict.raw_get( + '/Contents', decrypt=generic.EncryptedObjAccess.RAW + ).original_bytes + ) + self.reference_map[ref] = self.target.add_object( + generic.DictionaryObject(raw_dict), + obj_stream=None, + ) + def copy_into_new_writer( input_handler: PdfHandler, writer_kwargs: Optional[dict] = None @@ -1290,6 +1331,7 @@ def copy_into_new_writer( }, obj_stream=None, ) + importer.preprocess_signature_data() new_root_dict = importer.import_object(input_handler.root) # override the old root ref ix = (output_root_ref.generation, output_root_ref.idnum) diff --git a/pyhanko_tests/data/pdf/signed-encrypted-pubkey-with-catalog-ref.pdf b/pyhanko_tests/data/pdf/signed-encrypted-pubkey-with-catalog-ref.pdf new file mode 100644 index 0000000000000000000000000000000000000000..db1883957e79ac7533bfcd8fdf8230b1f35dca8f GIT binary patch literal 17450 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 Km11VU;eP`8<(KjR literal 0 HcmV?d00001 diff --git a/pyhanko_tests/test_sign_encrypted.py b/pyhanko_tests/test_sign_encrypted.py index b535be64..19586088 100644 --- a/pyhanko_tests/test_sign_encrypted.py +++ b/pyhanko_tests/test_sign_encrypted.py @@ -5,12 +5,14 @@ from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter from pyhanko.pdf_utils.reader import PdfFileReader +from pyhanko.pdf_utils.writer import copy_into_new_writer from pyhanko.sign import signers from pyhanko.sign.diff_analysis import ModificationLevel from pyhanko.sign.signers.pdf_signer import ( DSSContentSettings, SigDSSPlacementPreference, ) +from pyhanko.sign.validation import validate_pdf_signature from pyhanko_tests.samples import ( MINIMAL_AES256, MINIMAL_ONE_FIELD_AES256, @@ -18,11 +20,13 @@ MINIMAL_PUBKEY_ONE_FIELD_AES256, MINIMAL_PUBKEY_ONE_FIELD_RC4, MINIMAL_RC4, + PDF_DATA_DIR, PUBKEY_SELFSIGNED_DECRYPTER, ) from pyhanko_tests.signing_commons import ( DUMMY_HTTP_TS, FROM_CA, + SIMPLE_V_CONTEXT, live_testing_vc, val_trusted, ) @@ -175,3 +179,50 @@ def test_sign_encrypted_with_post_sign(requests_mock, password, file): assert status.modification_level == ModificationLevel.LTA_UPDATES assert len(r.embedded_regular_signatures) == 1 assert len(r.embedded_timestamp_signatures) == 1 + + +def test_copy_encrypted_signed_file(): + w = IncrementalPdfFileWriter(BytesIO(MINIMAL_ONE_FIELD_AES256)) + w.encrypt("ownersecret") + out = signers.sign_pdf( + w, + signers.PdfSignatureMetadata(), + signer=FROM_CA, + existing_fields_only=True, + ) + + r = PdfFileReader(out) + r.decrypt("ownersecret") + w = copy_into_new_writer(r) + out2 = BytesIO() + w.write(out2) + + r = PdfFileReader(out2) + assert not r.encrypted + s = r.embedded_signatures[0] + s.compute_integrity_info() + status = validate_pdf_signature(s, SIMPLE_V_CONTEXT(), skip_diff=True) + assert not status.intact + + +def test_copy_file_with_mdp_signature_and_backref(): + # This file has /Data in a signature reference dictionary + # pointing back to the root (which is sometimes still seen in + # FieldMDP signatures generated by Acrobat, among others) + + fname = f"{PDF_DATA_DIR}/signed-encrypted-pubkey-with-catalog-ref.pdf" + with open(fname, 'rb') as inf: + + r = PdfFileReader(inf) + r.decrypt_pubkey(PUBKEY_SELFSIGNED_DECRYPTER) + + w = copy_into_new_writer(r) + out2 = BytesIO() + w.write(out2) + + r = PdfFileReader(out2) + assert not r.encrypted + s = r.embedded_signatures[0] + s.compute_integrity_info() + status = validate_pdf_signature(s, SIMPLE_V_CONTEXT(), skip_diff=True) + assert not status.intact