v1.11.0 — Scalable Verification & External Anchoring
This release roots checkpoint trust outside the application and makes verifying a large ledger cheap, while hardening the write path against concurrency and tampering. It is fully backward compatible: a 1.10 ledger with anchoring disabled and no incremental flags verifies identically to 1.10, with no artifact-format change and no re-export needed.
Highlights
- External anchoring (opt-in). Copy a per-checkpoint digest —
sha256(id . chain_hash . created_at)— into an independent trust domain so a full internal compromise is detectable. Ships an RFC 3161 timestamp anchor in core (offline verify, no cloud SDK) and aNullAnchorfor dev/tests. A checkpoint that's been rewritten and re-signed with a valid key still failschronicle:verify --anchors. - Scalable verification. New
chronicle:verifymodes —--checkpoints-only,--from-checkpoint/--to-checkpoint,--since-last-checkpoint,--resume, and--anchors— trade scope for cost without losing rigor. Each falls back to full verify (with a warning) until checkpoints are backfilled. - Range-aware checkpoints. Checkpoints now record
head_id,entry_count, andprevious_checkpoint_id, andcheckpoint_idis populated on covered entries at creation — fixing a 1.10 gap where the checkpoint-verification branch never ran.checkpoint_idis unhashed; no signatures change. - Concurrency-safe ordering. A monotonic
sequencecolumn (assigned under the chain row-lock, withunique(sequence)/unique(chain_hash)) replaces ULID-sort ordering everywhere — a concurrent chain fork now fails loudly instead of corrupting the ledger. - Companion package: the new
laravel-chronicle/anchor-s3reference adapter anchors to an S3 Object Lock (WORM) bucket.
Added
- Monotonic
sequencecolumn onchronicle_entries(assigned under the chain row-lock;unique(sequence)+unique(chain_hash)), with a backfilling migration that's a no-op on fresh installs. IntegrityVerifier::verifyFrom(Checkpoint)— verify from a known-good checkpoint instead of genesis (signature verified before itschain_hashseeds the walk), so pruned-history ledgers stay verifiable.- Range-aware checkpoints:
head_id,entry_count,previous_checkpoint_idcolumns; newchronicle_checkpoint_anchorsand optionalchronicle_verification_runstables;chronicle.tables.*keys for both.CheckpointgainspreviousCheckpoint()andanchors()relations. CheckpointCreator::create()records head/count/linkage and populatescheckpoint_idon covered entries (unhashed — nopayload_hash/chain_hashchange).chronicle:checkpoints:backfill— chunked, idempotent backfill of the range columns +checkpoint_idfor pre-1.11 ledgers (--dry-runsupported).IntegrityVerifier::verifySegment()andCheckpointChainVerifier(fast O(checkpoints) attestation; signature path shared via theVerifiesCheckpointSignaturetrait).- New failure reasons:
checkpoint_chain_broken,checkpoint_head_mismatch,segment_discontinuous,anchor_invalid. chronicle:verifyincremental modes:--checkpoints-only,--from-checkpoint=/--to-checkpoint=,--since-last-checkpoint,--resume.- External anchoring: the
AnchorProvidercontract,AnchorReceipt,CheckpointDigest, andAnchorManager(opt-inchronicle.anchoring,enableddefaults false);NullAnchor; andRfc3161TimestampAnchor(offlineopenssl ts -verify; addssymfony/process). - Anchoring pipeline: a queued, retryable
AnchorCheckpointJobdispatched after the checkpoint commits (anchor failure never rolls a checkpoint back); the sharedCheckpointAnchorerwritespending → anchored/failed. - Anchor commands:
chronicle:checkpoint --anchor,chronicle:anchor:retry {--status=failed}(pending/failed),chronicle:anchor:verify {--checkpoint=}, andchronicle:verify --anchors. - Ledger order is derived from
sequenceeverywhere (ChainHashEntry,IntegrityVerifier,EntryVerifier,EntryExporter), eliminating falsechain_hash_mismatch/chain_invalidwhen entries share a millisecond across processes. payload,payload_hash,chain_hashareNOT NULLon fresh installs.CheckpointCreatorsigns a canonical object (id,chain_hash,algorithm,key_id,created_at, mirroringExportSigner); verification falls back to the legacy bare-hash format, so older checkpoints still verify.chronicle:prunewarns that from-genesis verify won't pass post-prune and points toverifyFrom().RateLimitPolicylogs a warning before rejecting an over-limit entry (audit suppression is now observable).- Genesis seed unified on
ChainHasher::GENESIS;chronicle:installpublishes migrations under their dated filenames (idempotent re-runs); checkpoint head resolved bysequence.
Fixed
- Export ordering matches chain/verification ordering — fixes export-verification false failures under clock skew.
- Corrected author email in
composer.jsonand aPendingEntrydocblock typo.
Security
- Verification now detects divergence between an entry's denormalized columns (
action,actor_id,metadata,diff, …) and its hash-coveredpayload— new codecolumn_payload_divergence(sharedComparesEntryColumnstrait). - Model diffs redact
$hiddenattributes and any$chronicleRedact/$redactedFieldsentries (records"[redacted]"), so secrets never enter the immutable, exportable audit diff. - External anchoring defeats a full internal compromise: even if an attacker rewrites the ledger and re-signs every checkpoint with a valid key (offline verify passes),
chronicle:verify --anchorsfails at the first anchored checkpoint.
Upgrading from 1.10
php artisan migrate(additive: checkpoint range columns, an index on the existingcheckpoint_identries column, and the two new tables).php artisan chronicle:checkpoints:backfill— populates the range columns +checkpoint_idfor pre-1.11 checkpoints (chunked, idempotent;--dry-runto preview). Incremental verify modes fall back to full verify until this runs.- Anchoring is opt-in via
chronicle.anchoring.enabled; no behavior change without it. - New runtime dependency:
symfony/process(a standard component, not a cloud SDK).
No artifact-format change; no re-export needed. See the Upgrade Guide.
Full Changelog: 1.10.0...1.11.0