Skip to content

05 — Annex: Cryptographic Bill of Materials

Every primitive, curve, domain, KDF, key and pinned library the platform actually uses, with evidence. A CBOM earns its keep in two situations: an algorithm has to be migrated, and a key has to be rotated after a compromise. Both need the same thing — an exhaustive list of where a primitive is reached and what hangs off each key — so the two load-bearing sections here are the key-derivation hierarchy (§5) and the key material inventory (§6).

The three roots

Nearly all member-side cryptography descends from one of three roots, and knowing which one a secret hangs from tells you its blast radius:

Root What it is What hangs off it Rotation story
Passkey PRF master seed 32 bytes from the WebAuthn PRF extension, per credential X25519 envelope key, X-Wing key, unified backup key, and the entire Bitcoin key tree None — the seed is a property of the credential; the four HKDF info strings and the PRF salt are wallet-breaking constants
Platform KMS keys EC_SIGN_SECP256K1_SHA256 in Cloud KMS Relayer gas signing, paymaster op authorization Key versions in KMS; material never exportable, so compromise means revoking use, not recovering the key
Floppy keystore mnemonic Air-gapped, physically-held Admin/deploy authority, paymaster owner Physical; deliberately outside every automated path

Below those sit three lower-assurance tiers: Secret Manager (deployer key, alto executor EOAs), the device secure enclave (the passkey credentials themselves), and member-held device-only credentials (RPC keys, the GutterToken sk-… key) — which are test-asserted absent from the backup registry, so they cannot leave the device that created them.

What this annex does not find

Stated because an absence verified is worth more than an absence assumed: there is no live ZK/Semaphore path (the Groth16 material is archive-only), no MPC/TSS/Shamir, and no weak primitive anywhere — MD5, SHA-1, DES, RC4 and ECB mode are all absent, and eth_sign appears only inside a refusal list.

Full CBOM

Scope: /home/user/prediction-dao-research @ version.json / package.json version 1.18.0. Method: static read of source + package-lock.json. Every row cites path:line. Read-only; nothing modified. Convention: "NOT DETERMINED" means the repo does not state it and I refused to guess.


0. Executive risk register (details in the sections cited)

