diff --git a/CHANGELOG.md b/CHANGELOG.md index 79226a80..b653db4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## XX-XX-XXXX + +- Remove `WebAuthn`, `SignerWebAuthn`, `ERC7913WebAuthnVerifier`. These contracts were migrated to `@openzeppelin/contracts>=5.5.0`. + ## 09-08-2025 - `ZKEmailUtils`: Simplify library implementation and remove `Verifier.sol` indirection for cleaner integration with a Groth16Verifier. diff --git a/contracts/mocks/account/AccountWebAuthnMock.sol b/contracts/mocks/account/AccountWebAuthnMock.sol deleted file mode 100644 index ae7daeff..00000000 --- a/contracts/mocks/account/AccountWebAuthnMock.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.24; - -import {Account} from "@openzeppelin/contracts/account/Account.sol"; -import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; -import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; -import {ERC7739} from "@openzeppelin/contracts/utils/cryptography/signers/draft-ERC7739.sol"; -import {ERC7821} from "@openzeppelin/contracts/account/extensions/draft-ERC7821.sol"; -import {SignerWebAuthn} from "../../utils/cryptography/signers/SignerWebAuthn.sol"; - -abstract contract AccountWebAuthnMock is Account, SignerWebAuthn, ERC7739, ERC7821, ERC721Holder, ERC1155Holder { - /// @inheritdoc ERC7821 - function _erc7821AuthorizedExecutor( - address caller, - bytes32 mode, - bytes calldata executionData - ) internal view virtual override returns (bool) { - return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData); - } -} diff --git a/contracts/utils/cryptography/README.adoc b/contracts/utils/cryptography/README.adoc index 4a90cb58..35fa487c 100644 --- a/contracts/utils/cryptography/README.adoc +++ b/contracts/utils/cryptography/README.adoc @@ -6,25 +6,17 @@ NOTE: This document is better viewed at https://docs.openzeppelin.com/community- A collection of contracts and libraries that implement various signature validation schemes and cryptographic primitives. These utilities enable secure authentication, multisignature operations, and advanced cryptographic operations in smart contracts. * {ZKEmailUtils}: Library for ZKEmail signature validation utilities, enabling email-based authentication through zero-knowledge proofs. - * {WebAuthn}: Library for verifying WebAuthn Authentication Assertions. * {SignerZKEmail}: Implementation of an https://docs.openzeppelin.com/contracts/5.x/api/utils/cryptography#AbstractSigner[AbstractSigner] that enables email-based authentication through zero-knowledge proofs. - * {SignerWebAuthn}: Implementation of https://docs.openzeppelin.com/contracts/5.x/api/utils/cryptography#SignerP256[SignerP256] that supports WebAuthn authentication assertions. - * {ERC7913ZKEmailVerifier}, {ERC7913WebAuthnVerifier}: Ready to use ERC-7913 signature verifiers for ZKEmail and WebAuthn. + * {ERC7913ZKEmailVerifier}: Ready to use ERC-7913 signature verifiers for ZKEmail. == Utils {{ZKEmailUtils}} -{{WebAuthn}} - == Abstract Signers {{SignerZKEmail}} -{{SignerWebAuthn}} - == Verifiers {{ERC7913ZKEmailVerifier}} - -{{ERC7913WebAuthnVerifier}} diff --git a/contracts/utils/cryptography/WebAuthn.sol b/contracts/utils/cryptography/WebAuthn.sol deleted file mode 100644 index 82455f1c..00000000 --- a/contracts/utils/cryptography/WebAuthn.sol +++ /dev/null @@ -1,260 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.24; - -import {Base64} from "@openzeppelin/contracts/utils/Base64.sol"; -import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; -import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; -import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; - -/** - * @dev Library for verifying WebAuthn Authentication Assertions. - * - * WebAuthn enables strong authentication for smart contracts using - * https://docs.openzeppelin.com/contracts/5.x/api/utils#P256[P256] - * as an alternative to traditional secp256k1 ECDSA signatures. This library verifies - * signatures generated during WebAuthn authentication ceremonies as specified in the - * https://www.w3.org/TR/webauthn-2/[WebAuthn Level 2 standard]. - * - * For blockchain use cases, the following WebAuthn validations are intentionally omitted: - * - * * Origin validation: Origin verification in `clientDataJSON` is omitted as blockchain - * contexts rely on authenticator and dapp frontend enforcement. Standard authenticators - * implement proper origin validation. - * * RP ID hash validation: Verification of `rpIdHash` in authenticatorData against expected - * RP ID hash is omitted. This is typically handled by platform-level security measures. - * Including an expiry timestamp in signed data is recommended for enhanced security. - * * Signature counter: Verification of signature counter increments is omitted. While - * useful for detecting credential cloning, on-chain operations typically include nonce - * protection, making this check redundant. - * * Extension outputs: Extension output value verification is omitted as these are not - * essential for core authentication security in blockchain applications. - * * Attestation: Attestation object verification is omitted as this implementation - * focuses on authentication (`webauthn.get`) rather than registration ceremonies. - * - * Inspired by: - * - * * https://github.com/daimo-eth/p256-verifier/blob/master/src/WebAuthn.sol[daimo-eth implementation] - * * https://github.com/base/webauthn-sol/blob/main/src/WebAuthn.sol[base implementation] - */ -library WebAuthn { - struct WebAuthnAuth { - bytes32 r; /// The r value of secp256r1 signature - bytes32 s; /// The s value of secp256r1 signature - uint256 challengeIndex; /// The index at which "challenge":"..." occurs in `clientDataJSON`. - uint256 typeIndex; /// The index at which "type":"..." occurs in `clientDataJSON`. - /// The WebAuthn authenticator data. - /// https://www.w3.org/TR/webauthn-2/#dom-authenticatorassertionresponse-authenticatordata - bytes authenticatorData; - /// The WebAuthn client data JSON. - /// https://www.w3.org/TR/webauthn-2/#dom-authenticatorresponse-clientdatajson - string clientDataJSON; - } - - /// @dev Bit 0 of the authenticator data flags: "User Present" bit. - bytes1 internal constant AUTH_DATA_FLAGS_UP = 0x01; - /// @dev Bit 2 of the authenticator data flags: "User Verified" bit. - bytes1 internal constant AUTH_DATA_FLAGS_UV = 0x04; - /// @dev Bit 3 of the authenticator data flags: "Backup Eligibility" bit. - bytes1 internal constant AUTH_DATA_FLAGS_BE = 0x08; - /// @dev Bit 4 of the authenticator data flags: "Backup State" bit. - bytes1 internal constant AUTH_DATA_FLAGS_BS = 0x10; - - /** - * @dev Performs standard verification of a WebAuthn Authentication Assertion. - */ - function verify( - bytes memory challenge, - WebAuthnAuth memory auth, - bytes32 qx, - bytes32 qy - ) internal view returns (bool) { - return verify(challenge, auth, qx, qy, true); - } - - /** - * @dev Performs verification of a WebAuthn Authentication Assertion. This variants allow the caller to select - * whether of not to require the UV flag (step 17). - * - * Verifies: - * - * 1. Type is "webauthn.get" (see {_validateExpectedTypeHash}) - * 2. Challenge matches the expected value (see {_validateChallenge}) - * 3. Cryptographic signature is valid for the given public key - * 4. confirming physical user presence during authentication - * 5. (if `requireUV` is true) confirming stronger user authentication (biometrics/PIN) - * 6. Backup Eligibility (`BE`) and Backup State (BS) bits relationship is valid - */ - function verify( - bytes memory challenge, - WebAuthnAuth memory auth, - bytes32 qx, - bytes32 qy, - bool requireUV - ) internal view returns (bool) { - // Verify authenticator data has sufficient length (37 bytes minimum): - // - 32 bytes for rpIdHash - // - 1 byte for flags - // - 4 bytes for signature counter - return - auth.authenticatorData.length > 36 && - _validateExpectedTypeHash(auth.clientDataJSON, auth.typeIndex) && // 11 - _validateChallenge(auth.clientDataJSON, auth.challengeIndex, challenge) && // 12 - _validateUserPresentBitSet(auth.authenticatorData[32]) && // 16 - (!requireUV || _validateUserVerifiedBitSet(auth.authenticatorData[32])) && // 17 - _validateBackupEligibilityAndState(auth.authenticatorData[32]) && // Consistency check - // P256.verify handles signature malleability internally - P256.verify( - sha256( - abi.encodePacked( - auth.authenticatorData, - sha256(bytes(auth.clientDataJSON)) // 19 - ) - ), - auth.r, - auth.s, - qx, - qy - ); // 20 - } - - /** - * @dev Validates that the https://www.w3.org/TR/webauthn-2/#type[Type] field in the client data JSON is set to - * "webauthn.get". - * - * Step 11 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion[verifying an assertion]. - */ - function _validateExpectedTypeHash( - string memory clientDataJSON, - uint256 typeIndex - ) private pure returns (bool success) { - assembly ("memory-safe") { - success := and( - // clientDataJson.length >= typeIndex + 21 - gt(mload(clientDataJSON), add(typeIndex, 20)), - eq( - // get 32 bytes starting at index typexIndex in clientDataJSON, and keep the leftmost 21 bytes - and(mload(add(add(clientDataJSON, 0x20), typeIndex)), shl(88, not(0))), - // solhint-disable-next-line quotes - '"type":"webauthn.get"' - ) - ) - } - } - - /** - * @dev Validates that the challenge in the client data JSON matches the `expectedChallenge`. - * - * Step 12 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion[verifying an assertion]. - */ - function _validateChallenge( - string memory clientDataJSON, - uint256 challengeIndex, - bytes memory challenge - ) private pure returns (bool) { - // solhint-disable-next-line quotes - string memory expectedChallenge = string.concat('"challenge":"', Base64.encodeURL(challenge), '"'); - string memory actualChallenge = string( - Bytes.slice(bytes(clientDataJSON), challengeIndex, challengeIndex + bytes(expectedChallenge).length) - ); - - return Strings.equal(actualChallenge, expectedChallenge); - } - - /** - * @dev Validates that the https://www.w3.org/TR/webauthn-2/#up[User Present (UP)] bit is set. - * - * Step 16 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion[verifying an assertion]. - * - * NOTE: Required by WebAuthn spec but may be skipped for platform authenticators - * (Touch ID, Windows Hello) in controlled environments. Enforce for public-facing apps. - */ - function _validateUserPresentBitSet(bytes1 flags) private pure returns (bool) { - return (flags & AUTH_DATA_FLAGS_UP) == AUTH_DATA_FLAGS_UP; - } - - /** - * @dev Validates that the https://www.w3.org/TR/webauthn-2/#uv[User Verified (UV)] bit is set. - * - * Step 17 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion[verifying an assertion]. - * - * The UV bit indicates whether the user was verified using a stronger identification method - * (biometrics, PIN, password). While optional, requiring UV=1 is recommended for: - * - * * High-value transactions and sensitive operations - * * Account recovery and critical settings changes - * * Privileged operations - * - * NOTE: For routine operations or when using hardware authenticators without verification capabilities, - * `UV=0` may be acceptable. The choice of whether to require UV represents a security vs. usability - * tradeoff - for blockchain applications handling valuable assets, requiring UV is generally safer. - */ - function _validateUserVerifiedBitSet(bytes1 flags) private pure returns (bool) { - return (flags & AUTH_DATA_FLAGS_UV) == AUTH_DATA_FLAGS_UV; - } - - /** - * @dev Validates the relationship between Backup Eligibility (`BE`) and Backup State (`BS`) bits - * according to the WebAuthn specification. - * - * The function enforces that if a credential is backed up (`BS=1`), it must also be eligible - * for backup (`BE=1`). This prevents unauthorized credential backup and ensures compliance - * with the WebAuthn spec. - * - * Returns true in these valid states: - * - * * `BE=1`, `BS=0`: Credential is eligible but not backed up - * * `BE=1`, `BS=1`: Credential is eligible and backed up - * * `BE=0`, `BS=0`: Credential is not eligible and not backed up - * - * Returns false only when `BE=0` and `BS=1`, which is an invalid state indicating - * a credential that's backed up but not eligible for backup. - * - * NOTE: While the WebAuthn spec defines this relationship between `BE` and `BS` bits, - * validating it is not explicitly required as part of the core verification procedure. - * Some implementations may choose to skip this check for broader authenticator - * compatibility or when the application's threat model doesn't consider credential - * syncing a major risk. - */ - function _validateBackupEligibilityAndState(bytes1 flags) private pure returns (bool) { - return (flags & AUTH_DATA_FLAGS_BE) == AUTH_DATA_FLAGS_BE || (flags & AUTH_DATA_FLAGS_BS) == 0; - } - - /** - * @dev Verifies that calldata bytes (`input`) represents a valid `WebAuthnAuth` object. If encoding is valid, - * returns true and the calldata view at the object. Otherwise, returns false and an invalid calldata object. - * - * NOTE: The returned `auth` object should not be accessed if `success` is false. Trying to access the data may - * cause revert/panic. - */ - function tryDecodeAuth(bytes calldata input) internal pure returns (bool success, WebAuthnAuth calldata auth) { - assembly ("memory-safe") { - auth := input.offset - } - - // Minimum length to hold 6 objects (32 bytes each) - if (input.length < 0xC0) return (false, auth); - - // Get offset of non-value-type elements relative to the input buffer - uint256 authenticatorDataOffset = uint256(bytes32(input[0x80:])); - uint256 clientDataJSONOffset = uint256(bytes32(input[0xa0:])); - - // The elements length (at the offset) should be 32 bytes long. We check that this is within the - // buffer bounds. Since we know input.length is at least 32, we can subtract with no overflow risk. - if (input.length - 0x20 < authenticatorDataOffset || input.length - 0x20 < clientDataJSONOffset) - return (false, auth); - - // Get the lengths. offset + 32 is bounded by input.length so it does not overflow. - uint256 authenticatorDataLength = uint256(bytes32(input[authenticatorDataOffset:])); - uint256 clientDataJSONLength = uint256(bytes32(input[clientDataJSONOffset:])); - - // Check that the input buffer is long enough to store the non-value-type elements - // Since we know input.length is at least xxxOffset + 32, we can subtract with no overflow risk. - if ( - input.length - authenticatorDataOffset - 0x20 < authenticatorDataLength || - input.length - clientDataJSONOffset - 0x20 < clientDataJSONLength - ) return (false, auth); - - return (true, auth); - } -} diff --git a/contracts/utils/cryptography/signers/SignerWebAuthn.sol b/contracts/utils/cryptography/signers/SignerWebAuthn.sol deleted file mode 100644 index 5aaa7e48..00000000 --- a/contracts/utils/cryptography/signers/SignerWebAuthn.sol +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.24; - -import {SignerP256} from "@openzeppelin/contracts/utils/cryptography/signers/SignerP256.sol"; -import {WebAuthn} from "../WebAuthn.sol"; - -/** - * @dev Implementation of {SignerP256} that supports WebAuthn authentication assertions. - * - * This contract enables signature validation using WebAuthn authentication assertions, - * leveraging the P256 public key stored in the contract. It allows for both WebAuthn - * and raw P256 signature validation, providing compatibility with both signature types. - * - * The signature is expected to be an abi-encoded {WebAuthn-WebAuthnAuth} struct. - * - * Example usage: - * - * ```solidity - * contract MyAccountWebAuthn is Account, SignerWebAuthn, Initializable { - * function initialize(bytes32 qx, bytes32 qy) public initializer { - * _setSigner(qx, qy); - * } - * } - * ``` - * - * IMPORTANT: Failing to call {_setSigner} either during construction (if used standalone) - * or during initialization (if used as a clone) may leave the signer either front-runnable or unusable. - */ -abstract contract SignerWebAuthn is SignerP256 { - /** - * @dev Validates a raw signature using the WebAuthn authentication assertion. - * - * In case the signature can't be validated, it falls back to the - * {SignerP256-_rawSignatureValidation} method for raw P256 signature validation by passing - * the raw `r` and `s` values from the signature. - */ - function _rawSignatureValidation( - bytes32 hash, - bytes calldata signature - ) internal view virtual override returns (bool) { - (bytes32 qx, bytes32 qy) = signer(); - (bool decodeSuccess, WebAuthn.WebAuthnAuth calldata auth) = WebAuthn.tryDecodeAuth(signature); - - return - decodeSuccess - ? WebAuthn.verify(abi.encodePacked(hash), auth, qx, qy) - : super._rawSignatureValidation(hash, signature); - } -} diff --git a/contracts/utils/cryptography/verifiers/ERC7913WebAuthnVerifier.sol b/contracts/utils/cryptography/verifiers/ERC7913WebAuthnVerifier.sol deleted file mode 100644 index 84a96146..00000000 --- a/contracts/utils/cryptography/verifiers/ERC7913WebAuthnVerifier.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.24; - -import {WebAuthn} from "../WebAuthn.sol"; -import {IERC7913SignatureVerifier} from "@openzeppelin/contracts/interfaces/IERC7913.sol"; - -/** - * @dev ERC-7913 signature verifier that supports WebAuthn authentication assertions. - * - * This verifier enables the validation of WebAuthn signatures using P256 public keys. - * The key is expected to be a 64-byte concatenation of the P256 public key coordinates (qx || qy). - * The signature is expected to be an abi-encoded {WebAuthn-WebAuthnAuth} struct. - * - * Uses {WebAuthn-verifyMinimal} for signature verification, which performs the essential - * WebAuthn checks: type validation, challenge matching, and cryptographic signature verification. - * - * NOTE: Wallets that may require default P256 validation may install a P256 verifier separately. - */ -contract ERC7913WebAuthnVerifier is IERC7913SignatureVerifier { - /// @inheritdoc IERC7913SignatureVerifier - function verify(bytes calldata key, bytes32 hash, bytes calldata signature) public view virtual returns (bytes4) { - (bool decodeSuccess, WebAuthn.WebAuthnAuth calldata auth) = WebAuthn.tryDecodeAuth(signature); - - return - decodeSuccess && - key.length == 0x40 && - WebAuthn.verify(abi.encodePacked(hash), auth, bytes32(key[0x00:0x20]), bytes32(key[0x20:0x40])) - ? IERC7913SignatureVerifier.verify.selector - : bytes4(0xFFFFFFFF); - } -} diff --git a/test/account/AccountERC7913.test.js b/test/account/AccountERC7913.test.js index 093a3398..990ed14b 100644 --- a/test/account/AccountERC7913.test.js +++ b/test/account/AccountERC7913.test.js @@ -4,7 +4,7 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { getDomain, PackedUserOperation } = require('@openzeppelin/contracts/test/helpers/eip712'); const { ERC4337Helper } = require('@openzeppelin/contracts/test/helpers/erc4337'); const { NonNativeSigner } = require('@openzeppelin/contracts/test/helpers/signers'); -const { ZKEmailSigningKey, WebAuthnSigningKey } = require('../helpers/signers'); +const { ZKEmailSigningKey } = require('../helpers/signers'); const { shouldBehaveLikeAccountCore, @@ -22,7 +22,6 @@ const emailNullifier = '0x00a83fce3d4b1c9ef0f600644c1ecc6c8115b57b1596e0e3295e2c const templateId = ethers.solidityPackedKeccak256(['string', 'uint256'], ['TEST', 0n]); // Prepare signer in advance -const signerWebAuthn = new NonNativeSigner(WebAuthnSigningKey.random()); const signerZKEmail = new NonNativeSigner( new ZKEmailSigningKey(domainName, publicKeyHash, emailNullifier, accountSalt, templateId), ); @@ -46,7 +45,6 @@ async function fixture() { const zkEmailVerifier = await ethers.deployContract('ZKEmailGroth16VerifierMock'); // ERC-7913 verifiers - const verifierWebAuthn = await ethers.deployContract('ERC7913WebAuthnVerifier'); const verifierZKEmail = await ethers.deployContract('$ERC7913ZKEmailVerifier'); // ERC-4337 env @@ -69,7 +67,6 @@ async function fixture() { return { helper, - verifierWebAuthn, verifierZKEmail, dkim, zkEmailVerifier, @@ -87,25 +84,6 @@ describe('AccountERC7913', function () { Object.assign(this, await loadFixture(fixture)); }); - // Using WebAuthn key with an ERC-7913 verifier - describe('WebAuthn key', function () { - beforeEach(async function () { - this.signer = signerWebAuthn; - this.mock = await this.makeMock( - ethers.concat([ - this.verifierWebAuthn.target, - this.signer.signingKey.publicKey.qx, - this.signer.signingKey.publicKey.qy, - ]), - ); - }); - - shouldBehaveLikeAccountCore(); - shouldBehaveLikeAccountHolder(); - shouldBehaveLikeERC1271({ erc7739: true }); - shouldBehaveLikeERC7821(); - }); - // Using ZKEmail with an ERC-7913 verifier describe('ZKEmail', function () { beforeEach(async function () { diff --git a/test/account/AccountWebAuthn.test.js b/test/account/AccountWebAuthn.test.js deleted file mode 100644 index 5b6f5f3d..00000000 --- a/test/account/AccountWebAuthn.test.js +++ /dev/null @@ -1,91 +0,0 @@ -const { ethers, predeploy } = require('hardhat'); -const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); - -const { getDomain } = require('@openzeppelin/contracts/test/helpers/eip712'); -const { ERC4337Helper } = require('@openzeppelin/contracts/test/helpers/erc4337'); -const { NonNativeSigner, P256SigningKey } = require('@openzeppelin/contracts/test/helpers/signers'); -const { WebAuthnSigningKey } = require('../helpers/signers'); -const { PackedUserOperation } = require('@openzeppelin/contracts/test/helpers/eip712-types'); - -const { - shouldBehaveLikeAccountCore, - shouldBehaveLikeAccountHolder, -} = require('@openzeppelin/contracts/test/account/Account.behavior'); -const { shouldBehaveLikeERC1271 } = require('@openzeppelin/contracts/test/utils/cryptography/ERC1271.behavior'); -const { shouldBehaveLikeERC7821 } = require('@openzeppelin/contracts/test/account/extensions/ERC7821.behavior'); - -const webAuthnSigner = new NonNativeSigner(WebAuthnSigningKey.random()); -const p256Signer = new NonNativeSigner(P256SigningKey.random()); - -async function fixture() { - // EOAs and environment - const [beneficiary, other] = await ethers.getSigners(); - const target = await ethers.deployContract('CallReceiverMock'); - - // ERC-4337 account - const helper = new ERC4337Helper(); - - // ERC-4337 Entrypoint domain - const entrypointDomain = await getDomain(predeploy.entrypoint.v08); - - // domain cannot be fetched using getDomain(mock) before the mock is deployed - const domain = { - name: 'AccountWebAuthn', - version: '1', - chainId: entrypointDomain.chainId, - }; - - const webAuthnMock = await helper.newAccount('$AccountWebAuthnMock', [ - webAuthnSigner.signingKey.publicKey.qx, - webAuthnSigner.signingKey.publicKey.qy, - 'AccountWebAuthn', - '1', - ]); - const p256Mock = await helper.newAccount('$AccountWebAuthnMock', [ - p256Signer.signingKey.publicKey.qx, - p256Signer.signingKey.publicKey.qy, - 'AccountWebAuthn', - '1', - ]); - - // This function signs using P256 signature for fallback testing - const signUserOp = function (userOp) { - return this.signer - .signTypedData(entrypointDomain, { PackedUserOperation }, userOp.packed) - .then(signature => Object.assign(userOp, { signature })); - }; - - return { helper, domain, webAuthnMock, p256Mock, target, beneficiary, other, signUserOp }; -} - -describe('AccountWebAuthn', function () { - beforeEach(async function () { - Object.assign(this, await loadFixture(fixture)); - }); - - describe('WebAuthn Assertions', function () { - beforeEach(async function () { - this.signer = webAuthnSigner; - this.mock = this.webAuthnMock; - this.domain.verifyingContract = this.mock.address; - }); - - shouldBehaveLikeAccountCore(); - shouldBehaveLikeAccountHolder(); - shouldBehaveLikeERC1271({ erc7739: true }); - shouldBehaveLikeERC7821(); - }); - - describe('as regular P256 validator', function () { - beforeEach(async function () { - this.signer = p256Signer; - this.mock = this.p256Mock; - this.domain.verifyingContract = this.mock.address; - }); - - shouldBehaveLikeAccountCore(); - shouldBehaveLikeAccountHolder(); - shouldBehaveLikeERC1271({ erc7739: true }); - shouldBehaveLikeERC7821(); - }); -}); diff --git a/test/helpers/signers.js b/test/helpers/signers.js index 2f45f534..a31ab133 100644 --- a/test/helpers/signers.js +++ b/test/helpers/signers.js @@ -1,16 +1,4 @@ -const { P256SigningKey } = require('@openzeppelin/contracts/test/helpers/signers'); -const { - AbiCoder, - ZeroHash, - assertArgument, - concat, - dataLength, - sha256, - solidityPacked, - toBigInt, - encodeBase64, - toUtf8Bytes, -} = require('ethers'); +const { assertArgument, dataLength, toBigInt, AbiCoder } = require('ethers'); class ZKEmailSigningKey { #domainName; @@ -86,38 +74,6 @@ class ZKEmailSigningKey { } } -class WebAuthnSigningKey extends P256SigningKey { - sign(digest /*: BytesLike*/) /*: { serialized: string } */ { - assertArgument(dataLength(digest) === 32, 'invalid digest length', 'digest', digest); - - const clientDataJSON = JSON.stringify({ - type: 'webauthn.get', - challenge: encodeBase64(digest).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', ''), - }); - - // Flags 0x05 = AUTH_DATA_FLAGS_UP | AUTH_DATA_FLAGS_UV - const authenticatorData = solidityPacked(['bytes32', 'bytes1', 'bytes4'], [ZeroHash, '0x05', '0x00000000']); - - // Regular P256 signature - const { r, s } = super.sign(sha256(concat([authenticatorData, sha256(toUtf8Bytes(clientDataJSON))]))); - - const serialized = AbiCoder.defaultAbiCoder().encode( - ['bytes32', 'bytes32', 'uint256', 'uint256', 'bytes', 'string'], - [ - r, - s, - clientDataJSON.indexOf('"challenge"'), - clientDataJSON.indexOf('"type"'), - authenticatorData, - clientDataJSON, - ], - ); - - return { serialized }; - } -} - module.exports = { ZKEmailSigningKey, - WebAuthnSigningKey, }; diff --git a/test/utils/cryptography/WebAuthn.t.sol b/test/utils/cryptography/WebAuthn.t.sol deleted file mode 100644 index e260d948..00000000 --- a/test/utils/cryptography/WebAuthn.t.sol +++ /dev/null @@ -1,184 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {Base64} from "@openzeppelin/contracts/utils/Base64.sol"; -import {WebAuthn} from "../../../contracts/utils/cryptography/WebAuthn.sol"; - -contract WebAuthnTest is Test { - /// forge-config: default.fuzz.runs = 512 - function testVerify(bytes memory challenge, uint256 seed) public view { - assertTrue( - _runVerify( - seed, - challenge, - _encodeAuthenticatorData(WebAuthn.AUTH_DATA_FLAGS_UP), - _encodeClientDataJSON(challenge), - false - ) - ); - } - - /// forge-config: default.fuzz.runs = 512 - function testVerifyInvalidType(bytes memory challenge, uint256 seed) public view { - assertFalse( - _runVerify( - seed, - challenge, - _encodeAuthenticatorData(WebAuthn.AUTH_DATA_FLAGS_UP | WebAuthn.AUTH_DATA_FLAGS_UV), - // solhint-disable-next-line quotes - string.concat('{"type":"webauthn.create","challenge":"', Base64.encodeURL(challenge), '"}'), - false - ) - ); - } - - /// forge-config: default.fuzz.runs = 512 - function testVerifyInvalidChallenge(bytes memory challenge, uint256 seed) public view { - assertFalse( - _runVerify( - seed, - challenge, - _encodeAuthenticatorData(WebAuthn.AUTH_DATA_FLAGS_UP | WebAuthn.AUTH_DATA_FLAGS_UV), - _encodeClientDataJSON(bytes("invalid_challenge")), - false - ) - ); - } - - /// forge-config: default.fuzz.runs = 512 - function testVerifyFlagsUP(bytes memory challenge, uint256 seed) public view { - // UP = false: FAIL - assertFalse( - _runVerify( - seed, - challenge, - _encodeAuthenticatorData(WebAuthn.AUTH_DATA_FLAGS_UV), - _encodeClientDataJSON(challenge), - false - ) - ); - } - - /// forge-config: default.fuzz.runs = 512 - function testVerifyFlagsUV(bytes memory challenge, uint256 seed) public view { - // UV = false, requireUV = false: SUCCESS - assertTrue( - _runVerify( - seed, - challenge, - _encodeAuthenticatorData(WebAuthn.AUTH_DATA_FLAGS_UP), - _encodeClientDataJSON(challenge), - false - ) - ); - // UV = false, requireUV = true: FAIL - assertFalse( - _runVerify( - seed, - challenge, - _encodeAuthenticatorData(WebAuthn.AUTH_DATA_FLAGS_UP), - _encodeClientDataJSON(challenge), - true - ) - ); - // UV = true, requireUV = true: SUCCESS - assertTrue( - _runVerify( - seed, - challenge, - _encodeAuthenticatorData(WebAuthn.AUTH_DATA_FLAGS_UP | WebAuthn.AUTH_DATA_FLAGS_UV), - _encodeClientDataJSON(challenge), - true - ) - ); - } - - /// forge-config: default.fuzz.runs = 512 - function testVerifyFlagsBEBS(bytes memory challenge, uint256 seed) public view { - // BS = true, BE = false: FAIL - assertFalse( - _runVerify( - seed, - challenge, - _encodeAuthenticatorData( - WebAuthn.AUTH_DATA_FLAGS_UP | WebAuthn.AUTH_DATA_FLAGS_UV | WebAuthn.AUTH_DATA_FLAGS_BS - ), - _encodeClientDataJSON(challenge), - false - ) - ); - // BS = false, BE = true: SUCCESS - assertTrue( - _runVerify( - seed, - challenge, - _encodeAuthenticatorData( - WebAuthn.AUTH_DATA_FLAGS_UP | WebAuthn.AUTH_DATA_FLAGS_UV | WebAuthn.AUTH_DATA_FLAGS_BE - ), - _encodeClientDataJSON(challenge), - false - ) - ); - // BS = true, BE = true: SUCCESS - assertTrue( - _runVerify( - seed, - challenge, - _encodeAuthenticatorData( - WebAuthn.AUTH_DATA_FLAGS_UP | - WebAuthn.AUTH_DATA_FLAGS_UV | - WebAuthn.AUTH_DATA_FLAGS_BE | - WebAuthn.AUTH_DATA_FLAGS_BS - ), - _encodeClientDataJSON(challenge), - false - ) - ); - } - - function _runVerify( - uint256 seed, - bytes memory challenge, - bytes memory authenticatorData, - string memory clientDataJSON, - bool requireUV - ) private view returns (bool) { - // Generate private key and get public key - uint256 privateKey = bound(seed, 1, P256.N - 1); - (uint256 x, uint256 y) = vm.publicKeyP256(privateKey); - - // Sign the message - bytes32 messageHash = sha256(abi.encodePacked(authenticatorData, sha256(bytes(clientDataJSON)))); - (bytes32 r, bytes32 s) = vm.signP256(privateKey, messageHash); - - // Verify the signature - return - WebAuthn.verify( - challenge, - WebAuthn.WebAuthnAuth({ - authenticatorData: authenticatorData, - clientDataJSON: clientDataJSON, - challengeIndex: 23, // Position of challenge in clientDataJSON - typeIndex: 1, // Position of type in clientDataJSON - r: r, - s: bytes32(Math.min(uint256(s), P256.N - uint256(s))) - }), - bytes32(x), - bytes32(y), - requireUV - ); - } - - function _encodeAuthenticatorData(bytes1 flags) private pure returns (bytes memory) { - return abi.encodePacked(bytes32(0), flags, bytes4(0)); - } - - function _encodeClientDataJSON(bytes memory challenge) private pure returns (string memory) { - // solhint-disable-next-line quotes - return string.concat('{"type":"webauthn.get","challenge":"', Base64.encodeURL(challenge), '"}'); - } -}