PyHanko中断模式签名无效:‘签名数据范围字节值异常’问题排查
问题
我在Flask API中搭建PAdES签名流程,客户端使用PKCS11设备,采用中断式签名流:
- 用户向
/pades/start接口POST其证书(PEM格式)与待签名PDF; - API返回摘要给客户端,客户端使用智能卡完成签名,同时返回唯一task_id;
- 用户向
/pades/complete接口POST task_id与生成的签名,API据此创建数字签名PDF。
当前流程可正常运行,但生成的PDF被判定为签名无效,错误信息为:
"Unexpected byte range values defining scope of signed data. Details: The signature byte range is invalid"
相关代码
/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("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), # 尝试过调整多个值都无效 ) 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()}
/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 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"
修复建议
1. 确保增量写入后的PDF文件状态一致
在/pades/start中调用writer.write_in_place()后,强制刷新缓冲区并确认文件状态,避免后续读取时出现内容不一致:
with open(task_dir / "document.pdf", "rb+") as f: writer = IncrementalPdfFileWriter(f) # ... 其他代码 ... writer.write_in_place() f.flush() # 强制刷新缓冲区到磁盘
同时检查task_dir下的PDF文件在start阶段结束后,是否已正确写入签名占位符,无文件损坏情况。
2. 匹配签名占位符与实际签名长度
你设置了固定的signature_value=bytes(8192)占位符,但实际生成的CMS签名长度可能与该值不匹配,导致字节范围计算错误:
- 改为根据证书密钥类型预估合理长度(例如RSA 2048位签名的CMS结构通常在1500-2000字节左右)
- 或在
complete阶段完成签名后,验证sig_cms的实际长度,若与占位符不符,调整占位符长度(需确认所用PDF签名库是否支持动态调整)
3. 避免复杂对象的pickle序列化
用pickle序列化prep_digest和post_sign_instructions存储到Redis,可能导致对象反序列化后丢失关键信息(如精确的字节范围值):
- 改为存储这些对象的关键参数(如字节范围的起始/结束值、摘要值等),在
complete阶段重新构建所需对象 - 或确认所用PDF签名库的对象是否支持安全的pickle序列化,部分库的内部状态无法被正确序列化
4. 修正签名完成阶段的文件操作
在/pades/complete中打开文件后,确保文件指针位置正确,避免写入偏移错误:
with open(task_dir / "document.pdf", "rb+") as f: f.seek(0) # 将文件指针移到文件开头 PdfTBSDocument.finish_signing( f, prepared_digest=task["prep_digest"], signature_cms=sig_cms, post_sign_instr=task["psi"], )
同时检查文件是否被其他进程锁定,导致写入不完整。
5. 对比有效PDF的字节范围
生成一个已知有效的PAdES签名PDF,用PDF解析工具(如pdftk)查看其签名的ByteRange值,对比你生成的PDF的ByteRange,定位是占位符阶段还是签名写入阶段的格式错误(如长度不匹配、起始位置错误)。
内容的提问来源于stack exchange,提问作者Varkal
相关产品推荐
相关产品推荐

