krz/gitbay

A CLI-first git forge.

clone: git clone https://gitbay.org/krz/gitbay.git

c0b0f1fca2fb9ed924070f80118015b0cdee2915

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-08-23T22:23:36Z

M1: SSH control plane and identity

- embedded SSH listener: publickey auth by fingerprint, exec dispatch,
  auto-generated ed25519 host key, scope enforcement in the dispatcher
- POSIX tokenizer for SSH exec strings; rejects unquoted metacharacters;
  round-trip and no-panic fuzz targets
- control command registry with per-command ReadsStdin; whoami, keys
  list/add/remove; JSON envelope with protocol_version
- store: users, ssh keys (epoch bump on change), emails with verified_by
- forged serve, admin user create, admin email verify
- e2e: bare OpenSSH client exercises whoami --json, scope denial,
  duplicate-key message, unknown-key rejection, spaced-arg round trip
 cmd/forged/main.go                 | 148 ++++++++++++++++++++++++-
 e2e/ssh_test.go                    | 218 +++++++++++++++++++++++++++++++++++++
 go.mod                             |   1 +
 go.sum                             |   4 +
 internal/control/control.go        | 140 ++++++++++++++++++++++++
 internal/control/control_test.go   |  56 ++++++++++
 internal/control/identity.go       | 133 ++++++++++++++++++++++
 internal/protocol/tokenize.go      |  89 +++++++++++++++
 internal/protocol/tokenize_test.go |  94 ++++++++++++++++
 internal/sshd/sshd.go              | 209 +++++++++++++++++++++++++++++++++++
 internal/store/users.go            | 185 +++++++++++++++++++++++++++++++
 11 files changed, 1273 insertions(+), 4 deletions(-)

diff --git a/cmd/forged/main.go b/cmd/forged/main.go
index 807126d..bec7d21 100644
--- a/cmd/forged/main.go
+++ b/cmd/forged/main.go
@@ -4,14 +4,33 @@ package main
 
 import (
 	"fmt"
+	"log/slog"
+	"net"
 	"os"
+	"path/filepath"
+	"strconv"
 
 	"github.com/spf13/cobra"
+	"golang.org/x/crypto/ssh"
 
 	"github.com/krazywarez/forge/internal/config"
+	"github.com/krazywarez/forge/internal/policy"
+	"github.com/krazywarez/forge/internal/sshd"
 	"github.com/krazywarez/forge/internal/store"
 )
 
+func openStore(cfg config.Config) (*store.Store, error) {
+	s, err := store.Open(filepath.Join(cfg.Server.Root, "forge.db"))
+	if err != nil {
+		return nil, err
+	}
+	if err := s.MigrateUp(); err != nil {
+		s.Close()
+		return nil, err
+	}
+	return s, nil
+}
+
 var configPath string
 
 func main() {
@@ -64,7 +83,29 @@ func serveCmd() *cobra.Command {
 		Use:   "serve",
 		Short: "run the ssh, http, and git listeners",
 		RunE: func(cmd *cobra.Command, args []string) error {
-			return fmt.Errorf("not implemented (M1)")
+			cfg, err := config.Load(configPath)
+			if err != nil {
+				return err
+			}
+			st, err := openStore(cfg)
+			if err != nil {
+				return err
+			}
+			defer st.Close()
+
+			if cfg.SSH.Mode != "embedded" {
+				return fmt.Errorf("ssh.mode = %q not implemented (M9)", cfg.SSH.Mode)
+			}
+			srv, err := sshd.New(cfg, st)
+			if err != nil {
+				return err
+			}
+			ln, err := net.Listen("tcp", net.JoinHostPort("", strconv.Itoa(cfg.SSH.Port)))
+			if err != nil {
+				return err
+			}
+			slog.Info("ssh listening", "addr", ln.Addr())
+			return srv.Serve(ln)
 		},
 	}
 }
@@ -109,17 +150,116 @@ func adminCmd() *cobra.Command {
 			Use:   use,
 			Short: short,
 			RunE: func(cmd *cobra.Command, args []string) error {
-				return fmt.Errorf("not implemented (M1)")
+				return fmt.Errorf("not implemented")
 			},
 		}
 	}
+	userCmd := &cobra.Command{Use: "user", Short: "manage users"}
+	userCmd.AddCommand(adminUserCreateCmd())
+	emailCmd := &cobra.Command{Use: "email", Short: "manage user emails"}
+	emailCmd.AddCommand(adminEmailVerifyCmd())
 	admin.AddCommand(
-		notImplemented("user", "create and manage users"),
+		userCmd,
+		emailCmd,
 		notImplemented("invite", "issue registration invites"),
-		notImplemented("email", "verify user emails"),
 		notImplemented("backup", "consistent backup: repos first, then database"),
 		notImplemented("gc", "run git gc across repositories"),
 		notImplemented("stats", "instance statistics"),
 	)
 	return admin
 }
