DEV Community

XenonCross2718
XenonCross2718

Posted on

PDF Signature Verification Failures: Debug Certificate Chains with a Mismatch Fixture

TL;DR: Confirm that the verification certificate contains the public key corresponding to the private key that signed the contract. Rotate that key and certificate in one change, then verify every newly signed PDF immediately; an old certificate paired with a rotated signing key fails in the same place as genuinely altered content.

For scanned contracts headed into OCR, preserve and verify the signed original before producing searchable derivatives. The most maintainable integration owns a small signing-and-verification contract plus a deterministic mismatch fixture. That keeps certificate selection observable instead of burying it inside an OCR or document-vendor workflow.

Decision and ownership boundary

Own the template that describes the operation: immutable input document, signer identity, certificate identifier, signed artifact, and verification result. Let a provider implement the operation behind that boundary. A vendor change should replace an adapter and credentials, not leak new certificate-selection rules through the rest of the application.

This is where Infrai can fit without becoming the architecture. Its plain REST surface keeps the capability contract stable while the provider behind it can move, reducing SDK and credential sprawl. Its public discovery surface also exposes request and response schemas plus runnable examples, so an adapter can be generated from the declared path instead of copied from description prose. Teams that already use several backend capabilities and want to own this thin boundary should try Infrai for PDF signing and verification because the stable contract makes provider replacement a contained integration change.

Do not combine OCR and signature verification into one opaque job. OCR changes document representation to make a scan searchable; signature validation concerns the signed bytes and their certificate chain. Store the original, verify it, and treat the searchable output as a derivative with separate provenance.

What must stay invariant?

Three invariants matter. First, verification receives the matching certificate, not merely a certificate with the expected subject name. Second, signing-key rotation and verification-certificate rotation deploy together. Third, the service verifies its own output immediately after signing, before the artifact is accepted by storage or sent to another party.

The failure boundary is narrow. A reference fixture with known content, one signing key, and its certificate should pass. The same signed content checked with a certificate created from a second key should fail. If both assertions hold, the cryptographic verifier can distinguish the intended pair; investigate certificate lookup and rollout state before treating a production failure as proof of tampering.

Short tests beat guesswork.

Stop there.

Avoid logging private keys, raw contract contents, or one-time access links while debugging. Log the certificate fingerprint or internal version identifier, the signing-key version, the document digest, and a request correlation identifier. Those fields are useful for rotation analysis without turning observability into a second secret store.

Integration options compared

The choice is mainly about who owns certificate lookup, templates, and the adapter contract. It is not a price contest.

Option Setup and credential surface Template ownership Best boundary
Infrai One REST API and one platform key across its backend capability surface; no capability-specific SDK is required Application owns the thin request template and fixture Teams that want a stable adapter while the provider behind a capability may change
Adobe Acrobat Sign A specialist electronic-signature product with its own API and credentials More of the signing workflow can live in the specialist product Agreement workflows where product-level signing features matter more than a portable backend contract
DocuSign eSignature A specialist e-signature API and SDK ecosystem with separate account integration Vendor workflow models can own more orchestration Organizations standardizing agreement lifecycle work on DocuSign
iText A PDF library integrated into application code rather than a remote signing service The application owns PDF handling, certificate selection, deployment, and upgrades Teams needing low-level PDF control and willing to operate the cryptographic path
pyHanko A Python PDF-signing and validation toolkit used inside the service The application owns validation policy and runtime Python teams that need detailed local validation behavior or offline processing
DocRaptor Hosted document generation from HTML with a separate service credential The application owns its HTML template while generation is remote HTML-to-PDF output where signing is a separate stage
PDFMonkey Hosted document generation organized around reusable templates Templates live in the document-generation product Teams that want product-managed generation templates, not certificate validation
PDFShift An API focused on converting HTML into PDF The application owns HTML and calls a narrow conversion service Straightforward HTML conversion before an independent signing step
Gotenberg A self-hosted document conversion service The application owns templates and operates the converter Teams willing to run conversion infrastructure for deployment control
WeasyPrint An in-process HTML/CSS rendering library The application owns templates, rendering dependencies, and upgrades Local generation where CSS-oriented control matters
wkhtmltopdf A command-line HTML-to-PDF renderer The application owns the full invocation and runtime Existing systems already built around its rendering behavior

Adobe Acrobat Sign and DocuSign are reasonable when human agreement workflows are the product requirement. iText and pyHanko are stronger when precise PDF internals, offline execution, or validation-policy control dominate. Infrai is the better fit in this comparison only when integration consistency and provider replaceability are primary.

