# Feature Request — Public accessor for the device's identity public key **To**: Shade SDK team **From**: Dispatch (browser-based Shade consumer) **Target**: Shade SDK 4.4.x (or whichever release vehicle fits) **Priority**: medium — unblocks real per-device fingerprint binding at enrollment time; consumers ship with placeholder keys until then Thanks for shipping `@shade/storage-indexeddb` so quickly — that unblocked Dispatch's Slice 2.5 (persistent browser identity). During integration we hit one more gap that's worth raising as a separate FR. --- ## Summary Expose the local device's Ed25519 identity public key as a public accessor on `Shade`, so applications can hand it to their own backend at enrollment time for per-device verification, audit, or peer-fingerprint computation. Today the SDK exposes `myAddress` and `fingerprint`, but the underlying identity public key — the cryptographic root that everything else binds to — is reachable only via the private `this.storage.getIdentityKeyPair()` call inside `Shade`. Consumers building enrollment flows have no way to hand the real key over. ## Problem A common pattern in Shade-using apps is: 1. Browser device generates Shade identity (via `createShade`) 2. User enters an enrollment token 3. Browser POSTs to its backend: `{ token, address, identityPublicKey, ... }` 4. Backend records the device, computes/stores a safety number from the identity key, opens a Shade session Step 3 today has nowhere to get a real `identityPublicKey` from. Dispatch currently sends `crypto.getRandomValues(new Uint8Array(32))` formatted as hex, with this comment in the source: ```ts /** * The browser submits a hex public-key field at enroll time so the schema * stays stable. Wiring this to the SDK-generated identity key requires a * Shade SDK addition (no public accessor for the raw identity key today). */ export function generatePlaceholderIdentityPublicKey(): string { ... } ``` The backend stores this placeholder but cannot verify anything against it, because it's not actually the device's key. Real cryptographic binding is deferred until the SDK exposes the underlying key. ## Why `fingerprint` isn't sufficient `Shade.fingerprint` returns a 12-groups-of-5-digits safety-number string designed for human side-channel comparison. That's the right output for a "compare these digits with your friend" UX, but it's a derived format, not the raw key. Backends that want to: - Store the key for later signature verification - Compute their own safety-number representation (Dispatch uses `deterministicSafetyNumber(localAddr, peerAddr)` based on the raw bytes) - Re-derive the fingerprint after an identity rotation …all need access to the raw 32-byte Ed25519 public key. ## Proposed API Add a single async accessor on `Shade`: ```ts class Shade { /** * The local device's Ed25519 identity public key (32 bytes). * * Stable for the lifetime of the identity. After `rotateIdentity()` * this returns the new key; the old key is preserved in retired- * identities storage for the configured grace period. */ get identityPublicKey(): Promise; } ``` Internally: ```ts get identityPublicKey(): Promise { if (!this.initialized) throw new Error('Not initialized'); return this.storage.getIdentityKeyPair().then((kp) => { if (!kp) throw new Error('Identity not yet generated'); return kp.publicKey; }); } ``` If returning a getter that produces a Promise feels off, the equivalent method form is fine: ```ts async getIdentityPublicKey(): Promise { ... } ``` Either shape works for consumers — pick whichever matches existing SDK conventions. ## Alternative considered: object accessor Returning an object would leave room to expose other identity-related fields later without a breaking change: ```ts get identity(): Promise<{ address: string; publicKey: Uint8Array; fingerprint: string; // future: registrationId, createdAt, etc. }>; ``` This would mildly duplicate `myAddress` + `fingerprint`, but consolidates identity-related accessors. Not a blocker for shipping the simpler single-purpose accessor — just flagging the option in case the SDK is considering broader API ergonomics. ## What this unblocks For Dispatch specifically: - Real device identity binding at enrollment (replaces the placeholder `crypto.randomUUID()`-derived hex bytes the backend currently stores) - Server-side `computePeerSafetyNumber()` can use the real key instead of the deterministic-from-address stand-in (`shade-identity-provider.ts:151`) - Future signature-based device authentication (sign a challenge with the device's identity key during enrollment) becomes possible without another SDK round For other Shade consumers: - Any app that hands a device key to its own backend for enrollment — multi-device pairing flows, contact verification UIs, push-notification targeting — gets an actual key to work with ## Acceptance criteria 1. New accessor exposed on `Shade` (getter or async method, SDK's preference) 2. Returns the 32-byte Ed25519 identity public key 3. Returns the **current** key after `rotateIdentity()` 4. Throws (or resolves to a clear error) when called before `initialize()` completes 5. Documented alongside `myAddress` and `fingerprint` in the SDK reference 6. One unit test in `shade-sdk` confirming the returned bytes match what `storage.getIdentityKeyPair()` holds 7. Mentioned in the changelog under the next release ## Out of scope - **Private key access** — consumers should never need it; signature operations go through `shade.send()` etc. Don't expose the secret half. - **Cross-peer key lookup** — getting *another* peer's identity key is a separate concern (related to peer-verification storage), not what this FR is about. This is strictly the local device's own key. - **Format conversions** — base64/hex/PEM helpers don't belong in the SDK. Consumers can encode the raw bytes however their wire format requires. ## Why this can't be done in consumer-land The identity keypair is generated by `MemoryStorage` / `SQLiteStorage` / `IndexedDBStorage` and consumed by `ShadeSessionManager`. Consumers can't reach into either layer without breaching the SDK's encapsulation. A public accessor is the only path that doesn't require monkey-patching private fields.