Blog
← All posts
Technical · 10 min read

The SDF Security Model: Signatures, Hashes, and Offline Verification

Y
Yunus YILDIZ
Founder, Etapsky

When we designed SDF's security model, we set one hard constraint: a recipient must be able to verify the authenticity and integrity of an SDF document 20 years from now, with no network connection, using only the document itself and the signer's public key. Everything else followed from that constraint.

Why document signing matters

PDF invoices and purchase orders are trivially forgeable. There is no cryptographic binding between the bytes in the file and the identity of the issuer. An attacker — or a rogue insider — can modify amounts, change bank account details, or alter line items. Downstream systems that parse the PDF via OCR have no way to detect tampering. The document looks the same to a human viewer.

Digital signatures solve two distinct problems: integrity (the content hasn't changed since signing) and authenticity (the document was signed by a specific key). SDF's security model addresses both, and adds a third property that most signing schemes ignore: offline verifiability — the ability to verify a signature without contacting any external service.

Design principle: SDF signatures are self-contained. The public key, the algorithm identifier, the signing timestamp, and the signature value are all embedded in meta.json. No OCSP, no CRL, no TSA endpoint required at verification time.

The signing pipeline

Signing an SDF document happens in four steps:

Why canonical JSON?

JSON does not have a canonical form. Two JSON values that are semantically identical can have different byte sequences: different key ordering, different whitespace, different Unicode normalization. If you sign raw JSON bytes, the signature will be invalid if the JSON is re-serialized — even if the data hasn't changed. This is a practical problem: JSON passes through serializers, formatters, and network stacks that may alter whitespace or key order.

SDF uses JSON Canonicalization Scheme (JCS, RFC 8785). JCS defines a deterministic serialization algorithm: keys sorted lexicographically, no whitespace, Unicode normalized to NFC, numbers serialized to IEEE 754 double precision. Any conforming JCS implementation on any platform will produce identical bytes from identical data.

meta.json — signatures block
{
  "signatures": {
    "algorithm": "ECDSA-P256",
    "publicKey": "MFkwEwYHKoZIzj0CAQYI...",
    "signerName": "Acme Corp",
    "signerEmail": "finance@acme.example",
    "signedAt": "2026-03-20T09:14:32Z",
    "hashes": {
      "data.json":   "sha256:e3b0c44298fc1c149afb...",
      "schema.json": "sha256:a665a45920422f9d417e..."
    },
    "value": "MEUCIQDz8...base64url...Ag=="
  }
}

ECDSA-P256 vs RSA-2048: the tradeoff

SDF supports two signing algorithms. ECDSA-P256 is the default for new documents; RSA-2048 is available for environments where EC keys are not yet supported.

ECDSA-P256 produces 64-byte signatures (DER-encoded: ~72 bytes). RSA-2048 produces 256-byte signatures. For a document that will be verified millions of times across a supply chain, smaller signatures matter — especially when embedded in a JSON field that may be logged or indexed.

ECDSA-P256 is also faster to verify. On a modern CPU, ECDSA-P256 verification runs in roughly 200 µs; RSA-2048 verification runs in roughly 50 µs — RSA is faster to verify but slower to sign, and ECDSA produces shorter signatures. In practice, the difference is negligible for single-document workflows. For bulk verification pipelines processing thousands of documents per minute, ECDSA's smaller signature size reduces memory pressure on deserialization.

Both algorithms are natively supported by Web Crypto API, which is the reason SDF uses them rather than curves like Ed25519 (not yet in Web Crypto) or RSA-4096 (excessive for this use case).

Why Web Crypto API?

The Web Crypto API (crypto.subtle) is available in every modern browser, Node.js 16+, Deno, Bun, and Cloudflare Workers. It is implemented in native code by each platform, with hardware acceleration where available. It requires no external dependencies.

The alternative — node-forge, jsrsasign, or OpenSSL bindings — introduces supply chain risk. Every dependency is an attack surface. A signing library with a vulnerability could silently produce invalid signatures or, worse, leak private keys. Web Crypto API's implementation is part of the browser/runtime vendor's security perimeter, subject to their security review and update cadence.

For server-side SDF signing in environments without Web Crypto (older Node.js, some embedded environments), sdf-kit falls back to the node:crypto module's native createSign / createVerify APIs, which use the same underlying OpenSSL primitives but don't require an external package.

sign.ts — sdf-kit signing internals
const key = await crypto.subtle.importKey(
  'pkcs8',
  privateKeyBuffer,
  { name: 'ECDSA', namedCurve: 'P-256' },
  false,         // not extractable
  ['sign']
)

const signature = await crypto.subtle.sign(
  { name: 'ECDSA', hash: 'SHA-256' },
  key,
  canonicalBytes   // JCS-serialized meta payload
)

return base64url(signature)

Key management

SDF does not mandate a PKI. The public key is embedded directly in meta.json as a base64-encoded DER SubjectPublicKeyInfo structure. There is no certificate chain, no root CA, no OCSP endpoint. This keeps the format simple and truly offline.

The tradeoff is that key trust is the application's responsibility. For an enterprise deployment, this typically means:

Verifying a document 20 years from now

The original hard constraint: offline verification, 20 years in the future. Let's walk through what that looks like concretely.

In 2046, an auditor has an invoice.sdf archive signed in 2026. They have the signer's public key (retrieved from the supplier's records at signing time and stored locally). They need to verify: was this document signed by that key, and has it been modified since?

The verifier:

No network request. No timestamp authority. No certificate revocation check. All the inputs — the document, the public key, the algorithm identifier — are present and static. ECDSA-P256 and SHA-256 are mature, well-specified algorithms with no known practical attacks. Their implementations will exist in standard libraries for decades.

What signing does not cover

SDF signatures cover data.json and schema.json. They do not cover visual.pdf. This is intentional: the visual PDF may be legitimately re-rendered (different locale, different paper size, updated branding) without invalidating the document's data integrity. The data is the authoritative record. The PDF is a rendering of that data.

Note: If you need the PDF to be tamper-evident too, sign it separately using PAdES or a similar PDF signature standard. SDF is not a replacement for PDF signatures when the visual presentation itself is legally significant. In most B2B scenarios it isn't — the invoice amount, line items, and payment terms in data.json are what matters legally, not the font or logo.

The meta.json file also includes a signedAt timestamp, but this timestamp is not trusted time — it is set by the signer and unverifiable without a trusted timestamp authority. For workflows that require trusted timestamping, SDF documents can be submitted to an RFC 3161 TSA and the resulting timestamp token stored alongside the archive. This is an optional extension, not a core requirement.

Key generation with the CLI

terminal
# Generate an ECDSA-P256 keypair
$ sdf keygen --algo ecdsa-p256 --out ./keys/acme

# Sign a document
$ sdf sign invoice.sdf \
    --key ./keys/acme.private.pem \
    --signer-name "Acme Corp" \
    --signer-email "finance@acme.example"

# Verify a signed document
$ sdf verify invoice.sdf --key ./keys/acme.public.pem
✓ Signature valid · ECDSA-P256 · signed 2026-03-20T09:14:32Z
✓ data.json hash matches
✓ schema.json hash matches

The security model will evolve. Post-quantum signatures (ML-DSA, formerly CRYSTALS-Dilithium) are on the roadmap for SDF v0.4 as the NIST PQC standards stabilize and runtime support matures. The algorithm field in meta.json is designed to accommodate new values without breaking existing verifiers.

Previous post Next post