Technical Reference

Verification Architecture

Technical reference for developers, security auditors, and AI tools. Covers cryptographic primitives, W3C VC structure, Merkle transparency log, DID infrastructure, and public APIs.

Last updated April 9, 2026

Table of Contents

  1. Overview
  2. Cryptographic Primitives
  3. W3C Verifiable Credential Structure
  4. Merkle Transparency Log
  5. DID Infrastructure
  6. PDF Embedded Files
  7. Signing Flow (Step by Step)
  8. Verification Flow (Step by Step)
  9. API Reference
  10. Offline Verification
  11. Security Model
  12. Comparison with Other Approaches

Overview

SignForge uses a multi-layered verification system that provides tamper evidence, portable proof, and public auditability for every signed document. The system is built on open standards and does not depend on any third-party trust authority.

Core principle: Your proof lives with the document, not locked in any vendor's database.

What SignForge Verification IS

  • A platform-issued receipt — SignForge attesting as a witness that a signing event occurred
  • Built on the W3C VC 2.0 model — machine-readable, open-standard
  • Offline-verifiable — embedded public keys allow verification without contacting SignForge
  • Publicly auditable — Merkle transparency log provides cryptographic proof of existence and ordering
  • EU Digital Identity Wallet aligned — built on the same W3C VC foundation that EU wallets target

What SignForge Verification is NOT

  • NOT a Qualified Electronic Signature (QES) — we don't use government-issued certificates
  • NOT an Adobe AATL signature — we use an open trust model instead of a vendor-managed trust list
  • NOT a signer-held PKI signature — SignForge signs the receipt as platform attestation; signers do not hold private keys
  • NOT a blockchain — we use a Merkle tree (same math, no chain dependency)

Cryptographic Primitives

Algorithms Used

PurposeAlgorithmStandardKey Size
VC signingECDSA P-256 (secp256r1)FIPS 186-4, W3C DataIntegrityProof256-bit
STH signingECDSA P-256 (secp256r1)Same256-bit
Document hashingSHA-256FIPS 180-4256-bit
Merkle treeSHA-256 with domain separationRFC 6962 pattern256-bit
Legacy JSON signingECDSA P-256Custom canonical JSON256-bit
Key encodingMultibase (base58btc)W3C Multibase, MulticodecVariable

Domain-Separated Hashing (Merkle Tree)

To prevent second-preimage attacks, the Merkle tree uses domain-separated hashing:

Leaf hash:   SHA-256(0x00 || data)
Node hash:   SHA-256(0x01 || left || right)

This follows the same pattern used in Certificate Transparency (RFC 6962).

Two-Key Architecture

SignForge uses two independent ECDSA P-256 keys:

KeyPurposeIdentifier
Issuer keySigns W3C VCs and legacy verification JSONdid:key:zIssuer...
Log keySigns Signed Tree Heads (STH)did:key:zLog...

Why two keys?

  • Compromise of the issuer key doesn't let an attacker forge the transparency log
  • Compromise of the log key doesn't let an attacker issue fake VCs
  • The log key signs checkpoints (STH), preventing self-referential proofs where a document proves itself

W3C Verifiable Credential Structure

Every completed signing produces a W3C VC 2.0 credential:

{
  "@context": [
    "https://www.w3.org/ns/credentials/v2"
  ],
  "type": ["VerifiableCredential", "SigningReceipt"],
  "id": "https://signforge.io/verify/sf_abc123def456",
  "issuer": {
    "id": "did:key:zDnae...",
    "name": "SignForge",
    "alsoKnownAs": "did:web:signforge.io"
  },
  "validFrom": "2026-04-09T12:00:00Z",
  "credentialSubject": {
    "type": "SigningEvent",
    "envelopeId": "550e8400-e29b-41d4-a716-446655440000",
    "envelopeTitle": "Service Agreement",
    "documentHash": {
      "algorithm": "sha256",
      "value": "a1b2c3d4e5f6..."
    },
    "signedDocumentHash": {
      "algorithm": "sha256",
      "value": "f6e5d4c3b2a1..."
    },
    "documentSizeBytes": 145832,
    "sender": {
      "name": "Alice Johnson",
      "email": "alice@company.com"
    },
    "signers": [
      {
        "name": "Bob Smith",
        "email": "bob@client.com",
        "signedAt": "2026-04-09T12:00:00Z"
      }
    ],
    "fieldTypes": ["signature", "date"],
    "pageCount": 3,
    "verificationCode": "sf_abc123def456"
  },
  "proof": {
    "type": "DataIntegrityProof",
    "cryptosuite": "ecdsa-jcs-2019",
    "created": "2026-04-09T12:00:00Z",
    "verificationMethod": "did:key:zDnae...#zDnae...",
    "proofPurpose": "assertionMethod",
    "proofValue": "z3hY9..."
  }
}

