DEV Community

rasmusberg6592
rasmusberg6592

Posted on

Verify Incoming Signed Invoice PDFs Against an Expected Certificate in Express

Short answer: verify the PDF signature against a pinned certificate chain, the document's byte-range digest, and a trusted signing time before an invoice enters fulfillment or accounting. Treat the uploaded bytes as immutable evidence, run verification in an isolated worker, and persist the verification result plus certificate fingerprints so a later reviewer can reproduce the decision. A green cryptographic check is necessary; it is not proof that the signer was authorized to sell the item.

A marketplace invoice is a particularly unforgiving input. The order service knows the seller, currency, tax lines, and total; the PDF arrives later through an Express endpoint and may have crossed a queue, a proxy, or a human download. The operational invariant I use is simple: the bytes we verify must be the bytes we archive, and every acceptance decision must name the certificate and policy that produced it.

That distinction saves investigations.

Reject uncertainty.

What failed in the intake path?

The failure mode is usually boring: a handler reads an upload, passes a temporary path to a PDF library, and then stores a newly generated copy. That copy can lose the original incremental update containing the signature, so an auditor sees a valid-looking invoice with no verifiable provenance. Another trap is checking only that a signature exists. An attacker can attach a valid signature from an unexpected certificate, or place a valid signature over an older revision while changing fields in a later revision.

I separate the path into four records: the original byte stream, a parsed invoice projection, the cryptographic result, and the authorization decision. The projection is disposable; the first and third records are evidence. A SHA-256 digest of the exact upload, a UTC receipt timestamp, and the certificate's SHA-256 fingerprint make accidental substitution visible. For capacity planning, the worker queue gets its own SLO: 99% of verification jobs complete within 30 seconds, while the API returns a durable job identifier instead of holding a request open during certificate and PDF parsing.

How should an Express service verify the expected certificate?

The endpoint should enforce content limits and stream to immutable storage. Verification then runs with a trust store that is versioned like code. The policy below is intentionally explicit: the leaf certificate must match the seller's registered fingerprint, its chain must validate to an approved root, the signing time must fall inside certificate validity (or a trusted timestamp policy), and the PDF byte ranges must cover the revision we archive. ISO 32000-2 defines the PDF signature model; the verifier still has to apply marketplace identity rules outside that standard.

package verify

import (
    "crypto/sha256"
    "crypto/x509"
    "encoding/hex"
    "fmt"
    "time"
)

type Evidence struct {
    PDFDigest       string
    LeafFingerprint string
    ChainOK         bool
    ByteRangeOK     bool
    SignedAt        time.Time
}

type Policy struct {
    ExpectedLeafFingerprint string
    Roots                   *x509.CertPool
}

func Accept(e Evidence, p Policy, now time.Time) error {
    if !e.ChainOK || !e.ByteRangeOK {
        return fmt.Errorf("signature evidence is incomplete")
    }
    if e.LeafFingerprint != p.ExpectedLeafFingerprint {
        return fmt.Errorf("unexpected signing certificate")
    }
    if e.SignedAt.After(now) {
        return fmt.Errorf("signature time is in the future")
    }
    return nil
}

func Digest(pdf []byte) string {
    sum := sha256.Sum256(pdf)
    return hex.EncodeToString(sum[:])
}
Enter fullscreen mode Exit fullscreen mode

The PDF library-specific step belongs behind this interface. It should return the signer certificate, validated byte ranges, and any embedded trusted timestamp, rather than a boolean that hides policy inputs. Keep private keys out of this service; verification needs public certificates and a controlled trust store. Rotate that trust store with a review record, because silently adding a root changes the meaning of every historical acceptance.

Which controls prevent a false green check?

First, bind business identity to certificate identity. Store the expected fingerprint (or an equivalent certificate identifier) with the seller account, and require an explicit re-enrollment workflow for changes. Second, reject malformed PDFs, duplicate signatures, and signatures that do not cover the final archived revision. Third, make retries idempotent: the job key is the upload digest, so a queue redelivery cannot create a second accounting event.

Observability should expose counts and latency, not invoice contents. I emit verification outcome, policy version, parser version, queue age, and certificate fingerprint; access to the original PDF remains audited and least-privileged. Alerts trigger on a rise in chain failures, unexpected fingerprints, or queue age breaching the SLO. A dead-letter queue preserves the evidence for review without silently converting an uncertain result into rejection or acceptance.

This design is not universal. If invoices are generated and signed inside the same trust boundary, an external-certificate enrollment flow may be unnecessary. If a regulator requires long-term validation, archive revocation material and trusted timestamps according to that jurisdiction's retention rules; a one-time online OCSP check is not a durable audit record.

There is a real trade-off here: pinning a leaf fingerprint gives a crisp identity check but makes certificate rotation a coordinated release, while trusting a broader issuer reduces rotation work and increases the blast radius of a compromised account. Small marketplaces may accept manual review during rotation; high-volume platforms usually automate dual-fingerprint overlap and keep the old certificate only for historical verification.

The decision rule is therefore operational: accept only when cryptographic coverage, expected signer identity, and an auditable evidence package agree. Everything else is a review state with a reason code. That keeps fulfillment fast for normal traffic while giving accounting a reproducible answer months later, even after certificates and software versions change.

Sources

Top comments (0)