From 210f4dcd40375ac03080b489eed7164a8963da5f Mon Sep 17 00:00:00 2001 From: Ilyas Date: Mon, 29 Dec 2025 21:44:25 +0100 Subject: [PATCH 1/2] Add smooth crypto-lib as submodule in lib/crypto-lib --- .gitmodules | 3 +++ lib/crypto-lib | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/crypto-lib diff --git a/.gitmodules b/.gitmodules index dc145e4..8190d06 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/openzeppelin/openzeppelin-contracts +[submodule "lib/crypto-lib"] + path = lib/crypto-lib + url = https://github.com/get-smooth/crypto-lib diff --git a/lib/crypto-lib b/lib/crypto-lib new file mode 160000 index 0000000..d714e98 --- /dev/null +++ b/lib/crypto-lib @@ -0,0 +1 @@ +Subproject commit d714e9824e2e2e44be3c8fd498e0de651ddc9425 From 6532b4fcf8d34b6337c241b3c4282b6179f6f277 Mon Sep 17 00:00:00 2001 From: Ilyas Date: Mon, 29 Dec 2025 23:23:26 +0100 Subject: [PATCH 2/2] Replace FreshCryptoLib with SmoothCryptoLib and update related documentation --- .gitmodules | 3 --- README.md | 4 ++-- lib/FreshCryptoLib | 1 - src/WebAuthn.sol | 47 ++++++++++++++++++++++++++-------------- test/Utils.sol | 10 ++++----- test/WebAuthn.t.sol | 6 ++--- test/Webauthn.fuzz.t.sol | 4 ++-- 7 files changed, 43 insertions(+), 32 deletions(-) delete mode 160000 lib/FreshCryptoLib diff --git a/.gitmodules b/.gitmodules index 8190d06..351eedb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std -[submodule "lib/FreshCryptoLib"] - path = lib/FreshCryptoLib - url = https://github.com/rdubois-crypto/FreshCryptoLib [submodule "lib/solady"] path = lib/solady url = https://github.com/vectorized/solady diff --git a/README.md b/README.md index afd85e3..0727b2f 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ Webauthn-sol is a Solidity library for verifying WebAuthn authentication assertions. It builds on [Daimo's WebAuthn.sol](https://github.com/daimo-eth/p256-verifier/blob/master/src/WebAuthn.sol). -This library is optimized for Ethereum layer 2 rollup chains but will work on all EVM chains. Signature verification always attempts to use the [RIP-7212 precompile](https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md) and, if this fails, falls back to using [FreshCryptoLib](https://github.com/rdubois-crypto/FreshCryptoLib/blob/master/solidity/src/FCL_ecdsa.sol#L40). +This library is optimized for Ethereum layer 2 rollup chains but will work on all EVM chains. Signature verification always attempts to use the [RIP-7212 precompile](https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md) and, if this fails, falls back to using [SmoothCryptoLib](https://github.com/get-smooth/crypto-lib/blob/main/src/lib/libSCL_RIP7212.sol#L24). > [!IMPORTANT] -> FreshCryptoLib uses the `ModExp` precompile (`address(0x05)`), which is not supported on some chains, such as [Polygon zkEVM](https://www.rollup.codes/polygon-zkevm#precompiled-contracts). This library will not work on such chains, unless they support the RIP-7212 precompile. +> SmoothCryptoLib provides universal EVM compatibility through pure Solidity implementation. Unlike the deprecated FCL, SCL doesn't require the `ModExp` precompile and works on all chains including Polygon zkEVM. Code excerpts diff --git a/lib/FreshCryptoLib b/lib/FreshCryptoLib deleted file mode 160000 index 76f3f13..0000000 --- a/lib/FreshCryptoLib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 76f3f135b7b27d2aa519f265b56bfc49a2573ab5 diff --git a/src/WebAuthn.sol b/src/WebAuthn.sol index 0df8506..5bfb052 100644 --- a/src/WebAuthn.sol +++ b/src/WebAuthn.sol @@ -1,8 +1,8 @@ // 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 {SCL_RIP7212} from "crypto-lib/src/lib/libSCL_RIP7212.sol"; +import {n} from "crypto-lib/src/fields/SCL_secp256r1.sol"; import {Base64} from "openzeppelin-contracts/contracts/utils/Base64.sol"; import {LibString} from "solady/utils/LibString.sol"; @@ -12,7 +12,7 @@ import {LibString} from "solady/utils/LibString.sol"; /// of Daimo. /// /// @dev Attempts to use the RIP-7212 precompile for signature verification. -/// If precompile verification fails, it falls back to FreshCryptoLib. +/// If precompile verification fails, it falls back to SmoothCryptoLib. /// /// @author Coinbase (https://github.com/base-org/webauthn-sol) /// @author Daimo (https://github.com/daimo-eth/p256-verifier/blob/master/src/WebAuthn.sol) @@ -45,7 +45,7 @@ library WebAuthn { bytes1 private constant _AUTH_DATA_FLAGS_UV = 0x04; /// @dev Secp256r1 curve order / 2 used as guard to prevent signature malleability issue. - uint256 private constant _P256_N_DIV_2 = FCL_Elliptic_ZZ.n / 2; + uint256 private constant _P256_N_DIV_2 = n / 2; /// @dev The precompiled contract address to use for signature verification in the “secp256r1” elliptic curve. /// See https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md. @@ -148,17 +148,32 @@ library WebAuthn { // 20. Using credentialPublicKey, verify that sig is a valid signature over the binary concatenation of authData // 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); + + // Try the RIP-7212 precompile first for better gas efficiency + return _verifySignature(messageHash, webAuthnAuth.r, webAuthnAuth.s, x, y); + } + + /// @dev Internal function to verify signature, reducing stack depth in main verify function + /// @param messageHash The hash of the message to verify + /// @param r The r value of the signature + /// @param s The s value of the signature + /// @param x The x coordinate of the public key + /// @param y The y coordinate of the public key + /// @return True if signature is valid, false otherwise + function _verifySignature(bytes32 messageHash, uint256 r, uint256 s, uint256 x, uint256 y) + private + view + returns (bool) + { + // Try the RIP-7212 precompile address + (bool success, bytes memory ret) = _VERIFIER.staticcall(abi.encode(messageHash, r, s, x, y)); + + // Check if precompile is available and returned valid result + if (success && ret.length > 0) { + return abi.decode(ret, (uint256)) == 1; + } + + // Fallback to SCL pure Solidity implementation + return SCL_RIP7212.verify(messageHash, r, s, x, y); } } diff --git a/test/Utils.sol b/test/Utils.sol index 2313d99..27a31b7 100644 --- a/test/Utils.sol +++ b/test/Utils.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {FCL_Elliptic_ZZ} from "FreshCryptoLib/FCL_elliptic.sol"; -import {Base64Url} from "FreshCryptoLib/utils/Base64Url.sol"; +import {n} from "crypto-lib/src/fields/SCL_secp256r1.sol"; +import {Base64} from "openzeppelin-contracts/contracts/utils/Base64.sol"; struct WebAuthnInfo { bytes authenticatorData; @@ -11,10 +11,10 @@ struct WebAuthnInfo { } library Utils { - uint256 constant P256_N_DIV_2 = FCL_Elliptic_ZZ.n / 2; + uint256 constant P256_N_DIV_2 = n / 2; function getWebAuthnStruct(bytes32 challenge) public pure returns (WebAuthnInfo memory) { - string memory challengeb64url = Base64Url.encode(abi.encode(challenge)); + string memory challengeb64url = Base64.encodeURL(abi.encode(challenge)); string memory clientDataJSON = string( abi.encodePacked( '{"type":"webauthn.get","challenge":"', challengeb64url, '","origin":"https://sign.coinbase.com","crossOrigin":false}' @@ -34,7 +34,7 @@ library Utils { /// it will pass malleability checks. function normalizeS(uint256 s) public pure returns (uint256) { if (s > P256_N_DIV_2) { - return FCL_Elliptic_ZZ.n - s; + return n - s; } return s; diff --git a/test/WebAuthn.t.sol b/test/WebAuthn.t.sol index 2b5a398..58fd11b 100644 --- a/test/WebAuthn.t.sol +++ b/test/WebAuthn.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {Base64Url} from "FreshCryptoLib/utils/Base64Url.sol"; +import {Base64} from "openzeppelin-contracts/contracts/utils/Base64.sol"; import {Test, console2} from "forge-std/Test.sol"; import {WebAuthn} from "../src/WebAuthn.sol"; @@ -15,7 +15,7 @@ contract WebAuthnTest is Test { WebAuthn.WebAuthnAuth memory auth = WebAuthn.WebAuthnAuth({ authenticatorData: hex"49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000101", clientDataJSON: string.concat( - '{"type":"webauthn.get","challenge":"', Base64Url.encode(challenge), '","origin":"http://localhost:3005"}' + '{"type":"webauthn.get","challenge":"', Base64.encodeURL(challenge), '","origin":"http://localhost:3005"}' ), challengeIndex: 23, typeIndex: 1, @@ -31,7 +31,7 @@ contract WebAuthnTest is Test { WebAuthn.WebAuthnAuth memory auth = WebAuthn.WebAuthnAuth({ authenticatorData: hex"49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763050000010a", clientDataJSON: string.concat( - '{"type":"webauthn.get","challenge":"', Base64Url.encode(challenge), '","origin":"http://localhost:3005","crossOrigin":false}' + '{"type":"webauthn.get","challenge":"', Base64.encodeURL(challenge), '","origin":"http://localhost:3005","crossOrigin":false}' ), challengeIndex: 23, typeIndex: 1, diff --git a/test/Webauthn.fuzz.t.sol b/test/Webauthn.fuzz.t.sol index f05b77f..12431d4 100644 --- a/test/Webauthn.fuzz.t.sol +++ b/test/Webauthn.fuzz.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {FCL_ecdsa} from "FreshCryptoLib/FCL_ecdsa.sol"; +import {n} from "crypto-lib/src/fields/SCL_secp256r1.sol"; import {Test, Vm, console, stdJson} from "forge-std/Test.sol"; import {WebAuthn} from "../src/WebAuthn.sol"; @@ -34,7 +34,7 @@ contract WebAuthnFuzzTest is Test { // Only interested in s > P256_N_DIV_2 cases. if (webAuthnAuth.s <= Utils.P256_N_DIV_2) { - webAuthnAuth.s = FCL_ecdsa.n - webAuthnAuth.s; + webAuthnAuth.s = n - webAuthnAuth.s; } bool res = WebAuthn.verify({challenge: challenge, requireUV: uv, webAuthnAuth: webAuthnAuth, x: x, y: y});