import { PrivateKey } from "@bsv/sdk"; import { parseAuthToken, verifyAuthToken } from "bitcoin-auth"; import type { MiddlewareHandler } from "hono"; // Cache the owner public key to avoid repeated derivation let cachedOwnerPubkey: string | null = null; /** * Derives the owner's public key from SIGMA_MEMBER_WIF env var. * Result is cached after first derivation. */ export function getOwnerPubkey(): string { if (cachedOwnerPubkey) { return cachedOwnerPubkey; } const wif = process.env.SIGMA_MEMBER_WIF; if (!wif) { throw new Error("SIGMA_MEMBER_WIF environment variable not set"); } try { const privateKey = PrivateKey.fromWif(wif); cachedOwnerPubkey = privateKey.toPublicKey().toString(); return cachedOwnerPubkey; } catch (error) { throw new Error( `Failed to derive public key from SIGMA_MEMBER_WIF: ${error}`, ); } } /** * Hono middleware that verifies Bitcoin Signed Message authentication tokens. * * Extracts and verifies the Authorization: Bearer header: * 1. Parses the token using bitcoin-auth parseAuthToken() * 2. Verifies the signature using bitcoin-auth verifyAuthToken() * 3. Compares the pubkey against the owner's public key (derived from SIGMA_MEMBER_WIF) * * Returns: * - 401 if token is missing, malformed, invalid signature, or expired * - 403 if token is valid but from wrong identity (pubkey mismatch) * - Calls next() if valid and matches owner identity */ export const sigmaAuth: MiddlewareHandler = async (c, next) => { const path = new URL(c.req.url).pathname; console.log(`[sigma-auth] ${path} start method=${c.req.method}`); // Extract Authorization header const authHeader = c.req.header("Authorization"); if (!authHeader || !authHeader.startsWith("Bearer ")) { return c.json({ error: "Missing or invalid Authorization header" }, 401); } const token = authHeader.substring(7); // Remove "Bearer " prefix try { // Parse token const parsed = parseAuthToken(token); if (!parsed) { console.log(`[sigma-auth] ${path} REJECT: invalid token format`); return c.json({ error: "Invalid token format" }, 401); } console.log( `[sigma-auth] ${path} parsed token OK, pubkey=${parsed.pubkey.slice(0, 12)}...`, ); const requestPath = path; // Read body if present (for POST/PUT/PATCH requests) let bodyText: string | undefined; if ( c.req.method === "POST" || c.req.method === "PUT" || c.req.method === "PATCH" ) { try { bodyText = await c.req.text(); } catch { bodyText = undefined; } } console.log( `[sigma-auth] ${path} body=${bodyText ? `${bodyText.length}b` : "none"}`, ); // Check token expiry (5 minute window) const tokenTimestamp = new Date(parsed.timestamp); const now = new Date(); const maxAgeMs = 5 * 60 * 1000; // 5 minutes if (now.getTime() - tokenTimestamp.getTime() > maxAgeMs) { console.log( `[sigma-auth] ${requestPath} REJECT: expired (age=${Math.round((now.getTime() - tokenTimestamp.getTime()) / 1000)}s)`, ); return c.json({ error: "Invalid or expired token" }, 401); } const authPayload = { requestPath, timestamp: parsed.timestamp, body: bodyText, }; // Verify token signature console.log(`[sigma-auth] ${path} verifying signature...`); const isValid = verifyAuthToken(token, authPayload); if (!isValid) { console.log( `[sigma-auth] ${requestPath} REJECT: signature verification failed`, ); return c.json({ error: "Invalid or expired token" }, 401); } // Verify identity matches owner let ownerPubkey: string; try { ownerPubkey = getOwnerPubkey(); } catch { return c.json( { error: "Server configuration error: Unable to verify identity", }, 500, ); } if (parsed.pubkey !== ownerPubkey) { console.log( `[sigma-auth] ${requestPath} REJECT: pubkey mismatch (got=${parsed.pubkey.slice(0, 12)}... want=${ownerPubkey.slice(0, 12)}...)`, ); return c.json({ error: "Forbidden: Identity mismatch" }, 403); } console.log( `[sigma-auth] ${requestPath} OK (pubkey=${parsed.pubkey.slice(0, 12)}...)`, ); await next(); } catch (err) { console.error( `[sigma-auth] ${path} CRASH: ${err instanceof Error ? err.message : String(err)}`, ); return c.json({ error: "Authentication processing error" }, 500); } };