Skip to content

feat(byok): add proof-of-possession registration to support divine-mobile#3359 #197

@realmeylisdev

Description

@realmeylisdev

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

  • POST /api/byok/challenge shipped with rate limiting, TTL, single-use enforcement
  • HeadlessRegisterRequest accepts optional byok_pubkey + byok_proof; rejects with a clear error code on missing/expired/invalid proof
  • BYOK accounts can register, exchange OAuth codes, and sign events end-to-end through the new contract
  • Playwright e2e test in e2e/ covers challenge → sign → register → token-exchange end-to-end through the new contract
  • Unit tests assert the legacy register shape returns the deprecation Sunset: header during Phase A
  • Sunset date for the legacy register shape documented in CHANGELOG.md and API docs
  • Coordination with security/infra on retention review for pre-fix request bodies that infra-layer components (Cloudflare, LB, proxies) may have captured — tracked as a separate workstream

Related

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions