Я пытаюсь настроить поток подписи Pades в нашем Flask API.
Поскольку мы используем устройства PKCS11 на клиентских компьютерах, нам необходимо использовать прерванный поток подписи:
/pades/start
со своим сертификатом в виде PEM-файла и PDF-файлом для подписи./pades/complete
со своим идентификатором задачи и вычисленной подписью. API использует эту подпись для создания PDF-файла с цифровой подписью.В настоящее время этот поток работает. Но сгенерированный PDF-файл считается имеющим недопустимую подпись с этим сообщением «Неожиданные значения диапазона байтов, определяющие объем подписанных данных». Подробности: диапазон байтов подписи недействителен».
# 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"
Что я могу попробовать исправить это сообщение об ошибке?
🤔 А знаете ли вы, что...
Python популярен в машинном обучении и искусственном интеллекте.
После многих часов поисков я нашел решение. Я оставлю это здесь для следующего, кому это понадобится.
Проблема возникла потому что:
Нам нужно вернуть signed_attrs.dump()
вместо prep_digest.document_digest
(и хэшировать его!)
Мы должны использовать pdf_signer.digest_doc_for_signing(pdf_out=writer,in_place=True)
во время вычисления дайджеста, чтобы обеспечить правильное обновление PDF-файла.
Это мой рабочий пример
# 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}")
Некоторые части не являются обязательными, но за это время я стал немного параноиком :-D