You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Rescoped 2026-06-07 (was: "Step 1c: device-key authentication for /dev/* signer endpoints").
The original Step 1c plan — add a per-request device-key (K10) signature to the daemon's /dev/derive-address + /dev/sign-message calls — is largely superseded. Most of its intent shipped via other mechanisms; only a narrow residue remains. This issue is rewritten to that residue and re-pointed onto main + the arch.md §14.2 / TEE (#74) direction. Original text preserved at the bottom.
What already closed the original intent
The broker-as-SPOF threat this issue was filed to remove is already mitigated — at the cap-mint layer rather than the signer edge:
K10 device-key auth already gates the call surface. Every cap-bearing request carries a K10 signature that the worker independently re-verifies against the on-chain SidecarRegistry before any signer / S3 / STS op. arch.md headline guarantee: "Broker-only compromise cannot mint a usable cap … signer-only compromise cannot escape the chain's scope assertions." (arch.md trust-boundaries table + the K10 row in the key inventory.) That is the property this issue wanted.
The signer is no longer daemon-facing by design.arch.md §14.2: "Callers: broker + workers only. Daemons never talk to the signer directly." The signer's real surface is the typed RPC over mTLS (/derive-cred-kek, /sts-credentials, /sign/audit-row, /verify/k10-sig, …); /dev/sign-message is now the legacy SIWE compat path.
The residual gap (this issue)
One daemon→signer call path still exists and is authenticated by bearer JWT only — the exact SPOF shape, on the legacy endpoint:
crates/agentkeys-mock-server/src/handlers/dev_keys.rs verifies only that JWT (verify_session_jwt) — no per-request device-sig check; and no agentkeys-core::device_key module exists (plan stage 1 was never built).
This contradicts arch.md §14.2 (daemons should not call the signer directly).
Acceptable resolutions — either closes this
Preferred — remove the direct daemon→signer call. Route the managed-wallet SIWE attestation (arch.md §5) through broker/worker mediation per §14.2, so no daemon-held bearer JWT authenticates a signer call. The legacy /dev/* daemon path goes away.
The TEE-worker replacement (#74) makes the end state mTLS + §14.2 typed-RPC, broker/worker-only — which subsumes resolution 1. If #74 step 2 lands first and deletes the daemon→signer path, this issue closes automatically. Track here only the interim hardening / §14.2 alignment if it lands before the TEE worker does.
Acceptance criteria
No daemon→signer call is authenticated by a broker-issued bearer JWT as the sole authenticator (either the call is removed per §14.2, or it carries a K10 device-sig the signer verifies).
Stale references fixed: deprecated evm branch → main; v1c per-identity PoP shapes marked superseded by arch.md §4 / §4a / §5a (and the plan doc docs/plan/issue-74-step-1c-device-key-auth.md updated to match).
Original issue text (pre-rescope, filed 2026-05-24)
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.
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:
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.
What already closed the original intent
The broker-as-SPOF threat this issue was filed to remove is already mitigated — at the cap-mint layer rather than the signer edge:
SidecarRegistrybefore any signer / S3 / STS op. arch.md headline guarantee: "Broker-only compromise cannot mint a usable cap … signer-only compromise cannot escape the chain's scope assertions." (arch.md trust-boundaries table + the K10 row in the key inventory.) That is the property this issue wanted./derive-cred-kek,/sts-credentials,/sign/audit-row,/verify/k10-sig, …);/dev/sign-messageis now the legacy SIWE compat path.The residual gap (this issue)
One daemon→signer call path still exists and is authenticated by bearer JWT only — the exact SPOF shape, on the legacy endpoint:
crates/agentkeys-daemon/src/proxy.rssigns the signer call with.bearer_auth(&state.session_jwt).crates/agentkeys-mock-server/src/handlers/dev_keys.rsverifies only that JWT (verify_session_jwt) — no per-request device-sig check; and noagentkeys-core::device_keymodule exists (plan stage 1 was never built).docs/spec/signer-protocol.mdis still v0 and lists "Authentication on the signer edge" as out of scope.This contradicts arch.md §14.2 (daemons should not call the signer directly).
Acceptable resolutions — either closes this
/dev/*daemon path goes away.signer-protocol.mdto v0.2 documenting the device-sig requirement.Relationship to #74
The TEE-worker replacement (#74) makes the end state mTLS + §14.2 typed-RPC, broker/worker-only — which subsumes resolution 1. If #74 step 2 lands first and deletes the daemon→signer path, this issue closes automatically. Track here only the interim hardening / §14.2 alignment if it lands before the TEE worker does.
Acceptance criteria
docs/spec/signer-protocol.mdreflects the chosen auth model (v0.2 device-sig, or "edge removed — see §14.2").evmbranch →main; v1c per-identity PoP shapes marked superseded by arch.md §4 / §4a / §5a (and the plan docdocs/plan/issue-74-step-1c-device-key-auth.mdupdated to match).Non-goals (carried forward)
Original issue text (pre-rescope, filed 2026-05-24)
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.