Verification
Hash chain integrity verification — detecting tampered or deleted events.
How the hash chain works
Every event in Chronicle is linked to its predecessor via SHA-256:
Hash = SHA256(prevHash | timestamp | action | resource | category |
resourceID | outcome | severity | metadata_json)Where | is a literal pipe byte separator and metadata_json uses sorted keys for determinism. The first event in a stream has PrevHash = "".
Any modification to any field changes that event's hash, which breaks every subsequent hash in the chain — making tampering detectable by re-deriving hashes from scratch.
Verification types
| Condition | Detected as |
|---|---|
| Event deleted from store | Gap (missing sequence number) |
| Event content modified | Tampered (stored hash ≠ recomputed hash) |
| Encryption key destroyed (GDPR erasure) | Not a break — hashes cover metadata, not payload |
Running verification
Via the Chronicle API
Verify the full chain for an app+tenant scope:
import "github.com/xraph/chronicle/verify"
report, err := c.VerifyChain(ctx, &verify.Input{
AppID: "myapp",
TenantID: "tenant-1",
// FromSeq and ToSeq are optional (0 = full chain)
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("valid=%v verified=%d gaps=%v tampered=%v\n",
report.Valid, report.Verified, report.Gaps, report.Tampered)Verify a single event by ID — recomputes the event's hash and confirms it matches the stored value:
import "github.com/xraph/chronicle/id"
eventID, _ := id.ParseAuditID("audit_01j9vk...")
ok, err := c.VerifyEvent(ctx, eventID)
if err != nil {
log.Fatal(err)
}
fmt.Println("event valid:", ok)VerifyEvent returns false (not an error) when the stored hash does not match the recomputed hash, indicating that the event content was modified after recording.
Via the HTTP API
POST /v1/verifyRequest body:
{
"stream_id": "stream_01j9vk...",
"from_seq": 0,
"to_seq": 0
}Response: verify.Report JSON.
verify.Input
| Field | Type | Description |
|---|---|---|
StreamID | id.ID | Stream to verify (optional — resolved from AppID+TenantID if omitted) |
FromSeq | uint64 | Start of verification range (0 = beginning) |
ToSeq | uint64 | End of verification range (0 = current head) |
AppID | string | Required for scope enforcement |
TenantID | string | Required for scope enforcement |
verify.Report
| Field | Type | Description |
|---|---|---|
Valid | bool | true if no gaps and no tampered hashes |
Verified | int64 | Number of events checked |
Gaps | []uint64 | Sequence numbers absent from the store |
Tampered | []uint64 | Sequence numbers with hash mismatches |
FirstEvent | uint64 | First sequence number checked |
LastEvent | uint64 | Last sequence number checked |
GDPR erasure and chain validity
GDPR crypto-erasure destroys the per-subject encryption key, making payload data irrecoverable. It does not modify the hash chain:
- Hashes are computed over event metadata (action, resource, category, timestamps), not the encrypted payload.
- After erasure,
event.Erased = trueandevent.ErasedAtis set, but the storedHashis unchanged. VerifyChainreportsValid = trueeven for streams with erased events.
verify.Store
The verify.Store interface provides the data access needed for verification:
type Store interface {
EventRange(ctx context.Context, streamID id.ID, fromSeq, toSeq uint64) ([]*audit.Event, error)
Gaps(ctx context.Context, streamID id.ID, fromSeq, toSeq uint64) ([]uint64, error)
}All Chronicle backends implement this interface as part of the composite store.Store.