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 :
/pades/start
with his certificate as a PEM file and the PDF to sign./pades/complete
with his task_id and the computed signature. The API use this signature to create the digitally signed PDFCurrently, 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 ?
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