PyHanko – неверная подпись с ошибкой «Неожиданные значения диапазона байтов, определяющие объем подписанных данных» в прерванном режиме

Я пытаюсь настроить поток подписи Pades в нашем Flask API.

Поскольку мы используем устройства PKCS11 на клиентских компьютерах, нам необходимо использовать прерванный поток подписи:

  • Пользователь POST на /pades/start со своим сертификатом в виде PEM-файла и PDF-файлом для подписи.
  • API возвращает дайджест клиенту, который использует смарт-карту для его подписи, а также уникальный идентификатор Task_id.
  • Пользователь POST на /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 популярен в машинном обучении и искусственном интеллекте.


1
50
1

Ответ:

Решено

После многих часов поисков я нашел решение. Я оставлю это здесь для следующего, кому это понадобится.

Проблема возникла потому что:

  • Нам нужно вернуть 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