// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; // OpenZeppelin v4.9 ships Base64.encode (standard) but not encodeURL — inline the URL variant. /// @title WebAuthn — secp256r1 WebAuthn assertion verifier for AERE /// @notice Precompile-only verification using Fusaka's RIP-7951 at address 0x100. /// AERE chain 2800 activated Fusaka at unix 1780220351 (block 2,106,606) so the /// precompile is unconditionally present from there onward — no FreshCryptoLib /// fallback needed. /// /// @dev Adapted from Coinbase Smart Wallet's WebAuthn library (base-org/webauthn-sol), /// which adapted from Daimo's p256-verifier. Verification logic is identical; /// we strip the FCL_ecdsa fallback path because the precompile is guaranteed on AERE. /// /// Audited reference: https://github.com/base-org/webauthn-sol (Coinbase, 2024) library WebAuthn { struct WebAuthnAuth { /// @dev Authenticator data — at minimum 37 bytes: rpIdHash(32) + flags(1) + signCount(4). bytes authenticatorData; /// @dev Client data JSON — the browser's WebAuthn response, contains the challenge. string clientDataJSON; /// @dev Index in clientDataJSON where "challenge":"..." begins. uint256 challengeIndex; /// @dev Index in clientDataJSON where "type":"..." begins. uint256 typeIndex; /// @dev secp256r1 signature r component. uint256 r; /// @dev secp256r1 signature s component. uint256 s; } /// @dev secp256r1 curve order / 2 — used to guard against signature malleability. /// n = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551 /// n/2 below. uint256 private constant P256_N_DIV_2 = 0x7fffffff80000000ffffffffffffffffde737d56d38bcf4279dce5617e3192a8; /// @dev RIP-7951 secp256r1 verifier precompile address (Fusaka). address private constant VERIFIER = address(0x100); /// @dev Authenticator data flag bits per WebAuthn spec. bytes1 private constant AUTH_FLAG_UP = 0x01; // User Present bytes1 private constant AUTH_FLAG_UV = 0x04; // User Verified /// @dev Expected "type":"webauthn.get" hash (21 bytes verbatim). bytes32 private constant EXPECTED_TYPE_HASH = keccak256('"type":"webauthn.get"'); /// @notice Verify a WebAuthn assertion signature. /// @param challenge The 32-byte challenge that was passed to navigator.credentials.get(). /// Must match the base64url-encoded value in clientDataJSON. /// @param requireUV If true, require the User Verified flag to be set (Touch ID / Face ID /// counts as UV; a simple "present" press of a YubiKey does not). /// @param auth Parsed WebAuthn assertion from the browser. /// @param x secp256r1 public key x-coordinate. /// @param y secp256r1 public key y-coordinate. /// @return ok True iff the assertion is valid. function verify( bytes memory challenge, bool requireUV, WebAuthnAuth memory auth, uint256 x, uint256 y ) internal view returns (bool ok) { // 1. Malleability guard. if (auth.s > P256_N_DIV_2) return false; // 2. Verify clientDataJSON contains "type":"webauthn.get" at typeIndex. bytes memory cd = bytes(auth.clientDataJSON); if (auth.typeIndex + 21 > cd.length) return false; bytes memory typeSlice = new bytes(21); for (uint256 i = 0; i < 21; i++) typeSlice[i] = cd[auth.typeIndex + i]; if (keccak256(typeSlice) != EXPECTED_TYPE_HASH) return false; // 3. Verify clientDataJSON contains the expected challenge at challengeIndex. bytes memory expectedChallenge = bytes( string.concat('"challenge":"', _base64url(challenge), '"') ); uint256 elen = expectedChallenge.length; if (auth.challengeIndex + elen > cd.length) return false; bytes memory actualChallenge = new bytes(elen); for (uint256 i = 0; i < elen; i++) actualChallenge[i] = cd[auth.challengeIndex + i]; if (keccak256(actualChallenge) != keccak256(expectedChallenge)) return false; // 4. Verify authenticatorData flags. if (auth.authenticatorData.length < 33) return false; if (auth.authenticatorData[32] & AUTH_FLAG_UP != AUTH_FLAG_UP) return false; if (requireUV && (auth.authenticatorData[32] & AUTH_FLAG_UV) != AUTH_FLAG_UV) return false; // 5. Compute the message hash that was signed: sha256(authData || sha256(clientDataJSON)). bytes32 clientDataHash = sha256(bytes(auth.clientDataJSON)); bytes32 msgHash = sha256(abi.encodePacked(auth.authenticatorData, clientDataHash)); // 6. Call the RIP-7951 precompile at 0x100. // Input: msgHash (32) || r (32) || s (32) || x (32) || y (32) — 160 bytes total. // Output: 32 bytes — 0x...01 if valid, empty if invalid (or 0x...00). bytes memory args = abi.encode(msgHash, auth.r, auth.s, x, y); (bool callOk, bytes memory ret) = VERIFIER.staticcall(args); if (!callOk || ret.length == 0) return false; return abi.decode(ret, (uint256)) == 1; } /// @dev Base64-URL encode (no padding, '-_' alphabet) — for the WebAuthn challenge. /// Standard library implementation; gas-light because typical input is 32 bytes. function _base64url(bytes memory data) private pure returns (string memory) { if (data.length == 0) return ""; bytes memory table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; uint256 dataLen = data.length; // base64url length without padding = ceil(dataLen * 4 / 3) uint256 encodedLen = (dataLen * 4 + 2) / 3; bytes memory result = new bytes(encodedLen); uint256 j; uint256 i; for (i = 0; i + 3 <= dataLen; i += 3) { uint256 v = (uint256(uint8(data[i])) << 16) | (uint256(uint8(data[i+1])) << 8) | uint256(uint8(data[i+2])); result[j++] = table[(v >> 18) & 0x3f]; result[j++] = table[(v >> 12) & 0x3f]; result[j++] = table[(v >> 6) & 0x3f]; result[j++] = table[v & 0x3f]; } uint256 rem = dataLen - i; if (rem == 1) { uint256 v = uint256(uint8(data[i])) << 16; result[j++] = table[(v >> 18) & 0x3f]; result[j++] = table[(v >> 12) & 0x3f]; } else if (rem == 2) { uint256 v = (uint256(uint8(data[i])) << 16) | (uint256(uint8(data[i+1])) << 8); result[j++] = table[(v >> 18) & 0x3f]; result[j++] = table[(v >> 12) & 0x3f]; result[j++] = table[(v >> 6) & 0x3f]; } return string(result); } }