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:
- 1Canonicalizedata.json and schema.json are serialized to canonical JSON (RFC 8785 / JCS). Key ordering is deterministic, whitespace is normalized. This ensures the same bytes are signed regardless of how the JSON was formatted.
- 2HashSHA-256 digests are computed over the canonical bytes of each file. The digests are embedded in meta.json under the signatures.hashes field.
- 3SignThe meta.json payload — with hashes but without the signature value field — is itself canonicalized and signed. The algorithm is ECDSA-P256 (default) or RSA-2048 (legacy compatibility mode). The signature is DER-encoded and base64url-encoded.
- 4EmbedThe signature value is written into meta.json. The archive is updated. The signed document is a valid .sdf ZIP with all four files.
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.
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.
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:
- Distributing signer public keys out-of-band (supplier onboarding, an API that returns trusted keys for a given signerEmail, a local key store).
- Using sdf-server-core's key registry, which maps tenant API keys to their signing public keys and makes them available to receiving systems via a REST endpoint.
- For regulated industries: embedding an X.509 certificate alongside the raw public key in an extended meta.json field, enabling full PKI validation when required while remaining backward-compatible with simpler verifiers.
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:
- 1Extracts meta.json from the ZIP archive.
- 2Reads the publicKey, algorithm, hashes, and value fields.
- 3Re-canonicalizes meta.json without the value field using JCS.
- 4Verifies the ECDSA-P256 signature over the canonical bytes using the embedded public key.
- 5Extracts data.json and schema.json, computes their SHA-256 digests, and compares against the hashes field.
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
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.