# Finding Severity Evidence
R1 Hand-rolled X-Wing hybrid KEM, commented "per IETF draft-connolly-cfrg-xwing-kem" but demonstrably divergent from it: the label literal '\\./\n\\./\n' evaluates to the 8 bytes 5C 2E 2F 0A 5C 2E 2F 0A, where the draft's label is the 6-byte \.//^\; and the ML-KEM/X25519 component seeds are expanded with three ad-hoc SHA3-256("xwing-mlkem-1"/"xwing-mlkem-2"/"xwing-x25519" ‖ seed) calls rather than the draft's expansion. Self-consistent (encap/decap agree) but not interoperable and unaudited. Combiner shape SHA3-256(label‖ss_M‖ss_X‖ct_X‖pk_X) does match. High (crypto-implementation) frontend/src/utils/crypto/envelopeEncryption.js:41 (label), :44-54 (combiner), :62-80 (keygen/seed expansion), :92-121 (encap), :132-169 (decap)
R2 Claim codes carry ~2^44 entropy and deterministically derive a secp256k1 private key that is the on-chain claimAuthority for an open challenge. High frontend/src/utils/claimCode/wordlist.js:4; frontend/src/utils/claimCode/deriveFromCode.js:47
R3 Envelope/backup/address-book keys are keccak256(signature_hex_string) — the wallet signature is the key material, so a leaked or replayed signature is a permanent key compromise with no rotation path. High frontend/src/lib/backup/backupCrypto.js:17,21; frontend/src/utils/crypto/envelopeEncryption.js (sig-derived paths)
R4 Two incompatible floppy-keystore MAC schemes coexist. keystore.js writes/verifies Web3-V3 keccak256(dk[16:32]‖ct) with a constant-time compare; loader.js + store-admin-key.js use HMAC-SHA256 with a non-constant-time mac.equals(), and at scrypt N=2^14 vs the config's 2^18. Medium-High scripts/operations/floppy-key/keystore.js:55-60,130-133; loader.js:278-283; store-admin-key.js:59,86; config.js:16,22
R5 script-src 'unsafe-inline' in production CSP alongside blob: (needed for mini-app package execution). The file itself documents the cost. Medium frontend/nginx.conf:96
R6 connect-src https: scheme-wide (BYO-RPC, spec 069) — any HTTPS origin is a valid exfiltration sink for an injected script. Accepted trade-off, documented. Medium frontend/nginx.conf:96
R7 Dead but present NaCl/XSalsa20 module. frontend/src/utils/encryption.js (tweetnacl nacl.box, x25519-xsalsa20-poly1305) has no importers in shipped or test paths; keys derived as keccak256(signature) seeded straight into nacl.box.keyPair.fromSecretKey. Attack surface with no owner. Medium frontend/src/utils/encryption.js:22-23,48-54,72-76; no import sites found
R8 Safe vault saltNonce = Date.now() — predictable, low entropy; identical owner-set + same-millisecond creation collides. Low-Medium frontend/src/components/custody/createflow/CreateVaultFlow.jsx:31
R9 Origin-lock is a shared secret compared by nginx map (not constant-time), and the doc itself names mTLS/Authenticated Origin Pulls as the stronger control, out of scope. Low-Medium infra/cloudflare/origin-lock.md:6,38-40,57-63
R10 x402 replay protection is in-process only (a Map in one gateway instance); durability delegated to the token's own authorizationState. Honest, but multi-instance dedup is not local. Low (documented) services/relay-gateway/src/x402/verify.js:63-87,273
R11 Two solc versions in the tree: root pinned 0.8.24 (the solcjs bytecode path), plus hardhat's own transitive solc@0.8.26. CLAUDE.md notes the byte gate does not cover the solcjs path. Low-Medium root package.json "solc": "0.8.24"; package-lock.jsonnode_modules/hardhat/node_modules/solc = 0.8.26; hardhat.config.js:198-232
R12 elliptic@6.6.1 present (pulled in transitively, force-overrides-pinned). Historically CVE-heavy ECDSA implementation; 6.6.1 is the remediated line but it is still in the graph. Low root package.json "overrides": { "elliptic": "^6.6.1" }; lock node_modules/elliptic = 6.6.1
R13 ethers@5.8.0 coexists with ethers@6.17.0 (under @chainlink/contracts, dev-only). Two signing stacks in one lockfile. Low lock: node_modules/@chainlink/contracts/node_modules/ethers = 5.8.0
R14 No @simplewebauthn/* server-side verification anywhere. WebAuthn assertion verification is on-chain only (FreshCryptoLib / RIP-7212). No off-chain attestation checking, and attestation is not requested at creation. Deliberate-looking, but it means credential provenance is never checked. Informational contracts/account/lib/webauthn-sol/WebAuthn.sol:152-162; frontend/src/lib/passkey/credentials.js:344-356 (no attestation key)

1. Signature schemes & curves

Scheme Curve / params Where used Evidence
secp256k1 ECDSA (raw tx) secp256k1, low-S normalized (EIP-2) Every EOA transaction; hardware signers; recovered legacy keys frontend/src/lib/hardware/hardwareSigner.js:76-104 (recover-and-verify before broadcast)
EIP-191 personal_sign secp256k1 Message signing (Protect ▸ Verify), encryption-key derivation messages, paymaster sponsorship signature frontend/src/lib/verify/verifyMessage.js:71-76; services/relay-gateway/src/paymaster/sign.js:22-27
EIP-712 typed data secp256k1 All gasless intents, pool actions, callsigns, EIP-3009, member-API grants §2
EIP-1271 contract signatures magic 0x1626ba7e Smart-account / vault signature verification; the ONLY accepted success value frontend/src/lib/verify/verifyMessage.js:41-42,137; contracts/upgradeable/SignerIntentBase.sol:95-107
ERC-1271 domain (account) CoinbaseSmartWalletMessage(bytes32 hash) replay-safe wrapper Passkey smart account contracts/account/ERC1271.sol:23,101-136
P-256 / ES256 WebAuthn secp256r1; COSE alg -7 ONLY Passkey credential creation + assertion frontend/src/lib/passkey/credentials.js:349 (pubKeyCredParams: [{ type: 'public-key', alg: -7 }])
P-256 on-chain verification RIP-7212 precompile at address(0x100), fallback to FreshCryptoLib FCL_ecdsa.ecdsa_verify WebAuthn.verify inside the smart account contracts/account/lib/webauthn-sol/WebAuthn.sol:50-52,152-162
P-256 malleability guard s > n/2 rejected same WebAuthn.sol:48,110
WebAuthn message hash sha256(authenticatorData ‖ sha256(clientDataJSON)) same WebAuthn.sol:146,150
BIP-340 Schnorr / Taproot Implemented via @scure/btc-signer p2tr — BIP-341 key-path tweak; BIP-86 derivation. No hand-rolled Schnorr. Bitcoin receive/send frontend/src/lib/bitcoin/addresses.js:27,66-70; derivation.js:49
Ed25519 RFC 8032 via @noble/curves/ed25519.js Solana send/sign (spec: recovered-account cross-chain) frontend/src/lib/solana/derivation.js:26,96,106-107
Safe multisig threshold approveHash / approvedHashes(owner, txHash) == 1, checked at nonce()-1, approver must still be isOwner SafePolicyGuardV2 contracts/custody/SafePolicyGuardV2.sol:11-12,213-223,583
KMS secp256k1 signing EC_SIGN_SECP256K1_SHA256, DER→{r,s} with low-S normalization + parity recovery Relayer gas keys, paymaster sponsorship signer infra/terraform/environments/prod/variables.tf:71-74; services/relay-gateway/src/paymaster/sign.js:35-76
Threshold / MPC NONE. No MPC, no TSS, no Shamir anywhere in shipped paths. grep: no shamir/mpc/tss hits in contracts/, frontend/src, services/
ZK / Semaphore REMOVED from live paths, ARCHIVED only. Spec 034 explicitly dropped Semaphore; pools are by public wallet address. script-src carries no WASM grant because of that removal. Archived Groth16/BN128 verifier is reference-only. contracts/pools/WagerPool.sol:36; contracts/pools/WagerPoolFactory.sol:26; frontend/nginx.conf:54; contracts-archive/privacy/ZKVerifier.sol:9-14 (Groth16, BN128 ecPairing 0x08) — contracts-archive/ is never imported or deployed
EIP-2098 compact sigs NOT FOUND. No compact-signature encoding in any contract or client path. grep 2098 → no hits

2. Typed-data / domain separation

2.1 EIP-712 domains — contract-verified (single source: packages/intent-types/src/index.js:65-94)

chainId and verifyingContract are runtime values; only name/version are static. Parity to Solidity __EIP712_init is gate-enforced by test/intent/TypehashParity.test.js.

Domain key name version Verifying contract Evidence
wagerRegistry FairWins WagerRegistry 1 proxy intent-types/src/index.js:66; contracts/wagers/WagerRegistry.sol:60,82
membershipManager FairWins MembershipManager 1 proxy index.js:67; contracts/access/MembershipManager.sol:123,134
wagerPool FairWins WagerPool 1 the clone index.js:71; contracts/pools/WagerPool.sol:171
wagerPoolFactory FairWins WagerPoolFactory 1 factory proxy index.js:72; contracts/pools/WagerPoolFactory.sol:119,129
callsignRegistry FairWins CallsignRegistry 1 registry proxy index.js:73; contracts/naming/CallsignRegistry.sol:113
fundingPool FairWins FundingPool 1 the clone index.js:77; contracts/pools/FundingPool.sol:136
fundingPoolFactory FairWins FundingPoolFactory 1 factory proxy index.js:78; contracts/pools/FundingPoolFactory.sol:108
(test only) Mock / 1 contracts/mocks/MockSignerIntent.sol:26

The domain/target split for pools: signature verified under the clone's domain, transaction targeted at the factory forwarderINTENT_ACTIONS[*].domainVerifier vs .verifier, intent-types/src/index.js:482-489.

2.2 EIP-712 domains — off-chain (no Solidity verifier, by design)

Domain name version chainId / verifyingContract Verifier Evidence
Member API capability token FairWins Member API 1 deliberately ABSENT both (chain-agnostic grant) services/relay-gateway/src/memberApi/auth.js packages/intent-types/src/offchain.js:50-63

2.3 EIP-712 domains — third-party token (EIP-3009), gateway-side

Chain paymentToken domain name version Evidence
Polygon 137 USD Coin 2 services/relay-gateway/src/config/chains.js:29
Amoy 80002 USDC 2 chains.js:43
ETC 61 / Mordor 63 nullno EIP-3009 (permit-only USC) chains.js:53,65

2.4 Struct / typehash inventory

Contract-verified (CONTRACT_VERIFIED_TYPES = INTENT_TYPES ∪ OPEN_ACCEPT_TYPES ∪ FUNDING_POOL_TYPES, intent-types/src/index.js:372). Common trailing fields on nearly all: nonce bytes32, validAfter uint256, validBefore uint256 (index.js:119-123).

Primary type Domain Notable fields Evidence (package) Evidence (Solidity typehash)
CreateWagerIntent wagerRegistry 15 fields + trailing, incl. metadataHash, termsVersionHash, paymentNonce index.js:136-153 contracts/wagers/WagerRegistryIntents.sol:35
AcceptWagerIntent wagerRegistry wagerId, taker, paymentNonce index.js:154-159 WagerRegistryIntents.sol:38
ClaimPayoutIntent wagerRegistry wagerId, claimant index.js:160-164 WagerRegistryIntents.sol:41
ClaimRefundIntent wagerRegistry wagerId, actor index.js:165 WagerRegistryIntents.sol:44
DeclareDrawIntent wagerRegistry wagerId, actor index.js:166 WagerRegistryIntents.sol:47
RevokeDrawIntent wagerRegistry wagerId, actor index.js:167 WagerRegistryIntents.sol:50
CancelOpenIntent wagerRegistry wagerId, actor index.js:168 WagerRegistryIntents.sol:53
DeclineIntent wagerRegistry wagerId, actor index.js:169 WagerRegistryIntents.sol:56
DeclareWinnerIntent wagerRegistry wagerId, winner, actor index.js:170-175 WagerRegistryIntents.sol:59
PurchaseTierIntent membershipManager role bytes32, tier uint8, acceptedTermsHash, member, paymentNonce index.js:176-183 contracts/access/MembershipManager.sol:30
UpgradeTierIntent membershipManager same shape index.js:184-191 MembershipManager.sol:33
ExtendMembershipIntent membershipManager role, member, paymentNonce index.js:192-197 MembershipManager.sol:36
RedeemVoucherIntent membershipManager voucherId, acceptedTermsHash, redeemer index.js:198-203 MembershipManager.sol:39
InvalidateNonce either (verifier: null) signer, nonce, validBeforeno validAfter index.js:206-210 contracts/upgradeable/SignerIntentBase.sol:27-28 (literal: InvalidateNonce(address signer,bytes32 nonce,uint256 validBefore))
ApproveOutcome wagerPool (clone) member, proposalId index.js:215-219 contracts/pools/WagerPool.sol:74 (APPROVE_TYPEHASH)
ClaimShare wagerPool (clone) winner, index, recipient index.js:220-225 WagerPool.sol:76 (CLAIM_TYPEHASH)
ProposeOutcome wagerPool (clone) creator, proposalId index.js:226-230 WagerPool.sol:78 (PROPOSE_TYPEHASH)
CloseJoining wagerPool (clone) creator index.js:231-234 WagerPool.sol:80 (CLOSE_TYPEHASH)
Cancel wagerPool (clone) creator index.js:235-238 WagerPool.sol:82 (CANCEL_TYPEHASH)
Refund wagerPool (clone) member index.js:239-242 WagerPool.sol:84 (REFUND_TYPEHASH)
CreatePool wagerPoolFactory creator, token, buyIn, maxMembers, thresholdBips, acceptDeadline, resolveDeadline index.js:243-252 contracts/pools/WagerPoolFactory.sol:52
CommitCallsignIntent callsignRegistry owner, commitment bytes32 (ENS-style commit) index.js:256-260 contracts/naming/CallsignRegistry.sol:34
RegisterCallsignIntent callsignRegistry owner, callsign string, salt bytes32 (reveal; hashed as keccak256(bytes(callsign)) in the struct encoding, per EIP-712 string handling) index.js:261-266 CallsignRegistry.sol:36, encoding at :336
ChangeCallsignIntent callsignRegistry owner, newCallsign, salt index.js:267-272 CallsignRegistry.sol:38, encoding at :345
ReleaseCallsignIntent callsignRegistry owner, callsignHash index.js:273-277 CallsignRegistry.sol:40
RequestRepointIntent callsignRegistry owner, callsignHash, newOwner index.js:278-283 CallsignRegistry.sol:42
CancelRepointIntent callsignRegistry owner, callsignHash index.js:284-288 CallsignRegistry.sol:44
OpenAccept wagerRegistry wagerId uint256, taker addressno nonce/validity, signed by the CLAIM-CODE-derived key, not the member index.js:307-312 contracts/wagers/WagerRegistryCore.sol:52 (OPEN_ACCEPT_TYPEHASH)
CloseFundingPool fundingPool (clone) organizer index.js:345-348 contracts/pools/FundingPool.sol:69
CancelFundingPool fundingPool (clone) organizer index.js:349-352 FundingPool.sol:71
VoteRefund fundingPool (clone) contributor index.js:353-356 FundingPool.sol:73
ClaimRefund fundingPool (clone) contributor index.js:357-360 FundingPool.sol:75
CreateFundingPool fundingPoolFactory organizer, token, goal, purposeHash = keccak256(bytes(purpose)), contributeDeadline, settleDeadline index.js:361-369 contracts/pools/FundingPoolFactory.sol:50

Off-chain-verified structs (absent from CONTRACT_VERIFIED_TYPES on purpose — no Solidity counterpart exists or should):

Primary type Fields Verifier Evidence
ApiKeyGrant account address, keyId bytes32, scopes **string** (canonical space-joined, sorted, deduped), issuedAt uint256, expiresAt uint256label is deliberately NOT signed relay-gateway memberApi/auth.js packages/intent-types/src/offchain.js:78-86,122-131; services/relay-gateway/src/memberApi/auth.js:147
ApiKeyRevocation account, keyId, revokedAtself-authorizing, no bearer token required same offchain.js:99-105; auth.js:190

EIP-3009 token structs (authority = Circle's deployed USDC, not this repo; verified against recorded vectors, since the only in-repo copy is contracts/mocks/MockUSDCPermit.sol:16):

Primary type Fields Submitter rule Evidence
ReceiveWithAuthorization from, to, value, validAfter, validBefore, nonce bytes32 token enforces to == msg.sender → only the recipient contract intent-types/src/index.js:392-401
TransferWithAuthorization identical field list any address may submit index.js:403-412; used by x402 payer path services/relay-gateway/src/x402/verify.js:239-250

Typehash derivation helper (flat structs only; throws on nested custom types rather than emitting a subtly wrong string): intent-types/src/index.js:428-441.


3. Hashes & KDFs

Primitive Parameters Purpose Evidence
keccak256 EIP-712 typehashes + domain separators; ERC-7201 storage namespaces; CREATE2 salts; mini-app manifest commitment; FeeRouter serviceIds; role ids; claim-code key derivation; symmetric-key derivation from signatures throughout; see below
keccak256 → serviceId ethers.id('earn.lend') etc. FeeRouter service ids frontend/src/lib/fees/feeQuote.js:37,43-61 (earn.lend, polymarket.taker, polymarket.maker, stake.lido, stake.polygon, bridge.transfer, liquidity.deposit, perps.hyperliquid.builder)
keccak256 → role keccak256("FEE_ADMIN_ROLE") AccessControl role ids contracts/fees/FeeRouter.sol:34
keccak256 → ERC-7201 slot keccak256(abi.encode(uint256(keccak256("fairwins.storage.SignerIntentBase")) - 1)) & ~bytes32(uint256(0xff)) Namespaced upgradeable storage contracts/upgradeable/SignerIntentBase.sol:30,36; contracts/account/MultiOwnable.sol:40-41 (coinbase.storage.MultiOwnable)
keccak256 → CREATE2 salt (platform) ethers.id(identifier), or ethers.id("tenant:<id>:<identifier>") when TENANT_ID is set Deterministic deploys; tenant isolation scripts/deploy/lib/helpers.js:73-76
keccak256 → CREATE2 salt (account) keccak256(abi.encode(owners, nonce)) ERC-4337 passkey account address contracts/account/CoinbaseSmartWalletFactory.sol:95-96
keccak256 → CREATE2 salt (Safe) solidityPackedKeccak256(['bytes32','uint256'], [keccak256(initializer), saltNonce]), then getCreate2Address(proxyFactory, salt, keccak256(deploymentData)) Same-address multichain vaults frontend/src/lib/custody/safeVault.js:80-87
keccak256 → mini-app manifest commitment keccak256(manifest.json raw bytes) — re-checked before parsing, on every retrieval incl. cache Code integrity frontend/src/lib/miniapps/integrity.js:33,150-151,180-184; loader.js:10-12; scripts/miniapps/publish.js:281,481-487
keccak256 → claim key keccak256(utf8("FairWins/claim/v1" ‖ normalizedCode)) = secp256k1 scalar; independent keccak256(utf8(TERMS_DOMAIN ‖ code)) = 32-byte AEAD key Open-challenge claim codes frontend/src/utils/claimCode/deriveFromCode.js:47,51
keccak256 → symmetric key from signature getBytes(keccak256(toUtf8Bytes(signature))) Backup key, envelope keys frontend/src/lib/backup/backupCrypto.js:17,21; also keccak256(concat([seed, utf8(DATA_BACKUP_MESSAGE_V1)])) for the passkey twin, backupCrypto.js:35
sha256 Mini-app per-file byte integrity; WebAuthn message hash; bytecode digests; KMS digest input frontend/src/lib/miniapps/integrity.js:34,139-140; contracts/account/lib/webauthn-sol/WebAuthn.sol:146,150; scripts/codegen/bytecode-digest.js:99-101; scripts/miniapps/record-build-digests.js:43; services/relay-gateway/src/paymaster/sign.js:47
SHA3-256 X-Wing combiner + component-seed expansion (R1) frontend/src/utils/crypto/envelopeEncryption.js:21,47-53,65-69,144-151
HMAC-SHA256 Double-ratchet MAC; Polymarket L2 auth over {ts}{METHOD}{path}{body} with base64url-decoded secret; admin floppy keystore MAC frontend/src/utils/crypto/primitives.js:15,63-65; services/relay-gateway/src/polymarket/client.js:9-11,37-47; scripts/operations/floppy-key/store-admin-key.js:86
HMAC-SHA512 key "ed25519 seed" SLIP-0010 ed25519 (Solana), hand-rolled on @noble frontend/src/lib/solana/derivation.js:7-8,32,47-69
HKDF-SHA256 — passkey KEK ikm = WebAuthn PRF output; salt = 32 zero bytes; info = "fairwins-kek-v1"; out = AES-GCM-256 key Wraps the master seed frontend/src/lib/passkey/prfKeys.js:23,41-47
HKDF-SHA256 — legacy-vault biometric KEK salt = 32 zero bytes; info = "fairwins-legacy-kek-v1" (domain-separated from the above on purpose) Passkey-PRF wrapping of imported legacy secrets frontend/src/lib/recovery/legacyKeys.js:34-37,196-199
HKDF-SHA256 — Bitcoin seed ikm = 32-byte master seed; salt = 32 zero bytes; info = "fairwins-btc-seed-v1"; L = 64 bytes BIP-32 seed for the BTC tree. WALLET-BREAKING IF CHANGED. frontend/src/lib/bitcoin/derivation.js:42-46,85-86
HKDF-SHA256 — passkey X25519 salt = 32 zero bytes; info = "fairwins-passkey-x25519-v1"; L = 32 Envelope keypair from master seed frontend/src/utils/crypto/envelopeEncryption.js:236,238,249
HKDF-SHA256 — passkey X-Wing salt = 32 zero bytes; info = "fairwins-passkey-xwing-v1"; L = 32 X-Wing seed from master seed envelopeEncryption.js:237-238,263
HKDF-SHA256 — ratchet caller-supplied salt/info Double-ratchet chain keys frontend/src/utils/crypto/primitives.js:13,51-55
PBKDF2-HMAC-SHA256 650,000 iterations, 16-byte random salt, → AES-GCM-256. Iteration count stored per-entry for forward compat. Legacy imported key/mnemonic vault at rest frontend/src/lib/recovery/legacyKeys.js:32,134-141,158-169,185
scrypt (mnemonic keystore) N=262144 (2^18), r=8, p=1, dklen=32 Floppy mnemonic keystore scripts/operations/floppy-key/config.js:16-19; keystore.js:6,38
scrypt (admin key keystore) N=16384 (2^14), r=8, p=1, dklen=32 — deliberately lower, in-file comment Floppy admin-private-key keystore scripts/operations/floppy-key/store-admin-key.js:58-71
Argon2 / bcrypt NONE in any shipped path. (bcrypt-pbkdf appears only as a transitive dev dep of sshpk.) lock: node_modules/bcrypt-pbkdf/node_modules/tweetnacl (dev)
Groth16 / BN128 pairing ecAdd 0x06, ecMul 0x07, ecPairing 0x08 ARCHIVED ONLY — never imported or deployed contracts-archive/privacy/ZKVerifier.sol:9-14,193,327; contracts-archive/README.md

AAD binding (terms-version tamper evidence): FairWins-TC|<schemaVersion>|<sha256hex> with TERMS_AAD_VERSION = '1.1' — byte-identical on seal and open; deliberately NOT in key derivation so keys stay versionless. frontend/src/utils/crypto/constants.js:203-226.


4. Symmetric encryption at rest

Channel Cipher Key size Key source IV / nonce Where ciphertext lives Evidence
Passkey master-seed wrap AES-GCM 256 HKDF-SHA256 over WebAuthn PRF output (fairwins-kek-v1) crypto.getRandomValues(12) per wrap, stored base64 with {v:1, iv, ct} Local blob store + spec-032 synced backup blobs frontend/src/lib/passkey/prfKeys.js:44-46,92-94,98
Legacy imported keys / mnemonics AES-GCM 256 PBKDF2-SHA256 650k over member passphrase, or HKDF over passkey PRF (biometric) random 12-byte IV; random 16-byte PBKDF2 salt userStorage key legacy_recovered_keys; rides spec-032 backup as legacyRecoveredKeys. Plaintext secret is NEVER persisted, transmitted, or logged. frontend/src/lib/recovery/legacyKeys.js:12-14,158-169,183-190; legacyRecoveredKeysStore.js
Unified data backup (spec 032) ChaCha20-Poly1305 256 keccak256(signMessage("FairWins Data Backup v1")), or keccak256(seed ‖ msg) for passkey random 12-byte nonce Envelope {format:'fairwins-data-backup', version:1, alg:'chacha20poly1305', nonce, ciphertext}; AAD = "fairwins-data-backup:1" frontend/src/lib/backup/backupCrypto.js:13-18,26-45; frontend/src/utils/crypto/primitives.js:84-89
Wager metadata envelope (classical) ChaCha20-Poly1305 (x25519-chacha20poly1305) 256 X25519 ECDH → HKDF-SHA256 (FairWins_Envelope_v1) random 12-byte nonce; AAD = terms binding IPFS (Pinata) frontend/src/utils/crypto/constants.js:137,147-157; envelopeEncryption.js
Wager metadata envelope (PQ) ChaCha20-Poly1305 / XChaCha20-Poly1305 (xwing-chacha20poly1305) 256 X-Wing hybrid KEM (see R1), info FairWins_XWing_Envelope_v1 random nonce IPFS constants.js:142,149,154-157; envelopeEncryption.js:22
Floppy mnemonic keystore AES-128-CTR (unauthenticated cipher + separate MAC, Web3 V3 style) 128 (dk[0:16]) scrypt 2^18 random 16-byte IV; random 32-byte salt .keystore/mnemonic-keystore.json on removable media scripts/operations/floppy-key/config.js:22; keystore.js:33-83
Floppy admin-key keystore AES-128-CTR 128 scrypt 2^14 random 16-byte IV / 32-byte salt same media store-admin-key.js:55-108
Dead code: NaCl box XSalsa20-Poly1305 (x25519-xsalsa20-poly1305) 256 nacl.box.keyPair.fromSecretKey(keccak256(signature)) nacl.randomBytes(nacl.box.nonceLength) (24) No importers found — R7 frontend/src/utils/encryption.js:22-23,48-54,100-106

NEVER stored anywhere (asserted by code + gate tests): - Bitcoin private keys, xprv, or xpubs — memory-only, never persisted/logged/transmitted (frontend/src/lib/bitcoin/derivation.js:90-95,118-133). - The passkey master seed in the clear (only the AES-GCM-wrapped blob). - The plaintext of any legacy imported key or mnemonic. - RPC endpoint credentials and the GutterToken sk-… key — device-scoped and deliberately absent from frontend/src/lib/backup/syncedObjects.js (a test asserts the absence): frontend/src/lib/assistant/guttertokenKeyStore.js:36. - Hardware-wallet store holds public metadata only ({address, vendor, path, label, addedAt}) — never key material, never an xpub.


5. Key derivation & hierarchies

Tree Path / formula Marked wallet-breaking? Evidence
Passkey master seed PRF(salt = "fairwins.prf.salt.v1" zero-padded to 32 B)HKDF-SHA256(info="fairwins-kek-v1") → KEK → AES-GCM(KEK, masterSeed) YES — changing the PRF salt or info string orphans every wrapped seed frontend/src/lib/passkey/prfKeys.js:5-7,22,35-38,41-47
Passkey → envelope X25519 HKDF-SHA256(seed, salt=0^32, info="fairwins-passkey-x25519-v1", 32)x25519.getPublicKey YES frontend/src/utils/crypto/envelopeEncryption.js:236,247-252
Passkey → X-Wing HKDF-SHA256(seed, 0^32, "fairwins-passkey-xwing-v1", 32)xwingKeygen(seed); component seeds SHA3-256("xwing-mlkem-1"‖s), SHA3-256("xwing-mlkem-2"‖s) (concat → 64 B ML-KEM seed), SHA3-256("xwing-x25519"‖s) YES envelopeEncryption.js:237,261-266,62-80
Passkey → backup key keccak256(seed ‖ utf8("FairWins Data Backup v1")) YES frontend/src/lib/backup/backupCrypto.js:35-39
Bitcoin (spec 061, current) HKDF-SHA256(masterSeed, 0^32, "fairwins-btc-seed-v1", 64)HDKey.fromMasterSeedm/84'/{coin}'/0' (segwit, P2WPKH) or m/86'/{coin}'/0' (taproot, P2TR); receive = account/0/i, 0 ≤ i < 2^31 non-hardened. coin = 0 mainnet, 1 testnet4. YES — explicitly frontend/src/lib/bitcoin/derivation.js:5-10,42-52,85-86,103,113-116,141-155
Bitcoin (legacy import, spec 062) m/44' P2PKH 1… · m/49' P2SH-wrapped 3… · m/84' P2WPKH bc1q… · m/86' P2TR bc1p…; account node m/{purpose}'/{coin}'/{account}' Discovery only frontend/src/lib/bitcoin/legacyDerivation.js:18-21,37,97
Solana SLIP-0010 ed25519, hardened-only (non-hardened ed25519 CKD undefined). Schemes: bip44Change = m/44'/501'/i'/0' (Phantom/Solflare), bip44 = m/44'/501'/i' (Ledger Live), bareSeed = first 32 B of the BIP-39 seed (solana-keygen). Coin type 501. Yes (address-changing) frontend/src/lib/solana/derivation.js:4-13,29-32,80-96
EVM hardware wallets live = m/44'/60'/i'/0/0; bip44 = m/44'/60'/0'/0/i. Reconnect RE-DERIVES the saved path and must match the saved address, else refuse. Yes frontend/src/lib/hardware/derivations.js:4-13,35; connectAccount.js
BIP-39 Accepted word counts 12, 15, 18, 21, 24 frontend/src/lib/recovery/legacyKeys.js:39
Claim codes 4 words from the BIP-39 English list (2048^4 = 2^44), uniform rejection sampling over 16-bit reads; keccak256("FairWins/claim/v1"‖code) → secp256k1 key; independent keccak256(TERMS_DOMAIN‖code) → 32-B AEAD key R2 frontend/src/utils/claimCode/wordlist.js:4,15-16,97-107; deriveFromCode.js:41-55
ERC-4337 account address LibClone.predictDeterministicAddress(initCodeHash(), keccak256(abi.encode(owners, nonce)), factory) — ERC-1967 proxy. Lookup, never derivation on the sign-in path (spec 104); derivation survives only for mode:'sign-up' or an explicitly acceptCounterfactual'd address. contracts/account/CoinbaseSmartWalletFactory.sol:50-61,77-78,95-96; frontend/src/lib/passkey/accountLookup.js
Safe vault address initializer = chain-independent spec-043 encoding (owners + threshold + canonical fallback handler, no policy setup); salt = keccak256(keccak256(initializer) ‖ saltNonce); CREATE2(proxyFactory, salt, keccak256(deploymentData)). saltNonce from Date.now() (R8). Policy rules installed post-deploy so the initializer stays byte-identical across chains. frontend/src/lib/custody/vaultDeployment.js:2-3,61-94; safeVault.js:80-87; CreateVaultFlow.jsx:31
Tenant-prefixed CREATE2 salts ethers.id("tenant:<TENANT_ID>:<identifier>"); default tenant unprefixed (IS the shared estate). TENANT_ID must match ^[a-z][a-z0-9-]{1,30}$ and must never change once a tenant has deployed. Yes (address-changing) scripts/deploy/lib/helpers.js:47-53,67-76
Safe singleton factory 0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7 (@safe-global/safe-singleton-factory) scripts/deploy/lib/constants.js:18; helpers.js:9

WebAuthn credential creation parameters (frontend/src/lib/passkey/credentials.js:337-372): - challenge: crypto.getRandomValues(new Uint8Array(32)) (line 337) - pubKeyCredParams: [{ type:'public-key', alg: -7 }]ES256/P-256 only (line 349) - authenticatorSelection: residentKey: 'required', userVerification: 'required' (lines 350-352) - extensions: { prf: { eval: { first: new Uint8Array(32) } } } (line 354) - Public key extracted from SPKI DER: x = point[1:33], y = point[33:65] (lines 363-369) - prfCapable = ext.prf?.enabled ?? ext.prf?.results (line 372) - attestation is not requested — no attestation statement is collected or verified (R14) - Assertion: userVerification: 'required' (lines 431-433) - Native bridge pre-encodes PRF salts to WebAuthn-JSON base64url and decodes prf.results.first/second back to ArrayBuffers, because the Capacitor plugin JSON-clones extensions (frontend/src/lib/native/nativeCredentials.js:16-22,63-71)


6. Key material inventory

Key / secret Algorithm Generated where Stored where Who can use it Rotation / revocation Blast radius if compromised Evidence
Deployer / admin EOA (PRIVATE_KEY) secp256k1 Operator (floppy flow preferred) GCP Secret Manager fairwins-deployer-key; delivered by npm run sec -- --profile deploy; never written to .env Workstation operator via impersonation (no SA key files) Manual key rotation + on-chain role handoff. DEFAULT_ADMIN_ROLE on bridgeRouter/liquidityRouter still held by a single hot deployer EOA (issue #966, per CLAUDE.md) Upgrade authority over UUPS proxies; repoint router fund-path addresses; catastrophic scripts/secrets/registry.js:75-79; infra/terraform/environments/prod/terraform.tfvars
Floppy keystore passwords (FLOPPY_KEYSTORE_PASSWORD, FLOPPY_MORDOR_PASSWORD, FLOPPY_NAZGUL_PRIME_PASSWORD) password (scrypt input) Operator Secret Manager, class PASSWORD; never falls back to process.env on a public network deploy profile Manual Decrypts the offline admin mnemonic/key if the media is also obtained scripts/secrets/registry.js:103-125; CLAUDE.md spec-097 rule (3)
Floppy mnemonic (BIP-39) BIP-39 → BIP-32 Operator, air-gapped Encrypted JSON on removable floppy (scrypt 2^18 + AES-128-CTR + keccak MAC), mounted noexec,nosuid,nodev,umask=077,sync Whoever holds media + password Physical Root of the entire admin key hierarchy scripts/operations/floppy-key/keystore.js:33-83; config.js:15-30
Creator / seed-player keys (CREATOR_PRIVATE_KEY, SEED_PLAYER_KEYS) secp256k1 Operator Secret Manager, seed profile Test seeding only Manual Testnet funds + seeded fixtures scripts/secrets/registry.js:83-95
Per-chain alto executor EOA (ALTO_EXECUTOR_PRIVATE_KEYS) secp256k1 Operator Secret Manager alto-executor-key-137, injected into the VM alto container One alto instance per (chain, EOA) — G-11 invariant; two altos on one EOA = nonce war, both healthy-looking, no in-band detection Manual; one executor key per chain, never shared Attacker submits bundles / drains the executor's gas; UserOps stop landing services/alto-bundler/deploy/service.yaml:42,127-132; infra/terraform/environments/prod/terraform.tfvars:67
ALTO_UTILITY_PRIVATE_KEY secp256k1 Operator Secret Manager → alto env alto Manual Bundler utility ops services/alto-bundler/deploy/service.yaml:42,132
Relayer gas keys (gas-key-polygon, gas-key-mordor) Google Cloud KMS EC_SIGN_SECP256K1_SHA256, ASYMMETRIC_SIGN Cloud KMS (never exported) KMS key ring, prevent_destroy; key VERSIONS are never Terraform-managed (unrecoverable) OZ Relayer SA with cloudkms.signerVerifier on those keys only Revoke the IAM binding / disable the key version. Key material cannot be exfiltrated. Attacker spends relayer gas; cannot steal member funds (the gateway can censor, never steal) services/oz-relayer/README.md:43-54; services/oz-relayer/config/config.json:50-77; infra/terraform/environments/prod/main.tf:229-255; variables.tf:71-74
Paymaster sponsorship signer (verifyingSigner) KMS secp256k1 (prod) / raw key (dev/CI only) Cloud KMS KMS; FairWinsVerifyingPaymaster.verifyingSigner on-chain relay-gateway POST /v1/paymaster On-chain rotation: setVerifyingSigner(newSigner) emits VerifyingSignerChanged; owner-only Attacker mints sponsorship signatures → drains the paymaster's EntryPoint deposit. Bounded: validation is signature-only/zero-storage and only owner (floppy keystore) can withdraw funds services/relay-gateway/src/paymaster/sign.js:2-10,35-58; contracts/account/FairWinsVerifyingPaymaster.sol:25-27,37-38,51-69,115-117
fairwins-rpc-access-signing-key NOT DETERMINED (key purpose not stated in the files read) Secret Manager infra/terraform/environments/prod/terraform.tfvars:61,164
Member passkey (P-256) ES256 / secp256r1 Device authenticator (platform passkey) Device secure enclave / OS keystore; syncable via the platform's own passkey sync. FairWins holds only {x, y} + credentialId The member, gated on userVerification: 'required' Add/remove controller via MultiOwnable (AddOwner/RemoveOwner); residentKey: 'required' Full control of the member's ERC-4337 account frontend/src/lib/passkey/credentials.js:344-381; contracts/account/MultiOwnable.sol
Passkey master seed (32 B) HKDF-derived; wrapped AES-GCM-256 Derived from the PRF output; memory-only in the clear Wrapped blob in local blob store + spec-032 synced backup Any PRF-capable credential holding a blob for the account Per-credential blobs — one credential can be de-provisioned without touching others All envelope keys, the Bitcoin tree, the backup key — the single root of member-side encryption frontend/src/lib/passkey/prfKeys.js:5-7,92-98,118-171
Member legacy imported keys / mnemonics secp256k1 / BIP-39 The member's old wallet (imported) userStorage.legacy_recovered_keys, AES-GCM-256 ciphertext only; rides spec-032 backup The member, after passphrase or passkey-PRF unwrap Delete the entry; sweep funds out (optional — sweepAllAssets, per-asset outcomes) Full control of the member's old EOA frontend/src/lib/recovery/legacyKeys.js:12-20,146-190
Bitcoin keys secp256k1 (BIP-84 ECDSA / BIP-86 Schnorr key-path) Derived client-side from the passkey master seed NOWHERE — memory-only for the duration of one signing op; xprv and xpub never persisted or sent The member's client only; the gateway sees bare addresses + signed raw txs Implied by master-seed rotation (which is itself wallet-breaking) The member's BTC frontend/src/lib/bitcoin/derivation.js:90-95,118-133; gatewayClient.js
Solana keys ed25519 (SLIP-0010) Derived from a recovered BIP-39 seed Not persisted separately The member's client The member's SOL frontend/src/lib/solana/derivation.js:80-107
Member RPC credentials bearer token / custom header The member's provider fw_global_prefs.network_endpoints, device-scoped, deliberately absent from syncedObjects.js The member's device Member deletes/replaces. Ride in a request HEADER, never in a URL; redacted at every display/log boundary via redactRpcUrl; attach to the PRIMARY endpoint only Read-only RPC quota abuse CLAUDE.md spec-069 rules; frontend/src/lib/network/endpointStore.js
GutterToken assistant key (sk-…) vendor API key The member, at app.guttertokens.com userStorage.assistant_guttertoken_key_v1, wallet-scoped, device-only, deliberately absent from syncedObjects.js (test-asserted) The member's browser only — the browser calls api.guttertokens.com directly; FairWins never holds, forwards, or sees it Member re-enters on 401 key_invalid; revoke at the vendor The member's GutterToken credit balance frontend/src/lib/assistant/guttertokenKeyStore.js:24,36-43,88-95,148-176
Member API fw1 capability token member-signed EIP-712 ApiKeyGrant — the gateway stores nothing to issue one The member's own wallet, in-app Wire format fw1.<b64url(grantJSON)>.<b64url(sig)> in Authorization: Bearer; held by the member's agent/MCP client Bearer holder, scoped to reads / typed-data BUILDS / assistant — never relay, never custody; the actor of every built intent is forced to the token account Revocation is in-process only (durable: false on every answer, surfaced to the member); expiry is the binding limit, TTL-capped at MEMBER_API_MAX_TTL_DAYS. ApiKeyRevocation is self-authorizing (no bearer token needed). Read access to the member's data + ability to have typed data built for them (still requires their signature to move value) packages/intent-types/src/offchain.js:28-38,88-105; services/relay-gateway/src/memberApi/auth.js:7-9,74-147,257-293
Polymarket CLOB L2 creds (POLY_API_KEY, secret, passphrase, address) HMAC-SHA256; L1 = one-time offline EIP-712 wallet signature Provisioned offline Gateway-only secrets; never reach the browser relay-gateway polymarket/ Re-provision at Polymarket Order placement / cancellation on FairWins' builder account; the member's wallet is the only order signer services/relay-gateway/src/polymarket/client.js:8-12,37-47; routes.js:7-9,48
Polymarket builder code bytes32 (0x6e03…93a3) — public config, not a secret Tenant/gateway config Config change Revenue attribution only CLAUDE.md spec-057
origin-lock-secret (X-Origin-Auth) 256-bit random (openssl rand -hex 32) Operator Secret Manager → Cloud Run env ORIGIN_LOCK_SECRET; mirrored in a Cloudflare Transform Rule. Read via a Terraform data source — the one accepted prevent_destroy-adjacent exception, and it lands in state nginx enforcement in every served location (/healthz exempt) Rotate Secret Manager + Transform Rule together Direct-to-origin access bypassing the geo gate (spec 007 legal control) infra/cloudflare/origin-lock.md:6-49; frontend/nginx.conf (via nginx.conf.template)
relay-webhook-secret, relay-engine-api-key shared secret / API key Operator Secret Manager relay-gateway ↔ engine Manual Forge engine callbacks / submit to the engine (still bounded by the gateway's policy layer) infra/terraform/environments/prod/terraform.tfvars:27-28,68-69
anthropic-api-key vendor API key Operator Secret Manager Gateway assistant rail (FairWins pays) Rotate at vendor Model spend terraform.tfvars:36,77
Pinata JWT, Graph deploy key, Graph API key, Etherscan key, QuickNode tokens/URLs bearer tokens Vendors Secret Manager, per-profile (publish, verify, rpc, deploy) Named profiles only Rotate at vendor IPFS pinning / subgraph deploy / explorer quota / RPC quota. QuickNode URLs were moved from public env to secrets because the token rides in the path scripts/secrets/registry.js:127-241
FinOps read tokens (finops-cloudflare-token, finops-quicknode-key, finops-grafana-cloud-token) bearer tokens Vendors Secret Manager finops-exporter — read-only by construction (no signer, no write route), binds loopback only; fetch-secrets.sh refuses to boot if key material reaches its env Rotate at vendor Billing-data read terraform.tfvars:31-33,72-74; CLAUDE.md spec-097
Android upload keystore JKS (algorithm inside: NOT DETERMINED) Operator Secret Manager fairwins-android-upload-keystore + …-password; materialised only into the ephemeral runner temp release.yml, gated on the ANDROID_SIGNING_SERVICE_ACCOUNT repo variable; unset ⇒ unsigned bundle recorded signed:false, loudly Google Play upload-key reset Publish a malicious update to the Play listing .github/workflows/release.yml:105-134
iOS signing identity Apple code-signing cert Apple Operator-held; never enters CI (research R8) Operator, manually Apple revocation App Store distribution .github/workflows/release.yml:161,177-193 (CODE_SIGNING_ALLOWED=NO, unsigned .xcarchive)
TLS certificates NOT DETERMINED in-repo Cloudflare (edge termination); origin is Cloud Run / VM nginx behind the origin-lock header. No mTLS to origin (documented as future hardening). Cloudflare-managed Cloudflare Edge TLS infra/cloudflare/origin-lock.md:1-6,57-63; frontend/nginx.conf:101 (Strict-Transport-Security: max-age=31536000)
Cloudflare API token (infra) bearer Cloudflare Secret Manager (Terraform reads containers + bindings only; never a secret_version resource — it would write the payload into state) Terraform apply identity Rotate at Cloudflare Both Cloudflare rulesets are authoritative for their phase — an apply deletes dashboard-added rules, including the HTTP-451 geo gate CLAUDE.md spec-087 rules (2),(5)

7. Transport & platform crypto

Control Detail Evidence
TLS termination Cloudflare at the edge; origin (Cloud Run / VM nginx) authenticated by the shared X-Origin-Auth header, not mTLS. HSTS max-age=31536000 (no includeSubDomains, no preload). infra/cloudflare/origin-lock.md:1-6; frontend/nginx.conf:101
Origin lock $http_x_origin_auth mapped → 403 on mismatch in every served location; /healthz exempt for probes; enforcement auto-enables only when the secret is present. nginx map comparison is not constant-time (R9). infra/cloudflare/origin-lock.md:35-49
Probe honesty Probes assert on CONTENT, not status: a plain 200 from the bundler proves nothing (the origin-lock nginx serves its own 200 that never reaches alto — the check that stayed green through the 2026-07-12 stall). CLAUDE.md spec-097; infra/observability/README.md
CSP as code-integrity control default-src 'self'; script-src 'self' 'unsafe-inline' blob: https://challenges.cloudflare.com https://*.cloudflareinsights.comblob: is for verified mini-app bytes ONLY, and https: must never be added to script-src; connect-src carries a scheme-wide https: + loopback http://localhost:* http://127.0.0.1:* grant for member-run nodes (spec 069); frame-src narrow (Cloudflare Turnstile, WalletConnect verify, connect.trezor.io). No wasm-unsafe-eval — a direct consequence of dropping Semaphore. frontend/nginx.conf:43-101 (policy string at :96)
CSP parity CSP_RPC_GRANTS (endpointStore.js) and both nginx configs must stay in sync — gated by src/test/nginxCspConnectSrc.test.js. The native meta CSP is DERIVED from nginx.conf by scripts/native/nativeCsp.js and parity-gated (script-src keeps blob:, never gains https:); the web nginx files stay byte-identical. scripts/native/nativeCsp.js:10-16; CLAUDE.md spec-103 rule (3)
Mini-app byte integrity Two-layer: (1) keccak256(manifest.json raw bytes) === the chain's approved.manifestHash, checked before parsing; (2) sha256(file bytes) === manifest.files[path].sha256 for every file executed or injected (entry + declared stylesheets). Re-checked after every retrieval, cache or network. SW cache fairwins-miniapp-packages-v1 is cache-first because CIDs are immutable and is not a trust boundary. verifyAllDeclaredFiles is OFF for a launch (files not used are not fetched), ON for a curator review. frontend/src/lib/miniapps/integrity.js:8-9,139-151,180-215; loader.js:10-12,246-248,340-371,438
Approval is content-committed approveApp(id, expectedManifestHash) reverts StaleProposal — reading the proposed tuple at execution time let a vendor swap the package after review. Never add an id-only overload. CLAUDE.md spec-073 rule (1)
Immutable IPFS CIDs Packages served from IPFS by plain CID (CID_PATTERN shape-vetted; ${gateway}/ipfs/${cid}/${encodedPath} with gateway failover). Zero/absent manifestHashno_approved_package, never a fetch. frontend/src/lib/miniapps/loader.js:246-248,319-371
Bytecode digest gate sha256(bytecode ‖ deployedBytecode) per compiled contract, recorded and diffed (--out / --compare). Catches a dependency hoist changing deployed bytecode. Runs in CI (test.yml job smart-contract-tests). Does NOT cover the solcjs path — a run using the native binary never exercises it, which is how a 0.8.24→0.8.36 solc bump passed (#1084). scripts/codegen/bytecode-digest.js:3-11,99-101; CLAUDE.md spec-075 rule (2)
Mini-app build digest gate sha256 of every built package file, recorded; the mini-app bytes are keccak-committed on-chain. Own CI job miniapp-bytes. Previously reported "output bytes unchanged" after a failed build. scripts/miniapps/record-build-digests.js:8,43
Storage-layout gate Offline comparison of compiled layout (hardhat build info) vs each live implementation's committed layout (.openzeppelin/ manifests). Gating in CI. UUPS storage must be append-only with a trailing __gap; OZ bases use ERC-7201 namespaced storage and contribute none. scripts/deploy/check-storage-layout.js:2-37; contracts/upgradeable/UUPSManaged.sol:45
Guard non-upgradeability SafePolicyGuard / SafePolicyGuardV2 are deliberately NOT upgradeable — an upgrade key over a policy guard is a backdoor across every vault. Migration is vault-consented via a threshold-approved setGuard. CLAUDE.md spec-068; contracts/custody/SafePolicyGuardV2.sol
x402 payment verification Everything verified before settlement, so a refused payment is never submitted and costs nothing; an engine outage is 503 settlement_unavailable, never a free serve. Acceptance is broadcast, not finality, stated on every surface. Contract-account payers are EOA-only refusals whose reason says so (a 1271 check would pass here and revert at the token). services/relay-gateway/src/x402/verify.js:20,63-87,139-273
Sanctions screening inside submit Screening happens inside the mini-app host's wallet.submit, before any rail is touched — strictly stronger than an app-side pre-check a package could skip. CLAUDE.md spec-073 rule (3)
IAM / IaC crypto hygiene google_project_iam_binding/_iam_policy rejected by npm run check:iac (authoritative → silently strips roles); never declare google_secret_manager_secret_version (payload lands in state in plaintext); the one exception (origin-lock header via a data source) is warned-on so it stays countable; KMS key versions + secret payloads carry prevent_destroy. CLAUDE.md spec-087 rules (1),(2),(3); infra/terraform/environments/prod/main.tf:229-255
Private module pinning The five shared Terraform modules are pinned by commit SHA, not tag (a tag can be repointed). infra/terraform/environments/prod/main.tf:266 (?ref=838c250b6dc8542fd0730b12ec7050462387bc53)

8. Library & version pins

Versions are from package-lock.json (resolved), declarations from the respective package.json. "Bytecode" = contributes to deployed EVM bytecode.

8.1 Solidity / bytecode-affecting (all EXACT-pinned)

Package Declared Locked Pin Bytecode? Evidence
@openzeppelin/contracts 5.4.0 5.4.0 (+ transitive 4.7.3 under @arbitrum/nitro-contracts, @offchainlabs/upgrade-executor, dev) EXACT YES root package.json; lock
@openzeppelin/contracts-upgradeable 5.4.0 5.4.0 (+ 4.7.3, 4.9.6 transitive) EXACT YES root package.json
@safe-global/safe-contracts 1.4.1 (with an ethers: $ethers override) 1.4.1 EXACT YES root package.json
@safe-global/safe-singleton-factory ^2.0.0 ranged address-affecting root package.json; scripts/deploy/lib/helpers.js:9
@chainlink/contracts 1.5.0 1.5.0 EXACT YES — a float 1.3.0→1.5.0 changed ChainlinkFunctionsOracleAdapter bytecode and only the byte gate caught it root package.json
solc (npm) 0.8.24 0.8.24 at root; 0.8.26 under node_modules/hardhat/node_modules/solc EXACT (root), transitive (hardhat's) YES on the solcjs pathhardhat.config.js resolves solc/soljson.js and compiles with it when FORCE_SOLCJS=true or in Codespaces. Dependabot-ignored for ANY update; the byte gate does not cover this path. root package.json; lock; hardhat.config.js:198-232
solc compiler profiles (hardhat config) 0.8.24 (optimizer: {enabled:true, runs:1}, viaIR:true, evmVersion:"paris", metadata.bytecodeHash:"none"); 0.8.23 (account bytecode, viaIR:true, evmVersion:"paris"); a 0.8.24 + evmVersion:"cancun" variant; a viaIR:false Safe profile in-repo config YES hardhat.config.js:321-374,456-506
@openzeppelin/hardhat-upgrades ^3.9.0 ranged tooling root package.json
@openzeppelin/upgrades-core ^1.44.2 1.46.0 ranged tooling (storage-layout gate) root package.json; lock

8.2 JS crypto primitives (client)

Package Declared (frontend) Locked (top-level) Pin Used for Evidence
@noble/curves ^2.3.0 2.4.0 (+ 1.2.0, 1.4.2, 1.8.0, 1.8.2, 1.9.0, 1.9.1, 1.9.7, 2.3.0 transitively) ranged X25519 ECDH, ed25519 (Solana) frontend/package.json; frontend/src/utils/crypto/primitives.js:12; lib/solana/derivation.js:26
@noble/hashes ^2.3.0 2.4.0 (+ 1.2.0, 1.3.2, 1.4.0, 1.7.0, 1.7.2, 1.8.0, 2.3.0) ranged HKDF, SHA-256, SHA3-256, HMAC primitives.js:13-15; envelopeEncryption.js:19-21; lib/bitcoin/derivation.js:32-33
@noble/ciphers ^1.0.0 1.3.0 (+ 2.4.0 under @noble/post-quantum) ranged ChaCha20-Poly1305, XChaCha20-Poly1305, randomBytes (webcrypto) primitives.js:16-18; envelopeEncryption.js:22-24
@noble/post-quantum ^0.7.0 0.7.1 ranged — pre-1.0 ML-KEM-768 inside the hand-rolled X-Wing (R1) frontend/package.json; envelopeEncryption.js:18
@scure/bip32 2.3.0 2.3.0 (+ 1.1.5, 1.4.0, 1.7.0) EXACT BIP-32 HD derivation (Bitcoin) frontend/package.json; lib/bitcoin/derivation.js:34
@scure/bip39 2.4.0 2.4.0 (+ 1.1.1, 1.3.0, 1.6.0) EXACT BIP-39 mnemonics frontend/package.json
@scure/btc-signer 2.4.1 2.4.1 EXACT P2WPKH/P2TR address encoding, BIP-341 taproot tweak, PSBT signing frontend/package.json; lib/bitcoin/addresses.js:27
@scure/base 2.4.0 2.4.0 (+ 1.1.9, 1.2.6, 2.3.0) EXACT bech32/bech32m/base58 encoding frontend/package.json
ethers ^6.17.0 (root + frontend + gateway) 6.17.0; 5.8.0 under @chainlink/contracts (dev) ranged keccak256, EIP-712 signing/verification, secp256k1, ERC-1271 calls package.json ×3; lock
viem ^2.53.1 2.56.0 ranged wagmi transport layer, UserOp / sendCalls frontend/package.json
wagmi ^3.6.21 3.7.7 ranged Wallet connection frontend/package.json
ox not declared (transitive) 0.9.3, 0.6.9, 0.14.34 transitive viem/AppKit crypto utilities lock
tweetnacl ^1.0.3 1.0.3 (+ 0.14.5 dev-transitive under sshpk, bcrypt-pbkdf) ranged nacl.box (X25519-XSalsa20-Poly1305) — dead module, R7 frontend/package.json; frontend/src/utils/encryption.js:22
tweetnacl-util ^0.15.1 0.15.1 ranged base64/utf8 for the above frontend/package.json
elliptic overrides: "^6.6.1" (root) 6.6.1 force-pinned range transitive ECDSA (R12) root package.json overrides; lock
ethereum-cryptography ^3.2.0 (root) 3.2.0 (+ 0.1.3, 1.2.0, 2.2.1) ranged scrypt + keccak256 for the floppy mnemonic keystore root package.json; scripts/operations/floppy-key/keystore.js:6-7
@noble/secp256k1 not declared 1.7.1 (dev-transitive) transitive lock
micro-eth-signer not declared 0.14.0 transitive lock
@adraffy/ens-normalize not declared 1.11.1 transitive ENS name normalization (identity resolution) lock

8.3 WebAuthn / passkey / hardware / native

Package Declared Locked Pin Notes Evidence
@capgo/capacitor-passkey 8.5.1 8.5.1 EXACT Native WebAuthn bridge; JSON-clones extensions, so the adapter pre-encodes PRF salts to base64url and decodes results frontend/package.json; frontend/src/lib/native/nativeCredentials.js:16-22,119
@simplewebauthn/server / browser NOT IN LOCK absent No off-chain WebAuthn verification exists. Verification is on-chain only (R14) lock query returned no entry
@capacitor/core / app / android / ios / cli 8.5.0 / 8.1.1 / 8.5.0 / 8.5.0 / 8.5.0 same EXACT Capacitor shells (spec 103); Java 21 + newest Xcode required frontend/package.json
@capacitor-community/bluetooth-le 8.3.0 8.3.0 EXACT Ledger-over-BLE transport rung frontend/package.json
@ledgerhq/hw-app-eth ^7.8.14 (with axios: ^1.19.0 override) 7.8.16 ranged Ledger EVM app frontend/package.json; root overrides
@ledgerhq/hw-transport ^6.35.7 ranged transport base frontend/package.json
@ledgerhq/hw-transport-web-ble ^6.35.0 6.35.0 ranged BLE frontend/package.json
@ledgerhq/hw-transport-webhid ^6.36.0 6.36.0 ranged WebHID frontend/package.json
@ledgerhq/hw-transport-webusb ^6.35.0 6.35.0 ranged WebUSB frontend/package.json
@trezor/connect-web ^9.7.3 9.7.3 ranged Trezor (frame-src https://connect.trezor.io) frontend/package.json; frontend/nginx.conf:96
In-repo Solidity WebAuthn vendored contracts/account/lib/webauthn-sol/WebAuthn.sol (Coinbase/Daimo lineage) + contracts/account/lib/FreshCryptoLib/{FCL_ecdsa,FCL_elliptic}.sol + contracts/account/lib/solady/ + contracts/account/lib/account-abstraction/ not npm-pinned — vendored source, version NOT DETERMINED
@google-cloud/kms ^6.0.0 (root + gateway) 6.0.0 ranged KMS secp256k1 signing; dynamically imported so dev/test needs no KMS package root + gateway package.json; services/relay-gateway/src/paymaster/sign.js:36
@polymarket/clob-client ^5.8.1 5.8.1 ranged CLOB order signing (client-side) frontend/package.json
@polymarket/builder-signing-sdk ^1.0.0 1.0.0 ranged Builder-code header signing (gateway-side) gateway package.json; src/polymarket/routes.js:15
@solana/kit ^8.1.0 8.2.0 (+ 2.3.0, 5.5.1) ranged Solana RPC / tx frontend/package.json
bitcoinjs-lib NOT IN LOCK absent Bitcoin uses @scure/btc-signer exclusively lock query
@noble/ed25519 NOT IN LOCK absent ed25519 comes from @noble/curves lock query

8.4 Explicitly absent (verified by grep, not assumed)

Not present Consequence
Semaphore, MACI, circom, snarkjs No ZK in live paths; no wasm-unsafe-eval needed in CSP (frontend/nginx.conf:54)
Any MPC / TSS / Shamir library No threshold key splitting
MD5, SHA-1, DES, RC4, AES-ECB No deprecated primitives found in contracts/, frontend/src, services/, packages/, scripts/
eth_sign as a signing method Appears only in the passkey connector's refusal set (frontend/src/connectors/passkey.js:35)
Math.random() in any crypto path One hit, in a demo component (frontend/src/components/StateManagementDemo.jsx:126) — not crypto

9. Explicitly NOT DETERMINED

Item Why
Vendored webauthn-sol / FreshCryptoLib / solady / account-abstraction upstream versions Source is vendored under contracts/account/lib/ with no version manifest read; no npm pin exists
fairwins-rpc-access-signing-key — algorithm and purpose Named only in terraform.tfvars:61,164; no consuming code found in the files read
Android upload keystore's internal key algorithm/size Only the JKS container and its password are referenced (release.yml:122-130)
TLS cipher suites / certificate algorithm Cloudflare-managed; nothing in-repo states them
KMS key rotation period google_kms_crypto_key declares purpose + version_template.algorithm and prevent_destroy; no rotation_period is set in infra/terraform/environments/prod/main.tf:241-255
Whether any FairWins X-Wing ciphertext has ever been produced by, or must interoperate with, a conformant X-Wing implementation Only that it would not — the label and seed expansion diverge (R1). Whether that matters depends on whether anything outside this repo ever decapsulates, which the repo does not say. R1 is therefore raised as non-interoperable and unaudited, not broken: encap and decap in this file agree with each other.
Whether the X-Wing secret key (a 32-byte seed, envelopeEncryption.js:62-80) provides the draft's implicit-rejection / FO-transform properties The implementation regenerates component keys from the seed on every decapsulation rather than storing the ML-KEM secret key; no reasoning about implicit rejection appears in the file