# SSHSIGCHAIN v1 Status: Draft 2 (pre-deployment) SSHSIGCHAIN is a transport-independent, OpenSSH-signed authority chain. It is intended to be implementable outside geth. A chain can be carried by JSONL, HTTP, an object store, Iroh, or another transport; SSH is a signature mechanism, not a transport. Version 1 is the first and only format. There is no legacy grammar or downgrade mode. ## 1. Security goals and non-goals SSHSIGCHAIN provides: - an explicit out-of-band `(chain_id, namespace, root_public_key)` trust tuple; - deterministic signing bytes and causal parent hashes, with no sequence number or timestamp ordering; - a mandatory public authority state machine for devices, keys, causal revocation, permissions, delegation, and anchor policy; - proof-of-possession when a key is added; - salted commitments for selectively disclosed application-profile payloads; - signed head claims and a generic receipt policy for independent anchor backends; and - fail-closed rollback and fork detection relative to a cached head or multiple verified histories. It does not provide consensus, guaranteed publication, trusted time, anonymity, zero-knowledge disclosure, payload encryption, or automatic compromise recovery. An authorized key can perform every transition its effective permissions allow. Outer links reveal the chain ID, signer, authority transition, profile IDs, and payload commitments even when payloads are hidden. ## 2. Trust inputs and identifiers A verifier obtains these values through an authenticated channel, never from a downloaded chain: 1. `chain_id`: 32 independently generated random bytes; 2. `namespace`: the 1–128 byte printable, non-whitespace SSHSIG namespace (`sshsigchain.v1` by default); and 3. `root_public_key`: canonical OpenSSH public-key text. Canonical key text is exactly: ```text SP ``` Comments, leading/trailing whitespace, and additional fields are prohibited. Keys are identified by: ```text BLAKE3("sshsigchain.key-id.v1\0" || canonical_public_key) ``` Protocol identifiers are 1–128 ASCII bytes containing letters, digits, `.`, `-`, `_`, `:`, or `/`. ## 3. Outer-link model A record contains: | Field | Meaning | | --- | --- | | `chain_id` | Exact configured 32-byte chain ID. | | `previous` | Absent only for genesis; otherwise the preceding record hash. | | `signer_key_id` | Hash-derived ID of `signer_public_key`. | | `signer_public_key` | Canonical OpenSSH public key used for SSHSIG verification. | | `authority` | Exactly one public authority transition. | | `extensions` | Strictly profile-ID-sorted payload commitments and optional disclosures. | | `signature` | OpenSSH SSHSIG signature over the canonical signing bytes. | There is deliberately no sequence field. A record's position and causality are fully determined by genesis plus the exact `previous` link. Record counts are local indexing metadata and are not signed protocol state. The record hash named by its child is: ```text BLAKE3("sshsigchain.record-hash.v1\0" || signing_bytes) ``` The signature is verified but excluded from record identity. This prevents two valid encodings of a signature over identical outer content from creating different chain identities. ## 4. Binary signing grammar Integers are unsigned big-endian. `string16` and `bytes16` are a `u16` byte length followed by that many bytes; `bytes32` uses a `u32` length. Lists begin with a `u16` element count. No alignment or terminator bytes are present. ```text "SSCS" 4 bytes 0x01 protocol version chain_id 32 bytes has_previous u8: 0 or 1 previous 32 bytes when present signer_key_id 32 bytes signer_public_key string16 authority_transition transition (section 5) extension_count u16 repeated extension_count times: profile_id string16 payload_commitment 32 bytes ``` Transport-only disclosure bytes and the SSHSIG signature are not part of these bytes. The `signer_key_id` MUST match the canonical public key. Extensions MUST be strictly sorted by profile ID with no duplicates. ### 4.1 Base signing vector For an unsigned structural link with a zero `chain_id`, absent `previous`, signer `ssh-ed25519 AQID`, `Noop` authority, and no extensions, the signing bytes are: ```text 5353435301000000000000000000000000000000000000000000000000000000000000000000eea8e117bae085d4a9793d8b7d726a89558b63e515a6e1ca32d526f93b24c8a200107373682d656432353531392041514944080000 ``` The derived signer key ID in that vector is `eea8e117bae085d4a9793d8b7d726a89558b63e515a6e1ca32d526f93b24c8a2`. This vector fixes the outer grammar; it is not a valid chain genesis because a valid first record must carry `Genesis` authority. ## 5. Mandatory Authority v1 state machine Authority is part of the protocol suite, not an optional application profile. Every record contains exactly one transition. Transition tags and fields are: | Tag | Transition | Fields after tag | | --- | --- | --- | | `0` | `Genesis` | device ID, root authority key, anchor policy | | `1` | `DeviceAdd` | device ID, permission ceiling | | `2` | `DeviceRevoke` | device ID | | `3` | `KeyAdd` | device ID, authority key, proof `bytes32` | | `4` | `KeyRevoke` | key ID | | `5` | `PermissionGrant` | key ID, permissions, delegable permissions | | `6` | `PermissionRevoke` | key ID, permissions, delegable permissions | | `7` | `AnchorPolicySet` | anchor policy | | `8` | `Noop` | no fields | An authority key encodes canonical public-key `string16`, permissions, then delegable permissions. Permission lists MUST be strictly sorted according to their encoded `(tag, profile ID)` ordering and contain no duplicates. ### 5.1 Genesis Genesis is the only record without `previous`. Its record signer and embedded root key MUST equal the pinned root. The root key starts on the named root device and MUST have both `All` authority and delegable `All` authority. Its outer signature proves root-key possession. Genesis selects the initial anchor policy. Another genesis is invalid. The pinned root has no permanent bypass: it can be revoked like another key after genesis. ### 5.2 Devices and keys Device IDs are stable opaque identifiers. `DeviceAdd` creates a device once and sets its immutable permission ceiling. `DeviceRevoke` is causal and permanent; it also disables every key attached to that device. IDs and keys are never reused or replaced in place. `KeyAdd` declares the new key's device, active permissions, delegable permissions, and a proof-of-possession SSHSIG made by the new key. The proof uses namespace `sshsigchain.key-proof.v1` and these bytes: ```text "SSKP" || 0x01 || chain_id || device_id:string16 || key_id:32 || public_key:string16 || permissions || delegable_permissions ``` The adding signer must be allowed to add a key to its own device or any device, as applicable. Every granted and delegable permission must be within both the signer's delegation scope and the target device ceiling. `KeyRevoke` is causal and permanent. ### 5.3 Permissions Permission tags are: | Tag | Permission | | --- | --- | | `0` | `All` | | `1` | `DeviceAdd` | | `2` | `DeviceRevoke` | | `3` | `KeyAddSelf` | | `4` | `KeyAddAny` | | `5` | `KeyRevokeSelf` | | `6` | `KeyRevokeAny` | | `7` | `ManagePermissions` | | `8` | `AnchorPolicy` | | `9` | `AnchorAttest` | | `10` | `ProfileWrite(profile_id)` | | `11` | `ProfileDelegate(profile_id)` | `All` satisfies every permission and is intended for bootstrap/recovery keys. An effective key is active only when both it and its device are active. `PermissionGrant` requires `ManagePermissions`; every new permission must also be within the signer's delegable set and the device ceiling. A delegable `ProfileDelegate(x)` authorizes delegation of `ProfileWrite(x)` without granting write access by itself. Revocation requires `ManagePermissions` and takes effect before the next record is authorized. An implementation MUST authorize each record from the state after its parent, never from a final-state key set or a payload timestamp. ## 6. Selective disclosure Each extension contains: ```text profile_id commitment = BLAKE3( "sshsigchain.payload.v1\0" || profile_id:string16 || nonce:32 || payload:bytes32 ) optional transport disclosure { nonce, payload } ``` The 32-byte nonce MUST be generated unpredictably for each payload. It prevents offline dictionary attacks against small hidden values; it is not encryption. A disclosure verifies only when recomputation equals the signed commitment. Removing or adding a valid disclosure does not change signing bytes or the record hash. Authority transitions are always public. Profile payloads MUST NOT create, remove, revoke, authorize, or grant permissions to devices or keys. A verifier can therefore verify chain authority without disclosures. If any needed disclosure is absent, the affected profile state is `incomplete`; absence MUST NOT be interpreted as an empty payload, deletion, or successful full replay. ## 7. Anchor policies and head claims Anchoring makes rollback or equivocation observable under a chosen policy; it does not make a backend a consensus system. An anchor policy contains separately weighted attesters and publication backends. Both lists and the required-class list are strictly sorted and unique: ```text attester_threshold u16 attester_count u16 per attester: key_id 32 bytes weight u16 (non-zero) required u8: 0 or 1 required_class_count u16 required classes repeated string16 backend_threshold u16 backend_count u16 per backend: backend_id string16 class string16 locator string16 weight u16 (non-zero) required u8: 0 or 1 ``` Attester key IDs and backend IDs are unique, and neither threshold can exceed its corresponding total weight. Required attesters must publish valid claims; required backends must have valid receipts independently of thresholds. At least one valid receipt must come from every required backend class. Classes such as `transparency`, `nostr`, `blockchain`, and `http` are policy labels; adapter-specific receipt verification defines their evidence. An empty attester list with threshold zero permits any active `AnchorAttest` key, but at least one valid claim is still required for an anchored history. A key with `AnchorAttest` signs a `HeadClaim` under namespace `sshsigchain.anchor.v1`: ```text "SSAH" || 0x01 || chain_id:32 || head:32 || signer_key_id:32 || signer_public_key:string16 ``` Every claim signer must be active and authorized at the claimed head. Distinct signers are counted once against the attester policy. A receipt binds a backend ID, one claim hash, and backend-defined evidence. A backend is counted once if it has a valid receipt for any accepted claim. Claim identity is `BLAKE3("sshsigchain.head-claim.v1\0" || claim_signing_bytes)`. Adapters conceptually implement: ```text publish(HeadClaim) -> AnchorReceipt fetch(chain_id) -> HeadClaim[] verify_receipt(policy_backend, HeadClaim, AnchorReceipt) -> bool ``` They do not choose a canonical head. After verifying claims, chains, authority, and receipts, a client: 1. satisfies required/weighted attesters and backend evidence under the policy which governs that head; 2. rejects histories that do not contain its cached accepted head; 3. selects the furthest valid descendant; 4. rejects incomparable valid histories as a fork; and 5. persists the newly accepted head before trusting later descendants. The exact head containing `AnchorPolicySet` MUST satisfy the previous policy. The new policy applies to its descendants. This prevents an authority from weakening anchoring and treating the weakening record as already witnessed only under the new policy. Nostr replaceable/addressable events and ordinary mutable HTTP resources are discovery conveniences, not strong rollback evidence by themselves. Prefer immutable events plus the SSH-signed claim, local head caching, independent witnesses, or a transparency log with verifiable inclusion/consistency proofs. A blockchain receipt proves inclusion under the configured backend rules; it does not decide between forked SSHSIGCHAIN histories. ## 8. Verification algorithm A conforming verifier MUST: 1. reject an empty chain, over-limit inputs, noncanonical keys/lists, and malformed commitments; 2. require the exact configured chain ID, absent genesis parent, and exact parent hash thereafter; 3. require genesis to install the pinned root and initial authority state; 4. verify every outer SSHSIG using the configured namespace; 5. authorize the signer and transition from only the parent authority state; 6. verify new-key possession proofs and apply the authority transition; 7. require `ProfileWrite(profile_id)` for every extension and verify every supplied disclosure; and 8. compute the record hash for the next link. Records are never reordered by timestamps, transport arrival, identifiers, or record counts. A verifier without all trust inputs fails closed. ## 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 ``, publish the bundle at: ```text /.well-known/sshsigchain/v1//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 use the names in this document. Object order and whitespace are irrelevant. Unknown or duplicate fields are rejected. 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. 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: ```sh ssh-keygen -Y sign -f -n sshsigchain.v1 ssh-keygen -Y verify -f -I sshsigchain \ -n sshsigchain.v1 -s < ``` The one-key `allowed_signers` file is only an input to cryptographic signature verification. It never establishes authorization or root trust. ## 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 revocation is in the accepted history. There is no trusted signature time. - Device ceilings and delegation checks prevent permission amplification but cannot protect against a legitimately authorized `All` key. - Selective disclosure leaks outer metadata and payload equality only when a 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 unsupported keys or SSHSIG algorithms. ## References - [OpenSSH `PROTOCOL.sshsig`](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig) - [OpenBSD `ssh-keygen(1)`](https://man.openbsd.org/ssh-keygen.1) - [Nostr NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) - [Sigstore Rekor](https://github.com/sigstore/rekor)