Context
Tracking issue (mobile side, already public): divinevideo/divine-mobile#3359 — labeled critical / security, scopes a credential-handling concern in the BYOK (bring-your-own-key) secure-account upgrade flow.
The mobile-side fix needs server support to preserve BYOK identity binding without the current client→server credential transfer shape. This issue scopes the server work; full evidence and threat model live in the linked mobile issue.
Requirements
Add a proof-of-possession ceremony for BYOK registration so the server can bind an OAuth account to a user-supplied Nostr pubkey without receiving the secret.
1. New endpoint — POST /api/byok/challenge
- Request:
{ "byok_pubkey": "<hex>" }
- Response:
{ "nonce": "<hex>", "expires_at": "<RFC3339>" }
- Per-IP rate limiting, short TTL (60 s), single-use, transient storage (DB table or in-memory KV — implementer's choice)
2. Modified endpoint — POST /api/headless/register
- Add optional
byok_pubkey: String and byok_proof: String fields. byok_proof is a hex BIP-340 Schnorr signature over the nonce bytes (suggest signing sha256(utf8(nonce))).
- When present, server verifies the proof against the pubkey and the previously-issued nonce, marks the nonce consumed, binds the registered account to the verified pubkey.
- For BYOK accounts, server holds the OAuth account ↔ pubkey binding without server-side custody of the signing key. Signing stays local on the client.
3. Token exchange
Once Phase C below ships, the BYOK branch of the token-exchange handler simplifies to standard RFC 7636 PKCE plus a lookup of the bound pubkey from the registered account record. No verifier-content parsing.
Phasing
- Phase A (this issue) — ship
/api/byok/challenge and the new optional register fields. Continue accepting the legacy register shape with a deprecation log + Sunset: response header.
- Phase B — mobile-side coordinates from divine-mobile#3359 and migrates to the new contract.
- Phase C — after legacy traffic drops to ~0 (observability-driven, suggest 30 days post-Phase-B GA), remove the legacy code paths.
Pre-Phase-B, the mobile team will ship an emergency-block PR that disables the BYOK preservation path entirely (server falls through to its existing server-managed key path — no protocol change required). Phase A is what unblocks restoring BYOK identity preservation in the mobile client.
Acceptance criteria
Related
Context
Tracking issue (mobile side, already public): divinevideo/divine-mobile#3359 — labeled
critical/security, scopes a credential-handling concern in the BYOK (bring-your-own-key) secure-account upgrade flow.The mobile-side fix needs server support to preserve BYOK identity binding without the current client→server credential transfer shape. This issue scopes the server work; full evidence and threat model live in the linked mobile issue.
Requirements
Add a proof-of-possession ceremony for BYOK registration so the server can bind an OAuth account to a user-supplied Nostr pubkey without receiving the secret.
1. New endpoint —
POST /api/byok/challenge{ "byok_pubkey": "<hex>" }{ "nonce": "<hex>", "expires_at": "<RFC3339>" }2. Modified endpoint —
POST /api/headless/registerbyok_pubkey: Stringandbyok_proof: Stringfields.byok_proofis a hex BIP-340 Schnorr signature over the nonce bytes (suggest signingsha256(utf8(nonce))).3. Token exchange
Once Phase C below ships, the BYOK branch of the token-exchange handler simplifies to standard RFC 7636 PKCE plus a lookup of the bound pubkey from the registered account record. No verifier-content parsing.
Phasing
/api/byok/challengeand the new optional register fields. Continue accepting the legacy register shape with a deprecation log +Sunset:response header.Pre-Phase-B, the mobile team will ship an emergency-block PR that disables the BYOK preservation path entirely (server falls through to its existing server-managed key path — no protocol change required). Phase A is what unblocks restoring BYOK identity preservation in the mobile client.
Acceptance criteria
POST /api/byok/challengeshipped with rate limiting, TTL, single-use enforcementHeadlessRegisterRequestaccepts optionalbyok_pubkey+byok_proof; rejects with a clear error code on missing/expired/invalid proofe2e/covers challenge → sign → register → token-exchange end-to-end through the new contractSunset:header during Phase ACHANGELOG.mdand API docsRelated