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 previousHash must equal the hash of 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.errors

Alternatively, 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.