Skip to content

[Feature]: Commit-Reveal Scheme #74

@chemonoworld

Description

@chemonoworld

Problem / motivation

Commit-Reveal Scheme Implementation Plan

Goal

Implement a distributed transaction system using commit-reveal scheme to prevent front-running when sending OAuth tokens to each key share node

Proposed solution

System Architecture

Participating Aggregates

  1. Client (embed/oko_attached): Client connected to DApp (only entity that can judge intermediate progress)
  2. Oko Server (backend/oko_api): Central coordinator (Single Source of Truth)
  3. Key Share Nodes (key_share_node): Decentralized key share nodes

Characteristics

  • Orchestration-based Saga pattern (HTTP communication only, no message queue)
  • Oko Server manages and coordinates session state
  • 5-minute timeout (all operations must complete within 5 minutes)
  • Automatic rollback on failure via batch runtime (cronjob or infinite loop service)
  • Idempotency: Same API calls within timeout period for same session are treated as identical
  • Client records progress to oko_server at each step (handles potential disconnections)
  • ECDSA-signed rollback instructions for secure rollback orchestration

Session ID Generation Strategy

UUID v7 (Standard Library)

We use UUID v7 (RFC 9562) for session IDs via standard library implementation.

import { v7 as uuidv7 } from 'uuid';

// Generate session ID
const session_id = uuidv7();
// Example: "018c8f5e-3e80-7a1b-a2c3-d4e5f6789012"

UUID v7 Format

018c8f5e-3e80-7a1b-a2c3-d4e5f6789012
└──────┬──────┘ └┬┘ └──────┬──────┘
  timestamp    ver=7   random
  (48 bits)   (4 bits) (74 bits)

Advantages

  1. Time-ordered: Natural chronological sorting in database indexes
  2. DB Performance: Sequential IDs improve B-tree index performance by ~30-40% vs random UUIDs
  3. Collision Resistant: 74 bits of entropy + timestamp uniqueness
  4. Debuggability: Can extract session creation time from session_id
  5. Standards Compliant: RFC 9562 compliant, battle-tested implementations
  6. Simple: No custom implementation needed, just use standard library

Why UUID v7 over UUID v4?

Feature UUID v4 UUID v7
Ordering Random Timestamp-ordered
DB Index Performance Fragmented inserts Sequential inserts (~30-40% faster)
Storage 128 bits 128 bits
Debuggability No temporal info Contains timestamp
Implementation Standard lib Standard lib (uuid >= 9.0.0)

Session Type & State Definition

Why Session Type?

The SessionType enum allows the system to support different transaction patterns beyond commit-reveal. This provides:

  1. Extensibility: Easy to add new transaction types (e.g., multi-party computation, atomic swaps)
  2. Type Safety: Different session types can have different validation rules and state machines
  3. Monitoring: Track metrics and performance per session type
  4. Routing: Backend can route to appropriate handlers based on session type

Type Definitions

// *** enum will be changed into union type

import type { Bytes33 } from '@oko-wallet/bytes';

// Type aliases (import Bytes33 directly from @oko-wallet/bytes, no re-export)
type NodeURL = string;     // Simple string for node endpoints

// Session types for different transaction patterns
type SessionType =
  | "OAUTH_COMMIT_REVEAL"  // Commit-reveal scheme for OAuth tokens
  // Future: Other transaction types can be added
  // | "MULTI_PARTY_COMPUTATION"
  // | "ATOMIC_SWAP"
  // etc.

// Session state enum
type CommitRevealSessionState =
  // Phase 1: Preparation
  | "INITIALIZED"           // Session created
  | "COMMIT_PHASE"          // Commit phase in progress
  // Phase 2: Reveal waiting
  | "COMMITTED"             // Commit completed on all nodes
  | "REVEAL_PHASE"          // Reveal phase in progress
  // Phase 3: Complete/Failed
  | "COMPLETED"             // Successfully completed
  | "FAILED"                // Failed (some nodes failed)
  | "TIMEOUT"               // 5-minute timeout exceeded
  | "ROLLED_BACK";          // Rollback completed

// Operation types
type OperationType = "signin" | "register" | "reshare";

// Rollback reasons
type RollbackReason =
  | "TIMEOUT"               // 5-minute timeout exceeded
  | "COMMIT_FAILED"         // Failed to commit on required nodes
  | "REVEAL_FAILED"         // Failed to reveal on threshold nodes
  | "USER_CANCELLED"        // User manually cancelled session
  | "NETWORK_ERROR"         // Network partition or node unreachable
  | "VALIDATION_ERROR";     // Validation failed (e.g., token mismatch)

// Node operation status (for commit/reveal tracking)
type NodeOperationStatus = "PENDING" | "SUCCESS" | "FAILED";

// Node status tracking
interface NodeStatus {
  node_name: string;               // Human-readable node identifier
  node_url: NodeURL;               // Node endpoint URL
  status: NodeOperationStatus;     // Operation status
  error_message?: string;          // Error details if status is "FAILED"
  timestamp: Date;                 // When this status was recorded
}

// Encrypted token data
interface EncryptedToken {
  ciphertext: string;       // AES-GCM encrypted OAuth token
  nonce: string;            // 96-bit nonce (hex)
  tag: string;              // Authentication tag (hex)
}

interface CommitRevealSession {
  // Session identification
  session_id: string;                    // UUID v7 (timestamp-ordered, from 'uuid' package)
  session_type: SessionType;             // Type of session/transaction
  client_public_key: Bytes33;            // Client public key for ECDHE (from @oko-wallet/bytes)
  oko_server_public_key: Bytes33;        // Oko Server's ECDHE public key (from @oko-wallet/bytes)
  sdk_version: string;                   // SDK version (semantic versioning, e.g., "1.2.3")

  // User/Wallet information
  user_email: string;
  public_key: Bytes33;                   // Wallet public key

  // OAuth token commitment (hash)
  token_hash: string;                    // SHA256("oko-v{major}-" + oauth_token_string)

  // State management
  state: CommitRevealSessionState;
  created_at: Date;
  updated_at: Date;
  expires_at: Date;                      // created_at + 5 minutes

  // Commit phase tracking
  commit_phase: {
    nodes_committed: NodeStatus[];       // Status of each node's commit
    total_nodes: number;                 // Total number of nodes
    encrypted_tokens: {                  // Store encrypted tokens per node
      [node_url: NodeURL]: EncryptedToken;
    };
  };

  // Reveal phase tracking
  reveal_phase: {
    nodes_revealed: NodeStatus[];        // Status of each node's reveal
    total_nodes: number;
  };

  // Operation type (specific to commit-reveal sessions)
  operation_type: OperationType;

  // Rollback information
  rollback_reason?: RollbackReason;
}

// Oko Server: Long-lived ECDHE keypair (separate from sessions)
interface OkoServerECDHEKeypair {
  id: number;                            // Primary key
  private_key: string;                   // Encrypted ECDHE private key
  public_key: Bytes33;                   // ECDHE public key (not encrypted)
  active: boolean;                       // Currently active keypair
  created_at: Date;
  rotated_at?: Date;                     // When this key was rotated out
}

// Oko Server: Session shared secret (encrypted, for performance)
interface OkoServerSessionSharedSecret {
  session_id: string;                    // FK to CommitRevealSession
  shared_secret: string;                 // Encrypted ECDH result
  created_at: Date;
}

// KS Node: Session data (metadata only, no sensitive keys)
interface KSNodeSession {
  session_id: string;                    // UUID v7 (matches client session)
  client_public_key: Bytes33;            // Client's ECDHE public key
  node_public_key: Bytes33;              // Node's ECDHE public key (from active keypair)
  token_hash: string;                    // SHA256 hash for verification
  sdk_version: string;                   // SDK version
  public_key: Bytes33;                   // Wallet public key
  state: "COMMITTED" | "REVEALED" | "ROLLED_BACK";
  operation_type: OperationType;
  created_at: Date;
  updated_at: Date;
  expires_at: Date;
}