Privacy Note

The embedded VC includes sender and signer names/emails. This is intentional — the VC is a signing receipt, and the identities of the parties are core to its purpose. The VC is embedded inside the signed PDF (which already contains this information) and is not published to the transparency log. The log contains only document hashes, not personal data. Future versions may support redacted or minimized embedded receipts for privacy-sensitive workflows.

Proof Generation (DataIntegrityProof)

The proof is generated using the ecdsa-jcs-2019 cryptosuite:

  1. Canonicalize proof options (everything in proof except proofValue) using JCS (RFC 8785)
  2. Hash the canonicalized proof options: SHA-256(proof_options_canonical)
  3. Canonicalize the VC (without proof field) using JCS
  4. Hash the canonicalized VC: SHA-256(vc_canonical)
  5. Concatenate both hashes: proof_hash || vc_hash
  6. Sign the concatenation with ECDSA P-256 using the issuer key
  7. Encode the signature as multibase base58btc (z prefix)

Proof Verification

  1. Extract proof from the VC
  2. Resolve verificationMethod to get the public key (from did:key or embedded keys)
  3. Repeat steps 1-5 from generation
  4. Verify the ECDSA signature against the concatenated hash using the public key

Embedded Copy vs. Downloadable Copy

Two copies of the VC exist:

CopyLocationContains signedDocumentHash?Purpose
EmbeddedInside the signed PDF within the signforge_proof.html proof bundleNo (null)Proves the VC was created at signing time. Self-contained with the document.
DownloadableAvailable via APIYesIncludes the final signed PDF hash (which can't be known until after embedding)

The embedded copy is the self-contained proof bundle — it travels with the document and works offline. The downloadable copy adds the signed document hash, which is useful for cross-referencing with the transparency log.


Merkle Transparency Log

Data Structure: Merkle Mountain Range (MMR)

The transparency log uses a Merkle Mountain Range, an append-only variant of a Merkle tree optimized for streaming data:

  • O(log n) peak hashes stored (not the full tree)
  • O(log n) hash operations per append
  • O(log n) proof size for inclusion verification
  • Suitable for millions of entries with sub-millisecond performance

Append Operation

State: tree_size=4, peaks=[root_of_4]

Append leaf 5:
1. Hash the leaf: leaf_hash = SHA-256(0x00 || document_hash)
2. Start with leaf_hash, check if bit 0 of tree_size is set
3. If set: merge with rightmost peak (pop peak, hash_node)
4. Repeat for each set bit
5. Push remaining hash as new peak
6. Compute root by folding all peaks right-to-left
7. Generate Merkle proof (merge steps + peak fold steps)
8. Sign the root as STH

State: tree_size=5, peaks=[root_of_4, leaf_5_hash]

Merkle Proof Format

[
  {"hash": "abc123...", "position": "left"},
  {"hash": "def456...", "position": "right"}
]

Each step in the proof indicates whether the sibling hash should be placed on the left or right when computing the parent hash.

Signed Tree Head (STH)

Every append produces a Signed Tree Head:

{
  "log_id": "did:key:zLog...",
  "tree_size": 42,
  "root_hash": "a1b2c3d4...",
  "timestamp": "2026-04-09T12:00:00.000000+00:00",
  "signature": "z3hY9..."
}

The STH is signed by the log key (not the issuer key). This prevents self-referential proofs — the issuer cannot forge log entries without the log key, and vice versa.

Log Entry Contents

Each transparency log entry contains:

FieldDescription
idSequential entry number
document_hashSHA-256 of the signed PDF (64 hex chars)
envelope_idRandom UUID (no personal data)
verification_codeDocument verification code (e.g., sf_abc123)
merkle_rootTree root hash at time of this append
merkle_proofInclusion proof (array of hash + position pairs)
signed_tree_headSTH signed by the log key
created_atTimestamp of log entry

The log does not contain document content, signer names, email addresses, or any personally identifiable information.


DID Infrastructure

did:key (Primary)

did:key is a Decentralized Identifier method from the W3C DID specification ecosystem. The did:key method itself is a W3C Credentials Community Group draft — widely adopted but not yet a full W3C Recommendation.

Identifiers are derived deterministically from public key bytes:

1. Get compressed public key bytes (33 bytes for P-256)
2. Prepend multicodec prefix: 0x80 0x24 (p256-pub)
3. Encode as multibase base58btc (prefix 'z')
4. Result: did:key:zDnae...

Advantages:

  • Offline-resolvable — no HTTP needed, the DID IS the public key
  • Domain-independent — works even if signforge.io is down
  • Deterministic — same key always produces the same DID

did:web (Alias)

did:web:signforge.io resolves to https://signforge.io/.well-known/did.json:

{
  "@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/multikey/v1"],
  "id": "did:web:signforge.io",
  "alsoKnownAs": ["did:key:zIssuer..."],
  "verificationMethod": [
    {
      "id": "did:web:signforge.io#issuer-key-1",
      "type": "Multikey",
      "controller": "did:web:signforge.io",
      "publicKeyMultibase": "zDnae..."
    },
    {
      "id": "did:web:signforge.io#log-key-1",
      "type": "Multikey",
      "controller": "did:web:signforge.io",
      "publicKeyMultibase": "zDnae..."
    }
  ],
  "assertionMethod": ["did:web:signforge.io#issuer-key-1"],
  "service": [
    {
      "id": "did:web:signforge.io#transparency-log",
      "type": "TransparencyLog",
      "serviceEndpoint": "https://signforge.io/api/transparency"
    },
    {
      "id": "did:web:signforge.io#verification",
      "type": "VerificationService",
      "serviceEndpoint": "https://signforge.io/api/verify"
    }
  ]
}