+
+func adminUserCreateCmd() *cobra.Command {
+	var keyPath, email string
+	var verified, isAdmin bool
+	cmd := &cobra.Command{
+		Use:   "create <username>",
+		Short: "create a user (host-local bootstrap; the only path in closed mode)",
+		Args:  cobra.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			username := args[0]
+			if err := policy.ValidateOwnerName(username); err != nil {
+				return err
+			}
+			cfg, err := config.Load(configPath)
+			if err != nil {
+				return err
+			}
+			st, err := openStore(cfg)
+			if err != nil {
+				return err
+			}
+			defer st.Close()
+
+			uid, err := st.CreateUser(username, isAdmin)
+			if err != nil {
+				return err
+			}
+			if email != "" {
+				verifiedBy := ""
+				if verified {
+					verifiedBy = "admin"
+				}
+				if err := st.AddEmail(uid, email, verifiedBy, true); err != nil {
+					return err
+				}
+			}
+			if keyPath != "" {
+				raw, err := os.ReadFile(keyPath)
+				if err != nil {
+					return err
+				}
+				pub, _, _, _, err := ssh.ParseAuthorizedKey(raw)
+				if err != nil {
+					return fmt.Errorf("%s: not a public key in authorized_keys format: %w", keyPath, err)
+				}
+				fp := ssh.FingerprintSHA256(pub)
+				if err := st.AddSSHKey(uid, fp, pub.Type(), pub.Marshal(), "full"); err != nil {
+					return err
+				}
+				fmt.Println("key", fp)
+			}
+			fmt.Println("created user", username)
+			return nil
+		},
+	}
+	cmd.Flags().StringVar(&keyPath, "key", "", "path to an SSH public key to register")
+	cmd.Flags().StringVar(&email, "email", "", "primary email address")
+	cmd.Flags().BoolVar(&verified, "verified", false, "mark the email verified (admin assertion)")
+	cmd.Flags().BoolVar(&isAdmin, "admin", false, "grant instance admin")
+	return cmd
+}
+
+func adminEmailVerifyCmd() *cobra.Command {
+	return &cobra.Command{
+		Use:   "verify <username> <address>",
+		Short: "mark an email verified by admin assertion",
+		Args:  cobra.ExactArgs(2),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			cfg, err := config.Load(configPath)
+			if err != nil {
+				return err
+			}
+			st, err := openStore(cfg)
+			if err != nil {
+				return err
+			}
+			defer st.Close()
+			u, err := st.UserByUsername(args[0])
+			if err != nil {
+				return fmt.Errorf("user %s: %w", args[0], err)
+			}
+			res, err := st.DB.Exec(
+				`UPDATE emails SET verified_at = strftime('%Y-%m-%dT%H:%M:%fZ','now'), verified_by = 'admin'
+				 WHERE user_id = ? AND address = ?`, u.ID, args[1])
+			if err != nil {
+				return err
+			}
+			if n, _ := res.RowsAffected(); n == 0 {
+				return fmt.Errorf("no address %s on user %s", args[1], args[0])
+			}
+			fmt.Println("verified", args[1])
+			return nil
+		},
+	}
+}
diff --git a/e2e/ssh_test.go b/e2e/ssh_test.go
new file mode 100644
index 0000000..44592e7
--- /dev/null
+++ b/e2e/ssh_test.go
@@ -0,0 +1,218 @@
+// Package e2e drives a real forged with the real ssh and git clients.
+package e2e
+
+import (
+	"encoding/json"
+	"fmt"
+	"net"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"testing"
+	"time"
+)
+
+type instance struct {
+	forged  string // path to built binary
+	root    string
+	config  string
+	port    int
+	proc    *exec.Cmd
+	sshDir  string // per-user client keys live here
+}
+
+func buildForged(t *testing.T) string {
+	t.Helper()
+	bin := filepath.Join(t.TempDir(), "forged")
+	cmd := exec.Command("go", "build", "-o", bin, "github.com/krazywarez/forge/cmd/forged")
+	cmd.Dir = ".."
+	if out, err := cmd.CombinedOutput(); err != nil {
+		t.Fatalf("build forged: %v\n%s", err, out)
+	}
+	return bin
+}
+
+func freePort(t *testing.T) int {
+	t.Helper()
+	ln, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer ln.Close()
+	return ln.Addr().(*net.TCPAddr).Port
+}
+
+func startInstance(t *testing.T) *instance {
+	t.Helper()
+	inst := &instance{
+		forged: buildForged(t),
+		root:   t.TempDir(),
+		port:   freePort(t),
+		sshDir: t.TempDir(),
+	}
+	inst.config = filepath.Join(inst.root, "config.toml")
+	cfg := fmt.Sprintf(`
+[server]
+root = %q
+site_url = "https://forge.test"
+[ssh]
+port = %d
+`, inst.root, inst.port)
+	if err := os.WriteFile(inst.config, []byte(cfg), 0o600); err != nil {
+		t.Fatal(err)
+	}
+
+	inst.proc = exec.Command(inst.forged, "--config", inst.config, "serve")
+	inst.proc.Stderr = os.Stderr
+	if err := inst.proc.Start(); err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		inst.proc.Process.Kill()
+		inst.proc.Wait()
+	})
+
+	// Wait for the listener.
+	deadline := time.Now().Add(10 * time.Second)
+	for {
+		conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", inst.port), 200*time.Millisecond)
+		if err == nil {
+			conn.Close()
+			return inst
+		}
+		if time.Now().After(deadline) {
+			t.Fatal("forged did not start listening")
+		}
+		time.Sleep(50 * time.Millisecond)
+	}
+}
+
+// admin runs a forged admin command against the instance's database.
+func (i *instance) admin(t *testing.T, args ...string) string {
+	t.Helper()
+	cmd := exec.Command(i.forged, append([]string{"--config", i.config}, args...)...)
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		t.Fatalf("forged %v: %v\n%s", args, err, out)
+	}
+	return string(out)
+}
+
+// newKey generates a client keypair and returns the private key path.
+func (i *instance) newKey(t *testing.T, name string) string {
+	t.Helper()
+	priv := filepath.Join(i.sshDir, name)
+	cmd := exec.Command("ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", name, "-f", priv)
+	if out, err := cmd.CombinedOutput(); err != nil {
+		t.Fatalf("ssh-keygen: %v\n%s", err, out)
+	}
+	return priv
+}
+
+// ssh runs the real OpenSSH client against the instance with the given key.
+func (i *instance) ssh(t *testing.T, key string, stdin string, args ...string) (string, string, int) {
+	t.Helper()
+	base := []string{
+		"-p", fmt.Sprint(i.port),
+		"-i", key,
+		"-o", "IdentitiesOnly=yes",
+		"-o", "StrictHostKeyChecking=no",
+		"-o", "UserKnownHostsFile=" + filepath.Join(i.sshDir, "known_hosts"),
+		"-o", "BatchMode=yes",
+		"git@127.0.0.1",
+	}
+	cmd := exec.Command("ssh", append(base, args...)...)
+	if stdin != "" {
+		cmd.Stdin = strings.NewReader(stdin)
+	}
+	var out, errOut strings.Builder
+	cmd.Stdout = &out
+	cmd.Stderr = &errOut
+	err := cmd.Run()
+	code := 0
+	if ee, ok := err.(*exec.ExitError); ok {
+		code = ee.ExitCode()
+	} else if err != nil {
+		t.Fatalf("ssh: %v", err)
+	}
+	return out.String(), errOut.String(), code
+}
+
+func TestControlPlaneOverBareSSH(t *testing.T) {
+	inst := startInstance(t)
+
+	aliceKey := inst.newKey(t, "alice")
+	inst.admin(t, "admin", "user", "create", "alice",
+		"--key", aliceKey+".pub", "--email", "alice@example.test", "--verified")
+
+	// whoami --json from bare OpenSSH.
+	out, errOut, code := inst.ssh(t, aliceKey, "", "whoami", "--json")
+	if code != 0 {
+		t.Fatalf("whoami exit %d, stderr: %s", code, errOut)
+	}
+	var env struct {
+		ProtocolVersion int `json:"protocol_version"`
+		Data            struct {
+			Username string `json:"username"`
+			KeyScope string `json:"key_scope"`
+		} `json:"data"`
+	}
+	if err := json.Unmarshal([]byte(out), &env); err != nil {
+		t.Fatalf("whoami output not JSON: %v\n%s", err, out)
+	}
+	if env.Data.Username != "alice" || env.ProtocolVersion != 1 || env.Data.KeyScope != "full" {
+		t.Fatalf("whoami = %+v", env)
+	}
+
+	// Unknown key is refused at auth.
+	strangerKey := inst.newKey(t, "stranger")
+	_, _, code = inst.ssh(t, strangerKey, "", "whoami")
+	if code == 0 {
+		t.Fatal("unknown key was authenticated")
+	}
+
+	// keys add over stdin, then list shows both.
+	secondKey := inst.newKey(t, "alice2")
+	pub, _ := os.ReadFile(secondKey + ".pub")
+	out, errOut, code = inst.ssh(t, aliceKey, string(pub), "keys", "add", "--scope", "git")
+	if code != 0 {
+		t.Fatalf("keys add exit %d, stderr: %s", code, errOut)
+	}
+	out, _, code = inst.ssh(t, aliceKey, "", "keys", "list")
+	if code != 0 || len(strings.Split(strings.TrimSpace(out), "\n")) != 2 {
+		t.Fatalf("keys list exit %d:\n%s", code, out)
+	}
+
+	// The git-scoped key authenticates but is denied control commands.
+	out, errOut, code = inst.ssh(t, secondKey, "", "whoami")
+	if code != 4 {
+		t.Fatalf("git-scoped whoami: exit %d (want 4), stdout %q stderr %q", code, out, errOut)
+	}
+	if !strings.Contains(errOut, "does not allow control commands") {
+		t.Fatalf("scope denial message missing: %q", errOut)
+	}
+
+	// Duplicate key registration: bob cannot claim alice's key, and the
+	// message is the exact spec text, naming no account.
+	bobKey := inst.newKey(t, "bob")
+	inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
+	alicePub, _ := os.ReadFile(aliceKey + ".pub")
+	_, errOut, code = inst.ssh(t, bobKey, string(alicePub), "keys", "add")
+	if code != 2 {
+		t.Fatalf("duplicate key add: exit %d, want 2", code)
+	}
+	want := "that key is already registered to another account; remove it there first or use a different key"
+	if !strings.Contains(errOut, want) {
+		t.Fatalf("duplicate key message = %q, want %q", errOut, want)
+	}
+	if strings.Contains(errOut, "alice") {
+		t.Fatalf("duplicate key message leaks account name: %q", errOut)
+	}
+
+	// Arguments with spaces survive the tokenizer round trip.
+	_, errOut, code = inst.ssh(t, aliceKey, "", "keys", "remove", "'no such fingerprint'")
+	if code != 3 {
+		t.Fatalf("keys remove with spaced arg: exit %d (want 3), stderr %q", code, errOut)
+	}
+}
diff --git a/go.mod b/go.mod
index 0b6db10..06b4318 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.27.0
 require (
 	github.com/BurntSushi/toml v1.6.0
 	github.com/spf13/cobra v1.10.2
+	golang.org/x/crypto v0.55.0
 	modernc.org/sqlite v1.57.0
 )
 
