An easy-to-host PDS on the ATProtocol, iPhone and MacOS. Maintain control of your keys and data, always.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: update project context for MM-146 DID ceremony flow

Add external signer API contract to crypto CLAUDE.md and update
identity-wallet CLAUDE.md with new perform_did_ceremony command,
DIDCeremonyError type, RelayClient methods, onboarding screens,
Keychain accounts, and relay endpoint dependencies.

authored by

Malpercio and committed by
Tangled
f8ba74cb fcf3bcf4

+34 -10
+15 -8
apps/identity-wallet/CLAUDE.md
··· 11 11 ### Frontend (SvelteKit 2 + Svelte 5) 12 12 13 13 **Exposes:** 14 - - `src/lib/ipc.ts` — typed wrappers for all Tauri IPC commands; import these instead of calling `invoke()` directly. Exports: `createAccount()`, `getOrCreateDeviceKey()`, `signWithDeviceKey()`, and their associated types (`DevicePublicKey`, `DeviceKeyError`, `CreateAccountResult`, `CreateAccountError`) 15 - - `src/lib/components/onboarding/` — five onboarding screen components (WelcomeScreen, ClaimCodeScreen, EmailScreen, HandleScreen, LoadingScreen) 16 - - `src/routes/+page.svelte` — root page: five-screen onboarding state machine (welcome -> claim_code -> email -> handle -> loading -> did_ceremony) 14 + - `src/lib/ipc.ts` — typed wrappers for all Tauri IPC commands; import these instead of calling `invoke()` directly. Exports: `createAccount()`, `getOrCreateDeviceKey()`, `signWithDeviceKey()`, `performDIDCeremony()`, and their associated types (`DevicePublicKey`, `DeviceKeyError`, `CreateAccountResult`, `CreateAccountError`, `DIDCeremonyResult`, `DIDCeremonyError`) 15 + - `src/lib/components/onboarding/` — seven onboarding screen components (WelcomeScreen, ClaimCodeScreen, EmailScreen, HandleScreen, LoadingScreen, DIDCeremonyScreen, DIDSuccessScreen) 16 + - `src/routes/+page.svelte` — root page: eight-step onboarding state machine (welcome -> claim_code -> email -> handle -> loading -> did_ceremony -> did_success -> shamir_backup) 17 17 18 18 **Guarantees:** 19 19 - SSR is disabled globally (`ssr = false` in `src/routes/+layout.ts`); the frontend is a fully static SPA loaded from disk by WKWebView ··· 31 31 - `src/lib.rs::create_account(claim_code, email, handle) -> Result<CreateAccountResult, CreateAccountError>` — Tauri IPC command: gets or creates device key via `device_key::get_or_create()`, POSTs to relay `/v1/accounts/mobile`, stores tokens in Keychain on success 32 32 - `src/lib.rs::get_or_create_device_key() -> Result<DevicePublicKey, DeviceKeyError>` — Tauri IPC command: delegates to `device_key::get_or_create()` 33 33 - `src/lib.rs::sign_with_device_key(data: Vec<u8>) -> Result<Vec<u8>, DeviceKeyError>` — Tauri IPC command: delegates to `device_key::sign()` 34 + - `src/lib.rs::perform_did_ceremony(handle: String) -> Result<DIDCeremonyResult, DIDCeremonyError>` — Tauri IPC command: fetches relay signing key (GET /v1/relay/keys), builds signed did:plc genesis op via `crypto::build_did_plc_genesis_op_with_external_signer` using device key as signer, POSTs genesis op to relay (POST /v1/dids with Bearer token), persists DID and upgraded session token in Keychain 34 35 - `src/device_key.rs` — P-256 device key management with `#[cfg]`-based dispatch: macOS/simulator uses software keys via `crypto` crate + Keychain storage; real iOS device uses Secure Enclave via `security-framework`. Public API: `get_or_create() -> Result<DevicePublicKey, DeviceKeyError>` (idempotent), `sign(data) -> Result<Vec<u8>, DeviceKeyError>` 35 36 - `src/keychain.rs` — iOS Keychain abstraction (`store_item`, `get_item`, `delete_item`) under service `"ezpds-identity-wallet"` 36 - - `src/http.rs` — `RelayClient` with compile-time base URL (localhost:8080 debug, relay.ezpds.com release) 37 + - `src/http.rs` — `RelayClient` with compile-time base URL (localhost:8080 debug, relay.ezpds.com release); methods: `post()`, `get()`, `post_with_bearer()`; static `base_url()` accessor 37 38 38 39 **Guarantees:** 39 40 - `crate-type = ["staticlib", "cdylib", "rlib"]` supports iOS (staticlib), Android (cdylib), and normal cargo builds (rlib) 40 41 - `src/main.rs` is the desktop entry point; `src/lib.rs::run()` is the iOS/Android entry point (via `#[cfg_attr(mobile, tauri::mobile_entry_point)]`) 41 42 - `tauri.conf.json` configures the bundle identifier, dev URL (`http://localhost:5173`), and frontend dist path (`../dist`) 42 43 - `create_account` maps relay HTTP error codes to typed `CreateAccountError` variants (EXPIRED_CODE, REDEEMED_CODE, EMAIL_TAKEN, HANDLE_TAKEN, NETWORK_ERROR, UNKNOWN) serialized as `{ code: "SCREAMING_SNAKE" }` for the frontend 44 + - `perform_did_ceremony` maps failures to typed `DIDCeremonyError` variants (KEY_NOT_FOUND, RELAY_KEY_FETCH_FAILED, NO_RELAY_SIGNING_KEY, SIGNING_FAILED, DID_CREATION_FAILED, KEYCHAIN_ERROR, NETWORK_ERROR) serialized as `{ code: "SCREAMING_SNAKE_CASE" }` for the frontend 43 45 - `device_key::get_or_create()` is idempotent -- returns the same key on every call for a given device 44 46 - `device_key::sign()` returns raw 64-byte r||s ECDSA signatures; deterministic (RFC 6979) on simulator, low-S normalized on real device 45 47 - `DeviceKeyError` variants serialize as `{ code: "SCREAMING_SNAKE_CASE" }` matching the `CreateAccountError` pattern ··· 59 61 - Rust backend -> `p256` (workspace dep: key reconstruction, signature types in both paths) 60 62 - Rust backend -> `multibase` (workspace dep: base58btc encoding for multibase/did:key output) 61 63 - Rust backend -> relay `/v1/accounts/mobile` endpoint (via `reqwest` HTTP at runtime) 64 + - Rust backend -> relay `GET /v1/relay/keys` endpoint (public, no auth; fetches active signing key for DID ceremony) 65 + - Rust backend -> relay `POST /v1/dids` endpoint (Bearer token auth; submits signed genesis op for DID promotion) 62 66 - Rust backend -> iOS Keychain (via `security-framework` crate with `OSX_10_12` feature for SE access control APIs) 63 67 - Rust backend -> Secure Enclave hardware (real iOS device only; via `security-framework` `SecKey`/`GenerateKeyOptions`/`Token::SecureEnclave`) 64 68 - `src-tauri/gen/` -> NOT tracked in git; generated per-developer by `cargo tauri ios init` (gitignored) ··· 181 185 - Keychain service name is always `"ezpds-identity-wallet"` (constant `keychain::SERVICE`); changing it orphans previously stored credentials 182 186 - `CreateAccountError` variant names serialize as SCREAMING_SNAKE_CASE to the frontend -- the TypeScript `CreateAccountError.code` union must match exactly 183 187 - `DeviceKeyError` variant names serialize as SCREAMING_SNAKE_CASE to the frontend -- the TypeScript `DeviceKeyError.code` union must match exactly 188 + - `DIDCeremonyError` variant names serialize as SCREAMING_SNAKE_CASE to the frontend -- the TypeScript `DIDCeremonyError.code` union must match exactly 184 189 - Keychain account `"device-rotation-key-priv"` stores the software P-256 private key (simulator/macOS path only); changing it orphans existing keys 185 190 - Keychain accounts `"device-rotation-key-pub"` and `"device-rotation-key-app-label"` store SE metadata (real iOS device path only); changing them orphans the SE key lookup 191 + - Keychain account `"session-token"` stores the pending (pre-DID) or full (post-DID) session token; `perform_did_ceremony` reads the pending token and overwrites it with the upgraded token on success 192 + - Keychain account `"did"` stores the user's did:plc after successful DID ceremony; persisted for use in subsequent app sessions 186 193 - `DevicePublicKey` serializes with `#[serde(rename_all = "camelCase")]` -- TypeScript receives `{ multibase, keyId }` (not `key_id`) 187 194 188 195 ## Key Files 189 196 190 197 - `src-tauri/tauri.conf.json` -- Tauri config: bundle ID, devUrl, frontendDist, window settings 191 - - `src-tauri/src/lib.rs` -- Tauri IPC commands (`create_account`, `get_or_create_device_key`, `sign_with_device_key`) and `run()` (mobile entry point) 198 + - `src-tauri/src/lib.rs` -- Tauri IPC commands (`create_account`, `get_or_create_device_key`, `sign_with_device_key`, `perform_did_ceremony`) and `run()` (mobile entry point) 192 199 - `src-tauri/src/device_key.rs` -- P-256 device key module: `#[cfg]`-dispatched `get_or_create()` and `sign()` (simulator software path vs. Secure Enclave) 193 200 - `src-tauri/src/main.rs` -- Desktop entry point (calls `lib::run()`) 194 201 - `src-tauri/src/keychain.rs` -- iOS Keychain abstraction (store_item, get_item, delete_item) 195 202 - `src-tauri/src/http.rs` -- RelayClient with compile-time base URL 196 203 - `src-tauri/.cargo/config.toml` -- Cargo toolchain overrides for iOS cross-compilation (CC, AR, linker per target) 197 - - `src/lib/ipc.ts` -- Typed TypeScript wrappers for all Tauri IPC commands (createAccount, getOrCreateDeviceKey, signWithDeviceKey) 198 - - `src/lib/components/onboarding/` -- Five onboarding screen components 199 - - `src/routes/+page.svelte` -- Onboarding state machine (welcome -> claim_code -> email -> handle -> loading -> did_ceremony) 204 + - `src/lib/ipc.ts` -- Typed TypeScript wrappers for all Tauri IPC commands (createAccount, getOrCreateDeviceKey, signWithDeviceKey, performDIDCeremony) 205 + - `src/lib/components/onboarding/` -- Seven onboarding screen components (WelcomeScreen, ClaimCodeScreen, EmailScreen, HandleScreen, LoadingScreen, DIDCeremonyScreen, DIDSuccessScreen) 206 + - `src/routes/+page.svelte` -- Onboarding state machine (welcome -> claim_code -> email -> handle -> loading -> did_ceremony -> did_success -> shamir_backup) 200 207 - `src/routes/+layout.ts` -- `ssr = false; prerender = false` (global SPA config) 201 208 - `svelte.config.js` -- adapter-static with `pages: 'dist'` (SPA mode, matches tauri.conf.json) 202 209 - `vite.config.ts` -- Tauri-compatible Vite server (clearScreen, HMR via TAURI_DEV_HOST, envPrefix)
+19 -2
crates/crypto/CLAUDE.md
··· 1 1 # Crypto Crate 2 2 3 - Last verified: 2026-03-14 3 + Last verified: 2026-03-20 4 4 5 5 ## Purpose 6 6 Provides cryptographic primitives for the ezpds workspace: P-256 key generation, ··· 64 64 - `signed_op_json` is ready to POST to `https://plc.directory/{did}` 65 65 - Deterministic: same inputs → same DID (RFC 6979 ECDSA + SHA-256 + base32) 66 66 - Errors: `CryptoError::PlcOperation` if `signing_private_key` is an invalid P-256 scalar 67 + - Delegates internally to `build_did_plc_genesis_op_with_external_signer` with a closure wrapping `SigningKey::sign` 68 + 69 + **`build_did_plc_genesis_op_with_external_signer`** 70 + ```rust 71 + pub fn build_did_plc_genesis_op_with_external_signer<F>( 72 + rotation_key: &DidKeyUri, // user's device key (rotationKeys[0]) 73 + signing_key: &DidKeyUri, // relay's signing key (rotationKeys[1] + verificationMethods.atproto) 74 + handle: &str, // e.g. "alice.example.com" 75 + service_endpoint: &str, // e.g. "https://relay.example.com" 76 + sign: F, // callback: &[u8] -> Result<Vec<u8>, CryptoError> 77 + ) -> Result<PlcGenesisOp, CryptoError> 78 + where F: FnOnce(&[u8]) -> Result<Vec<u8>, CryptoError> 79 + ``` 80 + - Same as `build_did_plc_genesis_op` but accepts a signing callback instead of raw private key bytes 81 + - Enables signing with non-extractable keys (e.g. Apple Secure Enclave) 82 + - Callback receives CBOR-encoded unsigned op bytes; must return raw 64-byte r||s P-256 ECDSA signature (big-endian, low-S canonical) 83 + - Errors: propagates any `CryptoError` returned by the callback, or `CryptoError::PlcOperation` for serialization failures 67 84 68 85 **`verify_genesis_op`** 69 86 ```rust ··· 111 128 112 129 ## Dependencies 113 130 - **Uses**: p256 (ECDSA/key generation), aes-gcm (AES-256-GCM), multibase (base58btc encoding), rand_core (OS RNG), base64 (storage encoding), zeroize (secret cleanup), ciborium (CBOR serialization for did:plc), data-encoding (base32-lowercase), sha2 (SHA-256), serde/serde_json (struct serialization) 114 - - **Used by**: `crates/relay/` (key generation, did:plc genesis building and verification in POST /v1/dids) 131 + - **Used by**: `crates/relay/` (key generation, did:plc genesis building and verification in POST /v1/dids), `apps/identity-wallet/` (external signer genesis op building in DID ceremony) 115 132 116 133 ## Invariants 117 134 - Private key bytes are always wrapped in `Zeroizing` -- callers must not copy them into non-zeroizing storage