feat(api): API keys, so a pin can be published without a browser - #101
Merged
Conversation
Every account-scoped endpoint authenticated by session cookie alone:
const s = readSession(req.cookies.get(SESSION_COOKIE)?.value);
So no CLI, script or CI job could call one. For key pins that is not an
inconvenience but a correctness problem. A Moshpit name's TLS is
unverifiable until its pin is published, and publishing meant running a
script, reading a base64 hash out of its output, and pasting that into a
web form. Three steps where the interesting one is invisible, so the
honest outcome is that most names never get a pin at all.
Adds `account_api_keys` and a bearer path, opted into by the two pin
routes rather than folded into `resolveAccountId` — a key path that
silently widened every account endpoint at once would be a much larger
change than the diff makes it look.
Ownership is unaffected: `addPin` already refuses an ending the account
does not own, so a token can do no more than its account could.
Security choices worth the review:
- only the hash is stored, so a leaked backup is not a set of credentials
- plain SHA-256, not a password KDF: this is 32 CSPRNG bytes rather than a
guessable secret, and it is verified on every request, where a slow hash
would be a self-inflicted DoS
- lookup is by hash, so the compare happens in the index — no string
comparison to leak timing, no probing for a valid token
- revocation is checked in the query, effective on the next request
- revoke is scoped to the account inside the UPDATE, so there is no
check-then-write window and a wrong id is indistinguishable from
someone else's
- keys cannot mint keys: that route is session-only, or one leak becomes
access that outlives revoking the key that leaked
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
vu1nz Security Review0 finding(s) in PR #? No security issues found. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The server half of moshcode#244. Without this, nothing but a browser can publish a key pin.
Why
Every account-scoped endpoint authenticates by session cookie alone:
So no CLI, script, or CI job can call one. For key pins that is not an inconvenience, it is a correctness problem: a Moshpit name's TLS is unverifiable until its pin is published, and publishing meant running
setup-origin.sh, reading a base64 hash out of its output, and pasting that into a web form.Three steps where the interesting one is invisible. The predictable result is that most names never get a pin, and unverifiable HTTPS looks exactly like protected HTTPS to the person who set it up.
What this adds
account_api_keys, a bearer path, and/api/account/keysto mint and revoke.The bearer path is a separate resolver,
resolveAccountIdOrToken, opted into by the two pin routes — not folded intoresolveAccountId. Folding it in would have widened every account-scoped endpoint in the codebase at once, which is a much larger change than the diff would appear to be. This way the routes that accept keys are visible in the diff.Ownership is unaffected:
addPinalready refuses an ending the account does not own, so a token can do no more than the account behind it could.Security choices worth arguing with
AND revoked_at IS NULL), so it takes effect on the next request rather than the next deploy.WHERE id = ? AND account_id = ?), so there is no check-then-write window, and a wrong id is indistinguishable from someone else's id — neither reveals whether that key exists./api/account/keysis session-only. Otherwise one leak becomes permanent access that outlives revoking the key that leaked, and the revoke button stops meaning anything.mpk_prefix, so a leaked key is greppable in a scan and obvious in a log; the first six characters are kept in clear so a person can tell two keys apart when revoking one, which is far too few to guess the remaining 32 bytes.Tests
5 tests on the properties that make a key safe to hand to a script: the token is 32 CSPRNG bytes, tokens do not repeat, a JWT or Basic credential is not mistaken for one of ours, no part of a token survives in its hash, and the retained prefix identifies without being usable.
147 pass, 0 fail. Typecheck clean.
Next
moshcode site --installcan now do key → cert → pin → publish in the one command people already run, with no hash ever visible to a person. That is the last piece of moshcode#244.