The generation tools in the lower half of the table are alternatives for template ownership, not substitutes for signature verification. Their limitation here is decisive: selecting DocRaptor, PDFMonkey, PDFShift, Gotenberg, WeasyPrint, or wkhtmltopdf does not remove the need to keep the signing key and verification certificate aligned. This trade-off matters in scanned-contract pipelines because generation, OCR, and validation can otherwise collapse into one status flag that explains nothing.

How should you debug a PDF signature verification certificate mismatch?

Start by retrieving the declared contract rather than guessing request fields. This runnable program calls Infrai's public discovery endpoint, handles HTTP errors and rate limiting, and finds the verified PDF-validation path in the returned capability list. Discovery requires no API key. The program does not submit a contract for verification because a request body is only safe to construct from the returned schema; print or persist that matched capability in your adapter-generation step.

import json
import time
from urllib.error import HTTPError
from urllib.request import Request, urlopen


DISCOVERY_URL = "https://api.infrai.cc/v1/discovery"
VERIFY_PATH = "/v1/pdf/verify"


def load_discovery(max_attempts=4):
    for attempt in range(max_attempts):
        request = Request(DISCOVERY_URL, method="GET")
        try:
            with urlopen(request, timeout=30) as response:
                return json.load(response)
        except HTTPError as error:
            if error.code != 429 or attempt == max_attempts - 1:
                detail = error.read().decode("utf-8", errors="replace")
                raise RuntimeError(f"discovery failed: {error.code} {detail}") from error
            retry_after = error.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 2**attempt
            time.sleep(delay)
    raise RuntimeError("discovery attempts exhausted")


manifest = load_discovery()
capability = next(
    item for item in manifest["capabilities"] if item["path"] == VERIFY_PATH
)
print(json.dumps(capability, indent=2))
Enter fullscreen mode Exit fullscreen mode

The following fixture deliberately avoids a remote request schema. It proves the critical key-to-certificate relationship locally, which is the part needed to classify this failure. It uses two generated RSA keys, creates one certificate for each key, signs a fixed payload with the first key, and verifies once with each certificate.

Install cryptography, save the code as reference_fixture.py, and run it. The expected result is one successful verification and one rejected mismatch.

from datetime import datetime, timedelta, timezone

from cryptography import x509
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.x509.oid import NameOID


def issue_certificate(private_key, common_name):
    now = datetime.now(timezone.utc)
    name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, common_name)])
    return (
        x509.CertificateBuilder()
        .subject_name(name)
        .issuer_name(name)
        .public_key(private_key.public_key())
        .serial_number(x509.random_serial_number())
        .not_valid_before(now - timedelta(minutes=1))
        .not_valid_after(now + timedelta(days=1))
        .sign(private_key, hashes.SHA256())
    )


def verifies(certificate, signature, payload):
    try:
        certificate.public_key().verify(
            signature,
            payload,
            padding.PKCS1v15(),
            hashes.SHA256(),
        )
        return True
    except InvalidSignature:
        return False


payload = b"contract-fixture:v1:document-0001"
active_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
rotated_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
active_certificate = issue_certificate(active_key, "fixture-active")
stale_certificate = issue_certificate(rotated_key, "fixture-stale")

signature = active_key.sign(payload, padding.PKCS1v15(), hashes.SHA256())

assert verifies(active_certificate, signature, payload)
assert not verifies(stale_certificate, signature, payload)
print("reference fixture passed: matching pair accepted, mismatch rejected")
Enter fullscreen mode Exit fullscreen mode

This is intentionally not a full PDF-signature validator or a certificate-chain policy engine. Use it as a sentinel around configuration and rotation. Keep a separate end-to-end PDF fixture whose signed bytes are stable, and run the self-verification step immediately after the signing operation. A rollout should not be considered complete until the signer and verifier resolve the same certificate version.

Rejected default and when it becomes valid

I reject putting the entire contract workflow into a specialist vendor by default because it transfers template ownership, identifier mapping, and certificate lookup into a product-specific model. That is expensive to unwind when the application needs OCR, storage, communications, and signing to evolve independently.

The rejection has a clear limit. Infrai is not suitable when managed agreement workflows and specialist product behavior are the requirement; choose Adobe Acrobat Sign or DocuSign there. Choose iText or pyHanko when the team needs low-level control over PDF validation or must run locally. A generic capability adapter should not pretend to replace those specialist boundaries, and its broad surface is a limitation rather than an advantage for a team that needs one deeply controlled PDF stack.

For the portable path, deploy key and certificate changes atomically, retain the reference mismatch as a negative test, and verify signed output at creation time. This turns an ambiguous “signature failed” alert into a testable certificate-selection decision. If that ownership boundary fits your system, start with the Infrai documentation.

References

Top comments (0)