# HAP-1 checkpoint witness profile 1

Reference release 0.5.0 adds an optional, separately runnable checkpoint witness.
It retains one issuer's signed journal checkpoint in its own durable store and
answers a caller's random challenge with that retained checkpoint. A recovery
inspection compares candidate **public history** with this response and a
previously retained endorsement. It never loads or restores an authority database.

The existing `HAP-1/1` runtime, tickets, receipts, checkpoints, permits, payment
outcomes and law vectors are unchanged. This profile uses new signature domains;
it does not reinterpret Echo Authority v0.2 or the site's separate Merkle-log
witness profile 2. A witness endorsement is not a permit, owner approval, payment
result, key-rotation certificate, consensus proof or statement of global freshness.

## Run the recovery drill

Use Node 22.13+ with built-in SQLite (Node 24 recommended):

```sh
node hap1/witness-drill.mjs
node --test scripts/hap1-witness-regressions.mjs
node scripts/hap1-witness-vectors.mjs --check
python3 hap1/witness-verify.py hap1/witness-vectors.json --vectors
```

The test suite and Python command require `cryptography==46.0.0`. The drill itself
has no third-party dependencies or network calls. It uses synthetic identities,
an isolated directory, separate SQLite authority/executor/witness stores and new
Node processes for witness commands. It grants 20 test units, consumes 12, publishes
that checkpoint, refuses pre-spend evidence, publishes a revocation, and refuses
pre-revocation evidence. It also refuses an unavailable witness. A matching history
still returns all authorization flags false. The authority state stays spent and
revoked, and the old evidence is unchanged. Temporary drill files are removed.

The drill does **not** restore an old database, contact an independently operated
host, perform a real payment or validate physical device recovery. Its report
explicitly says `independentOperator:false` and `realPayment:false`. The Python
verifier is a separate implementation authored by this project, not an independent
audit or third-party implementation. Public vectors use a publicly known RFC 8032
test key; never provision that key for an operator.

## Enrollment and local commands

An operator provisions a separate witness Ed25519 identity and pins the issuer's
public key through an authenticated channel. Each store serves exactly one issuer.
Protect the witness host, private directory, encrypted key and passphrase separately
from the authority device and its backups. Process separation on one host does not
establish an independent operator or protect against a compromised host.

Commands run locally on the witness host; authenticated transport and operator
access control are deployment responsibilities. There is no HTTP listener, public
service, discovery mechanism or default Money Musk endpoint in this profile.

```sh
# PASSPHRASE_FILE is an existing private regular file (0600), provisioned securely.
# Its secret contents never appear in argv. DIR must not exist yet.
node hap1/witness-cli.mjs init DIR ISSUER_PUBLIC_KEY PASSPHRASE_FILE
node hap1/witness-cli.mjs retain DIR CHECKPOINT_FILE BUNDLE_FILE PASSPHRASE_FILE
node hap1/witness-cli.mjs observe DIR CLIENT_NONCE PASSPHRASE_FILE
```

`init` creates DIR (0700), an encrypted `key.pem`, `state.sqlite`, and `pin.json`.
It prints the public pin. The witness uses the existing encrypted-key and durable
SQLite adapters. Ordinary startup cannot create a new identity or empty database.
Missing, corrupt, wrongly pinned or empty state refuses; an existing DIR cannot be
initialized again. Preserve files from a partial enrollment for investigation;
deleting a store and reenrolling is not recovery. Passphrases must satisfy the
existing identity adapter's minimum length of 16 characters.

The exact public pin fields are:

```json
{"schema":"HAP-1/witness-pin/1","issuerPublicKey":"<64 lowercase hex>","witnessPublicKey":"<64 lowercase hex>"}
```

Authenticate both public keys separately. The issuer and witness keys must differ.
Keep this pin and the latest verified endorsement **outside both the candidate
backup and the witness's own restore set**. An untrusted backup or response cannot
choose its own trusted keys or replace the retained endorsement. A `retain` result
must be verified against these pins before a client advances its retained state.
Advance that client state durably and monotonically, with continuity verification
and compare-and-swap when multiple writers are possible. The CLI does not manage a
client's retained state or key enrollment policy.

`retain` takes the existing signed HAP checkpoint and `HAP-1/bundle` history, with
no rewritten issuer fields. Use a consistent export: separate calls to
`authority.checkpoint()` and `authority.history()` can span a concurrent journal
advance. Such an inconsistent pair refuses; retry a consistent export instead of
suppressing the check. Publication is explicit. This release does not automatically
publish every grant, spend or revocation, or add a witness requirement to execution.

The client generates a fresh 32-byte random nonce for each observation attempt:

```sh
node hap1/witness-cli.mjs nonce
```

Send that nonce to the operator through your authenticated transport, obtain the
`observe` output, then inspect on the client without any private key:

```sh
node hap1/witness-cli.mjs inspect PIN_FILE RETAINED_ENDORSEMENT_FILE OBSERVATION_FILE CLIENT_NONCE CHECKPOINT_FILE BUNDLE_FILE
```

Treat the nonce as pending for one attempt only, enforce a local response deadline,
and discard it after use, failure or timeout. These lifecycle/deadline rules belong
to the caller; the stateless verifier can verify the same bytes more than once.
Do not fall back to a cached response if the witness is unreachable. Challenges
prevent reuse of an earlier response for a new attempt; they do not prove that the
operator has seen the latest issuer history or is honest.

