Background
Per Stage 7 design (#64) and aligned with the Wildmeta omni_account + TEE-signer pattern, the daemon's auth flow should converge on:
- One user-facing identity (email / OAuth2) → omni_account anchor
- Server-derived EVM wallet per omni_account (deterministic, never leaves the trust boundary)
- No local key management by the operator — recovery via re-authenticating any linked identity in
IdentityLinkStore
The pieces in place today:
The missing piece: a custodial signer that derives a keypair from omni_account and signs SIWE challenges on the user's behalf so the daemon never needs a local secp256k1 key.
Step 1 (this issue's first deliverable) — dev_key_service in mock-server
Ship a development-only signer module in crates/agentkeys-mock-server with the exact wire shape the eventual TEE worker will use, gated by env so production builds reject it.
Module
crates/agentkeys-mock-server/src/dev_key_service.rs
- Loads master secret from
\$DEV_KEY_SERVICE_MASTER_SECRET (32-byte hex). Refuse to enable if absent.
derive_keypair(omni_account) -> (secret_key, eth_address) via HKDF-SHA256 with info = "agentkeys-evm-wallet-v1:" || omni_account.
sign_eip191(omni_account, message) -> Signature.
- Strong "DEV ONLY — replace with TEE" warnings in the file header + every public method.
Endpoints (env-gated, return 503 if DEV_KEY_SERVICE_MASTER_SECRET unset)
POST /dev/derive-address — body { omni_account } → { address }
POST /dev/sign-message — body { omni_account, message_hex } → { signature }
Daemon integration
Update crates/agentkeys-daemon/src/main.rs to use the new flow:
- Operator authenticates via email/OAuth2 → broker mints session JWT for omni_email
- Daemon calls backend
/dev/derive-address with omni_email → derived EVM address
- Daemon calls broker
/v1/wallet/link (auth: omni_email session JWT) to register the derived address
- Per-mint: daemon calls broker
/v1/auth/wallet/start(derived_addr) → SIWE message
- Daemon calls backend
/dev/sign-message(omni_email, siwe_message) → signature
- Daemon calls broker
/v1/auth/wallet/verify → session JWT for omni_evm
- Daemon calls broker
/v1/mint-oidc-jwt → OIDC JWT → AssumeRoleWithWebIdentity → AWS temp creds
The signer call (step 5) is direct daemon → backend, mirroring the eventual daemon → TEE attested channel.
Removes
agentkeys init --mock-token legacy bootstrap (replaced by email/OAuth2 + auto-derive)
/v1/auth/exchange legacy bearer shim (no caller after daemon migrates)
- Broker → backend
/session/validate round-trip (no caller after exchange shim deletes)
Step 2 (follow-up issue) — Replace dev_key_service with TEE worker
Same wire surface (/dev/* endpoints become /tee/* or stay), real TEE backing:
- Master secret generated inside the enclave at first boot, sealed-data persisted
- Remote attestation so daemon can verify the worker is genuine before sending omni → key derivation requests
- Logs every signing operation with omni_account + message hash, no secret material
When TEE lands: dev_key_service.rs deletes; routing flips to TEE worker URL via env var; zero changes to daemon or broker.
Out of scope
- Threshold signing for high-value omni_accounts
- Master-secret rotation policy
- Multi-region TEE replication
- Production gating of
DEV_KEY_SERVICE_MASTER_SECRET (compile-time cfg(not(production)) could come later)
References
Background
Per Stage 7 design (#64) and aligned with the Wildmeta omni_account + TEE-signer pattern, the daemon's auth flow should converge on:
IdentityLinkStoreThe pieces in place today:
omni_account = sha256(client_id || identity_type || identity_value)(identity/omni_account.rs)The missing piece: a custodial signer that derives a keypair from omni_account and signs SIWE challenges on the user's behalf so the daemon never needs a local secp256k1 key.
Step 1 (this issue's first deliverable) —
dev_key_servicein mock-serverShip a development-only signer module in
crates/agentkeys-mock-serverwith the exact wire shape the eventual TEE worker will use, gated by env so production builds reject it.Module
crates/agentkeys-mock-server/src/dev_key_service.rs\$DEV_KEY_SERVICE_MASTER_SECRET(32-byte hex). Refuse to enable if absent.derive_keypair(omni_account) -> (secret_key, eth_address)via HKDF-SHA256 withinfo = "agentkeys-evm-wallet-v1:" || omni_account.sign_eip191(omni_account, message) -> Signature.Endpoints (env-gated, return 503 if
DEV_KEY_SERVICE_MASTER_SECRETunset)POST /dev/derive-address— body{ omni_account }→{ address }POST /dev/sign-message— body{ omni_account, message_hex }→{ signature }Daemon integration
Update
crates/agentkeys-daemon/src/main.rsto use the new flow:/dev/derive-addresswith omni_email → derived EVM address/v1/wallet/link(auth: omni_email session JWT) to register the derived address/v1/auth/wallet/start(derived_addr)→ SIWE message/dev/sign-message(omni_email, siwe_message)→ signature/v1/auth/wallet/verify→ session JWT for omni_evm/v1/mint-oidc-jwt→ OIDC JWT → AssumeRoleWithWebIdentity → AWS temp credsThe signer call (step 5) is direct daemon → backend, mirroring the eventual daemon → TEE attested channel.
Removes
agentkeys init --mock-tokenlegacy bootstrap (replaced by email/OAuth2 + auto-derive)/v1/auth/exchangelegacy bearer shim (no caller after daemon migrates)/session/validateround-trip (no caller after exchange shim deletes)Step 2 (follow-up issue) — Replace
dev_key_servicewith TEE workerSame wire surface (
/dev/*endpoints become/tee/*or stay), real TEE backing:When TEE lands:
dev_key_service.rsdeletes; routing flips to TEE worker URL via env var; zero changes to daemon or broker.Out of scope
DEV_KEY_SERVICE_MASTER_SECRET(compile-timecfg(not(production))could come later)References