MatthiasValvekens / pyHanko

pyHanko: sign and stamp PDF files
MIT License
483 stars 71 forks source link

TextStamp applied after signature invalidates the previous signature #259

Closed C-monC closed 1 year ago

C-monC commented 1 year ago

Hi,

My understanding of the IncrementalPdfFileWriter is that adding a stamp and then signing a document with signatures already should create a new version.

Here is code to reproduce the invalidation.

from pyhanko.sign.fields import SigFieldSpec
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
from pyhanko.sign import fields, signers
from pyhanko import stamp
from pyhanko.pdf_utils import images
from pyhanko.pdf_utils.layout import SimpleBoxLayoutRule, Margins, AxisAlignment
from pyhanko.sign import timestamps
from pyhanko.sign.fields import SigSeedSubFilter

timestamp_servers = [
    "https://rfc3161.ai.moda",
]

cms_signer = signers.SimpleSigner.load("key.pem", "cert.pem", key_passphrase=b"test")

with open("orgi.pdf", "rb+") as doc:
    w = IncrementalPdfFileWriter(doc)

    text_style = stamp.TextStampStyle(
        stamp_text="Hello World",
    )

    stamp.TextStamp(w, text_style).apply(0, x=100, y=100)

    fields.append_signature_field(
        w,
        sig_field_spec=SigFieldSpec(
            "Signature2",
            box=(200, 200, 300, 300),
            doc_mdp_update_value=fields.MDPPerm.ANNOTATE,
        ),
    )
    timestamper = timestamps.HTTPTimeStamper(
        url=timestamp_servers[0],
    )
    signature_meta = signers.PdfSignatureMetadata(
        field_name="Signature2",
        md_algorithm="sha256",
        docmdp_permissions=fields.MDPPerm.ANNOTATE,
        subfilter=SigSeedSubFilter.PADES,
        use_pades_lta=True,
    )
    pdf_signer = signers.PdfSigner(
        signature_meta,
        timestamper=timestamper,
        signer=cms_signer,
        stamp_style=stamp.TextStampStyle(
            timestamp_format="",
            background_layout=SimpleBoxLayoutRule(
                x_align=AxisAlignment.ALIGN_MID,
                y_align=AxisAlignment.ALIGN_MID,
                margins=Margins.uniform(0),
            ),
            background=images.PdfImage("image.png"),
            border_width=0,
            background_opacity=1,
        ),
    )
    with open("document-signed.pdf", "wb") as outf:
        pdf_signer.sign_pdf(
            pdf_out=w,
            output=outf,
        )

with open("document-signed.pdf", "rb+") as doc:
    w = IncrementalPdfFileWriter(doc)

    text_style = stamp.TextStampStyle(
        stamp_text="Hello World2",
    )

    stamp.TextStamp(w, text_style).apply(0, x=400, y=400)

    fields.append_signature_field(
        w,
        sig_field_spec=SigFieldSpec(
            "Signature",
            box=(300, 300, 400, 400),
            doc_mdp_update_value=fields.MDPPerm.ANNOTATE,
        ),
    )
    timestamper = timestamps.HTTPTimeStamper(
        url=timestamp_servers[0],
    )
    signature_meta = signers.PdfSignatureMetadata(
        field_name="Signature",
        md_algorithm="sha256",
        docmdp_permissions=fields.MDPPerm.ANNOTATE,
        subfilter=SigSeedSubFilter.PADES,
        use_pades_lta=True,
    )
    pdf_signer = signers.PdfSigner(
        signature_meta,
        timestamper=timestamper,
        signer=cms_signer,
        stamp_style=stamp.TextStampStyle(
            timestamp_format="",
            background_layout=SimpleBoxLayoutRule(
                x_align=AxisAlignment.ALIGN_MID,
                y_align=AxisAlignment.ALIGN_MID,
                margins=Margins.uniform(0),
            ),
            background=images.PdfImage("image.png"),
            border_width=0,
            background_opacity=1,
        ),
    )
    with open("document-signed2.pdf", "wb") as outf:
        pdf_signer.sign_pdf(
            pdf_out=w,
            output=outf,
        )

document-signed2.pdf has the first signature invalidated.

MatthiasValvekens commented 1 year ago

Hi @C-monC,

I presume that by "invalidated" you mean that Acrobat/pyHanko/... judges the signature as invalid? That is expected behaviour: an incremental update will indeed not destroy the underlying signature from a cryptographic point of view (the data remains intact), but the data appended can affect the appearance of the document.

Since such alterations can also significantly change the content of the document a priori, "serious" PDF signature validation implementations will also analyse incremental changes made after a signature, and fail validation if they don't consider them reasonable.

Unlike adding a new signature (which works with form fields/annotations), applying a TextStamp directly affects the content stream of a page. All incremental update validators that I'm aware of will invalidate prior signatures for page content changes, no questions asked.

Hope that helps.

I'll be moving this to the discussion board since it's not a bug. There might be a different way to achieve the outcome you want without breaking the PDF signature model. If you follow up with some more details about your use case, maybe we can find a more robust solution.