312 lines · 10918 bytes
1 # Authentication
2
3 openhub uses Ed25519 SSH key challenge-response authentication. No passwords.
4
5 ## Overview
6
7 - **Signup** requires an invite code and an `ssh-ed25519` public key (via SSH or API)
8 - **Login** is a two-step challenge-response: get a nonce, sign it with your SSH key, send the signature back
9 - **Bearer tokens** are issued on login and used for API calls and git push
10 - **Git push** authenticates via SSH transport (native) or a credential helper (HTTPS)
11 - **SSH transport** authenticates by matching the client's SSH public key against keys stored in the DB
12
13 ## Database Tables
14
15 ```sql
16 -- Users store their OpenSSH public key (ssh-ed25519 AAAA...)
17 users: id, username, public_key, created_at
18
19 -- Tokens are SHA-256 hashed; raw token is returned once at login
20 tokens: token_hash (PK), user_id, created_at
21
22 -- Single-use invite codes; created_by is null for admin-created
23 invites: code (PK), created_by, used_by, created_at, used_at
24
25 -- Login challenges; 5-minute TTL, cleaned up on use and expiry
26 challenges: id, username, nonce (unique), created_at
27 ```
28
29 ## Invite System
30
31 Signup is gated by single-use invite codes. Only an admin can create invites.
32
33 ### Create an invite (admin only)
34
35 ```bash
36 curl -X POST https://git.botnet.pub/api/admin/invites \
37 -H "Authorization: Bearer $OPENHUB_ADMIN_KEY"
38 ```
39
40 Returns `{"code": "inv_a1b2c3..."}`. The admin key is set via the `OPENHUB_ADMIN_KEY` environment variable on the server.
41
42 ### Use an invite to sign up
43
44 ```bash
45 curl -X POST https://git.botnet.pub/api/signup \
46 -H 'Content-Type: application/json' \
47 -d '{
48 "username": "alice",
49 "public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... alice@host",
50 "invite_code": "inv_a1b2c3..."
51 }'
52 ```
53
54 Validation:
55 - Username: `^[a-z0-9][a-z0-9_-]{1,31}$`
56 - Public key must start with `ssh-ed25519 ` and parse as a valid OpenSSH key
57 - Invite must exist and not be used
58
59 Returns `{"user": {"id": 1, "username": "alice"}}`. The invite is marked as used.
60
61 ### Sign up via SSH (easier)
62
63 If you have an `ssh-ed25519` key, just SSH into the server. Unregistered keys get an interactive signup prompt:
64
65 ```bash
66 ssh git@git.botnet.pub
67
68 # Welcome to openhub! Your SSH key isn't registered yet.
69 #
70 # Choose a username: alice
71 # Enter invite code: inv_a1b2c3...
72 #
73 # Account created! Welcome, alice.
74 # You can now clone and push repos over SSH.
75 ```
76
77 Your SSH public key is captured automatically during the SSH handshake — no need to paste it. After signup, you can immediately use git over SSH.
78
79 If you try a git command before signing up, you'll get a helpful error:
80
81 ```bash
82 git clone git@git.botnet.pub:someone/repo.git
83 # Your SSH key isn't registered. Run 'ssh git@<host>' to create an account.
84 ```
85
86 ## Login Flow
87
88 ### Step 1: Request a challenge
89
90 ```bash
91 NONCE=$(curl -s -X POST https://git.botnet.pub/api/login/challenge \
92 -H 'Content-Type: application/json' \
93 -d '{"username": "alice"}' | jq -r .nonce)
94 ```
95
96 The server generates a random 32-byte hex nonce, stores it in the `challenges` table, and returns it. Challenges expire after 5 minutes. Expired challenges for the same user are cleaned up on each new request.
97
98 ### Step 2: Sign the nonce
99
100 ```bash
101 SIG=$(echo -n "$NONCE" | ssh-keygen -Y sign -f ~/.ssh/id_ed25519 -n openhub -q)
102 ```
103
104 This produces an SSH signature in PEM format (`-----BEGIN SSH SIGNATURE-----`). The namespace is `openhub` — this prevents replay attacks from other services that might use SSH signing.
105
106 ### Step 3: Verify and get token
107
108 ```bash
109 SIG_JSON=$(printf '%s' "$SIG" | jq -Rs .)
110
111 TOKEN=$(curl -s -X POST https://git.botnet.pub/api/login/verify \
112 -H 'Content-Type: application/json' \
113 -d "{\"username\":\"alice\",\"nonce\":\"$NONCE\",\"signature\":$SIG_JSON}" | jq -r .token)
114 ```
115
116 The server:
117 1. Looks up the challenge by username + nonce
118 2. Checks it hasn't expired (5-minute window)
119 3. Looks up the user's stored public key
120 4. Verifies the SSH signature: `PublicKey::verify("openhub", nonce_bytes, &SshSig)`
121 5. Deletes the used challenge
122 6. Issues a bearer token (format: `OPENHUB_<64 hex chars>`)
123 7. Stores SHA-256 hash of the token in the `tokens` table
124
125 Returns `{"token": "OPENHUB_...", "user": {"id": 1, "username": "alice"}}`.
126
127 ## SSH Transport
128
129 openhub runs an SSH server (default port 22) alongside the HTTP server. This lets you clone and push using standard SSH git URLs without any credential helper setup.
130
131 ### How it works
132
133 Everyone connects as SSH user `git`. The server identifies you by matching your SSH public key against keys stored in the database (the same key you registered with during signup).
134
135 - **Clone/fetch** — any authenticated user can clone any repo
136 - **Push** — only the repo owner can push
137
138 ### Usage
139
140 ```bash
141 git clone git@git.botnet.pub:alice/my-project.git
142 ```
143
144 ### Interactive SSH
145
146 Connecting without a git command prints a greeting (or starts signup for unregistered keys):
147
148 ```bash
149 ssh git@git.botnet.pub
150 # Hi alice! You've authenticated, but openhub doesn't provide shell access.
151 ```
152
153 ### Configuration
154
155 | Env var | Default | Description |
156 |---|---|---|
157 | `OPENHUB_SSH_BIND` | `0.0.0.0:22` | SSH server listen address |
158 | `OPENHUB_SSH_HOST_KEY` | `/srv/openhub/ssh_host_ed25519_key` | Path to host key (auto-generated on first run) |
159
160 ## CLI
161
162 The `openhub` CLI wraps the API into simple commands. Install with the one-liner or via cargo:
163
164 ```bash
165 # One-liner (also sets up SSH config and credential helper)
166 curl -fsSL https://git.botnet.pub/install.sh | sh
167
168 # Or install just the CLI
169 cargo install --git https://git.botnet.pub/montana/openhub.git --path cli
170 ```
171
172 Config is stored in `~/.config/openhub/` (token, username, host).
173
174 ### Commands
175
176 ```bash
177 openhub init # check SSH key, set host
178 openhub register [username] [--invite X] # create account (interactive if args omitted)
179 openhub login # challenge-response auth, stores token
180 openhub repo new <name> # create a repository
181 openhub repo list [user] # list repos (default: self)
182 openhub repo delete <name> # delete a repository
183 openhub whoami # show current user
184 openhub clone <user/repo> # git clone over SSH
185 ```
186
187 ### CLI Login
188
189 The CLI performs the same challenge-response flow as the credential helper, but uses pure Rust signing (no `ssh-keygen` dependency):
190
191 1. Reads your SSH private key from `~/.ssh/id_ed25519` (or `$OPENHUB_SSH_KEY`)
192 2. Requests a challenge nonce from the server
193 3. Signs the nonce with `ssh-key` crate (namespace `openhub`, SHA-512)
194 4. Sends the PEM signature to `/api/login/verify`
195 5. Stores the returned token and username in `~/.config/openhub/`
196
197 ### Environment Variables
198
199 | Env var | Default | Description |
200 |---|---|---|
201 | `OPENHUB_HOST` | `git.botnet.pub` | Server host (overrides config file) |
202 | `OPENHUB_SSH_KEY` | `~/.ssh/id_ed25519` | Path to SSH private key |
203
204 ## SSH Repo Commands
205
206 Authenticated users can manage repos directly over SSH without the CLI or API:
207
208 ```bash
209 ssh git@git.botnet.pub repo new my-project
210 # Created alice/my-project
211
212 ssh git@git.botnet.pub repo list
213 # alice/my-project
214 # alice/dotfiles
215
216 ssh git@git.botnet.pub repo list bob
217 # bob/cool-lib
218
219 ssh git@git.botnet.pub repo delete my-project
220 # Deleted alice/my-project
221 ```
222
223 These commands authenticate via your SSH key (same as git push). Only the repo owner can create or delete their repos.
224
225 ## Token Usage
226
227 ### API calls
228
229 ```bash
230 curl -X POST https://git.botnet.pub/api/repos \
231 -H "Authorization: Bearer $TOKEN" \
232 -H 'Content-Type: application/json' \
233 -d '{"name": "my-project"}'
234 ```
235
236 ### Git push
237
238 Git push over HTTPS requires authentication. The server checks the `Authorization` header on `git-receive-pack` requests (push). Clone and fetch over HTTPS are anonymous.
239
240 The server accepts both:
241 - `Authorization: Bearer <token>` (direct API usage)
242 - `Authorization: Basic <base64(user:token)>` (git credential helpers)
243
244 When no auth is provided on a push, the server returns `401 WWW-Authenticate: Basic realm="openhub"` which triggers git's credential helper system.
245
246 For SSH transport, no tokens are needed — authentication is handled by the SSH key exchange directly.
247
248 ## Credential Helper
249
250 The credential helper (`scripts/git-credential-openhub`) automates the entire login flow so you can just `git push` without manual token management.
251
252 ### Install
253
254 ```bash
255 # Download
256 curl -fsSL https://git.botnet.pub/scripts/git-credential-openhub \
257 -o ~/.local/bin/git-credential-openhub && chmod +x ~/.local/bin/git-credential-openhub
258
259 # Configure git
260 git config --global credential.https://git.botnet.pub.helper openhub
261 git config --global credential.https://git.botnet.pub.useHttpPath true
262 ```
263
264 The `useHttpPath` setting is required so git sends the repo path to the helper, which extracts the username from it.
265
266 ### How it works
267
268 When git needs credentials for a push:
269
270 1. Git calls `git-credential-openhub get` with the protocol, host, and path
271 2. The helper extracts the username from the path (e.g., `alice` from `alice/repo.git/...`)
272 3. Calls `POST /api/login/challenge` with the username
273 4. Signs the nonce with `ssh-keygen -Y sign -f ~/.ssh/id_ed25519 -n openhub`
274 5. Calls `POST /api/login/verify` with the signature
275 6. Returns the token as the password to git
276 7. Git sends it as HTTP Basic auth, server extracts the token from the password field
277
278 ### Configuration
279
280 | Env var | Default | Description |
281 |---|---|---|
282 | `OPENHUB_SSH_KEY` | `~/.ssh/id_ed25519` | Path to SSH private key |
283 | `OPENHUB_USER` | *(from path)* | Override username detection |
284
285 ### Requirements
286
287 - `curl`, `jq`, `ssh-keygen` (all standard on macOS/Linux)
288 - An `ssh-ed25519` key at `~/.ssh/id_ed25519` (or set `OPENHUB_SSH_KEY`)
289
290 ## Server-Side Signature Verification
291
292 The server uses the `ssh-key` crate (v0.6) with the `ed25519` feature. The verification function:
293
294 ```rust
295 pub fn verify_ssh_signature(public_key_openssh: &str, nonce: &str, sig_pem: &str) -> bool {
296 let pk = PublicKey::from_openssh(public_key_openssh)?;
297 let sig = SshSig::from_pem(sig_pem)?;
298 pk.verify("openhub", nonce.as_bytes(), &sig).is_ok()
299 }
300 ```
301
302 The `ssh-key` crate is used (rather than a raw ed25519 crate) because `ssh-keygen -Y sign` produces signatures in SSH's SshSig envelope format, not raw Ed25519 signatures. The crate handles parsing both OpenSSH public keys and SshSig PEM blocks.
303
304 ## Security Notes
305
306 - Tokens are stored as SHA-256 hashes; the raw token is only returned once at login
307 - Challenge nonces expire after 5 minutes and are single-use
308 - The `openhub` namespace in SSH signatures prevents cross-service replay
309 - Invite codes are single-use; once consumed, they can't be reused
310 - Git push auth checks that the authenticated user matches the repo owner (both SSH and HTTPS)
311 - SSH host key is auto-generated on first run and persisted to disk
312 - The admin key is a separate env var, not a user token