Skip to content
This repository was archived by the owner on Feb 17, 2026. It is now read-only.

Latest commit

 

History

History
578 lines (368 loc) · 14.4 KB

File metadata and controls

578 lines (368 loc) · 14.4 KB

Embedded Signer Infrastructure — Ticket Backlog

Codex-ready backlog for the standalone embedded signer infrastructure (no marketplace/games/product language).


EPIC 0 — Repo + Guardrails

✅ T0.1 — Initialize repo (TypeScript lib)

Scope

  • Create repo as a TS library (not an app)
  • Tooling: pnpm, tsup (or Vite lib mode), eslint, prettier, vitest
  • Folder layout:
src/
  crypto/
  vault/
  storage/
  auth/
  sessions/
  permissions/
  signer/
  types/
  index.ts
test/

Definition of Done

  • pnpm i && pnpm lint && pnpm test && pnpm build succeed
  • Build outputs ESM + CJS + types

Notes

✅ T0.2 — Add constraints docs

Scope

  • AGENTS.md with hard rules:
    • no TODOs
    • no “optional”
    • no placeholder crypto
    • all public functions typed
  • SECURITY.md with invariants:
    • no plaintext secrets at rest
    • decrypt only in memory
    • strict origin binding
    • canonical challenge serialization
    • no blind signing (challenge must be structured)
  • Add CI check to fail on TODO/FIXME strings

Definition of Done

  • Docs exist and match implementation
  • CI fails on TODO/FIXME strings

Notes

  • AGENTS.md + SECURITY.md document the guardrails and invariants.
  • pnpm lint now runs scripts/check-no-todo.mjs, which scans source/config files (non-docs) for TODO/FIXME strings.

EPIC 1 — Types + Constants (Prevent Ambiguity)

✅ T1.1 — Define core types

