Background
PR #75 landed issue #74 step 1: the dev_key_service signer with the /dev/derive-address + /dev/sign-message wire contract per docs/spec/signer-protocol.md. That step ships the signer with no HTTP-layer auth (loopback-only assumption from signer-protocol.md §"What's intentionally out of scope at v0").
A follow-up (step 1b, separate issue) deploys signer.litentry.org as an independent listener with bearer-JWT verification — strict improvement over today, but the broker becomes a single point of compromise: forge a JWT at the broker → impersonate any omni at the signer.
This issue (step 1c) eliminates that SPOF.
Plan doc
docs/spec/plans/issue-74-step-1c-device-key-auth.md is the canonical plan. Highlights:
- Init: daemon generates a device keypair locally; identity ceremony (email-link click / OAuth2 callback / EVM-wallet signature / WebAuthn) binds the device pubkey to the omni at the broker.
- Per request: daemon signs
(omni || message_hex || nonce || timestamp) with the device key; signer verifies the per-request signature against the device pubkey extracted from the session JWT claim.
- Trust shape: signer never trusts the broker as a transitive authenticator. Broker compromise post-init does not enable forging new sign requests.
- Identity-type uniformity:
evm, email, oauth2_google, passkey all use the same per-request signature shape. Only the init-time binding ceremony differs.
- UX uniformity: one ceremony at init, automatic per-request signing thereafter (no MetaMask popup, no hardware-wallet prompt).
Why this matters
Per the user discussion that produced the plan: even with a public signer.<zone> listener and bearer-JWT auth, the broker is the single thing protecting /dev/sign-message. Broker compromise = forge requests for any user whose omni is known.
Per-request crypto signature with a user-controlled (device) key removes the SPOF. The pattern matches Heima's ClientAuth::EvmSiweSigned / BackendSigned tier — and is a strict upgrade because (a) the per-request key is user-controlled (not backend-controlled), (b) it's automatic (no per-request user interaction), (c) it's identity-type uniform (covers email/OAuth2 omnis where Heima today only has the bearer path).
External validation: WebAuthn/passkey, EIP-7702 session keys, and ERC-4337 session keys all use the same primitive (high-friction identity verification authorizes a low-friction signing key). The pattern is well-validated.
Implementation order
11 stages laid out in §"Implementation order" of the plan doc. Roughly:
signer-protocol.md v0.2 — wire contract revision
agentkeys-core::device_key module — keypair + canonical_json + sign helper
2-4. Broker: extend session JWT mint + identity-ceremony handlers to bind device pubkey
- dev_key_service handlers: per-request sig verification
init_flow updates: generate device key, register, bind
HttpSignerClient updates: send JWT + device sig
- Deprecate the bearer-JWT-only path (step 1b) — protocol doc + handler reject requests without device sig
- TEE-stub conformance test extended
- Demo doc + operator runbook updated
- Live broker host redeploy + smoke walkthrough
Rough total: ~1200 LOC + protocol-doc revision + 11 stage-gated test waves.
Dependencies
Blocks on step 1b landing first (public listener split) so the device-key auth replaces a working bearer-JWT auth rather than a no-auth deployment.
Blocks the TEE worker (step 2) because step 2's threat model assumes the signer can't be tricked by a compromised broker — which is exactly what step 1c delivers.
Out of scope
- Multi-device authorization per omni (v0.2)
- Hardware-backed device keys (Secure Enclave / TPM / YubiKey — v0.2)
- Operator-initiated rotation cadence (v0.2)
- Cross-device device-key migration (v0.2)
See plan doc §"Non-goals" for full list.
Open questions
See plan doc §"Open questions for review":
- JWT-claim vs separate signer-side registry for the device pubkey
- RFC 8785 vs hand-rolled canonical JSON
- Nonce LRU sizing
- Sandbox VM device-key persistence
CEO review pending before implementation lands.
Background
PR #75 landed issue #74 step 1: the
dev_key_servicesigner with the/dev/derive-address+/dev/sign-messagewire contract perdocs/spec/signer-protocol.md. That step ships the signer with no HTTP-layer auth (loopback-only assumption fromsigner-protocol.md§"What's intentionally out of scope at v0").A follow-up (step 1b, separate issue) deploys
signer.litentry.orgas an independent listener with bearer-JWT verification — strict improvement over today, but the broker becomes a single point of compromise: forge a JWT at the broker → impersonate any omni at the signer.This issue (step 1c) eliminates that SPOF.
Plan doc
docs/spec/plans/issue-74-step-1c-device-key-auth.mdis the canonical plan. Highlights:(omni || message_hex || nonce || timestamp)with the device key; signer verifies the per-request signature against the device pubkey extracted from the session JWT claim.evm,email,oauth2_google,passkeyall use the same per-request signature shape. Only the init-time binding ceremony differs.Why this matters
Per the user discussion that produced the plan: even with a public
signer.<zone>listener and bearer-JWT auth, the broker is the single thing protecting/dev/sign-message. Broker compromise = forge requests for any user whose omni is known.Per-request crypto signature with a user-controlled (device) key removes the SPOF. The pattern matches Heima's
ClientAuth::EvmSiweSigned/BackendSignedtier — and is a strict upgrade because (a) the per-request key is user-controlled (not backend-controlled), (b) it's automatic (no per-request user interaction), (c) it's identity-type uniform (covers email/OAuth2 omnis where Heima today only has the bearer path).External validation: WebAuthn/passkey, EIP-7702 session keys, and ERC-4337 session keys all use the same primitive (high-friction identity verification authorizes a low-friction signing key). The pattern is well-validated.
Implementation order
11 stages laid out in §"Implementation order" of the plan doc. Roughly:
signer-protocol.mdv0.2 — wire contract revisionagentkeys-core::device_keymodule — keypair + canonical_json + sign helper2-4. Broker: extend session JWT mint + identity-ceremony handlers to bind device pubkey
init_flowupdates: generate device key, register, bindHttpSignerClientupdates: send JWT + device sigRough total: ~1200 LOC + protocol-doc revision + 11 stage-gated test waves.
Dependencies
Blocks on step 1b landing first (public listener split) so the device-key auth replaces a working bearer-JWT auth rather than a no-auth deployment.
Blocks the TEE worker (step 2) because step 2's threat model assumes the signer can't be tricked by a compromised broker — which is exactly what step 1c delivers.
Out of scope
See plan doc §"Non-goals" for full list.
Open questions
See plan doc §"Open questions for review":
CEO review pending before implementation lands.