Purpose: Human-friendly discovery, service endpoint resolution, "Where do I check the log?" NOT used for: Proof verification (always use did:key for that)


PDF Embedded Proof

Every signed PDF contains a single embedded file: signforge_proof.html — a self-verifying proof document that bundles all cryptographic evidence into one portable file.

What's Inside

The proof HTML contains a <script id="proof-bundle" type="application/json"> tag with a JSON object containing 10 sections:

SectionContents
verifiable_credentialW3C VC 2.0 with DataIntegrityProof (ecdsa-jcs-2019)
jades_jwsJAdES compact JWS (EU-standard ES256)
merkle_proofInclusion proof, leaf hash, root, STH with log key signature
timestampRFC 3161 response (DigiCert TSA), signing time, TSA name
document_content_hashSHA-256 of the signed PDF (post-stamp)
signer_identity_vcsPer-signer VCs with name, email, IP, user-agent
keysBoth public keys (issuer + log) in JWK format
did_snapshotDID document snapshot at signing time
envelope_metadataTitle, sender, signers, privacy tier
verification_codesf_xxxxxxxxxxxx code for web lookup

The proof HTML also includes an interactive verification UI with a drop zone — drag any SignForge-signed PDF onto it for instant offline verification using Web Crypto API.

Extraction Example

# Simplest: use the standalone verifier packages
pip install signforge-verify
signforge-verify signed_document.pdf

# Or with Node.js:
npx @signforge/verify signed_document.pdf
# Programmatic extraction
import fitz, json
from bs4 import BeautifulSoup

doc = fitz.open("signed_document.pdf")
proof_html = doc.embfile_get("signforge_proof.html").decode()
soup = BeautifulSoup(proof_html, "html.parser")
bundle = json.loads(soup.find("script", id="proof-bundle").string)

Legacy Format

Older signed PDFs (pre-April 2026) may contain 3 separate files: signforge_verification.json, signforge_receipt.vc.json, signforge_keys.json. The standalone verifier packages (signforge-verify on PyPI and @signforge/verify on npm) support both formats.


Signing Flow (Step by Step)

When all recipients have signed an envelope:

1. COLLECT all field values (signatures, dates, text)

2. STAMP the PDF
   a. Render signature images at normalized coordinates
   b. Add QR code (bottom-right of last page)
   c. Compute SHA-256 of the stamped PDF

3. STORE the signed PDF

4. CREATE verification record
   (independent of the envelope — survives if envelope is deleted)

5. BUILD downloadable receipt
   a. Build W3C VC with signedDocumentHash + documentSizeBytes
   b. Sign with DataIntegrityProof (ecdsa-jcs-2019)
   c. Store as downloadable receipt

6. APPEND to transparency log
   a. Compute leaf hash from signed PDF SHA-256
   b. Append to Merkle Mountain Range
   c. Sign the new tree head with log key
   d. Store log entry with Merkle proof + STH

7. BUILD proof document (signforge_proof.html)
   a. Build W3C VC with DataIntegrityProof
   b. Build JAdES JWS (ES256 compact)
   c. Request RFC 3161 timestamp from DigiCert TSA
   d. Build signer identity VCs
   e. Snapshot DID document + public keys
   f. Bundle all 10 sections into proof HTML with interactive verifier UI
   g. Embed signforge_proof.html in the signed PDF