diff --git a/go.sum b/go.sum
index 2e44787..1ee1ac1 100644
--- a/go.sum
+++ b/go.sum
@@ -23,12 +23,16 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT
 github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
 github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
+golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
 golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
 golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
 golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
 golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
 golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
 golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
+golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
 golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
 golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/internal/control/control.go b/internal/control/control.go
new file mode 100644
index 0000000..46806d3
--- /dev/null
+++ b/internal/control/control.go
@@ -0,0 +1,140 @@
+// Package control implements the forge control commands executed over SSH.
+// Every command here is reachable from bare OpenSSH: argv in, JSON or plain
+// text on stdout, diagnostics on stderr, exit code out.
+package control
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"slices"
+
+	"github.com/krazywarez/forge/internal/config"
+	"github.com/krazywarez/forge/internal/protocol"
+	"github.com/krazywarez/forge/internal/store"
+)
+
+type Ctx struct {
+	User   store.User
+	Scope  string // scope of the key that authenticated this session
+	Store  *store.Store
+	Cfg    config.Config
+	Stdin  io.Reader
+	Stdout io.Writer
+	Stderr io.Writer
+	JSON   bool
+}
+
+type Command struct {
+	Path       []string // e.g. ["keys", "add"]
+	Summary    string
+	ReadsStdin bool
+	Run        func(c *Ctx, args []string) int
+}
+
+var registry []Command
+
+func register(cmd Command) { registry = append(registry, cmd) }
+
+// Commands returns the registry, for the bare-ssh reachability test.
+func Commands() []Command { return registry }
+
+// Lookup resolves argv to a command by longest path match, returning the
+// command and the remaining arguments.
+func Lookup(argv []string) (Command, []string, bool) {
+	best := -1
+	var found Command
+	for _, cmd := range registry {
+		if len(cmd.Path) <= len(argv) && slices.Equal(cmd.Path, argv[:len(cmd.Path)]) && len(cmd.Path) > best {
+			best = len(cmd.Path)
+			found = cmd
+		}
+	}
+	if best < 0 {
+		return Command{}, nil, false
+	}
+	return found, argv[best:], true
+}
+
+// Dispatch runs argv for an authenticated session. The dispatcher — not the
+// handlers — enforces key scope: control commands require a full-scope key.
+func Dispatch(c *Ctx, argv []string) int {
+	if len(argv) == 0 {
+		return c.fail(protocol.ExitUsage, "no command given; try: ssh <host> help")
+	}
+	cmd, rest, ok := Lookup(argv)
+	if !ok {
+		return c.fail(protocol.ExitUsage, "unknown command %q", argv[0])
+	}
+	if c.Scope != "full" {
+		return c.fail(protocol.ExitDenied, "this key's scope (%s) does not allow control commands", c.Scope)
+	}
+	// Strip the global --json flag wherever it appears.
+	args := rest[:0:0]
+	for _, a := range rest {
+		if a == "--json" {
+			c.JSON = true
+			continue
+		}
+		args = append(args, a)
+	}
+	if !cmd.ReadsStdin {
+		c.Stdin = emptyReader{}
+	}
+	return cmd.Run(c, args)
+}
+
+type emptyReader struct{}
+
+func (emptyReader) Read([]byte) (int, error) { return 0, io.EOF }
+
+// emit writes data as the command result: a JSON envelope under --json,
+// otherwise via the plain formatter.
+func (c *Ctx) emit(data any, plain func(w io.Writer)) int {
+	if c.JSON {
+		enc := json.NewEncoder(c.Stdout)
+		enc.SetEscapeHTML(false)
+		if err := enc.Encode(protocol.Envelope{ProtocolVersion: protocol.Version, Data: data}); err != nil {
+			return protocol.ExitFailure
+		}
+		return protocol.ExitOK
+	}
+	plain(c.Stdout)
+	return protocol.ExitOK
+}
+
+func (c *Ctx) fail(code int, format string, args ...any) int {
+	msg := fmt.Sprintf(format, args...)
+	if c.JSON {
+		enc := json.NewEncoder(c.Stdout)
+		enc.SetEscapeHTML(false)
+		enc.Encode(protocol.Envelope{ProtocolVersion: protocol.Version, Error: msg})
+	} else {
+		fmt.Fprintln(c.Stderr, msg)
+	}
+	return code
+}
+
+func init() {
+	register(Command{
+		Path:    []string{"help"},
+		Summary: "list available commands",
+		Run: func(c *Ctx, args []string) int {
+			for _, cmd := range registry {
+				fmt.Fprintf(c.Stdout, "%-24s %s\n", joinPath(cmd.Path), cmd.Summary)
+			}
+			return protocol.ExitOK
+		},
+	})
+}
+
+func joinPath(p []string) string {
+	out := ""
+	for i, s := range p {
+		if i > 0 {
+			out += " "
+		}
+		out += s
+	}
+	return out
+}
diff --git a/internal/control/control_test.go b/internal/control/control_test.go
new file mode 100644
index 0000000..37ddc30
--- /dev/null
+++ b/internal/control/control_test.go
@@ -0,0 +1,56 @@
+package control
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/krazywarez/forge/internal/protocol"
+)
+
+// TestEveryCommandReachableFromBareSSH asserts that each registered command's
+// path, rendered exactly as a user would type it after `ssh <host>`, resolves
+// back to that command through the tokenizer and Lookup. This is the guard
+// that keeps the forge CLI optional.
+func TestEveryCommandReachableFromBareSSH(t *testing.T) {
+	cmds := Commands()
+	if len(cmds) == 0 {
+		t.Fatal("no commands registered")
+	}
+	for _, cmd := range cmds {
+		line := strings.Join(cmd.Path, " ")
+		argv, err := protocol.Tokenize(line)
+		if err != nil {
+			t.Errorf("command %q not tokenizable: %v", line, err)
+			continue
+		}
+		got, rest, ok := Lookup(argv)
+		if !ok {
+			t.Errorf("command %q not found by Lookup", line)
+			continue
+		}
+		if strings.Join(got.Path, " ") != line || len(rest) != 0 {
+			t.Errorf("Lookup(%q) resolved to %q with rest %v", line, strings.Join(got.Path, " "), rest)
+		}
+		if cmd.Run == nil {
+			t.Errorf("command %q has no Run", line)
+		}
+		if cmd.Summary == "" {
+			t.Errorf("command %q has no summary", line)
+		}
+	}
+}
+
+func TestLookupLongestMatch(t *testing.T) {
+	// "keys list" must not resolve to a hypothetical shorter prefix and
+	// unknown commands must not match.
+	if _, _, ok := Lookup([]string{"keys"}); ok {
+		t.Error("bare \"keys\" resolved; group prefixes must not be runnable")
+	}
+	if _, _, ok := Lookup([]string{"nope"}); ok {
+		t.Error("unknown command resolved")
+	}
+	cmd, rest, ok := Lookup([]string{"keys", "list", "--json"})
+	if !ok || strings.Join(cmd.Path, " ") != "keys list" || len(rest) != 1 {
+		t.Errorf("Lookup keys list --json = %v %v %v", cmd.Path, rest, ok)
+	}
+}
diff --git a/internal/control/identity.go b/internal/control/identity.go
new file mode 100644
index 0000000..e544714
--- /dev/null
+++ b/internal/control/identity.go
@@ -0,0 +1,133 @@
+package control
+
+import (
+	"errors"
+	"fmt"
+	"io"
+
+	"golang.org/x/crypto/ssh"
+
+	"github.com/krazywarez/forge/internal/protocol"
+	"github.com/krazywarez/forge/internal/store"
+)
+
+func init() {
+	register(Command{
+		Path:    []string{"whoami"},
+		Summary: "show the authenticated account",
+		Run:     runWhoami,
+	})
+	register(Command{
+		Path:    []string{"keys", "list"},
+		Summary: "list registered SSH keys",
+		Run:     runKeysList,
+	})
+	register(Command{
+		Path:       []string{"keys", "add"},
+		Summary:    "register an SSH public key (authorized_keys format on stdin) [--scope full|git]",
+		ReadsStdin: true,
+		Run:        runKeysAdd,
+	})
+	register(Command{
+		Path:    []string{"keys", "remove"},
+		Summary: "remove an SSH key by fingerprint",
+		Run:     runKeysRemove,
+	})
+}
+
+func runWhoami(c *Ctx, args []string) int {
+	if len(args) != 0 {
+		return c.fail(protocol.ExitUsage, "usage: whoami [--json]")
+	}
+	type out struct {
+		Username string `json:"username"`
+		Admin    bool   `json:"admin"`
+		KeyScope string `json:"key_scope"`
+	}
+	d := out{Username: c.User.Username, Admin: c.User.IsAdmin, KeyScope: c.Scope}
+	return c.emit(d, func(w io.Writer) {
+		fmt.Fprintln(w, d.Username)
+	})
+}
+
+func runKeysList(c *Ctx, args []string) int {
+	if len(args) != 0 {
+		return c.fail(protocol.ExitUsage, "usage: keys list [--json]")
+	}
+	keys, err := c.Store.ListSSHKeys(c.User.ID)
+	if err != nil {
+		return c.fail(protocol.ExitFailure, "listing keys: %v", err)
+	}
+	type out struct {
+		Fingerprint string `json:"fingerprint"`
+		Algo        string `json:"algo"`
+		Scope       string `json:"scope"`
+	}
+	var ds []out
+	for _, k := range keys {
+		ds = append(ds, out{k.Fingerprint, k.Algo, k.Scope})
+	}
+	return c.emit(ds, func(w io.Writer) {
+		for _, d := range ds {
+			fmt.Fprintf(w, "%s\t%s\t%s\n", d.Fingerprint, d.Algo, d.Scope)
+		}
+	})
+}
+
+func runKeysAdd(c *Ctx, args []string) int {
+	scope := "full"
+	for i := 0; i < len(args); i++ {
+		switch args[i] {
+		case "--scope":
+			if i+1 >= len(args) {
+				return c.fail(protocol.ExitUsage, "--scope requires a value")
+			}
+			scope = args[i+1]
+			i++
+		default:
+			return c.fail(protocol.ExitUsage, "usage: keys add [--scope full|git] < key.pub")
+		}
+	}
+	if scope != "full" && scope != "git" {
+		// deploy:* scopes are granted via repo settings, not self-service.
+		return c.fail(protocol.ExitUsage, "scope must be full or git")
+	}
+	raw, err := io.ReadAll(io.LimitReader(c.Stdin, 64<<10))
+	if err != nil {
+		return c.fail(protocol.ExitFailure, "reading key: %v", err)
+	}
+	pub, _, _, _, err := ssh.ParseAuthorizedKey(raw)
+	if err != nil {
+		return c.fail(protocol.ExitUsage, "not a valid public key in authorized_keys format: %v", err)
+	}
+	fp := ssh.FingerprintSHA256(pub)
+	if err := c.Store.AddSSHKey(c.User.ID, fp, pub.Type(), pub.Marshal(), scope); err != nil {
+		if errors.Is(err, store.ErrDuplicateKey) {
+			return c.fail(protocol.ExitUsage, "%v", err)
+		}
+		return c.fail(protocol.ExitFailure, "adding key: %v", err)
+	}
+	type out struct {
+		Fingerprint string `json:"fingerprint"`
+		Scope       string `json:"scope"`
+	}
+	d := out{fp, scope}
+	return c.emit(d, func(w io.Writer) {
+		fmt.Fprintf(w, "added %s (%s)\n", d.Fingerprint, d.Scope)
+	})
+}
+
+func runKeysRemove(c *Ctx, args []string) int {
+	if len(args) != 1 {
+		return c.fail(protocol.ExitUsage, "usage: keys remove <fingerprint>")
+	}
+	if err := c.Store.RemoveSSHKey(c.User.ID, args[0]); err != nil {
+		if errors.Is(err, store.ErrNotFound) {
+			return c.fail(protocol.ExitNotFound, "no key with fingerprint %s on your account", args[0])
+		}
+		return c.fail(protocol.ExitFailure, "removing key: %v", err)
+	}
+	return c.emit(map[string]string{"removed": args[0]}, func(w io.Writer) {
+		fmt.Fprintf(w, "removed %s\n", args[0])
+	})
+}
diff --git a/internal/protocol/tokenize.go b/internal/protocol/tokenize.go
new file mode 100644
index 0000000..a5d15e6
--- /dev/null
+++ b/internal/protocol/tokenize.go
@@ -0,0 +1,89 @@
+package protocol
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+)
+
+// Tokenize splits an SSH exec command string into argv using POSIX
+// shell-word rules: whitespace separates words; single quotes preserve
+// everything literally; double quotes preserve everything except backslash
+// before \, ", or $; a bare backslash escapes the next character. There is
+// no expansion of any kind — no globbing, variables, or substitution. The
+// client's shell has already applied one layer of quoting before the string
+// reaches us.
+func Tokenize(s string) ([]string, error) {
+	var argv []string
+	var cur strings.Builder
+	inWord := false
+
+	i := 0
+	for i < len(s) {
+		c := s[i]
+		switch {
+		case c == ' ' || c == '\t' || c == '\n':
+			if inWord {
+				argv = append(argv, cur.String())
+				cur.Reset()
+				inWord = false
+			}
+			i++
+		case c == '\'':
+			inWord = true
+			end := strings.IndexByte(s[i+1:], '\'')
+			if end < 0 {
+				return nil, errors.New("unterminated single quote")
+			}
+			cur.WriteString(s[i+1 : i+1+end])
+			i += end + 2
+		case c == '"':
+			inWord = true
+			i++
+			closed := false
+			for i < len(s) {
+				c = s[i]
+				if c == '"' {
+					closed = true
+					i++
+					break
+				}
+				if c == '\\' && i+1 < len(s) {
+					switch s[i+1] {
+					case '\\', '"', '$', '`':
+						cur.WriteByte(s[i+1])
+						i += 2
+						continue
+					}
+				}
+				cur.WriteByte(c)
+				i++
+			}
+			if !closed {
+				return nil, errors.New("unterminated double quote")
+			}
+		case c == '\\':
+			if i+1 >= len(s) {
+				return nil, errors.New("trailing backslash")
+			}
+			inWord = true
+			cur.WriteByte(s[i+1])
+			i += 2
+		case c == '$' || c == '`' || c == ';' || c == '&' || c == '|' ||
+			c == '<' || c == '>' || c == '(' || c == ')' || c == '*' ||
+			c == '?' || c == '[' || c == '#' || c == '~':
+			// Unquoted shell metacharacters are rejected outright rather
+			// than passed through: there is no shell here, and silently
+			// treating them as literals would mask client quoting bugs.
+			return nil, fmt.Errorf("unquoted shell metacharacter %q", c)
+		default:
+			inWord = true
+			cur.WriteByte(c)
+			i++
+		}
+	}
+	if inWord {
+		argv = append(argv, cur.String())
+	}
+	return argv, nil
+}
diff --git a/internal/protocol/tokenize_test.go b/internal/protocol/tokenize_test.go
new file mode 100644
index 0000000..1f411e9
--- /dev/null
+++ b/internal/protocol/tokenize_test.go
@@ -0,0 +1,94 @@
+package protocol
+
+import (
+	"reflect"
+	"strings"
+	"testing"
+)
+
+func TestTokenize(t *testing.T) {
+	cases := []struct {
+		in   string
+		want []string
+	}{
+		{`whoami --json`, []string{"whoami", "--json"}},
+		{`repo create krz/newthing --private`, []string{"repo", "create", "krz/newthing", "--private"}},
+		{`git-upload-pack '/krz/hutch.git'`, []string{"git-upload-pack", "/krz/hutch.git"}},
+		{`issue create --title 'a b c'`, []string{"issue", "create", "--title", "a b c"}},
+		{`issue create --title "a \"b\" c"`, []string{"issue", "create", "--title", `a "b" c`}},
+		{`a\ b`, []string{"a b"}},
+		{`'it''s'`, []string{"its"}},
+		{`"don't"`, []string{"don't"}},
+		{"  spaced \t out  ", []string{"spaced", "out"}},
+		{`""`, []string{""}},
+		{``, nil},
+		{`--message "line1\nliteral"`, []string{"--message", `line1\nliteral`}},
+	}
+	for _, tc := range cases {
+		got, err := Tokenize(tc.in)
+		if err != nil {
+			t.Errorf("Tokenize(%q) error: %v", tc.in, err)
+			continue
+		}
+		if !reflect.DeepEqual(got, tc.want) {
+			t.Errorf("Tokenize(%q) = %#v, want %#v", tc.in, got, tc.want)
+		}
+	}
+}
+
+func TestTokenizeRejects(t *testing.T) {
+	bad := []string{
+		`echo $(rm -rf /)`,
+		"`id`",
+		`a; b`,
+		`a | b`,
+		`a > f`,
+		`a & b`,
+		`'unterminated`,
+		`"unterminated`,
+		`trailing\`,
+		`glob *`,
+		`~root`,
+	}
+	for _, in := range bad {
+		if got, err := Tokenize(in); err == nil {
+			t.Errorf("Tokenize(%q) = %#v, want error", in, got)
+		}
+	}
+}
+
+// shellQuote quotes one word the way a POSIX client shell would.
+func shellQuote(w string) string {
+	return "'" + strings.ReplaceAll(w, "'", `'\''`) + "'"
+}
+
+// FuzzTokenizeRoundTrip checks that any argv, single-quoted as a client
+// shell would emit it, tokenizes back to the identical argv.
+func FuzzTokenizeRoundTrip(f *testing.F) {
+	f.Add("whoami", "--json", "")
+	f.Add("issue create", "--title", "a 'quoted' \"title\" with $pecial\\chars")
+	f.Add("répo", "\t", "\n\n")
+	f.Fuzz(func(t *testing.T, a, b, c string) {
+		want := []string{a, b, c}
+		quoted := make([]string, len(want))
+		for i, w := range want {
+			quoted[i] = shellQuote(w)
+		}
+		got, err := Tokenize(strings.Join(quoted, " "))
+		if err != nil {
+			t.Fatalf("Tokenize error on %q: %v", strings.Join(quoted, " "), err)
+		}
+		if !reflect.DeepEqual(got, want) {
+			t.Fatalf("round trip: got %#v, want %#v", got, want)
+		}
+	})
+}
+
+// FuzzTokenizeNoPanic feeds arbitrary bytes; Tokenize must return, never panic.
+func FuzzTokenizeNoPanic(f *testing.F) {
+	f.Add(`repo create 'x`)
+	f.Add(`\\\'\"`)
+	f.Fuzz(func(t *testing.T, s string) {
+		_, _ = Tokenize(s)
+	})
+}
diff --git a/internal/sshd/sshd.go b/internal/sshd/sshd.go
new file mode 100644
index 0000000..32fbbb4
--- /dev/null
+++ b/internal/sshd/sshd.go
@@ -0,0 +1,209 @@
+// Package sshd implements the embedded SSH listener: public-key auth against
+// registered keys, then dispatch to git transport or control commands.
+package sshd
+
+import (
+	"crypto/ed25519"
+	"crypto/rand"
+	"encoding/pem"
+	"errors"
+	"fmt"
+	"log/slog"
+	"net"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+
+	"golang.org/x/crypto/ssh"
+
+	"github.com/krazywarez/forge/internal/config"
+	"github.com/krazywarez/forge/internal/control"
+	"github.com/krazywarez/forge/internal/protocol"
+	"github.com/krazywarez/forge/internal/store"
+)
+
+type Server struct {
+	cfg   config.Config
+	st    *store.Store
+	sshCfg *ssh.ServerConfig
+}
+
+func New(cfg config.Config, st *store.Store) (*Server, error) {
+	s := &Server{cfg: cfg, st: st}
+
+	sc := &ssh.ServerConfig{
+		PublicKeyCallback: s.authenticate,
+		ServerVersion:     "SSH-2.0-forged",
+	}
+	signers, err := loadHostKeys(cfg)
+	if err != nil {
+		return nil, err
+	}
+	for _, sg := range signers {
+		sc.AddHostKey(sg)
+	}
+	s.sshCfg = sc
+	return s, nil
+}
+
+// loadHostKeys loads the configured host keys, or generates an ed25519 key
+// under server.root/ssh/ when none are configured.
+func loadHostKeys(cfg config.Config) ([]ssh.Signer, error) {
+	paths := cfg.SSH.HostKeys
+	if len(paths) == 0 {
+		p := filepath.Join(cfg.Server.Root, "ssh", "host_ed25519")
+		if _, err := os.Stat(p); errors.Is(err, os.ErrNotExist) {
+			if err := generateHostKey(p); err != nil {
+				return nil, fmt.Errorf("generating host key: %w", err)
+			}
+			slog.Info("generated ssh host key", "path", p)
+		}
+		paths = []string{p}
+	}
+	var signers []ssh.Signer
+	for _, p := range paths {
+		raw, err := os.ReadFile(p)
+		if err != nil {
+			return nil, fmt.Errorf("host key %s: %w", p, err)
+		}
+		sg, err := ssh.ParsePrivateKey(raw)
+		if err != nil {
+			return nil, fmt.Errorf("host key %s: %w", p, err)
+		}
+		signers = append(signers, sg)
+	}
+	return signers, nil
+}
+
+func generateHostKey(path string) error {
+	if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
+		return err
+	}
+	_, priv, err := ed25519.GenerateKey(rand.Reader)
+	if err != nil {
+		return err
+	}
+	block, err := ssh.MarshalPrivateKey(priv, "")
+	if err != nil {
+		return err
+	}
+	return os.WriteFile(path, pem.EncodeToMemory(block), 0o600)
+}
+
+// authenticate resolves the presented key to a registered account. The SSH
+// username is ignored; identity comes from the key alone.
+func (s *Server) authenticate(_ ssh.ConnMetadata, pub ssh.PublicKey) (*ssh.Permissions, error) {
+	fp := ssh.FingerprintSHA256(pub)
+	key, err := s.st.SSHKeyByFingerprint(fp)
+	if err != nil {
+		return nil, fmt.Errorf("unknown key %s", fp)
+	}
+	return &ssh.Permissions{Extensions: map[string]string{
+		"user-id": strconv.FormatInt(key.UserID, 10),
+		"key-id":  strconv.FormatInt(key.ID, 10),
+		"scope":   key.Scope,
+	}}, nil
+}
+
+// Serve accepts connections on ln until it is closed.
+func (s *Server) Serve(ln net.Listener) error {
+	for {
+		conn, err := ln.Accept()
+		if err != nil {
+			return err
+		}
+		go s.handleConn(conn)
+	}
+}
+
+func (s *Server) handleConn(conn net.Conn) {
+	defer conn.Close()
+	sconn, chans, reqs, err := ssh.NewServerConn(conn, s.sshCfg)
+	if err != nil {
+		return
+	}
+	defer sconn.Close()
+	go ssh.DiscardRequests(reqs)
+
+	for newCh := range chans {
+		if newCh.ChannelType() != "session" {
+			newCh.Reject(ssh.UnknownChannelType, "only session channels are supported")
+			continue
+		}
+		ch, chReqs, err := newCh.Accept()
+		if err != nil {
+			continue
+		}
+		go s.handleSession(sconn, ch, chReqs)
+	}
+}
+
+func (s *Server) handleSession(sconn *ssh.ServerConn, ch ssh.Channel, reqs <-chan *ssh.Request) {
+	defer ch.Close()
+	for req := range reqs {
+		switch req.Type {
+		case "exec":
+			var payload struct{ Command string }
+			if err := ssh.Unmarshal(req.Payload, &payload); err != nil {
+				req.Reply(false, nil)
+				continue
+			}
+			req.Reply(true, nil)
+			code := s.runExec(sconn, ch, payload.Command)
+			sendExit(ch, code)
+			return
+		case "shell":
+			req.Reply(true, nil)
+			fmt.Fprintf(ch, "forge control plane: interactive shells are not available.\nTry: ssh %s help\n", s.cfg.Server.SiteURL)
+			sendExit(ch, protocol.ExitUsage)
+			return
+		case "pty-req", "env":
+			// Harmless; accept and ignore.
+			req.Reply(true, nil)
+		default:
+			req.Reply(false, nil)
+		}
+	}
+}
+
+func sendExit(ch ssh.Channel, code int) {
+	var msg = struct{ Status uint32 }{uint32(code)}
+	ch.SendRequest("exit-status", false, ssh.Marshal(&msg))
+}
+
+func (s *Server) runExec(sconn *ssh.ServerConn, ch ssh.Channel, cmdline string) int {
+	ext := sconn.Permissions.Extensions
+	userID, _ := strconv.ParseInt(ext["user-id"], 10, 64)
+	keyID, _ := strconv.ParseInt(ext["key-id"], 10, 64)
+	user, err := s.st.UserByID(userID)
+	if err != nil {
+		fmt.Fprintln(ch.Stderr(), "account no longer exists")
+		return protocol.ExitDenied
+	}
+	_ = s.st.TouchSSHKey(keyID)
+
+	if name, _, ok := strings.Cut(cmdline, " "); ok || name != "" {
+		switch name {
+		case "git-upload-pack", "git-receive-pack", "git-upload-archive":
+			fmt.Fprintln(ch.Stderr(), "git transport not implemented (M2)")
+			return protocol.ExitFailure
+		}
+	}
+
+	argv, err := protocol.Tokenize(cmdline)
+	if err != nil {
+		fmt.Fprintf(ch.Stderr(), "cannot parse command: %v\n", err)
+		return protocol.ExitUsage
+	}
+	ctx := &control.Ctx{
+		User:   user,
+		Scope:  ext["scope"],
+		Store:  s.st,
+		Cfg:    s.cfg,
+		Stdin:  ch,
+		Stdout: ch,
+		Stderr: ch.Stderr(),
+	}
+	return control.Dispatch(ctx, argv)
+}
diff --git a/internal/store/users.go b/internal/store/users.go
new file mode 100644
index 0000000..7350bbd
--- /dev/null
+++ b/internal/store/users.go
@@ -0,0 +1,185 @@
+package store
+
+import (
+	"database/sql"
+	"errors"
+	"fmt"
+	"strings"
+)
+
+type User struct {
+	ID       int64
+	Username string
+	IsAdmin  bool
+}
+
+type SSHKey struct {
+	ID          int64
+	UserID      int64
+	Fingerprint string
+	Algo        string
+	Blob        []byte
+	Scope       string
+}
+
+// ErrDuplicateKey carries the exact user-facing message from the spec. It
+// deliberately does not name the owning account (enumeration oracle).
+var ErrDuplicateKey = errors.New("that key is already registered to another account; remove it there first or use a different key")
+
+var ErrNotFound = errors.New("not found")
+
+func (s *Store) CreateUser(username string, isAdmin bool) (int64, error) {
+	res, err := s.DB.Exec("INSERT INTO users (username, is_admin) VALUES (?, ?)", username, boolInt(isAdmin))
+	if err != nil {
+		if isUniqueErr(err) {
+			return 0, fmt.Errorf("username %q is taken", username)
+		}
+		return 0, err
+	}
+	return res.LastInsertId()
+}
+
+func (s *Store) UserByUsername(name string) (User, error) {
+	var u User
+	var admin int
+	err := s.DB.QueryRow("SELECT id, username, is_admin FROM users WHERE username = ?", name).
+		Scan(&u.ID, &u.Username, &admin)
+	if errors.Is(err, sql.ErrNoRows) {
+		return u, ErrNotFound
+	}
+	u.IsAdmin = admin != 0
+	return u, err
+}
+
+func (s *Store) UserByID(id int64) (User, error) {
+	var u User
+	var admin int
+	err := s.DB.QueryRow("SELECT id, username, is_admin FROM users WHERE id = ?", id).
+		Scan(&u.ID, &u.Username, &admin)
+	if errors.Is(err, sql.ErrNoRows) {
+		return u, ErrNotFound
+	}
+	u.IsAdmin = admin != 0
+	return u, err
+}
+
+// AddSSHKey registers a key and bumps the key epoch in one transaction.
+func (s *Store) AddSSHKey(userID int64, fingerprint, algo string, blob []byte, scope string) error {
+	tx, err := s.DB.Begin()
+	if err != nil {
+		return err
+	}
+	defer tx.Rollback()
+	if _, err := tx.Exec(
+		"INSERT INTO ssh_keys (user_id, fingerprint, algo, blob, scope) VALUES (?, ?, ?, ?, ?)",
+		userID, fingerprint, algo, blob, scope); err != nil {
+		if isUniqueErr(err) {
+			return ErrDuplicateKey
+		}
+		return err
+	}
+	if err := bumpKeyEpoch(tx); err != nil {
+		return err
+	}
+	return tx.Commit()
+}
+
+// RemoveSSHKey removes a key owned by userID and bumps the key epoch.
+func (s *Store) RemoveSSHKey(userID int64, fingerprint string) error {
+	tx, err := s.DB.Begin()
+	if err != nil {
+		return err
+	}
+	defer tx.Rollback()
+	res, err := tx.Exec("DELETE FROM ssh_keys WHERE user_id = ? AND fingerprint = ?", userID, fingerprint)
+	if err != nil {
+		return err
+	}
+	if n, _ := res.RowsAffected(); n == 0 {
+		return ErrNotFound
+	}
+	if err := bumpKeyEpoch(tx); err != nil {
+		return err
+	}
+	return tx.Commit()
+}
+
+func (s *Store) SSHKeyByFingerprint(fingerprint string) (SSHKey, error) {
+	var k SSHKey
+	err := s.DB.QueryRow(
+		"SELECT id, user_id, fingerprint, algo, blob, scope FROM ssh_keys WHERE fingerprint = ?",
+		fingerprint).Scan(&k.ID, &k.UserID, &k.Fingerprint, &k.Algo, &k.Blob, &k.Scope)
+	if errors.Is(err, sql.ErrNoRows) {
+		return k, ErrNotFound
+	}
+	return k, err
+}
+
+func (s *Store) ListSSHKeys(userID int64) ([]SSHKey, error) {
+	rows, err := s.DB.Query(
+		"SELECT id, user_id, fingerprint, algo, blob, scope FROM ssh_keys WHERE user_id = ? ORDER BY id",
+		userID)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	var keys []SSHKey
+	for rows.Next() {
+		var k SSHKey
+		if err := rows.Scan(&k.ID, &k.UserID, &k.Fingerprint, &k.Algo, &k.Blob, &k.Scope); err != nil {
+			return nil, err
+		}
+		keys = append(keys, k)
+	}
+	return keys, rows.Err()
+}
+
+// TouchSSHKey records key use; best-effort, callers ignore the error.
+func (s *Store) TouchSSHKey(id int64) error {
+	_, err := s.DB.Exec(
+		"UPDATE ssh_keys SET last_used_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?", id)
+	return err
+}
+
+// AddEmail adds an address; verifiedBy is "" (unverified), "smtp", or "admin".
+func (s *Store) AddEmail(userID int64, address, verifiedBy string, primary bool) error {
+	var vAt, vBy any
+	if verifiedBy != "" {
+		vAt = "now"
+		vBy = verifiedBy
+	}
+	_, err := s.DB.Exec(
+		`INSERT INTO emails (user_id, address, verified_at, verified_by, is_primary)
+		 VALUES (?, ?, CASE WHEN ? IS NULL THEN NULL ELSE strftime('%Y-%m-%dT%H:%M:%fZ','now') END, ?, ?)`,
+		userID, address, vAt, vBy, boolInt(primary))
+	if isUniqueErr(err) {
+		return fmt.Errorf("address %q is already in use", address)
+	}
+	return err
+}
+
+func (s *Store) KeyEpoch() (int64, error) {
+	var v int64
+	err := s.DB.QueryRow("SELECT value FROM settings WHERE key = 'key_epoch'").Scan(&v)
+	return v, err
+}
+
+type execer interface {
+	Exec(query string, args ...any) (sql.Result, error)
+}
+
+func bumpKeyEpoch(tx execer) error {
+	_, err := tx.Exec("UPDATE settings SET value = value + 1 WHERE key = 'key_epoch'")
+	return err
+}
+
+func boolInt(b bool) int {
+	if b {
+		return 1
+	}
+	return 0
+}
+
+func isUniqueErr(err error) bool {
+	return err != nil && strings.Contains(err.Error(), "UNIQUE constraint failed")
+}