# 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. JSONL transport and limits 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. ## 10. 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. ## 11. 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. - 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)