// KS Node: Long-lived ECDHE keypair (separate from sessions)
interface KSNodeECDHEKeypair {
  id: number;                            // Primary key
  private_key: string;                   // Encrypted ECDHE private key
  public_key: Bytes33;                   // ECDHE public key (not encrypted)
  active: boolean;                       // Currently active keypair
  created_at: Date;
  rotated_at?: Date;                     // When this key was rotated out
}

// KS Node: Session shared secret (encrypted, for performance)
interface KSNodeSessionSharedSecret {
  session_id: string;                    // FK to KSNodeSession
  shared_secret: string;                 // Encrypted ECDH result
  created_at: Date;
}

Database Schema (PostgreSQL)

Oko Server: Commit-Reveal Session Table

CREATE TABLE commit_reveal_sessions (
  -- Session identification
  session_id UUID PRIMARY KEY,                 -- UUID v7 (timestamp-ordered)
  session_type VARCHAR(50) NOT NULL,           -- "OAUTH_COMMIT_REVEAL"
  client_public_key VARCHAR(66) NOT NULL,      -- Client's ECDHE public key (Bytes33 hex)
  oko_server_public_key VARCHAR(66) NOT NULL,  -- Oko Server's ECDHE public key (Bytes33 hex)
  sdk_version VARCHAR(20) NOT NULL,            -- SDK version (semantic versioning)

  -- User/Wallet info
  user_email VARCHAR(255) NOT NULL,
  public_key VARCHAR(66) NOT NULL,             -- Wallet public key (Bytes33 hex)

  -- OAuth token commitment
  token_hash VARCHAR(64) NOT NULL,             -- SHA256 hash (hex)

  -- State management
  state VARCHAR(50) NOT NULL,                  -- CommitRevealSessionState union type
  created_at TIMESTAMP NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
  expires_at TIMESTAMP NOT NULL,               -- created_at + 5 minutes

  -- Commit phase data (JSONB for flexibility)
  commit_phase JSONB NOT NULL DEFAULT '{"nodes_committed":[],"total_nodes":0,"encrypted_tokens":{}}',
  -- Structure:
  -- {
  --   "nodes_committed": [
  --     {
  --       "node_name": "ks-node-1",
  --       "node_url": "https://ks-node-1.com",
  --       "status": "success",
  --       "timestamp": "2025-11-18T12:00:00Z"
  --     }
  --   ],
  --   "total_nodes": 5,
  --   "encrypted_tokens": {
  --     "https://ks-node-1.com": {
  --       "ciphertext": "...",
  --       "nonce": "...",
  --       "tag": "..."
  --     }
  --   }
  -- }

  -- Reveal phase data
  reveal_phase JSONB NOT NULL DEFAULT '{"nodes_revealed":[],"total_nodes":0}',
  -- Structure:
  -- {
  --   "nodes_revealed": [
  --     {
  --       "node_name": "ks-node-1",
  --       "node_url": "https://ks-node-1.com",
  --       "status": "success",
  --       "timestamp": "2025-11-18T12:00:30Z"
  --     }
  --   ],
  --   "total_nodes": 5
  -- }

  -- Operation specific data
  operation_type VARCHAR(50) NOT NULL,         -- OperationType: "signin" | "register" | "reshare"

  -- Rollback info
  rollback_reason VARCHAR(50),                 -- RollbackReason union type

  -- Constraints
  CONSTRAINT valid_session_type CHECK (session_type IN ('OAUTH_COMMIT_REVEAL')),
  CONSTRAINT valid_state CHECK (state IN (
    'INITIALIZED', 'COMMIT_PHASE', 'COMMITTED',
    'REVEAL_PHASE', 'COMPLETED', 'FAILED',
    'TIMEOUT', 'ROLLED_BACK'
  )),
  CONSTRAINT valid_operation_type CHECK (operation_type IN ('signin', 'register', 'reshare')),
  CONSTRAINT valid_rollback_reason CHECK (
    rollback_reason IS NULL OR rollback_reason IN (
      'TIMEOUT', 'COMMIT_FAILED', 'REVEAL_FAILED',
      'USER_CANCELLED', 'NETWORK_ERROR', 'VALIDATION_ERROR'
    )
  ),
  CONSTRAINT valid_public_keys CHECK (
    LENGTH(client_public_key) = 66 AND LENGTH(public_key) = 66
  )
);

-- Indexes
CREATE INDEX idx_session_type ON commit_reveal_sessions(session_type);
CREATE INDEX idx_user_email ON commit_reveal_sessions(user_email);
CREATE INDEX idx_state ON commit_reveal_sessions(state);
CREATE INDEX idx_expires_at ON commit_reveal_sessions(expires_at);
CREATE INDEX idx_created_at ON commit_reveal_sessions(created_at DESC); -- DESC for time-ordered queries
CREATE INDEX idx_state_expires ON commit_reveal_sessions(state, expires_at)
  WHERE state NOT IN ('COMPLETED', 'ROLLED_BACK', 'FAILED'); -- Partial index for cleanup job

Benefits of session_type Column:

  1. Query Optimization: SELECT * FROM sessions WHERE session_type = 'OAUTH_COMMIT_REVEAL' AND state = 'TIMEOUT'
  2. Analytics: Easy to track metrics per session type
  3. Cleanup Jobs: Different cleanup strategies per session type
  4. Migration: Can add new types without schema changes

Oko Server: Long-lived ECDHE Keypair Table (Separate from Sessions)

CREATE TABLE oko_server_ecdhe_keypairs (
  -- Keypair identification
  id SERIAL PRIMARY KEY,

  -- ECDHE keys (ENCRYPTED AT REST)
  private_key TEXT NOT NULL,               -- Oko Server's ECDHE private key (encrypted)
  public_key VARCHAR(66) NOT NULL,         -- Oko Server's ECDHE public key (Bytes33 hex)

  -- Status
  active BOOLEAN NOT NULL DEFAULT true,    -- Currently active keypair

  -- Metadata
  created_at TIMESTAMP NOT NULL DEFAULT NOW(),
  rotated_at TIMESTAMP,                    -- When this key was rotated out
);

-- Only one active keypair at a time
CREATE UNIQUE INDEX idx_oko_ecdhe_active ON oko_server_ecdhe_keypairs(active) WHERE active = true;

-- Index for key rotation queries
CREATE INDEX idx_oko_ecdhe_created_at ON oko_server_ecdhe_keypairs(created_at DESC);

Security Notes:

  • Separate Table: Isolates long-lived cryptographic material from session data
  • Encrypted at Rest: private_key MUST be encrypted (not public_key)
  • Access Control: Strictest database permissions on this table
  • Audit Logging: Log all access to this table for security monitoring
  • Key Rotation: Support for key rotation with active flag
  • No Session FK: This is a global keypair, not tied to specific sessions

Why Separate from Sessions?

  1. Lifecycle: Keypairs are long-lived, sessions are ephemeral (5 min)
  2. Security Isolation: One of the most sensitive pieces of data
  3. Reusability: One keypair used across many sessions
  4. Key Rotation: Can rotate keys without affecting old session records
  5. Access Pattern: Rarely accessed (only during init/reveal), sessions accessed frequently

Oko Server: Session Shared Secrets Table (Performance Optimization)

