add canonical sigchain bundle format

This commit is contained in:
Eric Wendland 2026-07-19 03:16:14 +02:00
commit decff4b995
14 changed files with 1515 additions and 42 deletions

View file

@ -36,7 +36,12 @@ SSHSIGCHAIN v1 is redesigned before deployment:
- cached ancestors are never replaced by older heads and incomparable verified
histories fail as forks; and
- a policy-change head is witnessed under the preceding policy before the new
policy governs descendants.
policy governs descendants;
- canonical `.sscb` bundles deterministically store records, separately
discloseable payloads, claims, and receipts without embedding root trust;
- JSONL is noncanonical human-facing interchange; and
- distribution backends return untrusted bundle bytes through one interface,
with a static HTTP profile that requires no server application.
The generic core defines backend interfaces and deterministic policy behavior,
not Nostr, HTTP, blockchain, or transparency-log clients. Those adapters belong
@ -60,5 +65,6 @@ draft. No migration parser or version alias is retained.
Rollback protection still depends on persistent local accepted heads and the
operator's anchor policy. A Nostr relay, mutable HTTP URL, blockchain, or
transparency service has only the guarantees its receipt verifier and deployment
actually establish. Concrete publication, durable accepted-head storage, and
actually establish. Canonical bundle conversion and verification exist, while
automatic HTTP/Iroh fetching, durable accepted-head storage, and
cross-implementation vectors remain follow-up work.

View file

@ -492,6 +492,15 @@ test-only static workflow was removed rather than migrated. Iroh keychain sync
remains the current replicated local operation-log path while explicit
SSHSIGCHAIN production and import workflows are completed.
SSHSIGCHAIN's canonical `.sscb` bundle is the portable on-disk and full-snapshot
distribution boundary. It deterministically stores signed record objects,
separate disclosures, head claims, and anchor receipts without embedding the
root key. JSONL remains inspection/interchange only. Bundle sources implement a
backend-neutral untrusted fetch interface; the static HTTP profile publishes
the same bytes below `/.well-known/sshsigchain/v1/<chain-id>/chain.sscb`.
Fetching never grants trust or selects a head. Geth may use Iroh rather than
HTTP operationally without changing the format or verification path.
New devices can use the node enrollment flow instead of hand-editing keychain
state. `geth node enroll join` explicitly imports an owner admin public key as
the new node's trust anchor, imports the signed peer card only as untrusted

View file

@ -62,6 +62,7 @@ settles:
- `geth cas add-private`
- `geth cas get-private`
- `geth keychain verify-sigchain`
- `geth keychain bundle-create|bundle-extract`
Migration expectation: overlay membership records and authorization resources
should remain readable, but packet runtime flags, platform activation details,
@ -72,10 +73,10 @@ properties are explicitly out of scope. Prototype BLAKE3-XOR envelopes from
earlier pre-deployment builds are rejected with a clear error and should be
recreated from plaintext.
The SSHSIGCHAIN verifier is experimental while SSHSIGCHAIN signing, storage,
publication, import, head persistence, and independently generated wire vectors
are completed. The prior test-only static commands were removed and are not a
compatibility path.
The SSHSIGCHAIN verifier and canonical bundle conversion are experimental while
SSHSIGCHAIN signing, append storage, automatic distribution, import, head
persistence, and independently generated wire vectors are completed. The prior
test-only static commands were removed and are not a compatibility path.
## Adding Commands

View file

@ -576,10 +576,28 @@ resource-scoped capability decisions.
backend receipt policies, required backend classes, old-policy witnessing
of policy-change heads, cached-head rollback checks, and
incomparable-history fork failure.
- `[x]` Define and implement the canonical `.sscb` on-disk/full-snapshot
bundle with deterministic record, disclosure, claim, and receipt objects.
- `[x]` Decoding rejects trailing bytes, noncanonical order, duplicates,
unknown references, mixed chains, and over-limit inputs.
- `[x]` `bundle-create` and `bundle-extract` convert record JSONL while
clearly preserving claims/receipts only in the canonical bundle.
- `[x]` `verify-sigchain` auto-detects JSONL or canonical bundles and binds
a bundle's namespace and chain ID to local trust.
- `[x]` Specify the backend-neutral distribution-source contract and a static
HTTP full-snapshot profile at
`/.well-known/sshsigchain/v1/<chain-id>/chain.sscb`.
- `[ ]` Implement automatic distribution fetchers.
- Acceptance criteria: an HTTP fetcher enforces streaming limits,
conditional caching, redirect policy, trust-tuple matching, accepted-head
ancestry, anchor policy, and fork failure.
- Acceptance criteria: the Iroh fetcher returns the identical canonical
bundle/object model and remains the only geth node-to-node transport.
- `[~]` Add explicit CLI storage, signing, verification, publication, and
import workflows for a pinned SSHSIGCHAIN trust tuple.
- `[x]` `geth keychain verify-sigchain` verifies a JSONL transport file
against an operator-pinned chain ID and OpenSSH root public key.
- `[x]` `geth keychain verify-sigchain` verifies JSONL interchange or a
canonical bundle against an operator-pinned chain ID, namespace, and
OpenSSH root public key.
- `[ ]` Local record storage, signing, publication, import, and
head-advance workflows are complete.
- `[ ]` Persist accepted heads and require proof of extension before a source
@ -602,8 +620,9 @@ resource-scoped capability decisions.
- `[x]` An OpenSSH `ssh-keygen -Y` integration test signs and verifies an
Authority v1 genesis link.
- `[~]` The specification and reference implementation share base signing,
key-proof, commitment, and head-claim vectors.
- `[ ]` Publish the complete vector set in the specification.
canonical-bundle, key-proof, commitment, and head-claim vectors.
- `[x]` Publish outer-link and canonical-bundle framing vectors.
- `[ ]` Publish key-proof, commitment, head-claim, and receipt vectors.
- `[ ]` Add independently generated cross-implementation vectors.
- `[x]` SSH-admin-rooted keychain initialization.

