specify selective disclosure and anchor policy
Some checks failed
CI / fmt, clippy, docs (push) Failing after 5s
CI / test (ubuntu-latest) (push) Failing after 5s
CI / iroh integration smoke tests (push) Failing after 5s
CodeQL / Analyze Rust (push) Failing after 4s
Security / RustSec cargo-audit (push) Failing after 5s
CI / test (macos-latest) (push) Has been cancelled
CI / test (windows-latest) (push) Has been cancelled

This commit is contained in:
Eric Wendland 2026-07-18 22:05:57 +02:00
commit 76eb785ee2
11 changed files with 834 additions and 387 deletions

View file

@ -1,270 +1,382 @@
# SSHSIGCHAIN v1
Status: Draft 1 (pre-deployment)
Status: Draft 2 (pre-deployment)
This document specifies a deliberately small, generic append-only signature
chain for applications that use OpenSSH `sshsig` signatures. Version 1 is the
first and only defined version of SSHSIGCHAIN. It is transport
independent: a chain may be carried by a file, object store, HTTP, database, or
mesh protocol. It does not make SSH a transport.
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.
The reference implementation lives in geth's `geth-keychain` crate. Geth uses
the `geth.keychain.sshsigchain.v1` profile for identity-plane operations, but the format and
verification core do not depend on geth data types.
Version 1 is the first and only format. There is no legacy grammar or downgrade
mode.
## 1. Goals
## 1. Security goals and non-goals
SSHSIGCHAIN v1 provides:
SSHSIGCHAIN provides:
- a fixed, deterministic byte sequence for every signed record;
- an explicit, out-of-band root public key rather than trust bootstrapped from
downloaded chain content;
- one causally ordered, hash-linked history, so a later record cannot be made
earlier by changing a timestamp;
- application-defined authorization and state transitions evaluated at each
chain position;
- bounded field and chain sizes suitable for processing untrusted transport
input; and
- direct use of OpenSSH's `ssh-keygen -Y sign` / `-Y verify` SSHSIG format.
- 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 quorum, transparency, equivocation detection, secure clock
attestation, encrypted payloads, or a distributed consensus protocol. A valid
signature from an authorized key is still authority to make whatever change the
application profile permits.
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. Terminology and required inputs
## 2. Trust inputs and identifiers
An implementation has four separately configured trust inputs:
A verifier obtains these values through an authenticated channel, never from a
downloaded chain:
1. `chain_id`: exactly 32 random bytes, rendered as 64 hexadecimal characters
when displayed.
2. `profile`: an ASCII identifier naming the application payload rules.
3. `namespace`: the OpenSSH SSHSIG namespace passed to `ssh-keygen -Y`.
4. `root_public_key`: one canonical OpenSSH public-key line.
1. `chain_id`: 32 independently generated random bytes;
2. `namespace`: the 1128 byte printable, non-whitespace SSHSIG namespace
(`sshsigchain.v1` by default); and
3. `root_public_key`: canonical OpenSSH public-key text.
These values are a trust anchor. They must come from local configuration,
enrollment material, release metadata, or another authenticated out-of-band
channel. A fetched `allowed_signers` file, checkpoint, or first record MUST NOT
be used to discover or replace them.
The v1 core accepts a single root key. A threshold or witness scheme is a
separate protocol extension and must bind the same `chain_id`, profile, and
head digest; it is not implied by multiple signatures attached to a record.
## 3. Canonical public-key text
The public-key field is exactly two ASCII whitespace-separated fields:
Canonical key text is exactly:
```text
<key-type> SP <base64-encoded-OpenSSH-key-blob>
<key-type> SP <canonical-base64-key-blob>
```
Comments, leading/trailing whitespace, and extra fields are prohibited. The
base64 form is the standard canonical encoding of the decoded key blob. An
implementation MUST compare canonical text, not an operator-supplied comment,
when binding a record signer to policy state. The SSHSIG verifier MUST still
verify the actual OpenSSH key and signature; textual validation alone is not a
cryptographic verification.
Comments, leading/trailing whitespace, and additional fields are prohibited.
Keys are identified by:
## 4. Record model
```text
BLAKE3("sshsigchain.key-id.v1\0" || canonical_public_key)
```
A record has these logical fields:
Protocol identifiers are 1128 ASCII bytes containing letters, digits, `.`,
`-`, `_`, `:`, or `/`.
| Field | Type | Rule |
## 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 |
| --- | --- | --- |
| `chain_id` | 32 bytes | Must equal the configured chain ID. |
| `profile` | UTF-8 ASCII identifier | Must equal the configured profile. |
| `sequence` | unsigned 64-bit integer | Starts at zero and increases by exactly one. |
| `previous` | absent or 32 bytes | Absent only at sequence zero; otherwise the preceding record digest. |
| `payload` | opaque byte string | Profile-defined canonical payload, at most 1 MiB. |
| `signer_public_key` | canonical key text | The key presented to SSHSIG verification. |
| `signature` | byte string | An OpenSSH SSHSIG signature over the signing bytes below. |
| `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 |
The profile identifier is 1128 ASCII bytes containing only letters, digits,
`.`, `-`, and `_`. The namespace is 1128 printable ASCII bytes without
whitespace. Canonical public-key text is at most 16 KiB, signatures are at most
64 KiB, and a verifier MUST reject a chain over 100,000 records before
performing unbounded work. Implementations may set smaller limits.
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.
Timestamps are intentionally not fields in the generic ordering mechanism.
Applications may put timestamps in their payload, but MUST treat them as signed
metadata rather than a way to order, revoke, or retroactively authorize
records. Sequence and `previous` define causal order.
### 5.1 Genesis
## 5. Signing bytes
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.
The signing byte string is the following binary grammar. `u16` and `u32` are
unsigned big-endian lengths. `u64` is an unsigned big-endian integer. `bytes[n]`
contains exactly `n` bytes; no implicit terminator or alignment is present.
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
"SSCS" 4 bytes
0x01 1 byte (protocol version)
chain_id 32 bytes
sequence u64
has_previous 1 byte: 0 or 1
previous 32 bytes, only when has_previous is 1
profile_length u16
profile bytes[profile_length]
payload_length u32
payload bytes[payload_length]
signer_key_length u16
signer_public_key bytes[signer_key_length]
"SSKP" || 0x01 || chain_id || device_id:string16 || key_id:32 ||
public_key:string16 || permissions || delegable_permissions
```
The `signature` field is not in the signing bytes because SSHSIG signs those
bytes. The record digest, which binds the signature into the next link, is:
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
BLAKE3("sshsigchain.record-hash.v1\\0" || signing_bytes || u32(signature_length) || signature)
profile_id
commitment = BLAKE3(
"sshsigchain.payload.v1\0" ||
profile_id:string16 || nonce:32 || payload:bytes32
)
optional transport disclosure { nonce, payload }
```
`previous` in record `n + 1` MUST equal this digest for record `n`. A JSON or
JSONL transport envelope is allowed for convenience, but JSON bytes MUST NOT
be signed or hashed as the record representation.
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.
### JSONL transport
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.
This specification defines JSONL as a convenient interchange transport. Each
non-empty physical line contains exactly one JSON object with these fields:
`chain_id`, `profile`, `sequence`, `previous`, `payload`,
`signer_public_key`, and `signature`. `chain_id`, `previous` when present,
`payload`, and `signature` are JSON arrays of unsigned octets; `previous` is
`null` when absent. `profile` and `signer_public_key` are JSON strings, and
`sequence` is an unsigned integer. Unknown or duplicate fields MUST be
rejected. Object-member order and JSON whitespace carry no meaning.
## 7. Anchor policies and head claims
Blank lines MAY be ignored. A JSONL line is limited to 5 MiB and one complete
input to 64 MiB. Implementations reading an arbitrary stream MUST enforce those
limits before buffering an unbounded line or input. These limits are transport
limits in addition to the record limits above.
Anchoring makes rollback or equivocation observable under a chosen policy; it
does not make a backend a consensus system.
### Base test vector
This unsigned record fixes the field grammar independently of OpenSSH key
generation or signature randomness:
An anchor policy contains separately weighted attesters and publication
backends. Both lists and the required-class list are strictly sorted and unique:
```text
chain_id 32 zero bytes
profile "example.test"
sequence 0
previous absent
payload 01 02
signer_public_key "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJn8/JItLIoZOxodjYHXdd3Tv6SHzPOEUM+1BWPvCQc2"
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
```
Its signing bytes, encoded as lowercase hexadecimal, MUST be:
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
53534353010000000000000000000000000000000000000000000000000000000000000000000000000000000000000c6578616d706c652e7465737400000002010200507373682d65643235353139204141414143334e7a6143316c5a4449314e54453541414141494a6e382f4a49744c496f5a4f786f646a59485864643354763653487a504f45554d2b314257507643516332
"SSAH" || 0x01 || chain_id:32 || head:32 || signer_key_id:32 ||
signer_public_key:string16
```
## 6. Verification algorithm
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)`.
Given the configured trust input and an ordered candidate record list, a
conforming verifier MUST:
Adapters conceptually implement:
1. Reject an empty or over-limit chain.
2. For record `i`, require `sequence == i`, the configured chain ID and
profile, and the exact predecessor digest (or absent predecessor for `i=0`).
3. Require the sequence-zero record's canonical signer key to equal the
configured root public key. This is the only bootstrap rule.
4. Invoke OpenSSH SSHSIG verification using the configured namespace, the
canonical signing bytes, the record's public key, and its signature. Reject
on failure.
5. Ask the profile whether that signer is authorized by the state resulting
from records `0..i-1`. Reject on failure.
6. Apply the profile's deterministic transition. Reject on failure.
7. Hash the complete signed record and use that hash as the required
predecessor for the next record.
```text
publish(HeadClaim) -> AnchorReceipt
fetch(chain_id) -> HeadClaim[]
verify_receipt(policy_backend, HeadClaim, AnchorReceipt) -> bool
```
Records are never re-sorted by payload timestamp, identifier, signature time,
or transport arrival time. A verifier that cannot obtain its configured trust
input MUST fail closed.
They do not choose a canonical head. After verifying claims, chains, authority,
and receipts, a client:
For OpenSSH interoperability, implementations use commands equivalent to:
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 <private-key> -n <namespace> <signing-bytes-file>
ssh-keygen -Y verify -f <one-key-allowed-signers> -I <principal> \
-n <namespace> -s <signature-file> < <signing-bytes-file>
ssh-keygen -Y sign -f <private-key> -n sshsigchain.v1 <signing-bytes-file>
ssh-keygen -Y verify -f <one-key-allowed-signers> -I sshsigchain \
-n sshsigchain.v1 -s <signature-file> < <signing-bytes-file>
```
The allowed-signers file used for this individual cryptographic check contains
only the record's already-canonical signer key. Authorization remains the
profile's job; a downloaded allowed-signers projection is never a root of
trust.
The one-key `allowed_signers` file is only an input to cryptographic signature
verification. It never establishes authorization or root trust.
## 7. Application profiles
## 11. Security considerations
A profile defines its payload codec and reducer. It MUST document its profile
identifier, payload versioning, signer authorization rules, transition rules,
and any limits beyond this base specification. A profile MUST reject a payload
that decodes successfully but does not round-trip to exactly the same canonical
bytes.
### geth keychain profile
Geth's identifier is `geth.keychain.sshsigchain.v1`. Its payload is a versioned canonical
binary mirror of a keychain operation. The mirror is deliberately separate from
the human-facing, internally tagged JSON API type so that a decoder can prove a
unique payload byte sequence. Version 1 requires:
- sequence zero to be `KeychainInit`;
- sequence one to be an `AdminKeyAdd` that records the configured root signer;
- every signer to be an active admin public key in the preceding profile state;
- every keychain operation ID to occur only once in the chain;
- each `AdminKeyAdd` to carry a canonical public key whose BLAKE3 key ID
matches the declared key ID;
- `AdminKeyRevoke` to remove that key before any later record is authorized;
and
- no `valid_after_ms` or `valid_before_ms` policy fields. Key lifecycle is
causally represented by add and revoke records, not untrusted timestamps.
The configured root key seeds the profile's initial admin authorization state.
It can be revoked by a valid record; it is not a permanent bypass after genesis.
## 8. Publication and rollback
This base format proves only one supplied history. A publisher that controls a
root key can present different valid descendants to different readers. Clients
that need rollback or fork detection SHOULD persist the accepted `(chain_id,
head digest, sequence)` and only accept a later bundle after proving that it is
an extension of the stored head. A signed checkpoint alone is insufficient
unless its signer is an already pinned trust anchor and its claimed ancestry is
verified.
For stronger equivocation evidence, publish heads to independently operated
witnesses or a transparency log. This is intentionally outside SSHSIGCHAIN's
small core.
## 9. Security considerations
- SSHSIG namespaces are mandatory domain separation. Reusing a namespace for a
different protocol is unsafe.
- The root key, chain ID, profile, and namespace form one trust tuple. Changing
any member starts a different trust domain and needs an explicit operator
action.
- A compromised active admin key can still append records until a causally
later revocation is accepted. This is inherent to single-key authority, not a
claim of compromise recovery.
- SSHSIG signatures do not give a trusted signing time. Do not make expiry or
ordering decisions from payload timestamps without a separate, authenticated
time design.
- This protocol does not encrypt payloads and does not make bearer secrets or
untrusted discovery data into node identity.
- Verifiers should use OpenSSH versions that support `ssh-keygen -Y` and should
reject unsupported algorithms according to their local OpenSSH policy.
## 10. Compatibility
This specification defines one format: SSHSIGCHAIN version 1. It has no
compatibility mode, downgrade path, or alternate legacy record grammar.
Implementations MUST reject another protocol version. Geth removed its
pre-standard test-only static JSONL workflow rather than treating it as an
SSHSIGCHAIN variant.
- 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)` manual](https://man.openbsd.org/ssh-keygen.1)
- [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)