8. GENERATE audit certificate
   (includes transparency log section if available)

9. SEND completion emails to all parties

Degraded States

Steps 5-7 (downloadable receipt, transparency log, proof document) are best-effort. If they fail, the signing still completes successfully. The stamped PDF with QR code is already committed at step 2. In this degraded state:

  • The signed PDF is still fully valid with embedded proof
  • Verification by code or hash still works (via the verification record)
  • The transparency log entry and downloadable receipt may be missing
  • The system logs the failure for operator investigation

The system records degraded-state events for operator review and remediation. This ensures a signing event is never blocked by a non-critical enhancement.


Verification Flow (Step by Step)

Upload Verification (POST /api/verify/upload)

The system uses a multi-strategy cascade, trying the strongest method first:

Strategy 0: Proof Bundle Verification (highest priority)
  - Extract signforge_proof.html from PDF
  - Parse proof bundle JSON from embedded <script id="proof-bundle">
  - Verify W3C VC DataIntegrityProof signature
  - Verify JAdES JWS signature
  - Verify Merkle inclusion proof against STH
  - If valid: enrich with transparency log data (if available)
  - Works offline with embedded keys

Strategy 1: Hash Lookup
  - Compute SHA-256 of uploaded PDF
  - Look up in records by hash

Strategy 2: Legacy Embedded JSON + Record Lookup
  - Extract signforge_verification.json from PDF (older documents)
  - Look up verification_code in records
  - Compare uploaded file hash with stored hash

Strategy 3: Legacy ECDSA Signature Verification (offline fallback)
  - Extract signforge_verification.json from PDF (older documents)
  - Verify the ECDSA signature using the public key
  - Works even if SignForge's database is unavailable

Strategy 4: Not found
  - Return verified=false with the computed hash

Code Verification (GET /api/verify/{code})

Direct lookup by verification code, enriched with transparency log data when available.


API Reference

Transparency Log API (Public, No Auth)

EndpointMethodDescriptionRate Limit
/api/transparency/latestGETCurrent log state (tree_size, root, STH)30/min
/api/transparency/hash/{hash}GETLook up by document SHA-256 hash (64 hex chars)30/min
/api/transparency/entry/{id}GETLook up by sequential entry ID30/min
/api/transparency/keysGETPublic keys document (issuer + log keys in JWK)30/min
/api/transparency/did.jsonGETDID:web document (also at /.well-known/did.json)30/min

Receipt Download API

EndpointMethodAuthDescription
/api/sign/{token}/documents/receiptGETSigning tokenDownload VC JSON via signing token
/api/envelopes/{id}/documents/receiptGETJWT (sender)Download VC JSON via envelope ID

Verification API (Public, No Auth)

EndpointMethodDescriptionRate Limit
/api/verify/{code}GETVerify by code (includes transparency data)30/min
/api/verify/uploadPOSTUpload PDF for verification (multi-strategy cascade)10/min

Offline Verification

Truth Table

ScenarioVC SignatureMerkle ProofSTHLive LogOverall
SignForge onlineVerify via API or embedded keysVerify against live rootVerify with log keyCheck latest stateFull verification
SignForge offline, PDF + proof fileVerify with embedded keysSelf-consistent checkVerify with embedded log keyUnavailableStrong (self-consistent proof bundle)
SignForge offline, PDF onlyVerify with embedded keysUnavailableUnavailableUnavailableGood (issuer attestation only)
No prior trust anchor, no internetMath worksMath worksMath worksUnavailableMath valid, issuer identity unverifiable*

*This is a fundamental limit of all PKI systems. Without a prior trust anchor (a cached DID document, a previously verified SignForge document, or an external directory listing), you cannot confirm that the key belongs to SignForge specifically — only that the signatures are internally consistent.

Offline Verification Example

Use the standalone verifier packages for the simplest offline verification:

Python (PyPI):

pip install signforge-verify
signforge-verify signed_document.pdf

Node.js (npm):

npx @signforge/verify signed_document.pdf

Programmatic (Python):

from signforge_verify import verify
result = verify("signed_document.pdf")
print(result["valid"])  # True
print(result["checks"])  # Individual check results

Programmatic (Node.js):

import { SignForgeVerifier } from '@signforge/verify';
const result = await new SignForgeVerifier().verifyFromPdf('signed_document.pdf');
console.log(result.valid);  // true
console.log(result.checks);  // Individual check results

These packages verify all proof bundle sections: W3C VC DataIntegrityProof, JAdES JWS, Merkle inclusion, RFC 3161 timestamp, signer identity VCs, and DID snapshot — fully offline, no SignForge account needed.


