Status: Public Beta v0.2 · ProofSpec v0.1 · API v0.2 · Privacy-first · No blockchain
Protocol — ProofSpec
Open, normative specification to prove when data existed and that it has not changed. Hash locally, receive a proof file, verify later — without revealing your data.
Overview
ProofSpec defines how to create, sign, and verify digital proofs of existence in a privacy-first way.
Model: hash locally → receive a signed .tproof.json proof file → verify later (offline or via stateless verification) without access to the original data.
This document is normative for issuers and verifiers that claim to be ProofSpec v0.1 compliant.
ProofSpec vs TimeProofs
- ProofSpec defines the proof structure, canonical message rules, verification behaviour, and error semantics.
- TimeProofs API v0.2 is a stateless reference implementation: timestamp returns a portable proof file; verification is cryptographic and requires the full proof. No server-side storage.
File date ≠ proof
- A file date is local, easy to change, and not independently verifiable.
- A ProofSpec proof is a signed timestamp bound to an exact hash, verifiable by third parties later.
What it proves / What it does NOT prove
It proves:
- That specific bytes (identified by a SHA-256
hash) existed at or beforeissuedAt. - That the proof was issued by a specific issuer key (as identified by
keyId).
It does NOT prove:
- Authorship, ownership, intent, or legal status.
- Truthfulness/accuracy of the content, only integrity and timing.
- Any claim about the original content if the hash was computed over different bytes.
Normative language & scope
The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document are to be interpreted as described in RFC 2119 and RFC 8174.
- This specification applies to proof structure, signing rules, verification behaviour, and error reporting.
- Implementation details (infrastructure, billing, UI) are non-normative unless explicitly stated.
- Where this document uses “example”, the content is informative and does not create additional normative requirements.
Conformance
The following roles are defined:
- Issuer: a system that accepts a hash and returns a signed proof file.
- Verifier: a system that checks a proof file and returns a validation result.
- Client: any caller (app, backend, CLI, agent) that interacts with an issuer or verifier.
A ProofSpec-compliant Issuer:
- MUST only accept canonical hashes and MUST NOT require original content.
- MUST produce proof objects that satisfy the JSON schema and data model.
- MUST sign a deterministic canonical message that binds the hash and the issuance timestamp.
- MUST include a stable
keyIdidentifying the signing key.
A ProofSpec-compliant Verifier:
- MUST validate required fields and formats.
- MUST verify the signature over the canonical message.
- MUST reject proofs whose
issuedAtis not a valid UTC timestamp or is unreasonably far in the future. - SHOULD provide clear error codes as defined in the Errors section.
Clients that claim ProofSpec compliance SHOULD keep the full .tproof.json proof for long-term verification.
Canonicalization
- Issuers and clients MUST hash the exact bytes they intend to prove (“bytes-in, bytes-out”).
- Text inputs SHOULD be encoded as UTF-8 and SHOULD normalize line endings to LF (
\n) before hashing. - If a client applies preprocessing (whitespace normalization, stable JSON stringification, etc.), it MUST be documented and MUST be applied consistently.
Data model
| Field | Type | Notes |
|---|---|---|
| version | string | Protocol / implementation version. REQUIRED. |
| canonical | string | Deterministic message string signed by issuer. REQUIRED. |
| hash | object | { algorithm:"SHA-256", value:"<64-hex>" }. REQUIRED. |
| timestamp | object | { issuedAt, issuer, nonce }. REQUIRED. |
| proof | object | { algo, signature, keyId, publicKey? }. REQUIRED. |
| meta | object | Optional. MUST NOT contain personal data or secrets. |
ProofSpec v0.1 is designed for portable proofs (.tproof.json) and stateless verification.
Proof object (example)
{
"version":"timeproofs-0.2",
"canonical":"<hash>|<issuedAt>|<issuer>|<nonce>",
"hash": { "algorithm":"SHA-256", "value":"<64-hex>" },
"timestamp": {
"issuedAt":"2025-01-01T00:00:00.000Z",
"issuer":"https://api.timeproofs.io",
"nonce":"<hex>"
},
"proof": {
"algo":"Ed25519",
"signature":"<hex>",
"publicKey":"<base64 SPKI DER>",
"keyId":"tp-v0-2-main"
},
"meta": { "type":"event" }
}
The signature algorithm and encoding are issuer-specific. The TimeProofs reference implementation uses a deterministic, verifiable signature scheme and includes a keyId for key lifecycle.
Signing
Issuers MUST sign a deterministic canonical message. The reference canonical string is:
<hash>|<issuedAt>|<issuer>|<nonce>
hashMUST be a lowercase 64-hex SHA-256.issuedAtMUST be an ISO 8601 UTC timestamp.issuerSHOULD be a stable identifier (typically an HTTPS origin).nonceMUST be unpredictable to prevent replay across identical timestamps.
// Browser / Worker signature verify (example, WebCrypto)
// Note: algorithm support depends on platform. Use a verifier that matches the issuer's algo.
function b64ToBytes(b64){
const bin = atob(b64);
const out = new Uint8Array(bin.length);
for (let i=0;i<out.length;i++) out[i]=bin.charCodeAt(i);
return out;
}
function hexToBytes(hex){
const h=String(hex||"").toLowerCase().trim();
if(!/^[a-f0-9]+$/.test(h) || h.length%2) return null;
const out=new Uint8Array(h.length/2);
for(let i=0;i<out.length;i++) out[i]=parseInt(h.slice(i*2,i*2+2),16);
return out;
}
Issuers SHOULD implement key lifecycle via keyId and publish public keys for independent verification.
Some deployments enforce key freeze: verifiers trust only a pinned issuer public key and ignore the embedded publicKey.
Validation
hash.valueMUST be a 64-character lowercase hex string.timestamp.issuedAtMUST be a valid ISO 8601 UTC timestamp and SHOULD NOT be in the future beyond a reasonable tolerance.canonicalMUST exactly match the signing rules.- The verifier MUST verify the signature over
canonical.
JSON schema
{
"$schema":"https://json-schema.org/draft/2020-12/schema",
"type":"object",
"required":["version","canonical","hash","timestamp","proof"],
"properties":{
"version":{"type":"string"},
"canonical":{"type":"string","minLength":10},
"hash":{
"type":"object",
"required":["algorithm","value"],
"properties":{
"algorithm":{"type":"string","enum":["SHA-256"]},
"value":{"type":"string","pattern":"^[a-f0-9]{64}$"}
},
"additionalProperties":true
},
"timestamp":{
"type":"object",
"required":["issuedAt","issuer","nonce"],
"properties":{
"issuedAt":{"type":"string","format":"date-time"},
"issuer":{"type":"string","minLength":3},
"nonce":{"type":"string","minLength":8}
},
"additionalProperties":true
},
"proof":{
"type":"object",
"required":["algo","signature","keyId"],
"properties":{
"algo":{"type":"string"},
"signature":{"type":"string"},
"publicKey":{"type":"string"},
"keyId":{"type":"string"}
},
"additionalProperties":true
},
"meta":{"type":"object","additionalProperties":true}
},
"additionalProperties":true
}
Test vectors
// sha256("TimeProofs")
7c040f8633b8823d72ed63da9b3b2dfe9846e912c66a64c9433ec9c4815d76c2
Signature vectors depend on issuer keys. Validate real proofs using a verifier that trusts the issuer’s public key(s).
Verification
In stateless mode, verification requires the full .tproof.json proof file (a hash alone is not sufficient).
POST /verify
Content-Type: application/json
{ "bundle": <.tproof.json> }
→ {"ok":true,"valid":true,...}
Verifiers SHOULD validate structure, recompute/compare the canonical message, and verify the signature.
.tproof.json bundle
This portable proof is designed for offline inspection and long-term storage. It MUST NOT contain original content, secrets, or personal data.
{
"version":"timeproofs-0.2",
"canonical":"7c04...76c2|2025-01-01T00:00:00.000Z|https://api.timeproofs.io|a1b2c3...",
"hash": { "algorithm":"SHA-256", "value":"7c04...76c2" },
"timestamp": {
"issuedAt":"2025-01-01T00:00:00.000Z",
"issuer":"https://api.timeproofs.io",
"nonce":"a1b2c3..."
},
"proof": {
"algo":"Ed25519",
"signature":"<hex>",
"publicKey":"<base64 SPKI DER>",
"keyId":"tp-v0-2-main"
},
"meta": { "type":"document" }
}
- Offline inspection: anyone can read the proof without contacting the issuer.
- Online verification: a verifier can POST the proof to a stateless endpoint for validation.
- Key policy: deployments may enforce key freeze (pinned public key) and ignore embedded
publicKey.
Mapping
ProofSpec uses a self-describing proof format. The main correspondences are:
| Concept | Field | Notes |
|---|---|---|
| Hash | hash.value | Lowercase SHA-256 64-hex. |
| Issuance time | timestamp.issuedAt | ISO-8601 UTC. |
| Issuer id | timestamp.issuer | Stable issuer identifier (typically HTTPS origin). |
| Anti-replay | timestamp.nonce | Random, unpredictable. |
| Signed message | canonical | Signed by the issuer. |
| Signature | proof.signature | Encoding depends on issuer implementation. |
| Key id | proof.keyId | Identifies signing key. |
Reserved fields
Names starting with _tp_ are reserved for the specification and MUST NOT be used for custom metadata.
Errors
{"ok":false,"error":"invalid_hash"}
// Examples:
// {"ok":false,"error":"missing_bundle"}
// {"ok":false,"error":"missing_signature"}
// {"ok":false,"error":"invalid_signature"}
// {"ok":false,"error":"bad_request"}
Operational policy
- Issuers MUST NOT store original content. Hash-only inputs.
- Issuers SHOULD apply rate limits and abuse protection on timestamp and verify endpoints.
- Issuers SHOULD publish public keys and clearly document key lifecycle via
keyId.
Integration details and reproducible snippets are documented in API Docs.
Checklist
- Hash locally (no original content leaves the device).
- Hash the exact bytes you intend to prove (use consistent canonicalization).
- Keep the
.tproof.jsonproof file for long-term verification. - Verify using the proof file (offline or via stateless verification).
- Pin issuer keys when operating in “key freeze” mode (ignore embedded keys).
Versioning
This document describes ProofSpec v0.1 (Public Beta site v0.2). Additive changes SHOULD prefer minor/patch bumps; breaking changes MUST bump the major version.
Issuers and verifiers SHOULD clearly expose which ProofSpec version(s) they support.