Every change to a Kynver contents inventory or insurance claim — items added, valuations set, a claim submitted or settled — is recorded in an append-only audit trail. When the account's audit ledger is active, each record is cryptographically linked to the one before it, so any alteration, insertion, or deletion of a past record is detectable. This article explains how a carrier, adjuster, or independent auditor verifies that trail without trusting Kynver's word for it.
What You Need
- The audit-trail export — a JSONL file the policyholder downloads from their own account (GET /api/contents/audit-ledger?format=jsonl while signed in). Ask the policyholder to send you this file. Each line is standalone JSON: the first line is a manifest, and every following line is one audit record with all verified fields included verbatim.
- Kynver's verification key — fetch it yourself (do not accept it from the file or the policyholder): GET https://www.kynver.com/api/contents-ledger/public-key. The response contains publicKeyId, publicKeyHex (a raw 32-byte Ed25519 public key, hex-encoded), and usingDevFallbackKey (must be false for a production trail).
What Each Record Contains
Each row line's payload has exactly these verified fields:
- occurredAt — ISO-8601 timestamp of the event
- userId — the account the record belongs to
- inventoryId — the inventory involved (or null)
- action — what happened (e.g. create, update, status_change, valuation events)
- entityType and entityId — what it happened to (e.g. ContentsClaim + the claim id)
- before and after — the recorded state around the change (null when not applicable)
- chainPrev — the previous record's chainHash (null for the first chained record)
- chainHash — this record's own hash
- signature and signingKeyId — the digital signature, when signing is enabled
Recomputing the Chain
For each record, in order (the export is oldest-first):
If every record passes, the trail is intact: nothing was altered, reordered, or deleted since it was written. If any record fails, the chain is broken at that point and everything after it is unverifiable.
Example verification in Node.js:
const crypto = require('crypto');
function canonical(v) {
if (v === null || typeof v !== 'object') return JSON.stringify(v);
if (Array.isArray(v)) return '[' + v.map(canonical).join(',') + ']';
return '{' + Object.keys(v).sort().filter((k) => v[k] !== undefined)
.map((k) => JSON.stringify(k) + ':' + canonical(v[k])).join(',') + '}';
}
// For each row payload r (oldest first), with prev = previous chainHash or null:
const body = {
occurredAt: r.occurredAt, userId: r.userId, inventoryId: r.inventoryId,
action: r.action, entityType: r.entityType, entityId: r.entityId,
before: r.before, after: r.after, chainPrev: r.chainPrev,
};
const hash = crypto.createHash('sha256').update(canonical(body), 'utf8').digest('hex');
// hash must equal r.chainHash, and r.chainPrev must equal prev.
// Signature check (signed records):
const spki = Buffer.concat([
Buffer.from('302a300506032b6570032100', 'hex'),
Buffer.from(publicKeyHex, 'hex'),
]);
const key = crypto.createPublicKey({ key: spki, format: 'der', type: 'spki' });
const ok = crypto.verify(null, Buffer.from(r.chainHash, 'utf8'), key,
Buffer.from(r.signature, 'base64url'));There is not yet a standalone downloadable verifier script for contents audit trails — the fields above are the complete specification, and the snippet is a full verifier core. You can also POST the export to your own tooling; verification requires nothing from Kynver except the public key.
Honest Scope: Signed vs Unsigned Records
- Signed records (signature present) prove both integrity and origin: the record existed in this exact form and was written by Kynver's signing key. Signatures are active only when the deployment serving the account has ledger signing enabled — check signingConfigured on the public-key endpoint.
- Unsigned chained records (chainHash present, signature null) are hash-chained only: they prove the sequence has not been altered since export, but do not carry a platform signature.
- Records without chain data (chainHash null) were written before the account's audit ledger was activated. They are still part of the append-only trail, but cannot be cryptographically verified. The export manifest reports how many of each kind the file contains, plus a truncated flag when the trail exceeds the 10,000-row export cap (the export keeps the oldest records so the chain verifies from its start).
What the Manifest Tells You
The first line's payload summarizes the export: rowCount, truncated, chainVerified (Kynver's own check at export time — recompute it yourself, don't rely on it), headChainHash, signedRowCount, unsignedRowCount, legacyRowCount, and the publicKeyId the deployment currently signs with.