View file

@ -103,6 +103,23 @@ generated wire vectors. Those later workflows must preserve the same explicit
trust tuple and Authority v1 semantics; they must not introduce a compatibility
route for the removed static format.
The verifier also accepts the canonical `.sscb` on-disk bundle. JSONL is a
human-facing record interchange and does not preserve standalone head claims or
anchor receipts. Convert between them with:
```sh
geth keychain bundle-create --in chain.jsonl --out chain.sscb
geth keychain bundle-extract --in chain.sscb --out chain.jsonl
```
The canonical bundle keeps disclosures separate from signed record objects and
sorts every object class deterministically. Adding a disclosure changes the
bundle hash but not the chain head. The same file is the simple static HTTP
distribution artifact at
`/.well-known/sshsigchain/v1/<chain-id>/chain.sscb`. Distribution is untrusted:
the configured root, authority replay, anchor policy, locally accepted head,
and fork checks still decide acceptance.
## Local commands
Bootstrap an owner node:

View file

@ -334,7 +334,150 @@ A conforming verifier MUST:
Records are never reordered by timestamps, transport arrival, identifiers, or
record counts. A verifier without all trust inputs fails closed.
## 9. JSONL transport and limits
## 9. Canonical objects and on-disk bundle
The canonical on-disk and full-snapshot distribution format is a single
SSHSIGCHAIN Canonical Bundle (`.sscb`). It contains records, disclosures, head
claims, and anchor receipts while deliberately omitting the root public key.
The root remains an out-of-band trust input.
All objects start with four magic bytes and version `0x01`. `bytes32` means a
big-endian `u32` length followed by that many bytes.
```text
record object:
"SSCR" || 0x01 || record_signing_bytes:bytes32 || signature:bytes32
disclosure object:
"SSCD" || 0x01 || record_hash:32 || profile_id:string16 ||
nonce:32 || payload:bytes32
head-claim object:
"SSHC" || 0x01 || claim_signing_bytes:bytes32 || signature:bytes32
anchor-receipt object:
"SSRC" || 0x01 || backend_id:string16 || claim_hash:32 ||
evidence:bytes32
```
The bundle grammar is:
```text
"SSCB" 4 bytes
0x01 bundle version
chain_id 32 bytes
namespace string16
record_count u32
records record_count objects, each wrapped as bytes32
disclosure_count u32
disclosures disclosure_count objects, each wrapped as bytes32
claim_count u32
claims claim_count objects, each wrapped as bytes32
receipt_count u32
receipts receipt_count objects, each wrapped as bytes32
```
Canonical ordering and uniqueness rules are:
- records occur in causal genesis-to-head order and every parent must equal the
preceding record hash;
- disclosures are sorted by `(record_hash, profile_id)` and that pair is unique;
- claims are sorted by claim hash and claim hashes are unique;
- receipts are sorted by `(claim_hash, backend_id)` and that pair is unique;
- disclosures must open a commitment in the named bundled record;
- claims must name a record in the bundled history; and
- receipts must name a bundled claim.
A decoder MUST reject trailing bytes, out-of-order objects, duplicate objects,
noncanonical embedded signing bytes, mixed chain IDs, unknown references, or a
bundle which does not encode byte-for-byte identically when re-encoded. The
namespace is self-description only and MUST exactly match the locally configured
trust tuple before record verification.
Bundle identity is:
```text
BLAKE3("sshsigchain.bundle.v1\0" || canonical_bundle_bytes)
```
Bundle identity is not chain identity: adding a valid disclosure or receipt
changes the bundle hash without changing any record hash. A canonical bundle is
therefore suitable as the canonical single-file on-disk snapshot, attachment,
cache object, or static-server artifact. Implementations may maintain indexes
or databases internally, but export/import MUST round-trip through this format.
The reference maximum bundle size is 256 MiB and the maximum embedded object
size is 5 MiB. Implementations MUST enforce bounds while streaming, before
allocating lengths supplied by an untrusted bundle.
### 9.1 Base bundle vector
The following structural vector contains the section 4.1 zero-chain `Noop`
record, signature bytes `01`, namespace `sshsigchain.v1`, and no disclosures,
claims, or receipts:
```text
53534342010000000000000000000000000000000000000000000000000000000000000000000e737368736967636861696e2e7631000000010000006953534352010000005b5353435301000000000000000000000000000000000000000000000000000000000000000000eea8e117bae085d4a9793d8b7d726a89558b63e515a6e1ca32d526f93b24c8a200107373682d6564323535313920415149440800000000000101000000000000000000000000
```
It fixes bundle and record-object framing. Like the section 4.1 outer vector,
it is not a valid verified chain because the first transition is not `Genesis`
and `01` is not an OpenSSH SSHSIG signature.
## 10. Distribution profiles
Distribution is untrusted availability and routing. It does not grant
authority, choose a canonical head, replace the root trust tuple, or satisfy an
anchor policy merely because a file was fetched successfully.
A distribution endpoint has a local/configured `backend_id`, `class`, and
opaque `locator`. Backends conceptually implement:
```text
fetch_bundle(endpoint, chain_id, accepted_head) -> none | canonical_bundle_bytes
```
The `accepted_head` permits an incremental P2P backend to optimize its response,
but every returned result must decode to a complete verifiable snapshot that
contains the accepted head. HTTP, Iroh, removable-media, object-store, IPFS, or
application-specific backends can implement the same interface. Fetch failures
affect availability only. Results enter the same signature, authority,
anchoring, cached-head, and fork checks.
### 10.1 Static HTTP profile
The static HTTP profile needs no server application or directory listing. For
chain ID `<lowercase-chain-id>`, publish the bundle at:
```text
/.well-known/sshsigchain/v1/<lowercase-chain-id>/chain.sscb
```
The response media type SHOULD be:
```text
application/vnd.sshsigchain.bundle.v1
```
A client performs an ordinary `GET`, accepts a successful full response, caps
bytes while streaming, decodes the canonical bundle, requires its chain ID and
namespace to match local trust, verifies the chain, verifies applicable head
claims and receipts, then applies cached-head/fork rules. `404` means the source
currently has no bundle. Servers SHOULD replace `chain.sscb` atomically.
Servers MAY expose the lowercase bundle hash as a strong `ETag`; clients MAY use
`If-None-Match`. HTTP cache validators are performance hints, not trust. HTTPS
protects privacy, locators, and availability against network interference, but
SSHSIGCHAIN verification remains mandatory even over authenticated HTTPS.
Plain HTTP remains integrity-safe only in the narrow sense that tampering is
detected; it offers no confidentiality or reliable availability.
This full-snapshot profile intentionally favors deployment with a static file
server. A later chunked profile may add immutable hash-addressed objects and a
small mutable manifest, but it must preserve the object encodings and all
verification rules above.
## 11. JSONL interchange
The reference transport uses one JSON object per nonblank line with exactly the
record fields in section 3. Byte arrays use JSON arrays of octets; enum objects
@ -345,7 +488,11 @@ Reference limits are: 16 KiB canonical key, 64 KiB signature/proof, 1 MiB one
profile payload, 1,024 extensions per record, 100,000 records, 5 MiB one JSONL
line, and 64 MiB one JSONL input. Implementations may impose smaller limits.
## 10. OpenSSH interoperability
JSONL is a human-facing interchange representation, not the canonical on-disk
format. It carries records and optional disclosures only; canonical bundles are
required to preserve head claims and receipts.
## 12. OpenSSH interoperability
Outer records use commands equivalent to:
@ -358,7 +505,7 @@ ssh-keygen -Y verify -f <one-key-allowed-signers> -I sshsigchain \
The one-key `allowed_signers` file is only an input to cryptographic signature
verification. It never establishes authorization or root trust.
## 11. Security considerations
## 13. Security considerations
- Use independent random chain IDs and protocol-specific SSHSIG namespaces.
- A stolen active key retains its scoped authority until a causally prior
@ -369,6 +516,8 @@ verification. It never establishes authorization or root trust.
nonce is reused. Always use a fresh unpredictable nonce.
- Anchor strength is no greater than the configured independent backends and
receipt verifiers. Cached-head checks remain mandatory.
- Distribution sources and HTTP cache metadata are untrusted. A fetched bundle
must contain the cached head and pass the complete verifier before use.
- Resource bearer secrets, discovery records, and profile payloads cannot
mutate SSHSIGCHAIN authority.
- Implementations should apply a local OpenSSH algorithm policy and reject