Search code examples
pythonflaskpadespyhanko

PyHanko - Invalid signature with error "Unexpected byte range values defining scope of signed data" in interrupted mode


I try to setup a pades signature flow in our Flask API.

As we use PKCS11 devices on the clients computers, we need to use the interrupted signing flow :

  • User POST on /pades/start with his certificate as a PEM file and the PDF to sign.
  • The API return the digest to the client, who use the smartcard to sign it, and an unique task_id
  • User POST on /pades/complete with his task_id and the computed signature. The API use this signature to create the digitally signed PDF

Currently, this flow works. But the generated PDF is considered having an invalid signature with this message "Unexpected byte range values defining scope of signed data. Details: The signature byte range is invalid"

# Relevant part in the /pades/start route 
    with open(task_dir / "certificate.pem", "w") as f:
        f.write(body["certificate"])

    cert = load_cert_from_pemder(task_dir / "certificate.pem")

    with open(task_dir / "document.pdf", "rb+") as f:
        writer = IncrementalPdfFileWriter(f)
        fields.append_signature_field(
            writer,
            sig_field_spec=fields.SigFieldSpec("Signature", box=(200, 600, 400, 660)),
        )

        meta = signers.PdfSignatureMetadata(
            field_name="Signature",
            subfilter=fields.SigSeedSubFilter.PADES,
            md_algorithm="sha256",
        )

        ext_signer = signers.ExternalSigner(
            signing_cert=cert,
            cert_registry=registry.CertificateRegistry(),
            signature_value=bytes(8192), # I tried to adjust this with many different values without success
        )

        pdf_signer = signers.PdfSigner(meta, signer=ext_signer)

        prep_digest, tbs_document, _ = pdf_signer.digest_doc_for_signing(writer)

        post_sign_instructions = tbs_document.post_sign_instructions

        def async_to_sync(awaitable):
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            return loop.run_until_complete(awaitable)

        signed_attrs: asn1crypto.cms.CMSAttributes = async_to_sync(
            ext_signer.signed_attrs(
                prep_digest.document_digest, "sha256", use_pades=True
            )
        )

        task = {
            **(body or {}),
            "id": task_id,
            "prep_digest": prep_digest,
            "signed_attrs": signed_attrs,
            "psi": post_sign_instructions,
        }

        redis.set(
            f"task:{task_id}",
            pickle.dumps(task),
        )

        writer.write_in_place()

    return {"task": task_id, "digest": prep_digest.document_digest.hex()}

# Relevant part in the /pades/complete route 

    task_id = body["task"]
    task_str = redis.get(f"task:{task_id}")
    task = pickle.loads(task_str) if task_str else None
    task_dir = Path(get_task_dir(settings.WORKDIR, task_id))

    if not task:
        return {"error": "Task not found"}, 404

    ext_signer = signers.ExternalSigner(
        signing_cert=load_cert_from_pemder(task_dir / "certificate.pem"),
        signature_value=bytes.fromhex(body["signature"]),
        cert_registry=registry.CertificateRegistry(),
    )

    sig_cms = ext_signer.sign_prescribed_attributes(
        "sha256", signed_attrs=task["signed_attrs"]
    )

    with open(task_dir / "document.pdf", "rb+") as f:
        PdfTBSDocument.finish_signing(
            f,
            prepared_digest=task["prep_digest"],
            signature_cms=sig_cms,
            post_sign_instr=task["psi"],
        )

    redis.delete(f"task:{task_id}")

    return "ok"

What can I try to fix this error message ?


Solution

  • After many hours of search, I found the solution. I let it here for the next one who need it

    The problem occurred because :

    • We need to return the signed_attrs.dump() instead of the prep_digest.document_digest (and hash it !)

    • We should use pdf_signer.digest_doc_for_signing(pdf_out=writer,in_place=True) during digest computation, to allow the pdf to be correctly updated

    This is my working example

    # relevant part of /pades/start 
    
       with open(task_dir / "certificate.pem", "w") as f:
            f.write(body["certificate"])
    
        cert = load_cert_from_pemder(task_dir / "certificate.pem")
    
        with open(task_dir / "document.pdf", "rb+") as f:
            writer = IncrementalPdfFileWriter(f)
    
            fields.append_signature_field(
                writer,
                sig_field_spec=fields.SigFieldSpec(
                    sig_field_name=sig_name,
                    box=box,
                ),
            )
            writer.write_in_place()
    
        with open(task_dir / "document.pdf", "rb+") as f:
            writer = IncrementalPdfFileWriter(f)
            meta = signers.PdfSignatureMetadata(
                field_name=sig_name,
                md_algorithm="sha256",
                reason="Signature du parapheur",
                dss_settings=DSSContentSettings(include_vri=False),
                subfilter=fields.SigSeedSubFilter.PADES,
            )
    
            ext_signer = signers.ExternalSigner(
                signing_cert=cert,
                cert_registry=registry.CertificateRegistry(),
                signature_value=bytes(256),
            )
    
            pdf_signer = signers.PdfSigner(
                signature_meta=meta,
                signer=ext_signer,
                stamp_style=stamp.TextStampStyle(
                    stamp_text="",
                    border_width=0,
                    background=images.PdfImage(str(signature.path)),
                    background_opacity=1,
                ),
            )
    
            prep_digest, _, _ = pdf_signer.digest_doc_for_signing(
                pdf_out=writer,
                in_place=True,
            )
    
            def async_to_sync(awaitable):
                loop = asyncio.new_event_loop()
                asyncio.set_event_loop(loop)
                return loop.run_until_complete(awaitable)
    
            signed_attrs: asn1crypto.cms.CMSAttributes = async_to_sync(
                ext_signer.signed_attrs(
                    data_digest=prep_digest.document_digest,
                    digest_algorithm="sha256",
                    use_pades=True,
                )
            )
    
            task = {
                **(body or {}),
                "id": task_id,
                "prep_digest": prep_digest,
                "signed_attrs": signed_attrs,
            }
    
            redis.set(
                f"task:{task_id}",
                pickle.dumps(task),
            )
    
        return {
            "task": task_id,
            "digest": hashlib.sha256(signed_attrs.dump()).hexdigest(),
        }
        
    # relevant part of /pades/complete 
      
        task_id = body["task"]
        task_str = redis.get(f"task:{task_id}")
        task = pickle.loads(task_str) if task_str else None
        task_dir = Path(get_task_dir(settings.WORKDIR, task_id))
    
        if not task:
            return {"error": "Task not found"}, 404
    
        signed_attrs: asn1crypto.cms.CMSAttributes = task["signed_attrs"]
    
        ext_signer = signers.ExternalSigner(
            signing_cert=load_cert_from_pemder(task_dir / "certificate.pem"),
            signature_value=bytes.fromhex(body["signature"]),
            cert_registry=registry.CertificateRegistry(),
            signature_mechanism=SignedDigestAlgorithm({"algorithm": "rsassa_pkcs1v15"}),
        )
    
        sig_cms = ext_signer.sign_prescribed_attributes(
            digest_algorithm="sha256",
            signed_attrs=signed_attrs,
        )
    
        prep: PreparedByteRangeDigest = task["prep_digest"]
    
        with open(task_dir / "document.pdf", "rb+") as f:
            PdfTBSDocument.finish_signing(
                output=f,
                signature_cms=sig_cms,
                prepared_digest=prep,
            )
    
        redis.delete(f"task:{task_id}")  
    

    Some part are optional, but i've become a little bit paranoid along the way :-D