Chronicle

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

ConditionDetected as
Event deleted from storeGap (missing sequence number)
Event content modifiedTampered (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/verify

Request body:

{
  "stream_id": "stream_01j9vk...",
  "from_seq": 0,
  "to_seq": 0
}

Response: verify.Report JSON.

verify.Input

FieldTypeDescription
StreamIDid.IDStream to verify (optional — resolved from AppID+TenantID if omitted)
FromSequint64Start of verification range (0 = beginning)
ToSequint64End of verification range (0 = current head)
AppIDstringRequired for scope enforcement
TenantIDstringRequired for scope enforcement

verify.Report

FieldTypeDescription
Validbooltrue if no gaps and no tampered hashes
Verifiedint64Number of events checked
Gaps[]uint64Sequence numbers absent from the store
Tampered[]uint64Sequence numbers with hash mismatches
FirstEventuint64First sequence number checked
LastEventuint64Last 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 = true and event.ErasedAt is set, but the stored Hash is unchanged.
  • VerifyChain reports Valid = true even 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.

On this page