You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PyHanko中断模式签名无效:‘签名数据范围字节值异常’问题排查

问题

我在Flask API中搭建PAdES签名流程,客户端使用PKCS11设备,采用中断式签名流:

  1. 用户向/pades/start接口POST其证书(PEM格式)与待签名PDF;
  2. API返回摘要给客户端,客户端使用智能卡完成签名,同时返回唯一task_id;
  3. 用户向/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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 08:44:52