Wire input files use the HAP canonical JSON encoding, optionally followed by one
LF. Duplicate keys, alternative encodings and extra signed fields refuse. The CLI
limits input files to 16 MiB and rejects symbolic links/nonregular files. Public
output is canonical JSON with one LF; `nonce` prints only the hex string. Nonzero
exit is refusal. Archive/vector JSON may be pretty printed for the Python verifier,
which rejects duplicate keys and recomputes canonical signed messages.

## Signed wire formats

Both formats use the existing HAP canonical JSON grammar and key identifiers from
[SPEC.md](SPEC.md): exact fields, sorted ASCII keys, safe integer numbers, no unknown
properties, and lowercase hex. `issuer` and `witness` are the existing `keyId` hashes
of the separately pinned Ed25519 public keys. Signatures are 64 bytes represented
by 128 lowercase hex characters. The signature message is exactly:

```text
UTF8(schema + LF + canonical(object with only its signature field removed))
```

The `schema` field stays in the signed body. Nested signatures stay in that body.

| Object | Exact fields | Signature domain |
| --- | --- | --- |
| Endorsement | `schema`, `witness`, `issuer`, `checkpoint`, `previous`, `signature` | `HAP-1/witness-checkpoint/1` |
| Observation | `schema`, `witness`, `issuer`, `nonce`, `endorsement`, `signature` | `HAP-1/witness-observation/1` |

An endorsement's `checkpoint` is the existing signed `HAP-1/checkpoint` object;
its issuer signature must verify. `previous` is null on the first retained
checkpoint, otherwise exactly `{sequence,head}` for the previously retained
checkpoint. Its sequence must be strictly less than the new checkpoint's sequence.
Sequence bounds remain 0–4096, with the existing all-zero head at sequence 0.
`previous` is a signed statement of the witness's prior cursor; it is not itself a
continuity proof or a hash chain over all endorsements. Verify the issuer's history.

An observation embeds the complete signed endorsement and a `nonce` of exactly
64 lowercase hex characters. Verify both witness signatures, the inner issuer
signature, all schemas/fields and the expected caller nonce. There is no timestamp,
expiration or claim that the retained checkpoint is globally latest. The observation
describes the store snapshot read for that response; concurrent retention can
advance the store after that read.

## Retention and persistence

The internal store object has exactly `schema`, `issuer`, `witness`, `endorsement`;
its schema is `HAP-1/witness-state/1`. Explicit initialization creates a null
endorsement. An empty initialized witness cannot answer `observe`. Every operation
revalidates the stored pins and signed endorsement. The store adapter supplies a
nonnegative revision plus atomic, durable compare-and-swap; production callers must
not substitute the test-only memory store for durable storage.

For a retention request, the witness verifies the candidate issuer checkpoint,
rejects decreasing sequence or a different head at the same sequence, and verifies
signed journal continuity through its previously retained cursor to exactly the
candidate checkpoint. An initial retention verifies from the zero cursor. A bundle
may include earlier history only if it passes through that retained cursor. An
empty bundle is accepted only when its `from`, retained cursor and candidate cursor
are identical. This is an explicit witness-profile rule, not a change to the core
`verifyContinuity` API, which still requires nonempty receipts.

The witness signs and commits the new endorsement with compare-and-swap **before
returning it**. On a competing write, it rereads state and repeats all checks; after
32 failed attempts it refuses `WITNESS_BUSY`. Two writers cannot commit conflicting
histories through the supplied SQLite adapter. SQLite uses the existing full-sync
transaction settings; durability still depends on the host/storage honoring them.
A storage error returns no endorsement. If a committed acknowledgement is lost,
retrying that exact checkpoint returns the original endorsement. An identical
checkpoint needs no new write and makes no promise to be the latest at return time.

## Recovery inspection and its limits

`inspectWitnessRecovery` verifies the separately retained endorsement, fresh-nonce
observation, and both pinned roles. A witnessed sequence below the retained one
refuses. A different head at the same sequence refuses. The candidate checkpoint
must exactly match the observed checkpoint, including its signature, and the
candidate history must extend the retained cursor to that checkpoint. Evidence
ahead of the observed checkpoint also refuses; publish it consistently first.

A match returns:

```json
{"schema":"HAP-1/witness-recovery/1","status":"HISTORY_MATCHES_WITNESS","sequence":1,"head":"<hash>","witnessResponse":"CHALLENGE_MATCHED","freshness":"UNKNOWN","executionAuthorized":false,"restoreAuthorized":false,"keyRotationAuthorized":false}
```

This is an unsigned local inspection result, not a transferable authority token.
It verifies public history, **not** the integrity or completeness of a candidate
SQLite authority/executor database. It does not reconstruct budget counters, prove
that a downstream effect occurred, cancel pending permits, load keys, rotate keys,
or activate a replacement device. Those operations require a separately reviewed
recovery design. Existing fresh-permit and durable-consumption rules still apply.

A retained client endorsement can expose rollback of the witness itself, even if
the rolled-back witness signs a new nonce. If the client pin and witness are both
rolled back, this profile cannot discover their lost history. Nor can it discover
an unpublished spend/revoke, prevent a malicious signer from equivocating to new
clients, or establish agreement among independent witnesses. It adds no gossip,
quorum, trusted clock, secure hardware counter or transparency consensus.

The next deployment evidence is a separately operated witness with authenticated
enrollment, durable client pins, explicit publication/availability policy and a
reproduced failure drill. Until those exist, this is an executable foundation for
recovery comparison, not production device recovery or independent adoption.

Primary references: [Node SQLite](https://nodejs.org/api/sqlite.html),
[RFC 8032](https://www.rfc-editor.org/rfc/rfc8032.html).
