commit 45f8ce2388551924c62436eb4e69326ddc0e38b2
parent ca8ce61dba115340a2a31b8fd9c9f09a4f5e0b1c
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 26 Aug 2026 05:41:00 -0700
Document SSH key authentication
Diffstat:
4 files changed, 24 insertions(+), 111 deletions(-)
diff --git a/README.md b/README.md
@@ -7,7 +7,7 @@
- Hot, latest, personalized, and live feeds
- Full reply threads with parent context and foldable reply branches
- Search, profiles, tags, follows, blocks, and reporting
-- Email-code sign-in, posting, replies, editing, and account settings
+- Email-code or SSH Ed25519 key sign-in, posting, replies, editing, and account settings
- Rich terminal rendering for links, mentions, tags, code, and ASCII art
- Configurable Textlog-compatible instance and light, dark, or automatic themes
@@ -79,4 +79,5 @@ go test ./...
go test -race ./...
go vet ./...
go build ./cmd/textlog
+just e2e-key-server # requires ../gotextlog-srv
```
diff --git a/doc/api.md b/doc/api.md
@@ -48,6 +48,15 @@ Changing the configured instance also changes which server receives the saved
bearer token. Credentials and accounts are local to an instance; this client
does not implement federation or cross-instance identity.
+Compatible instances may instead authenticate with an SSH Ed25519 key. The
+login screen switches between email and key modes with Tab. Key authentication
+uses `POST /auth/key/challenge` with an OpenSSH public key and optional
+first-use handle, signs the returned base64-decoded message byte-for-byte, and
+sends the SSH wire-encoded signature to `POST /auth/key/verify`. Verification
+returns the same session envelope and bearer token as email verification. The
+client signs with `ssh-agent` or a matching unencrypted private-key file; it
+never sends private-key material.
+
In the endpoint tables, **optional** means the endpoint does not require a
session but receives the bearer token when one is available. **Required** means
the TUI invokes it for a signed-in operation. **None** identifies sign-in
@@ -155,6 +164,8 @@ provide hashtag follow/block actions.
| --- | --- | --- | --- |
| `POST /auth/request` | None | `{"email": string}` | Emails a sign-in code. The client accepts `data.sent`. |
| `POST /auth/verify` | None | `{"email": string, "code": string}` | Returns a session envelope containing a token, expiry, and current user. |
+| `POST /auth/key/challenge` | None | `{"public_key": string, "handle": optional string}` | Returns a key challenge, expiry, and registration state. |
+| `POST /auth/key/verify` | None | `{"challenge_id": string, "signature": base64 string}` | Verifies an SSH signature and returns the normal session envelope. |
| `DELETE /auth/session` | Required | No body | Revokes the active token; the client accepts `data.revoked`. |
| `GET /me` | Required | No parameters | Loads the current account in a data envelope. |
| `PATCH /me` | Required | `{"bio": string}` | Updates the current account bio and returns the account envelope. |
@@ -218,7 +229,8 @@ A hashtag reference contains `tag`, `post_count`, `follower_count`, `url`,
and `api_url`.
The current-user response contains `handle`, `email`, `bio`,
-`email_verified`, and `can_post`. A verified session adds `token` and
+`email_verified`, and `can_post`; key-only instances may return an empty email
+and false `email_verified`. A verified session adds `token` and
`expires_at` around that user object.
### Activity
diff --git a/doc/plan/key-auth-server.md b/doc/plan/key-auth-server.md
@@ -1,106 +0,0 @@
-# Key-authenticated Textlog server
-
-## Cross-repository contract
-
-The server lives at `../gotextlog-srv` and the client remains in this repository.
-The server implements the API consumed in `doc/api.md`, except that account
-creation and login use SSH Ed25519 keys. The existing email-code methods remain
-available in the client for other Textlog instances.
-
-Key authentication uses these endpoints under `/api/v1`:
-
-- `POST /auth/key/challenge` accepts `{"public_key": <OpenSSH authorized-key string>, "handle": <optional string>}`. It rejects algorithms other than `ssh-ed25519`. An unknown key requires a handle and creates the account only after proof succeeds. It returns `{"data":{"challenge_id":string,"message":base64,"expires_at":RFC3339,"registered":bool}}`.
-- `POST /auth/key/verify` accepts `{"challenge_id":string,"signature":base64}`, where signature is the SSH wire encoding of `ssh.Signature` over the decoded message. It consumes the challenge and returns the existing session envelope.
-- The challenge message is server-generated and domain-separated with `gotextlog-auth-v1`, the configured public origin, challenge ID, nonce, purpose, and expiry. Challenges expire after five minutes and are single-use.
-
-The TUI discovers `ssh-ed25519` public keys from `~/.ssh/*.pub`. A selected
-public key is signed through `SSH_AUTH_SOCK` when available, otherwise through
-the matching unencrypted private-key file. Encrypted private keys must be loaded
-into an agent. The login screen allows switching between existing email-code
-login and key login, selecting a discovered key, and entering a handle for
-first use.
-
-Bearer sessions remain unchanged. The server returns empty `email` and false
-`email_verified` fields so the existing current-user decoder remains compatible.
-Opaque cursors represent stable `(created_at,id)` pagination positions.
-
-## Ownership and boundaries
-
-- `gotextlog-srv/internal/store` owns schema execution, SQL, transactions,
- persistence models, cursor ordering inputs, relationship invariants, and
- activity records.
-- `gotextlog-srv/internal/server` owns HTTP routing, validation, auth
- middleware, API JSON shapes, URL derivation, cursor encoding, and SSE fanout.
-- `gotextlog-srv/internal/keyauth` owns challenge message construction and SSH
- public-key/signature verification.
-- `gotextlog/internal/textlog` owns wire types and key-auth HTTP methods.
-- `gotextlog/internal/sshkey` owns local key discovery and signing.
-- `gotextlog/internal/tui` owns login-mode state and presentation.
-
-## W1: Complete server
-
-Files: all files under `../gotextlog-srv`. Do not edit this repository.
-
-Deliverables:
-
-- Initialize a Go repository and command at `cmd/gotextlog-srv`.
-- Start with a coarse failing HTTP integration test covering registration by
- key, login, two users, posts/replies, feeds/search, profiles, follow/block,
- tags, activities, reports, session revocation, and SSE. Make it pass.
-- Implement the full endpoint inventory in `doc/api.md`, including cursor
- pagination and response fields consumed by the client.
-- Use SQLite through cgo. Vendor `github.com/mattn/go-sqlite3`, including its
- amalgamation, and make normal builds use the vendor directory.
-- Add `doc/schema.md` with tables, columns, constraints, indexes, ownership,
- and migration policy. Add `doc/api.md` for the key-auth difference.
-- Add migrations, foreign keys, WAL/busy timeout, graceful shutdown, public
- origin/listen/database flags, JSON errors, and tests.
-- Add a justfile and companion scripts under `dev/` for build, test, and an
- end-to-end smoke test.
-
-Verification: `CGO_ENABLED=1 go test -race ./...`, `just build`, and the
-server smoke script.
-
-## W2: Client key-auth transport and local SSH keys
-
-Files: `internal/textlog/client.go`, `internal/textlog/client_test.go`,
-`internal/textlog/types.go`, `internal/textlog/types_test.go`, new
-`internal/sshkey/*`, and `go.mod`/`go.sum`. Do not edit TUI, docs, command,
-or config files.
-
-Deliverables:
-
-- Start with a coarse failing test that performs challenge and verify against
- an `httptest.Server` using a temporary Ed25519 private key.
-- Add wire types and client methods for the fixed key-auth contract.
-- Discover sorted Ed25519 public keys and sign with an injected signer source;
- production supports ssh-agent and matching unencrypted private files.
-- Keep email-code APIs and types intact.
-
-Verification: `go test -race ./internal/textlog ./internal/sshkey`.
-
-## W3: TUI key-login flow
-
-Depends on W2. Files: `internal/tui/backend.go`,
-`internal/tui/backend_test.go`, `internal/tui/model.go`,
-`internal/tui/model_test.go`, and `internal/tui/view.go`. Do not edit client,
-SSH-key, config, docs, or command files.
-
-Deliverables:
-
-- Start with a coarse failing model test covering mode switch, key selection,
- first-use handle entry, successful signing/login, token save, and account
- transition.
-- Add backend operations that request a challenge, sign its exact message, and
- verify it.
-- Preserve the email-code flow and make key-login errors actionable.
-
-Verification: `go test -race ./internal/tui`.
-
-## Integration and acceptance
-
-Handled after W1-W3. Add durable client API/usage documentation, then run two
-real TUI backends against a temporary server/database using generated keys.
-The acceptance test registers two accounts, logs both in, creates a post and
-reply, follows, observes feeds/activity, searches, blocks, and revokes sessions.
-Run all tests in both repositories and remove this completed plan.
diff --git a/doc/usage.md b/doc/usage.md
@@ -52,9 +52,14 @@ in the platform user configuration directory: `$XDG_CONFIG_HOME/textlog` on
Linux, Application Support on macOS, and AppData on Windows. The configuration
file is written atomically with owner-only permissions where supported.
-Sign-in uses Textlog's email-code flow. The client persists the token, restores
-the last authenticated feed, supports bio editing and sign-out, and opens URLs
-with the platform browser command.
+Sign-in supports Textlog's email-code flow and compatible instances that use
+SSH Ed25519 keys. Press Tab on the login screen to switch modes. Key login
+discovers `~/.ssh/*.pub`, lets you select an Ed25519 key, and asks for a handle
+when registering it with an instance. Signing uses a matching key from
+`SSH_AUTH_SOCK` first, then an unencrypted matching private-key file. Load an
+encrypted private key into `ssh-agent` before signing in. The client persists
+the resulting bearer token, restores the last authenticated feed, supports bio
+editing and sign-out, and opens URLs with the platform browser command.
## API coverage
@@ -72,4 +77,5 @@ go test ./...
go test -race ./...
go vet ./...
go build ./cmd/textlog
+just e2e-key-server # requires ../gotextlog-srv
```