// Theseus ID for the gateway: checks sign-ins for project "pithos" // (DESIGN-theseus-id.md §4.3, §11.3). // // message.mjs and verify.mjs are verbatim copies of TheseusID/lib, so the // gateway agrees with Theseus and the login service byte for byte; a test // fails if they drift. The curve maths is libauth's, which the gateway // already carries for the sign-up gate, instead of the noble packages the // reference library is usually given. import { sha256, hash160, secp256k1, utf8ToBin, base64ToBin, decodeCashAddress, binToHex } from '@bitauth/libauth'; import { createVerifier } from './verify.mjs'; export const PROJECT = 'pithos'; // The registry entry's origins (TheseusID/registry/projects.json) plus the // login service. Theseus fills Origin from what it saw, so a sign-in made // anywhere else does not verify here. export const ORIGINS = ['https://pithos.sia', 'https://s3.silentmode.st', 'theseus-addon://pithos']; export const PROVIDER_ORIGIN = 'https://id.theseus.x'; function varInt(n) { if (n < 0xfd) return [n]; if (n <= 0xffff) return [0xfd, n & 0xff, n >> 8]; return [0xfe, n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >>> 24) & 0xff]; } // BIP-137 over the real varint (sign-in messages run past 252 bytes), then // the recovered key's hash compared with the claimed address. Recovery alone // proves nothing: any signature recovers to some key. Theseus ID keys are // compressed, so only headers 31-34 are accepted. export function verifyAddress(message, signatureB64, address) { let sig; try { sig = base64ToBin(String(signatureB64 || '')); } catch { return false; } if (!(sig instanceof Uint8Array) || sig.length !== 65 || sig[0] < 31 || sig[0] > 34) return false; const magic = utf8ToBin('Bitcoin Signed Message:\n'); const msg = utf8ToBin(String(message)); const digest = sha256.hash(sha256.hash(Uint8Array.from([...varInt(magic.length), ...magic, ...varInt(msg.length), ...msg]))); const pub = secp256k1.recoverPublicKeyCompressed(sig.slice(1), (sig[0] - 31) & 3, digest); if (typeof pub === 'string') return false; const want = decodeCashAddress(String(address || '')); if (typeof want === 'string' || want.type !== 'p2pkh') return false; return binToHex(want.payload) === binToHex(hash160(pub)); } /** * @param {(nonce: string) => boolean} consumeNonce true once for a nonce this * gateway issued; called last, so a bad request cannot burn a good nonce. */ export function createPithosVerifier(consumeNonce, now = () => Date.now()) { return createVerifier({ crypto: { verifyAddress }, projectId: PROJECT, origins: ORIGINS, providerOrigin: PROVIDER_ORIGIN, consumeNonce, now, }); }