Scope

  • Create src/types/core.ts defining:
    • Hex32, Hex64, B64
    • Origin (normalized scheme://host:port)
    • VaultBlobV1
    • ChallengeV1
    • Scope, PermissionRecord, SessionRecord
    • UnsignedEvent, SignedEvent (generic event signing)

Definition of Done

  • Types exported from src/index.ts
  • Compile passes strict TypeScript

Notes

  • Core type exports live in src/types/core.ts and are re-exported via src/index.ts.
  • test/core-types.test.ts instantiates representative values so Vitest protects against accidental shape regressions.

✅ T1.2 — Define canonical serialization rules

Scope

  • Create src/types/canonical.ts with:
    • canonicalJsonStringify(obj) (stable keys, no whitespace)
    • utf8Encode(str) helper

Definition of Done

  • Unit test: identical objects stringify identically
  • Unit test: key order differences don’t change output

Notes

  • src/types/canonical.ts includes canonicalJsonStringify + utf8Encode plus exported JsonValue helper type.
  • test/canonical.test.ts covers determinism, key ordering, and utf8 encoding behavior.

EPIC 2 — Crypto Primitives

✅ T2.1 — SHA-256 helpers

Scope

  • sha256(bytes) -> Uint8Array
  • sha256Hex(utf8String) -> hex

Definition of Done

  • Unit tests against known SHA-256 outputs

Notes

  • src/crypto/sha256.ts exposes sha256(bytes) and sha256Hex(string) (Hex32, 32-byte digest) and is re-exported via src/index.ts.
  • test/sha256.test.ts uses standard vectors (empty string, abc, pangram) to lock in both byte + hex outputs.

✅ T2.2 — secp256k1 BIP340 schnorr sign/verify

Scope

  • Using @noble/secp256k1 implement:
    • schnorrSign(hash32Hex, privkeyHex) -> sigHex
    • schnorrVerify(hash32Hex, sigHex, pubkeyHex) -> boolean
    • pubkeyFromPrivkey(privkeyHex) -> pubkeyHex (x-only 32 bytes)

Definition of Done

  • Tests include:
    • 1 valid BIP340 vector
    • 1 invalid verify vector
    • Verify returns false on malformed lengths

Notes

  • src/crypto/schnorr.ts implements schnorrSign, schnorrVerify, and pubkeyFromPrivkey with strict hex validation and optional deterministic aux randomness for tests.
  • test/schnorr.test.ts encodes the BIP340 reference vector (all-zero hash/private + zero aux) plus tampered / malformed cases.

✅ T2.3 — Randomness

Scope

  • randomBytes(n) using crypto.getRandomValues (browser)
  • Add fallback error if not available

Definition of Done

  • Test in jsdom environment verifies correct length and non-zero probability (basic sanity)

Notes

  • src/crypto/random.ts exposes randomBytes(length) that enforces integer length and requires crypto.getRandomValues.
  • test/random.test.ts polyfills globalThis.crypto via Node webcrypto and asserts length + non-zero bytes; also covers error paths (missing crypto, invalid lengths).

EPIC 3 — Vault (KDF + AES-GCM)

✅ T3.1 — KDF: Argon2id primary, PBKDF2 fallback

Scope

  • Choose a WASM Argon2id library usable in browser
  • Implement:
    • deriveKey(password, salt, params) -> CryptoKey/bytes
    • PBKDF2-SHA256 fallback if Argon2 not available (explicit flag)

Definition of Done

  • Unit test: same inputs → same derived key
  • Unit test: different salt → different derived key
  • KDF params persisted in vault blob

Notes

  • Tests mock argon2-browser to keep the suite fast; production builds continue using the real WASM implementation.

✅ T3.2 — AES-GCM encrypt/decrypt

Scope

  • encryptAesGcm(keyBytes, plaintextBytes, iv) -> ciphertextBytes
  • decryptAesGcm(...) throws on failure

Definition of Done

  • Roundtrip test passes
  • Wrong key fails
  • Wrong IV fails

Notes

  • Helpers live in src/vault/aes.ts and leverage WebCrypto; tests rely on Node's webcrypto shim.

✅ T3.3 — Vault blob encode/decode (V1)

Scope

  • Implement VaultBlobV1:
{
  "version": 1,
  "pubkey": "hex",
  "kdf": { "algo": "argon2id|pbkdf2", "params": {...} },
  "salt": "base64",
  "iv": "base64",
  "ciphertext": "base64"
}
  • Ciphertext contains canonical JSON payload:
{ "v": 1, "privkey": "hex", "created_at": 1700000000 }

Definition of Done

  • Encode/decode roundtrip
  • Blob includes version + KDF params
  • No plaintext secret in blob

Notes

  • src/vault/blob.ts exposes encode/decode helpers plus base64 utilities so later vault APIs can reuse the same logic; payload JSON is always canonicalized before encryption.

✅ T3.4 — Vault API

Scope

  • createIdentity(password, meta?) -> { pubkey, vaultBlob }
  • unlockIdentity(password, vaultBlob) -> { pubkey, privkey }
  • changePassword(oldPw, newPw, vaultBlob) -> vaultBlob

Definition of Done

  • Unlock with correct password works
  • Unlock with wrong password fails
  • Password change preserves pubkey

Notes

  • src/vault/api.ts implements createIdentity, unlockIdentity, and changePassword atop the Argon2/PBKDF2 + AES helpers; tests exercise success/wrong password + password rotation flows with deterministic Argon params.

EPIC 4 — Storage (IndexedDB)

✅ T4.1 — IndexedDB schema + adapter

Scope

  • Implement StorageAdapter interface and IndexedDbAdapter
  • Tables:
    • vaults (id, pubkey, vaultBlob, created_at, updated_at)
    • permissions (origin, pubkey, scopes, created_at, updated_at)
    • sessions (session_id, origin, pubkey, scopes, expires_at, revoked_at?)

Definition of Done

  • CRUD tests run in fake-indexeddb env
  • Migrations versioned (v1)

Notes

  • IndexedDbAdapter in src/storage/indexeddb.ts implements the StorageAdapter interface with v1 stores for vaults, permissions, and sessions; test/storage-indexeddb.test.ts exercises CRUD via fake-indexeddb.

✅ T4.2 — Origin normalization

Scope

  • normalizeOrigin(inputUrlOrOrigin) -> Origin
  • Rules:
    • exact scheme://host:port
    • if port missing: infer 443 for https, 80 for http

Definition of Done

  • Tests cover:
    • https default port
    • http default port
    • preserves explicit ports
    • rejects invalid inputs

Notes

  • normalizeOrigin lives in src/auth/origin.ts and is exported via src/index.ts; test/origin.test.ts verifies canonicalization, custom ports, localhost support, and invalid inputs.

EPIC 5 — Challenge Auth (No Blind Signing)

✅ T5.1 — Challenge V1 format + hashing

Scope

  • ChallengeV1: { "v": 1, "origin": "scheme://host:port", "nonce": "base64|hex", "timestamp": 1700000000 }
  • createChallenge(origin) -> ChallengeV1 (nonce random, timestamp now)
  • hashChallenge(challenge) -> hash32Hex using canonical JSON stringify + sha256

Definition of Done

  • Unit tests:
    • hashing is deterministic
    • changing any field changes hash

Notes

  • src/auth/challenge.ts exposes createChallenge and hashChallenge, using normalizeOrigin, canonical JSON, and SHA-256; test/challenge.test.ts covers deterministic hashing, tamper detection, and normalized challenge creation via injected entropy.

✅ T5.2 — Challenge signing + verification

Scope

  • signChallenge(privkey, challenge) -> sig
  • verifyChallenge(pubkey, challenge, sig) -> boolean

Definition of Done

  • Valid signature verifies true
  • Tampered challenge fails verification

Notes

  • signChallenge/verifyChallenge live in src/auth/challenge.ts, reuse canonical hashing + Schnorr helpers, and are covered by test/challenge-sign.test.ts for success, tamper, and invalid signature cases.

EPIC 6 — Permissions (Scopes)

✅ T6.1 — Scope schema + matcher

Scope

  • Implement Scope union:
    • auth.sign_challenge
    • nostr.sign_event with kinds: number[]
    • session.refresh
  • Validation:
    • reject unknown types
    • reject empty kind arrays for nostr.sign_event

Definition of Done

  • Unit tests for validation
  • Unit tests for “is scope allowed” matcher

Notes

  • Scope validation + matcher live in src/permissions/scope.ts with helpers exported via the main entry point; test/permissions-scope.test.ts covers happy paths and failure cases for validation plus kind matching logic.

✅ T6.2 — Permission prompts (library-level)

Scope

  • Implement callback interface ConsentUI.requestApproval(origin, pubkey, requestedScopes) -> approvedScopes | throw
  • Library persists approved scopes in storage
  • Default implementation throws “no consent UI provided”

Definition of Done

  • Tests use a mock consent UI to approve scopes and persist them

Notes

  • PermissionManager + ConsentUI live in src/permissions/consent.ts; it normalizes origins, validates/matches scopes, persists grants via the storage adapter, and throws when no consent UI is configured. test/permissions-consent.test.ts covers consent-required errors, persistence, and insufficient approvals.

EPIC 7 — Sessions (TTL + Refresh)

✅ T7.1 — Session issuance

Scope

  • createSession(origin, pubkey, scopes, ttlSeconds) -> SessionRecord
  • Persist to storage
  • Enforce origin-bound sessions

Definition of Done

  • Session created and stored
  • Fetching by id returns correct record

Notes

  • Tests: pnpm test test/sessions-manager.test.ts

✅ T7.2 — Session enforcement

Scope

  • requireSession(origin, pubkey, requiredScope) -> SessionRecord
  • Reject missing/expired/revoked sessions
  • Reject scope mismatch

Definition of Done

  • Tests cover:
    • expired denied
    • revoked denied
  • wrong origin denied
  • missing scope denied

Notes

  • SessionManager.requireSession handles origin/pubkey binding, expiry, revocation, and scope checks; test/sessions-manager.test.ts includes coverage for success and all rejection paths.

✅ T7.3 — Session refresh

Scope

  • refreshSession(session_id):
    • requires session.refresh scope
    • re-issues expiry (sliding window)
    • uses re-signed ChallengeV1
    • add shouldRefresh(session, now) helper (refresh when <20% TTL remains)

Definition of Done

  • Refresh extends expiry
  • Refresh fails if locked / no signer available
  • Refresh signs new challenge and verifies internally

Notes

  • Tests: pnpm test test/sessions-manager.test.ts

EPIC 8 — Event Signing (Generic)

✅ T8.1 — Nostr-style event id + signing (generic)

Scope

  • Implement:
    • serializeEventForId([0, pubkey, created_at, kind, tags, content])
    • getEventId(unsigned) -> id
    • signEvent(privkey, unsigned) -> SignedEvent
    • Ensure id always recomputed internally

Definition of Done

  • Test vector for event ID hashing
  • Signed event verifies with schnorr verify over id hash

Notes

  • Tests: pnpm test test/event-signing.test.ts

EPIC 9 — EmbeddedSigner Facade (Public Entry Point)

✅ T9.1 — Implement EmbeddedSigner class

Scope

  • Constructor requires:
    • storage: StorageAdapter
    • consentUI?: ConsentUI
    • timeProvider?: () => number (for tests)
  • Public methods:
    • createIdentity({ password, username? }) -> pubkey
    • importIdentityMnemonic({ password, mnemonic }) -> pubkey
    • importIdentityKeyfile({ password, keyfileBlob }) -> pubkey
    • exportEncryptedKeyfile({ password }) -> keyfileBlob
    • exportMnemonic({ password }) -> mnemonic
    • unlock({ password })
    • lock()
    • getPublicKey()
    • requestScopes({ origin, scopes })
    • signChallenge({ origin, challenge })
    • signEvent({ origin, unsignedEvent, requiredKinds })
    • createSession({ origin, scopes, ttl })
    • refreshSession({ origin, sessionId })
    • revokeSession({ origin, sessionId })

Definition of Done

  • Locked signer refuses signing/refresh
  • Origin normalized everywhere
  • Permission checks enforced for every privileged method

Notes

  • Tests: pnpm test test/embedded-signer.test.ts test/mnemonic.test.ts

EPIC 10 — Test Suite (Non-negotiable)

✅ T10.1 — Crypto vectors

Scope

  • BIP340 valid + invalid vectors
  • SHA-256 vectors

Definition of Done

  • All pass in CI

✅ T10.2 — Vault tests

Scope

  • create/unlock/change password
  • wrong password fails
  • vault blob contains no plaintext secret

Definition of Done

  • Passes

✅ T10.3 — Challenge + session refresh tests

Scope

  • challenge hash determinism
  • verify fails on tamper
  • refresh extends expiry
  • refresh denied if expired/revoked/no scope

Definition of Done

  • Passes

✅ T10.4 — Permissions tests

Scope

  • approval persists
  • origin separation
  • scope enforcement

Definition of Done

  • Passes

EPIC 11 — Packaging + Minimal Example

✅ T11.1 — Minimal example (not an app)

Scope

  • Add examples/ with a tiny script showing:
    • create identity
    • unlock
    • create challenge
    • sign + verify
    • create session + refresh
  • Command: pnpm example

Definition of Done

  • Example runs and prints expected outputs

If you share your preferred environment target (browser-only vs browser+node) we can tighten the KDF/crypto choices accordingly (Argon2 wasm packaging differs).