Architecture
How attest is put together: the split between the engine and the CLI, the canonical
serialization that anchors signatures, the git-notes storage model, the optional signing model,
and the verify / export flow.
Two layers: AttestKit vs the attest CLI
attest is deliberately split so the engine is testable with zero I/O.
AttestKit(Sources/AttestKit/) is the engine library. It owns the data model (Attestation,Verdict), the canonical serialization, Ed25519 signing/verification, thePolicy+Verifier, theExporter, augur ingestion, and the storage protocol. It depends only on Apple’sswift-crypto: no third-party packages, no networking, no global state.attest(Sources/attest/) is the CLI. It usesswift-argument-parser, resolves git revisions and ranges, loads.attest.json, and renders human or JSON output. It is a thin driver overAttestKit.
The boundary matters for two reasons:
- Testability. The engine reads attestations through the
AttestationStoreprotocol, so the test suite drives it against an in-memory fake (InMemoryStore) without shelling out togit. The sameVerifierandEd25519Verifierthe CLI uses are exercised directly. - Determinism. All time-dependent behavior (today: the
maxAgeDaysfreshness rule) takes an injected reference time rather than reading the system clock inside the engine. The CLI suppliesInt(Date().timeIntervalSince1970)at its boundary; tests supply a fixed epoch.
┌──────────────────────────────┐
│ attest (CLI) │ argument-parser, git resolution,
│ Sources/attest/ │ .attest.json loading, rendering,
│ │ injects `now` = current epoch
└───────────────┬──────────────┘
│ drives
┌───────────────▼──────────────┐
│ AttestKit (engine) │ Attestation, Canonical, Ed25519,
│ Sources/AttestKit/ │ Policy/Verifier, Exporter, Augur,
│ │ AttestationStore protocol
└───────────────┬──────────────┘
│ persists through
┌───────────────▼──────────────┐
│ AttestationStore │ NotesStore (git notes) │ InMemoryStore (tests)
└──────────────────────────────┘
The data model
An Attestation (Models.swift) is a provenance record keyed to a git commit SHA:
| Field | Meaning |
|---|---|
commit | the commit SHA this record is about |
reviewer | who or what reviewed, e.g. agent:claude, human:leif, ci:runner |
confidence | reviewer confidence, clamped to 0…1 at construction |
verdict | optional proceed / review / block (mirrors augur’s vocabulary) |
testsPassed | whether the change’s tests passed |
humanApproved | whether a human explicitly approved |
timestamp | Unix epoch seconds when the attestation was made |
note | optional free text |
signature / publicKey | optional base64 Ed25519 pair (present only when signed) |
Verdict is Comparable (proceed < review < block) so a policy can express “at least review”.
Multiple attestations can accrue on a single commit; a store appends, never replaces.
Canonical serialization: the signature contract
Signatures are only meaningful if everyone agrees on which bytes are signed. Canonical.swift
defines that contract:
Attestation.canonicalData()encodes a fixed subset of the record (everything exceptsignatureandpublicKey) as JSON with sorted keys and slashes left unescaped.- Optional fields (
note,verdict) are omitted whennil(encodeIfPresent), keeping the bytes compact and stable.
Two consequences hold by design and are covered by tests:
- Attaching a signature never changes the signed bytes. An unsigned record and its signed copy share canonical bytes, because the canonical form excludes the signature pair.
- Identical content always yields identical bytes, independent of field declaration order or platform.
The canonical form is the contract. Changing it invalidates every existing signature, so it is treated as a breaking change and must never drift.
Storage: git notes
NotesStore (NotesStore.swift) implements AttestationStore over git notes under a dedicated
ref, refs/notes/attest:
- Each commit’s note holds JSON Lines (one attestation per line), so appending a new
attestation is concatenating a line, and each record stays individually parseable
(
AttestationCodec). - Notes are portable: no service, no database. They travel with
git push origin "refs/notes/*"and never touch the working tree. NotesStoreshells out togitviaProcess, reads stdout, and treats a missing note (a non-zero exit fromgit notes show) as “no attestations” rather than an error.- Reading attestations is strict: a corrupt JSON line surfaces
AttestError.malformedRecordrather than being silently dropped. Corruption in an audit ledger must be loud, not lossy.
The InMemoryStore fake mirrors the same protocol for tests and dry runs, guarded by a lock.
Signing model
Signing is optional end to end (Ed25519Signer.swift, KeyStore.swift):
attest keygengenerates a Curve25519 (Ed25519) keypair and writes the private key as base64 to~/.config/attest/key(or$XDG_CONFIG_HOME/attest/key) with0600permissions.attest sign --signloads that key and produces a detached base64 signature overcanonicalData(), embedding the signer’s base64 public key on the record so any party can verify it later without a key server.Ed25519Verifier.verifyrecomputes the canonical bytes and checks the embedded signature against the embedded public key (and, optionally, an expected key for pinning). Any mutation of signed content fails verification.
An unsigned attestation is a fully valid record. Signing is what lets a policy trust a record
(requireSignature, trustedKeys, signerPinning). See Signing & identity.
The verify flow
attest verify (and the Attest.verify facade) does:
- Resolve the target commits: a single commit, an oldest-first range
(
NotesStore.commits(inRange:)), orHEAD. - Load the
Policyfrom.attest.json(or the permissive default if absent). - For each commit, read its attestations and run
Verifier.evaluate, collectingViolations. - Return a
VerificationResult(passed,checkedCommits,violations). The CLI exits non-zero whenpassedis false; that exit code is the contract CI and agent loops read.
The verifier injects a reference time now (defaulted to the current epoch at the CLI boundary)
used only by the maxAgeDays freshness rule. See Policy reference for every
rule.
The export flow
attest export (Exporter.swift) produces a single, stable JSON AuditReport for compliance
archival, distinct from attest log (a human/diagnostic listing):
- The caller resolves the range to an ordered commit list (oldest first), exactly as
verify/logdo. The exporter does no git walking of its own. - For each commit, every attestation is paired with a computed
VerificationStatus(signed, and for signed recordsverified), reusing the sameEd25519Verifier. - When a
Policyis supplied, each commit gets apolicyPassedand the report a top-levelallPassed, computed with the sameVerifierasattest verify.
Output is deterministic: commits in supplied order, records in store order (oldest first), sorted JSON keys, so identical inputs yield byte-identical JSON and the document diffs cleanly. See CLI reference for flags and CI integration for the archival step.