add canonical sigchain bundle format
This commit is contained in:
parent
76eb785ee2
commit
decff4b995
14 changed files with 1515 additions and 42 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue