gotextlog

gotextlog
git clone https://git.ryansepassi.com/git/gotextlog.git
Log | Files | Refs | README

commit 45f8ce2388551924c62436eb4e69326ddc0e38b2
parent ca8ce61dba115340a2a31b8fd9c9f09a4f5e0b1c
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 26 Aug 2026 05:41:00 -0700

Document SSH key authentication

Diffstat:
MREADME.md | 3++-
Mdoc/api.md | 14+++++++++++++-
Ddoc/plan/key-auth-server.md | 106-------------------------------------------------------------------------------
Mdoc/usage.md | 12+++++++++---
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 ```