Files
glm-5.3-flash f4b5f896b4 docs: sweep em dashes from all markdown prose
Renames the written-usage rule into practice across the contract surface:
docs/ (architecture, research, plans, sdd), tasks/ (all 43 task files,
taskgraph validate passes), AGENTS.md, README.md, CHANGELOG.md, and the
.opencode/agents/ prompts. Every replacement is a sense-preserving
rewrite (colons, semicolons, commas, parentheses, sentence splits, varied),
never a mechanical swap; no spaced hyphens were substituted. Enum fields,
frontmatter, tables, wire data, and cited identifiers survive verbatim;
en dashes inside numeric ranges keep their meaning.

Verification: taskgraph validate (43 tasks), repo grep clean (single
exempt hit: the verification command's own search pattern in AGENTS.md),
cargo test, clippy --all-targets -D warnings, fmt --check, doc all green.
2026-11-04 20:28:00 +00:00

133 lines
6.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
status: reviewed
last_updated: 2026-10-28
---
# Auth: identity is the family's, not SSH's
## The model
One resolution path exists on every surface: a **verified key's
fingerprint → alkcall `IdentityProvider::resolve_from_fingerprint` →
`Identity`**. russh performs all cryptographic work (ADR-002 pin):
`auth_publickey` fires *after* signature and ownership verification,
and invalid/malformed signatures produce `USERAUTH_FAILURE` before
any handler hook runs (POC-1/POC-2 facts). alkssh never sees or
verifies a signature; its auth surface is *dispositional* (ADR-004).
## Server direction
### The bridge
1. `auth_publickey(user, VerifiedPublicKey)` fires (post-verification;
the 0.65.0 shape has the key as the `.public_key` field, and the
client's signature `.algorithm` is exposed for optional hardening).
2. alkssh computes the fingerprint via the `ssh-key` re-export
(`HashAlg::Sha256`): `SHA256:` + unpadded base64 over the
standard alphabet (`ssh-keygen -lf` byte-format; not the
url-safe alphabet). The string format is a **contract invariant**
(ADR-004): parity with assembly-registered values is proven
against independent issuers (`ssh-keygen -lf`; POC-1 §1, pinned
by the committed vectors in `tests/vectors/`).
3. `resolve_from_fingerprint` → `Some(identity)` → accept; `None` →
reject, fail-closed; the resolved-identity-`None` path never
creates a "weakly authorized" session.
4. On success the identity rides the session (`auth_succeeded`): for
mapped channels it propagates through the alkcall transport
(`set_identity`, once) so `AccessControl::check(peer_identity)`
gates every mapped open; the **visible-surface invariant** holds
in the SSH direction with no SSH-shaped exception. POC-3 §C.3
observed the propagation live (the establisher witness).
### Dispositions (ADR-004, summarized)
`publickey` is the only implemented method, and the only accepted key
algorithm is **ed25519**, so `auth_publickey` declines non-ed25519
public keys explicitly (the family's ed25519-only posture, ADR-002
decision 4; one key algorithm carries the family's identity: the
same ed25519 keys behind alktls's self-signed certs, iroh, and the
alknet rewrite). `auth_publickey_offered`
accepts-and-delegates (keeping `max_auth_attempts` honest; the hook
does not consume an attempt, POC-1 verified); `auth_none` /
`auth_password` / `auth_keyboard_interactive` / `auth_openssh_certificate`
reject explicitly (at this pin russh's defaults already reject; the
explicit implementations are the family contract, since a pin bump could
change defaults silently). `Config::methods` advertises `{publickey}`,
exactly what is implemented. The username is **not identity and
carries no semantics** (OQ-03 resolved); log exposure is deployment
policy. `max_auth_attempts` defaults `10` (the OpenSSH `none`-probe
consumes one, POC-1 §4).
## Client direction
### Key custody (ADR-005)
Held keys are bearer-token-class secrets: **vault-encrypted at rest
(alkvault `EncryptedData`, OpenSSH-PEM inner), decrypted at use,
zeroized ownership at the adapter seam**; never env vars, never
plaintext at rest, never persisted after use. The vault is a secret
store on this path, not a key generator, not a custodian, and never
involved in resolving *peers'* fingerprints. Failure modes are all
fail-closed with distinct errors: locked / absent / decryption
failure / unknown `key_version`, and rotation preserves key identity.
The same model covers **host keys** (decrypt-at-construction into the
server's `Config::keys`; locked vault → **no server is constructed**;
POC-2 §5).
### Host-key policy (OQ-05 resolution; the client-side trust contract)
Two cases, one seam; the crate is **policy-blind**:
1. **Known beforehand** (anchors exist; family-built runners mint
their host key at assembly time): assembly-supplied pins, with a
**hard decline on mismatch** at `check_server_key`.
2. **Unknown beforehand** (vanilla `sshd` on rental fleets; no anchor
until contact): the crate surfaces
(host, port, presented fingerprint) to an **assembly-provided
`HostKeyPolicy`**, which chooses prompt-and-record,
provider-metadata match, learn-and-record, or accept-and-continue
(the ephemeral-fleet shape). The OpenSSH "add to known_hosts?"
moment becomes assembly policy.
Crate rules: default policy is fail-closed (reject unknown); the
crate never decides trust and never hard-codes a trust model, so
accept-all is not a built-in crate option, but a permissive case-2
policy is the assembly's to choose where its fleet model calls for
it (alkminer's accept-all for rental fleets is that shape, made
deliberately: fresh container per rent → fresh host key, so no pin
can exist beforehand; the security anchor there is hub-held
per-instance dial keys (ADR-005's custody model) plus what rides
the tunnels, not host pins).
## What is out of scope
- Password / keyboard-interactive anywhere (server: declined; client:
non-goal until a use case).
- OpenSSH certificate auth (`auth_openssh_certificate` declined
unconditionally; OQ-04 resolved: no CA-trust story exists in the
family identity model; future support is new-ADR territory with the
upstream identity-model work as its prerequisite).
- ssh-agent integration/forwarding (external signer processes are out
of family scope; the vault is the custody story).
- TOFU *as crate behavior*, since policy lives at the assembly: the
crate is policy-blind (above).
## Open Questions
None open or deferred; see [open-questions.md](open-questions.md)
(all of OQ-01…13 resolved). Doc-local contract anchors: OQ-03 (the
username disposition, §Dispositions), OQ-04 (the certificate decline,
§out-of-scope), OQ-05 (the host-key policy seam, §Host-key policy),
OQ-06 / OQ-07 (custody boundaries, ADR-005).
## References
- ADR-004 (dispositions + parity invariant), ADR-005 (custody),
ADR-002 (the pinned russh/auth pipeline facts)
- `docs/research/auth-bridge.md` (the surface mapping + host-key
refinement), `poc-1-auth-bridge.md` (§1–4), `poc-2-vault-keys.md`
(§2–5), `reverse-runner.md` (lessons 5–6)
- alkcall ADRs 002/003/006 (identity surface) and 010 (secret-material
flow, alkhttp ADR-014's alkcall analog), alkhttp ADR-073 (one
backend behind every door surface), family ADR-002 decision 5 /
family ADR-003 decision 2