CREATE TABLE oko_server_session_shared_secrets (
  -- Session identification
  session_id UUID PRIMARY KEY,             -- FK to commit_reveal_sessions

  -- Shared secret (ENCRYPTED AT REST)
  shared_secret TEXT NOT NULL,             -- ECDH(oko_server_private_key, client_public_key) (encrypted)

  -- Metadata
  created_at TIMESTAMP NOT NULL DEFAULT NOW(),

  -- Foreign key constraint
  CONSTRAINT fk_oko_session FOREIGN KEY (session_id)
    REFERENCES commit_reveal_sessions(session_id)
    ON DELETE CASCADE
);

-- Index for cleanup (tied to parent session expiration)
CREATE INDEX idx_oko_shared_secret_created_at ON oko_server_session_shared_secrets(created_at);

Why Store Shared Secrets?

  1. Performance: AES-256-GCM decryption (~1-5 μs) is 50-100x faster than ECDH calculation (~200-300 μs)
  2. Scalability: Critical for high-throughput environments (1000+ sessions/sec)
  3. Security: Still encrypted at rest with same encryption as private keys
  4. Trade-off: Extra storage (~64 bytes per session) for massive performance gain

Performance Comparison:

  • On-demand ECDH: ~200-300 μs per operation
  • Decrypt stored secret: ~1-5 μs per operation
  • Speedup: 50-100x faster

Flow:

  • Commit Phase: Calculate ECDH once, encrypt and store
  • Reveal Phase: Decrypt stored secret (no ECDH calculation needed)

KS Node: Session Storage Table

CREATE TABLE ks_node_sessions (
  -- Session identification
  session_id UUID PRIMARY KEY,                 -- UUID v7 (matches client session)
  client_public_key VARCHAR(66) NOT NULL,      -- Client's ECDHE public key (Bytes33 hex)
  node_public_key VARCHAR(66) NOT NULL,        -- Node's ECDHE public key (Bytes33 hex)

  -- OAuth token commitment
  token_hash VARCHAR(64) NOT NULL,             -- SHA256 hash for verification
  sdk_version VARCHAR(20) NOT NULL,            -- SDK version

  -- Wallet info
  public_key VARCHAR(66) NOT NULL,             -- Wallet public key (Bytes33 hex)

  -- State management
  state VARCHAR(50) NOT NULL,                  -- "COMMITTED", "REVEALED", "ROLLED_BACK"
  created_at TIMESTAMP NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
  expires_at TIMESTAMP NOT NULL,               -- created_at + 5 minutes

  -- Operation info
  operation_type VARCHAR(50) NOT NULL,         -- "signin" | "register" | "reshare"
);

-- Indexes
CREATE INDEX idx_ks_session_state ON ks_node_sessions(state);
CREATE INDEX idx_ks_session_expires_at ON ks_node_sessions(expires_at);
CREATE INDEX idx_ks_session_state_expires ON ks_node_sessions(state, expires_at)
  WHERE state NOT IN ('COMPLETED', 'ROLLED_BACK'); -- Partial index for cleanup job

-- Automatic updated_at trigger
CREATE OR REPLACE FUNCTION update_ks_session_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER ks_session_updated_at
BEFORE UPDATE ON ks_node_sessions
FOR EACH ROW
EXECUTE FUNCTION update_ks_session_updated_at();

KS Node: Long-lived ECDHE Keypair Table (Separate from Sessions)

CREATE TABLE ks_node_ecdhe_keypairs (
  -- Keypair identification
  id SERIAL PRIMARY KEY,

  -- ECDHE keys (ENCRYPTED AT REST)
  private_key TEXT NOT NULL,               -- KS Node's ECDHE private key (encrypted)
  public_key VARCHAR(66) NOT NULL,         -- KS Node's ECDHE public key (Bytes33 hex)

  -- Status
  active BOOLEAN NOT NULL DEFAULT true,    -- Currently active keypair

  -- Metadata
  created_at TIMESTAMP NOT NULL DEFAULT NOW(),
  rotated_at TIMESTAMP,                    -- When this key was rotated out

  -- Constraints
  CONSTRAINT valid_public_key CHECK (LENGTH(public_key) = 66)
);

-- Only one active keypair at a time
CREATE UNIQUE INDEX idx_ks_ecdhe_active ON ks_node_ecdhe_keypairs(active) WHERE active = true;

-- Index for key rotation queries
CREATE INDEX idx_ks_ecdhe_created_at ON ks_node_ecdhe_keypairs(created_at DESC);

Security Architecture - Separated Tables:

Why Separate Keypair Tables from Sessions?

  1. Lifecycle Mismatch: Keypairs are long-lived (weeks/months), sessions are ephemeral (5 min)
  2. Security Isolation: Most sensitive data isolated from frequently-accessed session data
  3. Access Control: Strictest database permissions only on keypair tables
  4. Audit Trail: Separate audit logging for key access
  5. Reusability: One keypair serves thousands of sessions
  6. Key Rotation: Rotate keys without touching session history
  7. Performance: Session queries never need to decrypt keys
  8. Compliance: Easier to meet regulatory requirements (PCI-DSS, SOC2)

Encryption Requirements:

  • private_key: MUST be encrypted at rest (public_key is public, no encryption needed)
  • Use application-level AES-256-GCM encryption (faster than pgcrypto)
  • Encryption keys managed via KMS/Vault
  • Never log or expose private keys in plaintext

KS Node: Session Shared Secrets Table (Performance Optimization)

CREATE TABLE ks_node_session_shared_secrets (
  -- Session identification
  session_id UUID PRIMARY KEY,             -- FK to ks_node_sessions

  -- Shared secret (ENCRYPTED AT REST)
  shared_secret TEXT NOT NULL,             -- ECDH(node_private_key, client_public_key) (encrypted)

  -- Metadata
  created_at TIMESTAMP NOT NULL DEFAULT NOW(),

  -- Foreign key constraint
  CONSTRAINT fk_ks_session FOREIGN KEY (session_id)
    REFERENCES ks_node_sessions(session_id)
    ON DELETE CASCADE
);

-- Index for cleanup (tied to parent session expiration)
CREATE INDEX idx_ks_shared_secret_created_at ON ks_node_session_shared_secrets(created_at);

Shared Secret Handling:

  • Stored encrypted in database for performance (50-100x faster than on-demand ECDH)
  • Calculated once during commit phase: shared_secret = ECDH(node_private_key, client_public_key)
  • Encrypted with same AES-256-GCM encryption as private keys
  • Client provides different client_public_key per session
  • Therefore, shared secret is unique per session despite reusing node keypair
  • CASCADE delete ensures automatic cleanup when session is deleted

Example Application-Level Encryption (TypeScript):

import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';

// Encrypt before storing
function encryptKey(plaintext: string, kmsKey: Buffer): string {
  const iv = randomBytes(16);
  const cipher = createCipheriv('aes-256-gcm', kmsKey, iv);
  const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
  const authTag = cipher.getAuthTag();

  // Format: iv:authTag:ciphertext (all hex)
  return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted.toString('hex')}`;
}

// Decrypt after retrieving
function decryptKey(encrypted: string, kmsKey: Buffer): string {
  const [ivHex, authTagHex, ciphertextHex] = encrypted.split(':');
  const iv = Buffer.from(ivHex, 'hex');
  const authTag = Buffer.from(authTagHex, 'hex');
  const ciphertext = Buffer.from(ciphertextHex, 'hex');

  const decipher = createDecipheriv('aes-256-gcm', kmsKey, iv);
  decipher.setAuthTag(authTag);

  return decipher.update(ciphertext) + decipher.final('utf8');
}

// Usage
const kmsKey = await getKMSEncryptionKey(); // From AWS KMS, Vault, etc.
const encryptedPrivateKey = encryptKey(nodePrivateKey, kmsKey);

await db.query(`
  INSERT INTO ks_node_session_keypairs (session_id, node_private_key, shared_secret_with_client)
  VALUES ($1, $2, $3)
