From 9d86fbb5c8d2e3f780858ecbf5139ef56d8634aa Mon Sep 17 00:00:00 2001 From: Amie Corso Date: Tue, 4 Nov 2025 18:21:55 -0800 Subject: [PATCH 1/2] use openzeppelin's strategy --- .gitmodules | 5 +++- foundry.lock | 20 ++++++++++++++ lib/ozp256 | 1 + src/WebAuthn.sol | 72 ++++++++++++++++++++++++++++++++++-------------- 4 files changed, 77 insertions(+), 21 deletions(-) create mode 100644 foundry.lock create mode 160000 lib/ozp256 diff --git a/.gitmodules b/.gitmodules index dc145e4..9616cdd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -9,4 +9,7 @@ url = https://github.com/vectorized/solady [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts - url = https://github.com/openzeppelin/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/ozp256"] + path = lib/ozp256 + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 0000000..1def8cf --- /dev/null +++ b/foundry.lock @@ -0,0 +1,20 @@ +{ + "lib/FreshCryptoLib": { + "rev": "76f3f135b7b27d2aa519f265b56bfc49a2573ab5" + }, + "lib/forge-std": { + "rev": "ae570fec082bfe1c1f45b0acca4a2b4f84d345ce" + }, + "lib/openzeppelin-contracts": { + "rev": "5705e8208bc92cd82c7bcdfeac8dbc7377767d96" + }, + "lib/ozp256": { + "tag": { + "name": "v5.5.0", + "rev": "fcbae5394ae8ad52d8e580a3477db99814b9d565" + } + }, + "lib/solady": { + "rev": "e7024bee47b1623f436ee491ca9458a6dc8abce9" + } +} \ No newline at end of file diff --git a/lib/ozp256 b/lib/ozp256 new file mode 160000 index 0000000..fcbae53 --- /dev/null +++ b/lib/ozp256 @@ -0,0 +1 @@ +Subproject commit fcbae5394ae8ad52d8e580a3477db99814b9d565 diff --git a/src/WebAuthn.sol b/src/WebAuthn.sol index 0df8506..42186d7 100644 --- a/src/WebAuthn.sol +++ b/src/WebAuthn.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {FCL_ecdsa} from "FreshCryptoLib/FCL_ecdsa.sol"; import {FCL_Elliptic_ZZ} from "FreshCryptoLib/FCL_elliptic.sol"; import {Base64} from "openzeppelin-contracts/contracts/utils/Base64.sol"; +import {P256} from "ozp256/contracts/utils/cryptography/P256.sol"; import {LibString} from "solady/utils/LibString.sol"; /// @title WebAuthn @@ -55,6 +55,14 @@ library WebAuthn { /// See https://www.w3.org/TR/webauthn-2/#dom-collectedclientdata-type bytes32 private constant _EXPECTED_TYPE_HASH = keccak256('"type":"webauthn.get"'); + // Known-valid P-256 test vector used to disambiguate precompile presence vs invalid signature. + // Values are from Wycheproof and match OpenZeppelin's probe. + bytes32 private constant _PROBE_H = 0xbb5a52f42f9c9261ed4361f59422a1e30036e7c32b270c8807a419feca605023; + bytes32 private constant _PROBE_R = bytes32(uint256(5)); + bytes32 private constant _PROBE_S = bytes32(uint256(1)); + bytes32 private constant _PROBE_QX = 0xa71af64de5126a4a4e02b7922d66ce9415ce88a4c9d25514d91082c8725ac957; + bytes32 private constant _PROBE_QY = 0x5d47723c8fbe580bb369fec9c2665d8e30a435b9932645482e7c9f11e872296b; + /// /// @notice Verifies a Webauthn Authentication Assertion as described /// in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion. @@ -95,11 +103,11 @@ library WebAuthn { /// - Does NOT verify the attestation object: this assumes that response.attestationObject is NOT present in the response, /// i.e. the RP does not intend to verify an attestation. /// - /// @param challenge The challenge that was provided by the relying party. - /// @param requireUV A boolean indicating whether user verification is required. + /// @param challenge The challenge that was provided by the relying party. + /// @param requireUV A boolean indicating whether user verification is required. /// @param webAuthnAuth The `WebAuthnAuth` struct. - /// @param x The x coordinate of the public key. - /// @param y The y coordinate of the public key. + /// @param x The x coordinate of the public key. + /// @param y The y coordinate of the public key. /// /// @return `true` if the authentication assertion passed validation, else `false`. function verify(bytes memory challenge, bool requireUV, WebAuthnAuth memory webAuthnAuth, uint256 x, uint256 y) @@ -113,7 +121,7 @@ library WebAuthn { } // 11. Verify that the value of C.type is the string webauthn.get. - // bytes("type":"webauthn.get").length = 21 + // bytes("type":"webauthn.get").length = 21 string memory _type = webAuthnAuth.clientDataJSON.slice(webAuthnAuth.typeIndex, webAuthnAuth.typeIndex + 21); if (keccak256(bytes(_type)) != _EXPECTED_TYPE_HASH) { return false; @@ -135,7 +143,7 @@ library WebAuthn { } // 17. If user verification is required for this assertion, verify that the User Verified bit of the flags in - // authData is set. + // authData is set. if (requireUV && (webAuthnAuth.authenticatorData[32] & _AUTH_DATA_FLAGS_UV) != _AUTH_DATA_FLAGS_UV) { return false; } @@ -146,19 +154,43 @@ library WebAuthn { bytes32 clientDataJSONHash = sha256(bytes(webAuthnAuth.clientDataJSON)); // 20. Using credentialPublicKey, verify that sig is a valid signature over the binary concatenation of authData - // and hash. + // and hash. bytes32 messageHash = sha256(abi.encodePacked(webAuthnAuth.authenticatorData, clientDataJSONHash)); - bytes memory args = abi.encode(messageHash, webAuthnAuth.r, webAuthnAuth.s, x, y); - // try the RIP-7212 precompile address - (bool success, bytes memory ret) = _VERIFIER.staticcall(args); - // staticcall will not revert if address has no code - // check return length - // note that even if precompile exists, ret.length is 0 when verification returns false - // so an invalid signature will be checked twice: once by the precompile and once by FCL. - // Ideally this signature failure is simulated offchain and no one actually pay this gas. - bool valid = ret.length > 0; - if (success && valid) return abi.decode(ret, (uint256)) == 1; - - return FCL_ecdsa.ecdsa_verify(messageHash, webAuthnAuth.r, webAuthnAuth.s, x, y); + // Attempt RIP-7212 precompile; if ambiguous/false, probe with known-valid vector to + // detect precompile presence and avoid unnecessary software fallback when present. + if (_rip7212(messageHash, webAuthnAuth.r, webAuthnAuth.s, x, y)) { + return true; + } + + if (_rip7212(_PROBE_H, uint256(_PROBE_R), uint256(_PROBE_S), uint256(_PROBE_QX), uint256(_PROBE_QY))) { + // Precompile is present; original signature invalid. + return false; + } + + // Precompile absent; fall back to OpenZeppelin's on-chain verifier. + return P256.verifySolidity(messageHash, bytes32(webAuthnAuth.r), bytes32(webAuthnAuth.s), bytes32(x), bytes32(y)); + } + + /// @dev RIP-7212 precompile call. Writes output to scratch space to distinguish + /// empty returns from zero words. Returns true if signature is valid. + function _rip7212(bytes32 h, uint256 r, uint256 s, uint256 qx, uint256 qy) private view returns (bool isValid) { + assembly { + let ptr := mload(0x40) + mstore(ptr, h) + mstore(add(ptr, 0x20), r) + mstore(add(ptr, 0x40), s) + mstore(add(ptr, 0x60), qx) + mstore(add(ptr, 0x80), qy) + + // Zero scratch space. If the precompile returns nothing, it will remain zero. + mstore(0x00, 0) + + // Call RIP-7212 precompile (address 0x100). Return data (32 bytes) is written to 0x00. + // staticcall returns success even if address has no code; output length may be zero. + // We do not branch on the success flag; instead we read the scratch word. + pop(staticcall(gas(), 0x100, ptr, 0xa0, 0x00, 0x20)) + + isValid := mload(0x00) + } } } From e2c98a83e86bda9a1d50ec0ce426d32ca345e91b Mon Sep 17 00:00:00 2001 From: Amie Corso Date: Wed, 5 Nov 2025 09:52:29 -0800 Subject: [PATCH 2/2] use FCL instead of OZ verifier --- src/WebAuthn.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/WebAuthn.sol b/src/WebAuthn.sol index 42186d7..3e11fe6 100644 --- a/src/WebAuthn.sol +++ b/src/WebAuthn.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +import {FCL_ecdsa} from "FreshCryptoLib/FCL_ecdsa.sol"; import {FCL_Elliptic_ZZ} from "FreshCryptoLib/FCL_elliptic.sol"; import {Base64} from "openzeppelin-contracts/contracts/utils/Base64.sol"; -import {P256} from "ozp256/contracts/utils/cryptography/P256.sol"; import {LibString} from "solady/utils/LibString.sol"; /// @title WebAuthn @@ -167,8 +167,8 @@ library WebAuthn { return false; } - // Precompile absent; fall back to OpenZeppelin's on-chain verifier. - return P256.verifySolidity(messageHash, bytes32(webAuthnAuth.r), bytes32(webAuthnAuth.s), bytes32(x), bytes32(y)); + // Precompile absent; fall back to FreshCryptoLib's software verifier. + return FCL_ecdsa.ecdsa_verify(messageHash, webAuthnAuth.r, webAuthnAuth.s, x, y); } /// @dev RIP-7212 precompile call. Writes output to scratch space to distinguish