Aere Network public source. Everything here can be checked against the live chain (chain id 2800, https://rpc.aere.network). Scope note, stated up front rather than buried: consensus on chain 2800 is classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at the signature, precompile, account and transport layers. Nothing here makes the consensus post-quantum, and no document in it should be read as claiming so.
135 lines
6.7 KiB
Solidity
135 lines
6.7 KiB
Solidity
// 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);
|
|
}
|
|
}
|