`, [sessionId, encryptedPrivateKey, encryptedSharedSecret]);

Complete Flow

Phase 0: Preparation (Client)

Client:
1. Query Key Share Node metadata and SDK version from Oko Server
   - GET /tss/v1/keyshare_nodes
   - Response: {
       nodes: [node1_url, node2_url, node3_url],
       threshold: 2,
       sdk_version: "1.2.3"
     }

2. Generate Session ID using UUID v7 standard library
   - const session_id = uuidv7();

3. Generate ECDHE Key Pair (secp256k1)
   - private_key: 32 bytes (secure random)
   - public_key: 33 bytes (compressed)

4. Obtain OAuth token (existing flow)
   - Google/Auth0 OAuth callback
   - id_token verification

5. Calculate OAuth token hash
   - Extract major version from SDK version (e.g., "1.2.3" -> "1")
   - prefix = "oko-v" + major + "-" (e.g., "oko-v1-")
   - token_hash = SHA256(prefix + oauth_token_string)
   - Store token_hash as hex string (64 characters)

Phase 1: Session Initialization and Commit

┌─────────────────────────────────────────────────────────────────┐
│ Step 1.1: Register Session (Client → Oko Server)                │
└─────────────────────────────────────────────────────────────────┘

Client → Oko Server:
POST /tss/v1/session/commit-reveal/init
Headers:
  Authorization: Bearer {oauth_id_token}
Body:
{
  "session_id": "a1b2c3d4-e5f6-7890-1234-567890123456",
  "session_type": "OAUTH_COMMIT_REVEAL", // Type of transaction session
  "client_public_key": "02a1b2c3d4...",  // ECDHE public key (33 bytes hex)
  "public_key": "02abcdef...",            // Wallet public key
  "token_hash": "e3b0c44298fc1c...",      // SHA256("oko-v{major}-" + token)
  "sdk_version": "1.2.3",                 // SDK version
  "operation_type": "signin" | "register" | "reshare",
  "node_urls": ["https://ks-node-1.com", "https://ks-node-2.com", ...]
}

Oko Server Processing:
1. Verify OAuth token (existing middleware)
2. Verify token_hash:
   - Extract major version from sdk_version
   - Recalculate: SHA256("oko-v{major}-" + oauth_token_from_header)
   - Compare with received token_hash
3. Retrieve active ECDHE keypair:
   - Query: SELECT * FROM oko_server_ecdhe_keypairs WHERE active = true
   - Get oko_server_public_key (no decryption needed)
   - Decrypt oko_server_private_key for ECDH calculation
4. Calculate Shared Secret:
   - shared_secret = ECDH(oko_server_private_key, client_public_key)
   - This is calculated once and stored
5. Create Session in DB (transaction):
   a) Insert into commit_reveal_sessions:
      - Check session_id duplication (idempotency: same session within 5 min = same request)
      - state = INITIALIZED
      - expires_at = now + 5 minutes
      - Store token_hash, sdk_version, oko_server_public_key, client_public_key
   b) Insert into oko_server_session_shared_secrets:
      - Encrypt and store shared_secret
6. Response:
{
  "success": true,
  "data": {
    "session_id": "a1b2c3d4-...",
    "expires_at": "2025-11-18T12:05:00Z",
    "state": "INITIALIZED",
    "oko_server_public_key": "03d4e5f6..."  // Oko Server's ECDHE public key
  }
}

┌─────────────────────────────────────────────────────────────────┐
│ Step 1.2: Perform ECDHE and Commit with each node               │
│           (Client → KS Nodes)                                    │
└─────────────────────────────────────────────────────────────────┘

FOR EACH Key Share Node:

Client → KS Node:
POST /keyshare/v1/commit-reveal/commit
Body:
{
  "session_id": "a1b2c3d4-...",
  "client_public_key": "02a1b2c3d4...",    // ECDHE public key
  "public_key": "02abcdef...",              // Wallet public key
  "token_hash": "e3b0c44298fc1c...",        // SHA256("oko-v{major}-" + token)
  "sdk_version": "1.2.3",                   // SDK version
  "operation_type": "signin" | "register" | "reshare"
}

KS Node Processing:
1. Retrieve active ECDHE keypair:
   - Query: SELECT * FROM ks_node_ecdhe_keypairs WHERE active = true
   - Get node_public_key (no decryption needed)
   - Decrypt node_private_key for ECDH calculation
2. Calculate Shared Secret:
   - shared_secret = ECDH(node_private_key, client_public_key)
   - This is calculated once and stored
3. Store Session info in PostgreSQL (transaction):
   a) Insert into ks_node_sessions:
      {
        session_id,
        client_public_key,
        node_public_key,      // Node's ECDHE public key (from active keypair)
        token_hash,           // Store token hash for later verification
        sdk_version,          // Store SDK version
        public_key,           // Wallet public key
        state: "COMMITTED",
        operation_type,
        created_at: now,
        expires_at: now + 5 minutes
      }
   b) Insert into ks_node_session_shared_secrets:
      - Encrypt and store shared_secret
4. Response:
{
  "success": true,
  "data": {
    "session_id": "a1b2c3d4-...",
    "node_public_key": "03f1e2d3c4...",    // Node's ECDHE public key
    "state": "COMMITTED"
  }
}

Client Processing (after receiving responses from all nodes and Oko Server):
1. Calculate Shared Secret with each node and Oko Server:
   - Per node: ECDH(client_private_key, node_public_key)
   - With Oko Server: ECDH(client_private_key, oko_server_public_key)
2. Derive Encryption Keys:
   - Extract major version from SDK version (e.g., "1.2.3" -> "1")
   - prefix = "oko-v" + major + "-" (e.g., "oko-v1-")
   - encryption_key = SHA256(prefix + shared_secret)
3. Encrypt OAuth token (per node and Oko Server):
   - Use AES-256-GCM
   - Key: encryption_key (derived above)
   - Nonce: 96 bits random
   - encrypted_token = AES-GCM.encrypt(encryption_key, nonce, oauth_token_string)
   - Result: {ciphertext, nonce, tag}
4. Verify all nodes responded successfully

┌─────────────────────────────────────────────────────────────────┐
│ Step 1.3: Report Commit Complete (Client → Oko Server)          │
└─────────────────────────────────────────────────────────────────┘

Client → Oko Server:
POST /tss/v1/session/commit-reveal/commit-complete
Headers:
  Authorization: Bearer {oauth_id_token}
Body:
{
  "session_id": "a1b2c3d4-...",
  "encrypted_tokens": {
    "https://ks-node-1.com": {
      "ciphertext": "e8f9a0b1c2...",
      "nonce": "d3e4f5a6b7c8...",
      "tag": "c9d0e1f2a3b4..."
    },
    "https://ks-node-2.com": { ... },
    ...
  }
}

Oko Server Processing:
1. Query and verify Session:
   - state == INITIALIZED or COMMIT_PHASE
   - Check timeout (expires_at > now)
   - OAuth token email == session user_email
2. Update Session:
   - state = COMMITTED
   - Store commit_phase.encrypted_tokens
   - commit_phase.nodes_committed = all node URLs
3. Response:
{
  "success": true,
  "data": {
    "session_id": "a1b2c3d4-...",
    "state": "COMMITTED",
    "can_proceed_to_reveal": true
  }
}

Phase 2: Reveal Phase

┌─────────────────────────────────────────────────────────────────┐
│ Step 2.1: Start Reveal (Client → KS Nodes)                      │
└─────────────────────────────────────────────────────────────────┘

FOR EACH Key Share Node:

Client → KS Node:
POST /keyshare/v1/commit-reveal/reveal
Headers:
  Authorization: Bearer {oauth_id_token}   // Send plaintext OAuth token
Body:
{
  "session_id": "a1b2c3d4-...",
  "public_key": "02abcdef...",             // Wallet public key
  "operation_type": "signin" | "register" | "reshare",

  // Additional data per operation
  // For signin: none
  // For register:
  "key_share": "point256_encoded_string",

  // For reshare:
  "reshared_key_share": "point256_encoded_string"
}

KS Node Processing:
1. Query Session:
   - Retrieve session info from ks_node_sessions by session_id
   - Verify state == "COMMITTED"
   - Check timeout (expires_at > now)
2. Retrieve and decrypt shared secret:
   - Query: SELECT * FROM ks_node_session_shared_secrets WHERE session_id = ?
   - Decrypt shared_secret (AES-256-GCM decryption: ~1-5 μs)
3. Verify OAuth token (existing bearerTokenMiddleware):
   - Google/Auth0 verification
   - Extract email from OAuth token
4. Verify Token Commitment:
   a) Extract major version from stored sdk_version
   b) Calculate token hash: SHA256("oko-v{major}-" + received_oauth_token)
   c) Compare calculated hash with stored token_hash
   d) If mismatch: reject with "TOKEN_MISMATCH" error
5. Verify token was not front-run:
   a) Derive encryption key: SHA256("oko-v{major}-" + shared_secret)
   b) Encrypt received token: AES-GCM.encrypt(encryption_key, nonce, received_oauth_token)
   c) This ensures the token matches the commitment from commit phase

6. Perform Operation:
   a) signin: getKeyShare(email, public_key)
   b) register: registerKeyShare(email, public_key, key_share)
   c) reshare: reshareKeyShare(email, public_key, reshared_key_share)

7. Update Session state:
   - state = "REVEALED"

8. Response:
{
  "success": true,
  "data": {
    "session_id": "a1b2c3d4-...",
    "state": "REVEALED",
    // Response data per operation
    // signin: { "share": "..." }
    // register: {}
    // reshare: {}
  }
}

┌─────────────────────────────────────────────────────────────────┐
│ Step 2.2: Report Reveal Complete (Client → Oko Server)          │
└─────────────────────────────────────────────────────────────────┘

Client Processing (after receiving JWT from KS nodes):
1. Prepare completion message:
   {
     "session_id": "a1b2c3d4-...",
     "nodes_succeeded": ["https://ks-node-1.com", "https://ks-node-2.com", ...],
     "nodes_failed": [],  // List of failed nodes (if any)
     "timestamp": "2025-11-18T12:03:45Z"
   }
2. Encrypt with Oko Server's shared secret:
   - encryption_key = SHA256("oko-v{major}-" + shared_secret_with_oko_server)
   - encrypted_message = AES-GCM.encrypt(encryption_key, nonce, JSON.stringify(message))

Client → Oko Server:
POST /tss/v1/session/commit-reveal/reveal-complete
Headers:
  Authorization: Bearer {oauth_id_token}
Body:
{
  "encrypted_completion": {
    "ciphertext": "...",
    "nonce": "...",
    "tag": "..."
  }
}

Oko Server Processing:
1. Query session from commit_reveal_sessions by session_id
2. Retrieve and decrypt shared secret:
   - Query: SELECT * FROM oko_server_session_shared_secrets WHERE session_id = ?
   - Decrypt shared_secret (AES-256-GCM decryption: ~1-5 μs)
3. Decrypt completion message using shared_secret
4. Verify Session:
   - state == COMMITTED or REVEAL_PHASE
   - Check timeout
   - Verify OAuth token email matches session
5. Determine success criteria:
   - nodes_succeeded.length >= threshold
6. Update Session:
   a) Success: state = COMPLETED
   b) Failure: state = FAILED, start rollback
7. Response:
{
  "success": true,
  "data": {
    "session_id": "a1b2c3d4-...",
    "state": "COMPLETED",
    "timestamp": "2025-11-18T12:03:45Z"
  }
}

Error Handling and Rollback

Cases Requiring Rollback

  1. Commit Phase Failure

    • Commit failed on some nodes
    • Criteria: Less than majority of nodes succeeded in commit
    • Action: Request commit cancellation to all nodes
  2. Reveal Phase Failure

    • Reveal failed on some nodes (OAuth token mismatch, verification failure, etc.)
    • Criteria: Less than threshold nodes succeeded in reveal
    • Action: For register/reshare, delete data from successful nodes
  3. Timeout

    • Automatic rollback after 5 minutes
    • Oko Server background job: Periodically check and rollback expired sessions

Rollback Flow

┌─────────────────────────────────────────────────────────────────┐
│ Rollback Flow                                                    │
└─────────────────────────────────────────────────────────────────┘

Oko Server Processing (initiated by timeout batch job or failure):
1. Prepare rollback instruction:
   {
     "session_id": "a1b2c3d4-...",
     "reason": "TIMEOUT" | "COMMIT_FAILED" | "REVEAL_FAILED",
     "timestamp": "2025-11-18T12:10:00Z",
     "operation": "ROLLBACK"
   }
2. Sign with Oko Server's ECDSA private key:
   - message = JSON.stringify(rollback_instruction)
   - signature = ECDSA.sign(oko_server_private_key, SHA256(message))
3. Send to all KS Nodes

Oko Server → All KS Nodes:
POST /keyshare/v1/commit-reveal/rollback
Body:
{
  "rollback_instruction": {
    "session_id": "a1b2c3d4-...",
    "reason": "TIMEOUT" | "COMMIT_FAILED" | "REVEAL_FAILED",
    "timestamp": "2025-11-18T12:10:00Z",
    "operation": "ROLLBACK"
  },
  "signature": "3045022100...",          // ECDSA signature
  "oko_server_public_key": "02abcd..."   // Oko Server's ECDSA public key for verification
}

KS Node Processing:
1. Verify ECDSA signature:
   - Verify signature against known oko_server_public_key
   - If signature invalid: reject request
2. Verify timestamp (prevent replay):
   - Check timestamp is recent (within 5 minutes)
3. Query Session
4. Handle based on state:
   - COMMITTED: Delete session info
   - REVEALED (register/reshare): Delete corresponding key share
5. Response:
{
  "success": true,
  "data": {
    "session_id": "a1b2c3d4-...",
    "rolled_back": true
  }
}

Oko Server:
1. Update Session state: state = ROLLED_BACK
2. Record rollback reason

Timeout Management

Oko Server: Background Job

// Run every minute
async function cleanupExpiredSessions() {
  const expiredSessions = await db.query(`
    SELECT session_id FROM commit_reveal_sessions
    WHERE expires_at < NOW()
      AND state NOT IN ('COMPLETED', 'ROLLED_BACK', 'FAILED')
  `);

  for (const session of expiredSessions) {
    await rollbackSession(session.session_id, "TIMEOUT");
  }
}

setInterval(cleanupExpiredSessions, 60_000);  // 1 minute

Note: No need to clean up ECDHE keypairs since they are long-lived and stored separately from sessions.

Key Share Node: Session Cleanup

// Periodically clean up expired sessions in PostgreSQL
async function cleanupExpiredNodeSessions() {
  const expiredSessions = await db.query(`
    SELECT session_id FROM ks_node_sessions
    WHERE expires_at < NOW()
      AND state NOT IN ('COMPLETED', 'ROLLED_BACK')
  `);

  for (const session of expiredSessions) {
    await db.query('DELETE FROM ks_node_sessions WHERE session_id = $1', [session.session_id]);
  }
}

setInterval(cleanupExpiredNodeSessions, 60_000);  // 1 minute

Note: No need to clean up ECDHE keypairs since they are long-lived and stored separately from sessions.


API Endpoint Definitions

Oko Server (backend/oko_api or backend/tss_api)

// Initialize Session
POST /tss/v1/session/commit-reveal/init
Headers: Authorization: Bearer {oauth_id_token}
Body: InitCommitRevealSessionRequest
Response: InitCommitRevealSessionResponse

// Report Commit Complete
POST /tss/v1/session/commit-reveal/commit-complete
Headers: Authorization: Bearer {oauth_id_token}
Body: CommitCompleteRequest
Response: CommitCompleteResponse

// Report Reveal Complete
POST /tss/v1/session/commit-reveal/reveal-complete
Headers: Authorization: Bearer {oauth_id_token}
Body: RevealCompleteRequest
Response: RevealCompleteResponse

// Query Session State
GET /tss/v1/session/commit-reveal/{session_id}
Headers: Authorization: Bearer {oauth_id_token}
Response: CommitRevealSession

// Cancel Session (Manual Rollback)
POST /tss/v1/session/commit-reveal/{session_id}/cancel
Headers: Authorization: Bearer {oauth_id_token}
Response: CancelSessionResponse

Key Share Node (key_share_node)

// Commit Phase: ECDHE public key exchange
POST /keyshare/v1/commit-reveal/commit
Body: CommitRequest
Response: CommitResponse (includes node_public_key)

// Reveal Phase: Submit OAuth token and perform operation
POST /keyshare/v1/commit-reveal/reveal
Headers: Authorization: Bearer {oauth_id_token}
Body: RevealRequest
Response: RevealResponse

// Rollback Request (Only callable by Oko Server)
POST /keyshare/v1/commit-reveal/rollback
Body: RollbackRequest
Response: RollbackResponse

Security Considerations

1. Two Types of Keypairs

A. Static ECDSA Keypairs (for Rollback Signing):

  • Each party (oko_server and ks_nodes) maintains a long-lived ECDSA keypair for:
    • Signing rollback instructions (Oko Server)
    • Verifying rollback instructions (KS Nodes)
  • Public keys must be publicly accessible:
    • GET /api/v1/ecdsa-public-key (Oko Server)
    • GET /keyshare/v1/ecdsa-public-key (KS Nodes)
  • Key rotation policy:
    • Rotate keys every 90 days (recommended)
    • Immediate rotation if compromise suspected
    • Old keys backed up for forensics
  • Key storage:
    • Private keys stored in secure key management system (e.g., AWS KMS, HashiCorp Vault)
    • Never log or transmit private keys

B. Long-lived ECDHE Keypairs (for Token Encryption):

  • Each party (oko_server and ks_nodes) maintains ONE active long-lived ECDHE keypair
  • Reused across thousands of sessions
  • Public keys must be publicly accessible:
    • GET /api/v1/ecdhe-public-key (Oko Server)
    • GET /keyshare/v1/ecdhe-public-key (KS Nodes)
  • Key Refreshing via CLI:
    # Oko Server
    oko-server ecdhe-keys rotate --backup-old
    
    # KS Node
    ks-node ecdhe-keys rotate --backup-old
  • Key rotation policy:
    • Rotate keys every 30-90 days (recommended)
    • Immediate rotation if compromise suspected
    • Graceful rotation with overlap period for in-flight sessions
  • Storage:
    • Separate database table from sessions
    • Private keys encrypted at rest via KMS/Vault
    • Public keys stored plaintext

C. Client Session-specific ECDHE Keypairs:

  • Client generates NEW keypair per session
  • Used only within that session, discarded after
  • Ensures shared secret is unique per session

2. ECDHE Shared Secret Protection

  • Independent shared secret between Client and each KS Node
  • Independent shared secret between Client and Oko Server
  • Shared secrets stored encrypted for performance (50-100x faster than on-demand ECDH)
  • Calculated once during commit phase: ECDH(node_private_key, client_public_key)
  • Stored encrypted with same AES-256-GCM encryption as private keys
  • Retrieved and decrypted during reveal phase (~1-5 μs vs ~200-300 μs for ECDH)
  • CASCADE deleted when parent session is deleted
  • Derive encryption keys with version prefix: SHA256("oko-v{major}-" + shared_secret)

3. OAuth Token Encryption

  • Use AES-256-GCM (authenticated encryption)
  • Nonce: 96 bits random (prevent reuse)
  • Integrity verification through tag
  • Encryption key derivation: SHA256("oko-v{major}-" + shared_secret)
  • Token hash commitment: SHA256("oko-v{major}-" + oauth_token)

4. Replay Attack Prevention

  • Session ID is one-time use (cannot reuse same session_id)
  • Cannot commit again with same session_id after commit complete
  • Node verifies session state during reveal
  • ECDSA-signed rollback instructions with timestamps (5-minute freshness)
  • Idempotency: Same API calls within timeout are treated as identical

5. Front-running Attack Prevention

  • Token commitment via hash in commit phase
  • Token revealed only after all nodes committed
  • Each node independently verifies token hash matches commitment
  • Parallel reveal prevents timing-based front-running

6. Timing Attack Prevention

  • Perform reveal simultaneously on all nodes (Client makes parallel requests)
  • Oko Server waits for all nodes to complete reveal
  • Constant-time comparison for token hash verification

7. Man-in-the-Middle Prevention

  • All communication enforces HTTPS
  • Client verifies TLS certificate of each node
  • ECDHE provides perfect forward secrecy

8. Rollback Security

  • Rollback instructions signed with Oko Server's ECDSA key
  • KS Nodes verify signature before executing rollback
  • Timestamp verification prevents replay attacks
  • Only authorized Oko Server can issue rollback commands

Integration with Existing OAuth Flow

Before (Current)

Client:
1. Obtain OAuth token
2. Send plaintext token directly to KS Node
   POST /keyshare/v1/register
   Authorization: Bearer {id_token}

After (Commit-Reveal)

Client:
1. Obtain OAuth token
2. Generate ECDHE Key Pair → Generate Session ID
3. Register session with Oko Server (Phase 1.1)
4. Commit to each KS Node (Phase 1.2)
5. Report commit complete to Oko Server (Phase 1.3)
6. Reveal to each KS Node - Send OAuth token (Phase 2.1)
7. Report reveal complete to Oko Server (Phase 2.2)

Migration Strategy

  1. Phase 1: Add New Endpoints (Backward Compatible)

    • Keep existing /keyshare/v1/register
    • Add new /keyshare/v1/commit-reveal/*
    • Client can choose via feature flag
  2. Phase 2: Gradual Migration

    • New users use commit-reveal
    • Existing users transition at reshare time
  3. Phase 3: Deprecate Old Endpoints

    • Remove old endpoints after grace period

Implementation Priorities

Milestone 1: Core Infrastructure & Keypair Management

  • Oko Server & KS Nodes: Implement static ECDSA keypair generation and storage (for rollback signing)
  • Oko Server & KS Nodes: Implement public key exposure endpoints (for ECDSA and ECDHE)
  • Oko Server & KS Nodes: Implement CLI for key refreshing (both ECDSA and ECDHE)
  • Oko Server: Design Session management DB schema
    • commit_reveal_sessions table (add token_hash, sdk_version, oko_server_public_key)
    • oko_server_ecdhe_keypairs table (long-lived keypairs, separate from sessions)
    • oko_server_session_shared_secrets table (encrypted, FK CASCADE to sessions)
  • KS Node: Design Session storage PostgreSQL table schema
    • ks_node_sessions table (metadata only)
    • ks_node_ecdhe_keypairs table (long-lived keypairs, separate from sessions)
    • ks_node_session_shared_secrets table (encrypted, FK CASCADE to sessions)
  • Oko Server & KS Nodes: Implement application-level AES-256-GCM encryption
  • Oko Server & KS Nodes: Integrate KMS/Vault for encryption key management
  • Oko Server & KS Nodes: Generate and store initial ECDHE keypairs
  • Oko Server: Implement Session CRUD API
  • KS Node: Implement Session CRUD
  • Client: ECDHE Key Pair generation and UUID v7 Session ID utilities
  • Client: Implement token hash calculation utility

Milestone 2: Commit Phase

  • Oko Server: Implement POST /commit-reveal/init with token_hash verification
  • Oko Server: Retrieve active ECDHE keypair and calculate shared secret (ECDH)
  • Oko Server: Encrypt and store shared secret in oko_server_session_shared_secrets
  • KS Node: Implement POST /commit-reveal/commit with token_hash storage
  • KS Node: Retrieve active ECDHE keypair and calculate shared secret (ECDH)
  • KS Node: Encrypt and store shared secret in ks_node_session_shared_secrets
  • Client: Implement Commit flow with token hash calculation
  • Client: Implement encryption key derivation (SHA256 with version prefix)
  • Oko Server: Implement POST /commit-reveal/commit-complete

Milestone 3: Reveal Phase

  • KS Node: Implement POST /commit-reveal/reveal with token hash verification
  • KS Node: Retrieve and decrypt shared secret from ks_node_session_shared_secrets
  • KS Node: Verify token commitment matches reveal
  • Client: Implement Reveal flow with parallel node requests
  • Client: Encrypt completion message with Oko Server's shared secret
  • Oko Server: Implement POST /commit-reveal/reveal-complete
  • Oko Server: Retrieve and decrypt shared secret from oko_server_session_shared_secrets
  • Oko Server: Decrypt completion message

Milestone 4: Error Handling & Rollback

  • Oko Server: Implement ECDSA-signed rollback instruction generation
  • KS Node: Implement Rollback endpoint with signature verification
  • Oko Server: Implement Timeout background job (batch runtime)
  • KS Node: Implement Timeout background job for session cleanup
  • Oko Server: Implement idempotency checks (5-minute window)
  • KS Node: Implement connection pooling for PostgreSQL performance
  • Oko Server & KS Nodes: Implement database ACLs for ECDHE keypair tables
  • Oko Server & KS Nodes: Implement ECDHE key rotation mechanism

Milestone 5: Integration & Testing

  • Client: Integrate with existing OAuth flow
  • E2E tests: Normal flow with token hash verification
  • E2E tests: Error cases and rollback with ECDSA signatures
  • E2E tests: Idempotency tests (duplicate requests within timeout)
  • E2E tests: ECDHE keypair reusability across multiple sessions
  • E2E tests: ECDHE key rotation without service interruption
  • E2E tests: Shared secret CASCADE deletion when session expires
  • Load Testing: Concurrent session handling performance
  • Load Testing: Verify AES-GCM decryption performance vs ECDH calculation
  • Security Audit: Key management and encryption implementation
  • Security Audit: Database ACLs and access patterns for keypair and shared secret tables
  • Security Audit: ECDHE keypair lifecycle and rotation procedures
  • Security Audit: Shared secret encryption and storage security

Open Questions and Review Items

1. Session ID Generation Method

Current Proposal: UUID v7 using standard library (e.g., uuid npm package v9.0.0+)
Benefits:

  • Timestamp-ordered for better DB index performance (~30-40% faster inserts)
  • Debuggable (can extract session creation time)
  • RFC 9562 compliant
  • Battle-tested implementation
  • Simple, no custom logic needed
    Alternative Considered: UUID v4 (pure random)
    Why UUID v7 Chosen: Sequential inserts improve B-tree performance significantly

2. Token Hash vs Encrypted Token Storage

Current Proposal: Store token_hash (SHA256) in commit phase, verify during reveal
Benefits:

  • Commitment scheme: proves token wasn't changed
  • No need to store encrypted token
  • Each node independently verifies
    Alternative: Store encrypted tokens and compare
    Choice: Token hash approach (simpler, more secure)

3. Verification Method During Reveal

Current Proposal: KS Node independently verifies OAuth token and token_hash
Benefits:

  • Maintains decentralization
  • Each node independently validates commitment
  • No reliance on Oko Server for verification
    Alternative: Request verification from Oko Server (centralization)
    Choice: Maintain current proposal (preserve decentralization characteristics)

4. ECDHE vs RSA

Current Proposal: ECDHE with secp256k1 (reuse existing curve)
Alternative: RSA-OAEP
Choice: ECDHE (smaller key size, leverage existing crypto stack, perfect forward secrecy)

5. Key Derivation Strategy

Current Proposal: SHA256("oko-v{major}-" + input)
Benefits:

  • Version-specific derivation (major version only)
  • Simple and fast
  • SDK version compatibility enforcement
    Considerations:
  • Major version bumps require migration
  • Prefix prevents cross-version attacks
    Alternative: HKDF (more complex but more flexible)
    Choice: SHA256 with version prefix (simplicity, sufficient security)

6. Static ECDSA Keypair Management

Current Proposal:

  • Long-lived keypairs for signing/verifying rollback instructions
  • CLI-based key rotation
  • Public keys exposed via API
    Benefits:
  • Simple deployment
  • Easy key rotation
  • Public key discovery
    Considerations:
  • Key compromise detection strategy needed
  • Key backup and recovery procedures
  • Multi-region deployment key synchronization
    Recommendation: Start with single keypair, add HSM integration later

7. Session Storage (KS Node)

Decision: PostgreSQL table
Benefits:

  • Consistent with Oko Server storage approach
  • Strong consistency guarantees
  • ACID transactions for state updates
  • Simplified infrastructure (no additional service)
  • Built-in encryption at rest support
    Considerations:
  • TTL management via periodic cleanup job
  • Sensitive data (node_private_key, shared_secret) must be encrypted at rest
  • Index on expires_at for efficient cleanup queries
  • Consider connection pooling for performance

8. Background Job Implementation

Oko Server:

  • Node.js setInterval
  • Cron job (PM2 ecosystem)
  • Bull Queue (more robust)
    Recommendation: Start with setInterval, migrate to Bull Queue later
    Considerations:
  • Distributed locking for multi-instance deployments
  • Batch size optimization
  • Failed rollback retry strategy

9. Session Type Enum Design

Current Proposal: Single SessionType enum for all transaction types
Benefits:

  • Extensible: Easy to add new transaction patterns (MPC, atomic swaps, etc.)
  • Type-safe routing in backend
  • Better metrics and monitoring per session type
  • Schema evolution without breaking changes
    Considerations:
  • Each session type may need its own state machine
  • Different timeout policies per session type
  • Validation rules vary by session type

10. Idempotency Implementation

Current Proposal: Same session_id within 5-minute window = identical request
Benefits:

  • Handles client disconnections gracefully
  • Prevents duplicate sessions
  • Simple to implement
    Considerations:
  • Session state must be checked on duplicate requests
  • Response must be identical or indicate current state
  • Edge case: request arrives during state transition

Expected Challenges

1. Timing Issues

  • Problem: Some nodes respond slowly when making parallel requests to multiple nodes
  • Solution: Use Promise.allSettled on Client, set timeout
  • New consideration: Client tracks progress to oko_server at each step

2. Network Partition

  • Problem: Some nodes temporarily unreachable
  • Solution: Threshold-based success determination (e.g., OK if 3/5 succeed)
  • New consideration: Client disconnection during progress reporting

3. Rollback Failure

  • Problem: Stale data remains on some nodes when rollback request fails
  • Solutions:
    • TTL-based automatic expiration (5 minutes)
    • Periodic cleanup job
    • Overwrite on next operation
    • ECDSA signature verification prevents unauthorized rollbacks

4. Concurrent Sessions

  • Problem: Same user performing concurrent operations from multiple browsers
  • Solution: No issue as Session IDs differ (each is independent transaction)
  • Idempotency: Same session_id within timeout = same request

5. Front-running Attack Scenario

  • Attack: Malicious KS Node attempts reveal immediately after commit
  • Defense:
    • Token hash commitment prevents token modification
    • Oko Server verifies state == COMMITTED during reveal
    • Only Client can initiate reveal (requires OAuth token)
    • Nodes cannot share information (independent ECDHE)
    • Each node independently verifies token_hash matches received token

6. Key Management Challenges

  • Problem: Managing static ECDSA keypairs across multiple nodes
  • Challenges:
    • Key rotation without downtime
    • Key compromise detection
    • Multi-region key synchronization
  • Solutions:
    • Graceful key rotation with overlap period
    • Key versioning in API responses
    • Centralized key management service (future)

7. Idempotency Edge Cases

  • Problem: Client retries during state transition
  • Example: Client sends commit-complete, disconnects, retries while state is updating
  • Solution:
    • Database transaction isolation
    • Return current state if session already processed
    • Client handles "already completed" responses gracefully

8. Batch Runtime Scaling

  • Problem: Large number of expired sessions to rollback
  • Challenges:
    • Background job performance
    • Distributed deployment coordination
    • Failed rollback retry logic
  • Solutions:
    • Batch processing with configurable size
    • Distributed locking (Redis)
    • Dead letter queue for failed rollbacks

9. Token Hash Collision

  • Problem: SHA256 collision (theoretical)
  • Probability: ~2^-256 (negligible)
  • Mitigation: Use full token during reveal verification (not just hash)

10. Client Progress Tracking Failure

  • Problem: Client cannot record progress to oko_server
  • Example: Client commits to all nodes but disconnects before reporting to oko_server
  • Solution:
    • Batch runtime detects orphaned sessions (committed on nodes but not in oko_server)
    • Timeout-based cleanup
    • Client retry with idempotency

11. PostgreSQL Performance for Session Storage

  • Problem: High-frequency session reads/writes on KS Nodes
  • Challenges:
    • Connection pool exhaustion
    • Index contention on high-traffic sessions
    • Encryption/decryption overhead for sensitive data
  • Solutions:
    • Connection pooling with appropriate pool size (e.g., pg-pool, pgbouncer)
    • Read replicas for query distribution (if needed)
    • Optimize index usage (partial indexes on expires_at)
    • Consider in-memory caching layer for active sessions (with TTL)
    • Application-level encryption for sensitive fields (faster than pgcrypto)
    • Batch cleanup operations during low-traffic periods

12. Database Schema Migration

  • Problem: Schema changes across distributed KS Nodes
  • Challenges:
    • Zero-downtime migrations
    • Version compatibility during rollout
    • Rollback procedures
  • Solutions:
    • Blue-green deployment strategy
    • Backward-compatible schema changes
    • Feature flags for new schema features
    • Comprehensive migration testing

Summary of Key Requirements

Token Commitment & Verification

  1. Commit Phase: Client sends token_hash = SHA256("oko-v{major}-" + oauth_token)
  2. Reveal Phase: KS Node verifies hash matches received token
  3. Prevents: Token modification and front-running attacks

Encryption Key Derivation

  1. Token Hash: SHA256("oko-v{major}-" + oauth_token_string)
  2. Encryption Key: SHA256("oko-v{major}-" + ecdhe_shared_secret)
  3. Version Prefix: "oko-v1-" for SDK version 1.x.x (major version only)

Client Progress Tracking

  1. Client is the only entity that can judge intermediate progress
  2. Client records progress to oko_server at each step
  3. Handles disconnections gracefully through idempotency
  4. Same session_id within 5-minute timeout = identical request

Batch Runtime for Rollback

  1. Background job (cronjob or infinite loop service) on oko_server
  2. Periodically checks for timeout sessions
  3. Issues ECDSA-signed rollback instructions
  4. Handles orphaned sessions (committed on nodes but not reported)

Static ECDSA Keypair Management

  1. Purpose: Signing/verifying rollback instructions
  2. Exposure: Public keys accessible via API endpoints
  3. Rotation: CLI-based key refresh with backup
  4. Storage: Secure key management system (KMS/Vault)

Long-lived ECDHE Keypairs (Oko Server & KS Nodes)

  1. Purpose: Perfect forward secrecy for token encryption across many sessions
  2. Lifecycle: Long-lived (weeks/months), rotated periodically via CLI
  3. Reusability: One keypair serves thousands of sessions
  4. Storage: Separate table from sessions, encrypted at rest
  5. Parties:
    • Oko Server: One active ECDHE keypair
    • Each KS Node: One active ECDHE keypair
    • Client: Generates new ECDHE keypair per session

SDK Version Management

  1. Stored with each session for compatibility tracking
  2. Major version extracted for key derivation prefix
  3. Enables version-specific protocol handling

Session Storage Architecture

  1. Oko Server: PostgreSQL tables
    • commit_reveal_sessions: Session metadata (ephemeral, 5-minute TTL)
    • oko_server_ecdhe_keypairs: Long-lived ECDHE keypairs (weeks/months lifetime)
    • oko_server_session_shared_secrets: Encrypted shared secrets (ephemeral, FK CASCADE)
  2. KS Nodes: PostgreSQL tables
    • ks_node_sessions: Session metadata (ephemeral, 5-minute TTL)
    • ks_node_ecdhe_keypairs: Long-lived ECDHE keypairs (weeks/months lifetime)
    • ks_node_session_shared_secrets: Encrypted shared secrets (ephemeral, FK CASCADE)
  3. Security Isolation:
    • Long-lived keypairs completely separated from ephemeral sessions
    • No FK constraint between sessions and keypairs (different lifecycles)
    • Shared secrets linked to sessions via FK CASCADE (auto-cleanup)
    • Keypairs isolated for strictest access control
  4. Encryption at Rest:
    • Application-level AES-256-GCM encryption via KMS/Vault
    • Private keys encrypted, public keys remain plaintext
    • Shared secrets encrypted with same mechanism
  5. Shared Secrets:
    • Stored encrypted for performance (50-100x faster than on-demand ECDH)
    • Calculated once during commit: ECDH(node_private_key, client_public_key)
    • Retrieved and decrypted during reveal (~1-5 μs vs ~200-300 μs for ECDH)
    • Unique per session despite reusing node keypair
    • CASCADE deleted when parent session expires
  6. Cleanup Jobs:
    • Periodic session cleanup (5-minute expiry)
    • Shared secrets automatically deleted via CASCADE
    • No keypair cleanup (manually rotated via CLI)
  7. Performance: Connection pooling, partial indexes, encrypted shared secret storage

Next Steps

  1. Gather Feedback: Review and revise this plan
  2. Type Definitions: Detailed TypeScript interface definitions
  3. DB Schema: PostgreSQL table design
    • Session tables with token_hash and sdk_version (ephemeral)
    • Long-lived ECDHE keypair tables (separate lifecycle)
    • Encrypted shared secret tables (ephemeral, FK CASCADE to sessions)
    • Database ACLs and permissions setup
  4. Keypair Infrastructure:
    • Static ECDSA keypair generation and CLI (for rollback signing)
    • Long-lived ECDHE keypair generation and CLI (for token encryption)
    • Application-level encryption utilities (AES-256-GCM)
    • KMS/Vault integration
    • Key rotation procedures
  5. Prototype: Start implementation of Phase 1
    • Generate and store initial ECDHE keypairs
    • Session initialization using active keypairs
    • Calculate ECDH once during commit, encrypt and store shared secret
    • Decrypt stored shared secret during reveal (50-100x faster than on-demand ECDH)

Checks

  • I searched for existing feature requests.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions