Skip to main content

Data Formats

This page documents the JSON payloads, ciphertext format, and API request/response schemas used by SignChain.

Anchor Payload (Signer Data)

The anchor payload contains the signer's identity and document metadata. It is serialized as compact JSON, encrypted, and stored on the API server. The composite hash (SHA-256 of this JSON) is stored on-chain.

{
"d": "a1b2c3...f0",
"s": {
"t": "individual",
"n": "John Doe",
"e": "john@example.com",
"c": "Acme Corp",
"p": "CTO",
"tr": "google",
"v": true
},
"ts": 1774288200,
"g": {
"la": 6.9271,
"ln": 79.8612
},
"salt": "e4f5a6...b7c8"
}

Field Reference

FieldKeyTypeRequiredDescription
Document hashdstringYesSHA-256 hex of the PDF content (before QR embedding)
Signer infosobjectYesSigner identity
Signer types.tstringYes"individual" or "company"
Names.nstringYesSigner's display name
Emails.estringYesSigner's email address
Companys.cstringNoCompany name (omitted if empty)
Positions.pstringNoJob title (omitted if empty)
Trust anchors.trstringNoAuth provider: "email", "google", "microsoft"
Verifieds.vbooleanNoWhether identity was verified by the trust anchor
TimestamptsintegerYesUnix epoch seconds (UTC)
GeolocationgobjectNoGPS coordinates at signing time
Latitudeg.lanumberIf g presentDecimal degrees
Longitudeg.lnnumberIf g presentDecimal degrees
SaltsaltstringYes32 random bytes, hex-encoded (64 chars)

The tr and v fields are populated from the signer's JWT claims. They allow verifiers to display a trust badge indicating how the signer authenticated (e.g. "Authenticated via Google").

Short keys are used to minimize JSON size. The payload must fit within the QR byte budget after base64url encoding.

Composite Hash

The composite hash is computed by SHA-256 hashing the serialized JSON bytes:

composite_hash = "0x" + hex(SHA-256(json_bytes))

This hash is the only value stored on the blockchain. It commits to the entire payload -- any change to any field produces a different hash.

Salt Purpose

The 32-byte random salt ensures that:

  • Two identical signings produce different composite hashes
  • The composite hash cannot be brute-forced from known signer data
  • Each signature is cryptographically unique

Ciphertext Format

The encrypted payload uses AES-128-GCM with a prepended nonce:

Ciphertext format

ComponentSizeDescription
Nonce12 bytesRandom, generated per encryption
Encrypted dataVariableAES-128-GCM ciphertext
Auth tag16 bytesGCM authentication tag (appended by AES-GCM)

The entire ciphertext (nonce + encrypted + tag) is base64url-encoded for storage and transmission.

Key

  • Size: 16 bytes (128 bits)
  • Generation: CSPRNG (OsRng)
  • Encoding: Base64url without padding (22 characters)
  • Location: QR code URL fragment only

API Schemas

POST /api/relay -- Submit Anchor

Request:

{
"compositeHash": "0xabc123...def789",
"previousTxHash": "0x000...000",
"encryptedPayload": "<base64url-encoded ciphertext>"
}
FieldTypeValidationDescription
compositeHashstringRequired, non-empty0x-prefixed SHA-256 hex (66 chars)
previousTxHashstringRequired, non-empty0x-prefixed tx hash or zero hash for first signature
encryptedPayloadstringRequired, non-emptyBase64url-encoded AES-128-GCM ciphertext

Response:

{
"txHash": "0x123...789",
"blockNumber": 42
}

GET /api/verify/:txHash -- Verify Signature

URL parameter: txHash -- 0x-prefixed transaction hash (66 chars)

Response:

{
"txHash": "0x123...789",
"compositeHash": "0xabc...def",
"signer": "0xWalletAddress",
"timestamp": 1711094400,
"previousTxHash": "0x000...000",
"chain": [
{
"txHash": "0x123...789",
"compositeHash": "0xabc...def",
"signer": "0xWalletAddress",
"timestamp": 1711094400,
"previousTxHash": "0x000...000"
}
],
"encryptedPayload": "<base64url-encoded ciphertext>"
}

The chain array contains all signatures in the document's chain, ordered from first to latest. Each entry represents one DocumentAnchored event from the smart contract.

QR URL Format

https://signchain.app/v/<base64url(txHashBytes)>#<base64url(keyBytes)>
ComponentRaw sizeEncoded sizeEncoding
Base URL + path prefix--27 charsPlain text
/v/ separator--3 charsPlain text
Transaction hash32 bytes43 charsBase64url, no padding
# separator--1 charPlain text
Encryption key16 bytes22 charsBase64url, no padding
Total--~96 chars--

PDF Metadata (SignChain JSON)

Embedded in each PDF revision as a metadata stream, stored in the document's cross-reference table:

{
"version": 2,
"signatures": [
{
"signer_name": "John Doe",
"signer_email": "john@example.com",
"signer_type": "individual",
"company": null,
"position": null,
"timestamp": "2025-01-15T10:30:00Z",
"doc_hash": "a1b2c3...f0",
"composite_hash": "0xabc...def",
"tx_hash": "0x123...789",
"qr_url": "https://signchain.app/v/...#...",
"salt": "e4f5a6...b7c8",
"geo": [6.9271, 79.8612]
}
]
}

This metadata is used by the desktop app's verification feature to:

  1. Identify all signatures in the document
  2. Extract QR URLs for blockchain verification
  3. Walk the signature chain

Fields marked with #[serde(default)] in the Rust struct ensure backward compatibility -- PDFs signed with version 1 can still be read by version 2 code.