Security Model

What We Guarantee

  1. Tamper evidence: Any modification to the signed PDF changes the SHA-256 hash, breaking verification
  2. Issuer attestation: The ECDSA signature on the VC can only be produced by SignForge's issuer key
  3. Temporal ordering: The Merkle tree provides a total ordering of all signed documents
  4. Non-repudiation of log: The STH is signed by a separate key, preventing forged log entries
  5. Portable proof: Verification data is embedded in the PDF, not locked in our database

What We Do NOT Guarantee

  1. Signer identity: We verify email delivery, not government ID. The signer is "whoever clicked the link"
  2. Document content: We attest that THIS document was signed, not that its content is truthful
  3. Legal validity: E-signatures are legally valid in most jurisdictions (ESIGN Act, eIDAS), but specific compliance depends on the use case
  4. Key secrecy: If our keys are compromised, past signatures remain valid but new ones could be forged until key rotation

Threat Model

ThreatMitigation
Forged VCECDSA P-256 with 128-bit security level
Forged log entrySeparate log key; STH prevents self-referential proofs
Modified PDFSHA-256 hash mismatch detected on verification
Database lossEmbedded proof bundle in PDF enables database-free verification
Domain seizuredid:key (offline) is primary; did:web is just an alias
Key compromiseTwo-key separation limits blast radius

Comparison with Other Approaches

vs. Adobe AATL (Approved Trust List)

AspectSignForgeAdobe AATL
CostFreeRequires a paid signing certificate
Trust modelOpen standards (W3C VC, DID)Vendor-managed trust list
Reader integrationVerify via web, API, or embedded keysBlue badge in Adobe Acrobat
Offline verificationYes (embedded keys + VC)Yes (certificate chain)
Open standardW3C VC 2.0, DIDPDF 2.0 digital signatures

AATL is best for workflows that need native Adobe Acrobat trust indicators. SignForge is optimized for portable, tool-agnostic verification — built on open standards (W3C VC, DID, Merkle proofs) that work across any tool, online and offline, without requiring a paid certificate.

vs. Blockchain Timestamping

AspectSignForgeBlockchain
Data structureMerkle tree (MMR)Merkle tree + consensus
Write costNegligibleGas fees ($0.01-$50+)
LatencyMillisecondsSeconds to minutes
DependencySignForge serversBlockchain network
VerificationPublic API + offlineBlockchain node/explorer
GuaranteesTamper-evident, orderedTamper-evident, ordered, decentralized

Trade-off: Blockchain adds decentralization (no single operator can tamper). Our log is operated by SignForge, but proofs are embedded in every PDF and distributed to all signing parties, making tampering detectable.

vs. Certificate Transparency (CT)

AspectSignForgeCT (RFC 6962)
PurposeDocument signing receiptsTLS certificate logging
Tree typeMMR (peaks only)Full Merkle tree
Signed checkpointsSTH (every append)STH (periodic)
Public monitoringAPI onlyMultiple independent logs
AuditingSingle operator (SignForge)Multiple CT log operators

Similarity: We follow the same patterns (domain-separated hashing, signed tree heads, inclusion proofs) adapted for document signing.


For AI Tools (ChatGPT, Claude, etc.)

Quick Verification Prompt

To verify a SignForge document:

  1. Hash the PDF: sha256sum document.pdf
  2. Check the log: GET https://signforge.io/api/transparency/hash/{hash}
  3. If found: The document exists in the transparency log with entry #{id}
  4. If not found: Either not signed through SignForge, or the PDF was modified after signing

Note: Log absence alone is not conclusive — the document may have been generated during a degraded-state event where the transparency log append did not complete. Verify using the embedded VC or verification code as fallback.

Extracting Embedded Data

import fitz, json
from bs4 import BeautifulSoup

doc = fitz.open("signed.pdf")
names = doc.embfile_names()  # ['signforge_proof.html']
proof_html = doc.embfile_get("signforge_proof.html").decode()
soup = BeautifulSoup(proof_html, "html.parser")
bundle = json.loads(soup.find("script", id="proof-bundle").string)
print(bundle["verifiable_credential"]["credentialSubject"]["verificationCode"])  # sf_abc123

Or use the standalone verifier packages which handle extraction automatically:

from signforge_verify import verify
result = verify("signed.pdf")  # Extracts and verifies everything

DID Resolution

# Online (did:web)
curl https://signforge.io/.well-known/did.json

# Offline (did:key) — the DID IS the public key
# did:key:zDnae... → decode multibase → strip multicodec prefix → compressed P-256 public key