Security & Trust Model
How unTamper makes records provable
The complete technical specification of our hash chain model, verification protocol, and infrastructure security.
What gets hashed
Every event is serialized into a canonical JSON payload before hashing. The payload includes:
- ·Actor — ID and type of the entity that performed the action
- ·Action — the action string
- ·Target — ID and type of the affected resource
- ·Timestamp — UTC ISO-8601, sourced from the server (not the client)
- ·Metadata — all custom key-value pairs
- ·Previous hash — hash of the immediately preceding event in the same project
The canonical form is deterministic: keys are sorted, whitespace is stripped. This ensures the same payload always produces the same hash.
The hash is then signed with an ECDSA private key held by the ingestion pipeline. The corresponding public key is fetchable via /api/v1/public-key and is used by the SDK's verification service to confirm events were written by unTamper — not injected directly into the database.
Chain model
Events form a singly-linked hash chain per project. Each event stores previousHash — the hash of the event immediately before it, identified by sequence number.
The first event in a project has previousHash: null. This anchors the chain.
Why this matters: To alter any event, an attacker must also recompute every subsequent hash in the chain, and forge a valid ECDSA signature for each one — which requires the private key. Any attempt to alter or reorder events produces detectable hash and signature failures.
Verification protocol
Each event is verified in three steps:
- ·Hash check — recompute the hash of the canonical payload. If it matches
event.hash, the payload is intact. - ·Signature check — verify the ECDSA signature against the project's public key. Proves the event was written by unTamper's ingestion pipeline.
- ·Chain check — each event's
previousHashmust equal thehashof its predecessor. Sequence gaps are also detected.
The result includes valid, totalLogs, validLogs, brokenAt, and a full errors array — returned by verification.verifyLogs() in the SDK.
Auditor access
An auditor with a scoped read-only API key can verify chain integrity directly using the SDK — no admin access or infrastructure access required:
const client = new UnTamperClient({ projectId: PROJECT_ID, apiKey: AUDITOR_READ_KEY })
await client.initialize()
const { logs } = await client.logs.queryLogs({ limit: 50000 })
const result = await client.verification.verifyLogs(logs)
// result.valid, result.brokenAt, result.errorsAlternatively, the /api/v1/verify/chain API endpoint accepts a Bearer token and returns a machine-readable JSON report. A read key is sufficient — write or admin access is not required.
API key model
API keys are scoped per-project. Keys are hashed (SHA-256) at rest — we store only the hash, never the plaintext key.
- ·Write keys — event ingestion only
- ·Read keys — querying and verification (sufficient for auditors)
- ·Admin keys — project management and key rotation
Key rotation is non-destructive: old keys remain valid until explicitly revoked.
Infrastructure
Events are persisted to PostgreSQL with GIN indexes for full-text and JSONB search.
The ingestion path goes through a durable queue (Upstash QStash) before hitting the database — events are not lost if the processor is temporarily unavailable.
Chain integrity is enforced at write time: the previous event's hash is fetched before the new event hash is computed. This prevents race conditions from producing a broken chain.