Compare commits
No commits in common. "b34475b4fb1dd7a012bf7fd872fa4bb097399939" and "4e7d6c8add7cef8d301d06bd8fc189e9ba696eee" have entirely different histories.
b34475b4fb
...
4e7d6c8add
@ -1,117 +0,0 @@
|
|||||||
# TASK METADATA
|
|
||||||
|
|
||||||
- **Target Files:** `server/audit.ts`, `server/audit_merkle.ts`,
|
|
||||||
`server/valkey.ts`, `server/db.ts`, and database schema/migration files.
|
|
||||||
- **Core Objective:** Implement an append-only RFC 6962 Merkle Tree audit ledger
|
|
||||||
in PostgreSQL with an in-process Deno micro-batcher for STH computation and
|
|
||||||
Valkey pub/sub broadcast.
|
|
||||||
- **Dependencies:** Deno 2.x WebCrypto (Ed25519), Valkey connection, PostgreSQL
|
|
||||||
schema updates, and SPIFFE workload identity key material.
|
|
||||||
- **Additional Important Notes:** Must use Deno's native WebCrypto (no external
|
|
||||||
crypto npm packages). Valkey session keys should use raw strings without
|
|
||||||
'session:' prefix (not fully applicable here, but good standard). Ensure tests
|
|
||||||
mock Valkey, PostgreSQL, and SPIFFE cleanly.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architectural Considerations & Risks
|
|
||||||
|
|
||||||
Before implementing the RFC 6962 Merkle Tree Ledger, we must evaluate the
|
|
||||||
current architecture and explicitly identify risks, regressions, and necessary
|
|
||||||
constraints.
|
|
||||||
|
|
||||||
**Risks & Tradeoffs:**
|
|
||||||
|
|
||||||
1. **In-Process Micro-Batcher Memory & State:** The micro-batcher runs
|
|
||||||
in-process inside Deno using asynchronous intervals (e.g., `setInterval` or
|
|
||||||
explicit flush). If the Deno process crashes abruptly, unflushed audit logs
|
|
||||||
in memory could be lost before the batch is committed to PostgreSQL and the
|
|
||||||
Merkle tree head is updated.
|
|
||||||
- _Mitigation:_ Ensure `auditLog` still performs an immediate async insert of
|
|
||||||
the _record_ to the database (as it currently does via `server/audit.ts`),
|
|
||||||
but the `leaf_hash` and tree updates are computed dynamically or buffered
|
|
||||||
for the batched STH computation. The database remains the source of truth;
|
|
||||||
the batcher just finalizes the cryptographical proof.
|
|
||||||
2. **Database Schema Locking:** Adding a `leaf_hash` column to the
|
|
||||||
`audit_records` table and creating the `audit_sths` table might cause locking
|
|
||||||
if the table is large.
|
|
||||||
- _Mitigation:_ The `leaf_hash` can be computed prior to insert.
|
|
||||||
3. **SPIFFE Identity Dependency:** STH generation requires the Deno process to
|
|
||||||
sign using the SPIFFE workload identity/server signing key via WebCrypto. If
|
|
||||||
SPIFFE/FFI fails or the keys are rotating during a batch, the STH generation
|
|
||||||
might fail.
|
|
||||||
- _Mitigation:_ Graceful error handling and retry mechanism for STH signing.
|
|
||||||
4. **No Third-Party Dependencies:** Adhering to the memory context, we must
|
|
||||||
exclusively use Deno 2.x native `crypto.subtle` API for SHA-256 and
|
|
||||||
Ed25519/ECDSA, rather than any Node.js polyfills or npm packages.
|
|
||||||
|
|
||||||
**Alternatives Evaluated:**
|
|
||||||
|
|
||||||
- _External Worker:_ Using a dedicated worker (e.g., a separate service or CRON)
|
|
||||||
for computing the Merkle root. _Decision:_ We rejected this to keep the
|
|
||||||
application self-contained and zero-dependency, per user guidance.
|
|
||||||
- _Synchronous Tree Updates:_ Recomputing the tree on every single write.
|
|
||||||
_Decision:_ Rejected. This would severely block relational write throughput. A
|
|
||||||
micro-batcher (e.g., 30-60s or size threshold) is the optimal path for
|
|
||||||
decoupling heavy crypto logic from the request hot-path.
|
|
||||||
|
|
||||||
## Proposed Implementation
|
|
||||||
|
|
||||||
The execution of this feature will be broken down into the following structured
|
|
||||||
phases:
|
|
||||||
|
|
||||||
### Phase 1: Database Schema Expansion
|
|
||||||
|
|
||||||
1. Update PostgreSQL schema to alter `audit_records` and add a `leaf_hash TEXT`
|
|
||||||
column.
|
|
||||||
2. Create the new `audit_sths` table:
|
|
||||||
```sql
|
|
||||||
CREATE TABLE audit_sths (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
tree_size BIGINT NOT NULL,
|
|
||||||
root_hash TEXT NOT NULL,
|
|
||||||
signature TEXT NOT NULL,
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 2: RFC 6962 Cryptographic Primitives (`server/audit_merkle.ts`)
|
|
||||||
|
|
||||||
1. Create `server/audit_merkle.ts`.
|
|
||||||
2. Implement RFC 6962 leaf hashing: `SHA-256(0x00 || entry_bytes)`.
|
|
||||||
3. Implement internal tree hashing:
|
|
||||||
`SHA-256(0x01 || left_child || right_child)`.
|
|
||||||
4. Implement the logic to construct the Merkle Tree from an array of leaves and
|
|
||||||
generate an inclusion proof (`verifyInclusionProof`).
|
|
||||||
5. Ensure all crypto utilizes `crypto.subtle.digest('SHA-256', ...)`.
|
|
||||||
|
|
||||||
### Phase 3: The Micro-Batcher & STH Signing (`server/audit.ts`)
|
|
||||||
|
|
||||||
1. Refactor `auditLog` to compute the `leaf_hash` synchronously before the async
|
|
||||||
database insert.
|
|
||||||
2. Implement a background micro-batcher in `server/audit.ts` (or
|
|
||||||
`server/audit_merkle.ts` if better decoupled) using `setInterval` (e.g.,
|
|
||||||
every 1000ms, or on explicit flush).
|
|
||||||
3. The batcher will:
|
|
||||||
- Query new `leaf_hash` entries from the database since the last STH
|
|
||||||
`tree_size`.
|
|
||||||
- Compute the new Merkle Root.
|
|
||||||
- Access the SPIFFE server signing key.
|
|
||||||
- Sign the STH (tree size, root hash) using `crypto.subtle.sign` (Ed25519).
|
|
||||||
- Insert the new STH into the `audit_sths` table.
|
|
||||||
|
|
||||||
### Phase 4: Valkey STH Broadcast (`server/valkey.ts`)
|
|
||||||
|
|
||||||
1. Once the STH is successfully saved to PostgreSQL, update the cache.
|
|
||||||
2. Use `valkey.set('auth:audit:latest_sth', json_payload)`.
|
|
||||||
3. Broadcast the update to independent witness nodes via pub/sub:
|
|
||||||
`valkey.publish('auth:audit:sth', json_payload)`.
|
|
||||||
|
|
||||||
### Phase 5: Testing & Quality Gates
|
|
||||||
|
|
||||||
1. Write hermetic tests in `server/audit_merkle.test.ts`.
|
|
||||||
2. Use `@std/testing/mock` to mock `valkey`, `sqlWrapper`, and SPIFFE keys.
|
|
||||||
3. Validate leaf hashing correctness against RFC 6962 test vectors.
|
|
||||||
4. Verify the Valkey `PUBLISH` payload formatting.
|
|
||||||
5. Execute standard quality gates: `deno fmt`, `deno task lint`,
|
|
||||||
`deno task check`, and `deno task test`.
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
# TASK METADATA
|
|
||||||
|
|
||||||
- **Target Files:** `server/main.ts`, `server/db.ts`, `ui/public/auth-client.js`, `ui/components/RegisterPage.tsx`, `ui/components/LoginPage.tsx`
|
|
||||||
- **Core Objective:** Implement WebAuthn PRF extension support for progressive feature detection and Key Encryption Key (KEK) derivation during registration and login, with graceful fallback.
|
|
||||||
- **Dependencies:** WebCrypto API natively in Deno/browser, SimpleWebAuthn v13 for passing PRF extension options.
|
|
||||||
- **Additional Important Notes:** This task establishes the PRF derivation pipeline. SSS multi-share reconstruction will integrate in a future story (3.3). If PRF is unsupported, registration/login must proceed normally without breaking standard WebAuthn flows.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architectural Considerations & Risks
|
|
||||||
|
|
||||||
- **Risks:**
|
|
||||||
- **Authenticator Compatibility:** Not all authenticators support the WebAuthn PRF extension. A hard failure when PRF is missing would lock users out. The progressive fallback design is critical to prevent regressions in standard authentication.
|
|
||||||
- **Extension Types & SDK Mapping:** Passing the exact extension payloads for PRF (`eval.first`, `eval.second`) in `generateRegistrationOptions` and `generateAuthenticationOptions` might require careful type mapping if `SimpleWebAuthn` types are strict.
|
|
||||||
- **Database Migrations:** Modifying the `passkeys` table to include `prf_enabled` and `prf_salt` must maintain compatibility with existing passkey rows (which will default to false/null).
|
|
||||||
- **Alternatives:**
|
|
||||||
- Traditional server-side wrapping (HSM/KMS) or user passwords could be used for key derivation. However, the WebAuthn PRF extension natively binds the encryption key material to the hardware authenticator itself, preserving Auth-Yes's passwordless UX and zero-trust properties without transmitting raw secrets.
|
|
||||||
|
|
||||||
## Proposed Implementation
|
|
||||||
|
|
||||||
### 1. Database Schema Updates (`server/db.ts`)
|
|
||||||
- Modify the `passkeys` table schema to include a `prf_enabled BOOLEAN DEFAULT FALSE` column.
|
|
||||||
- Add a `prf_salt` column (binary or hex string) to store the 32-byte cryptographic salt generated during registration.
|
|
||||||
|
|
||||||
### 2. Registration Flow (Server & Client)
|
|
||||||
- **Server (`server/main.ts`)**: In the `/api/register/challenge` endpoint, ensure the `prf: {}` extension is requested via `generateRegistrationOptions`.
|
|
||||||
- **Client (`ui/public/auth-client.js`)**: Execute `navigator.credentials.create()` through the client SDK. Extract `getClientExtensionResults()?.prf`.
|
|
||||||
- **Server (`server/main.ts`)**: In the `/api/register/verify` endpoint, inspect the extension results to check if PRF is enabled (`prf.enabled === true`). If supported, generate a 32-byte secure random salt (`prf_salt`). Store `prf_enabled: true` and the `prf_salt` alongside the new passkey record.
|
|
||||||
|
|
||||||
### 3. Login Flow (Server & Client)
|
|
||||||
- **Server (`server/main.ts`)**: In the `/api/login/challenge` endpoint, retrieve the user's `prf_salt` if their passkey has `prf_enabled`. Include the `prf: { eval: { first: <prf_salt> } }` extension payload in `generateAuthenticationOptions`.
|
|
||||||
- **Client (`ui/public/auth-client.js`)**:
|
|
||||||
- Execute `navigator.credentials.get()` with the provided PRF evaluation salt.
|
|
||||||
- Check `getClientExtensionResults()?.prf?.results?.first` for the PRF output.
|
|
||||||
- **Client-Side KEK Derivation**:
|
|
||||||
- If PRF output exists, use it as Input Keying Material (IKM) for WebCrypto HKDF to derive a 256-bit AES-GCM Key Encryption Key (KEK).
|
|
||||||
- **HKDF Parameters**:
|
|
||||||
- Hash: `SHA-256` (RFC 5869)
|
|
||||||
- Salt: 32-byte cryptographic salt (stored with passkey record)
|
|
||||||
- Info: `new TextEncoder().encode("auth-yes:prf:device-share:v1")`
|
|
||||||
- **Progressive Fallback**:
|
|
||||||
- If `getClientExtensionResults()?.prf` is missing or fails, gracefully bypass the KEK derivation step and continue standard signature-only WebAuthn login.
|
|
||||||
|
|
||||||
### 4. UI Integration (`ui/components/RegisterPage.tsx`, `ui/components/LoginPage.tsx`)
|
|
||||||
- (Optional but recommended) Include minor, non-blocking UI indicators or debug logs to signify when advanced hardware encryption (PRF) is successfully negotiated, aiding in development and progressive feature adoption.
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
# TASK METADATA
|
|
||||||
|
|
||||||
- **Target Files:** `ui/components/RecoveryPage.tsx`, `server/main.ts`, `server/recovery.ts` (new), `wasm/sss_recovery/` (new Rust module)
|
|
||||||
- **Core Objective:** Implement constant-time 2-of-3 Shamir's Secret Sharing (SSS) key splitting and reconstruction in WebAssembly/Rust for the client-side zero-downgrade recovery portal, with mandatory in-place memory zeroization.
|
|
||||||
- **Dependencies:** Deno WebCrypto API, SimpleWebAuthn (client & server), Rust/Wasm toolchain (`wasm-pack`), IndexedDB.
|
|
||||||
- **Additional Important Notes:** Share choreography uses a Device Share (IndexedDB via WebAuthn PRF), Hot Server Share (PostgreSQL via PIN), and Cold Voucher (BIP-39 mnemonic). The execution sandbox must use Web Workers or strict in-memory client modules with mandatory `Uint8Array.fill(0)` zeroization; isolated iframes are rejected to prevent `postMessage` memory leakage.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architectural Considerations & Risks
|
|
||||||
|
|
||||||
- **Risks:**
|
|
||||||
- **Garbage Collection Leaks:** Transferring ArrayBuffers between JavaScript and Wasm can leave un-zeroed memory in V8. Strict lifecycle management and immediate `Uint8Array.fill(0)` on all JS-side buffers is mandatory before losing references.
|
|
||||||
- **Side-Channel Attacks:** Polynomial interpolation in Rust over GF(256) must be constant-time to avoid timing attacks when processing recovery shares.
|
|
||||||
- **WebAuthn PRF Extension Support:** The Device Share in IndexedDB relies on the WebAuthn PRF extension. A fallback or clear UX flow must be designed if the user's authenticator lacks PRF support.
|
|
||||||
- **Brute-Forcing Server Share:** The Hot Server Share is gated by a recovery PIN/code. Robust rate-limiting on the `/api/recovery/challenge` endpoint is critical to prevent brute-forcing the server share.
|
|
||||||
- **Alternatives:**
|
|
||||||
- **Execution Context:** We explicitly rejected using an isolated sandbox iframe. `postMessage` serializes data, creating uncontrollable memory copies in the DOM that cannot be deterministically zeroed. We will use a Web Worker or direct WebAssembly instantiation in the main thread with explicit TypedArray zeroization.
|
|
||||||
- **Implementation Language:** Pure TypeScript SSS was rejected due to lack of constant-time execution guarantees and poor low-level memory control compared to Rust/Wasm.
|
|
||||||
|
|
||||||
## Proposed Implementation
|
|
||||||
|
|
||||||
### Phase 1: Wasm Core Engine (Rust)
|
|
||||||
1. Scaffold a new Rust crate (e.g., `wasm/sss_recovery`) compiling to `wasm32-unknown-unknown`.
|
|
||||||
2. Implement a constant-time 2-of-3 Shamir's Secret Sharing reconstruction algorithm over GF(256).
|
|
||||||
3. Expose FFI boundaries that accept two share buffers and output the reconstructed master secret.
|
|
||||||
4. Utilize `zeroize` crate in Rust to ensure Wasm linear memory is purged of intermediate polynomial data before returning control to JavaScript.
|
|
||||||
|
|
||||||
### Phase 2: Client-Side Choreography (`ui/components/RecoveryPage.tsx`)
|
|
||||||
1. Implement the UI flow for the two recovery scenarios:
|
|
||||||
- **Scenario A (Lost Key):** Fetch Device Share (IndexedDB + WebAuthn PRF) + Server Share (via PIN).
|
|
||||||
- **Scenario B (Lost Device):** Prompt for Cold Voucher (12-word BIP-39) + Server Share (via PIN).
|
|
||||||
2. Instantiate the Wasm SSS module.
|
|
||||||
3. Pass the two gathered shares to the Wasm module to reconstruct the master secret.
|
|
||||||
4. Import the reconstructed master secret directly into WebCrypto as an `extractable: false` `CryptoKey`.
|
|
||||||
5. **Memory Purge:** Immediately execute `Uint8Array.fill(0)` on the share inputs, intermediate buffers, and the raw reconstructed byte array.
|
|
||||||
6. Use the WebCrypto key to derive the ephemeral recovery token and sign the challenge for the new passkey registration.
|
|
||||||
|
|
||||||
### Phase 3: Server-Side Share Gating (`server/main.ts`, `server/recovery.ts`)
|
|
||||||
1. Implement backend storage for the Hot Server Share within the `recovery_shares` table (or similar schema extension).
|
|
||||||
2. Update `/api/recovery/challenge` to validate the recovery PIN and release the Hot Server Share only upon success, enforcing strict rate-limiting.
|
|
||||||
3. Update `/api/recovery/verify` to validate the ephemeral token signature derived from the reconstructed master secret.
|
|
||||||
4. Complete the recovery cycle by binding the new WebAuthn passkey, revoking the old credentials, and generating a new 2-of-3 share matrix for the new passkey.
|
|
||||||
Loading…
x
Reference in New Issue
Block a user