├── .gas-snapshot ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── LICENSE.md ├── README.md ├── audits └── report-review-coinbase-webauthn.pdf ├── foundry.toml ├── src └── WebAuthn.sol └── test ├── Utils.sol ├── WebAuthn.t.sol ├── Webauthn.fuzz.t.sol ├── fixtures └── assertions_fixture.json └── helpers ├── app.py ├── assertions_generator.py ├── readme.md └── requirements.txt /.gas-snapshot: -------------------------------------------------------------------------------- 1 | WebAuthnFuzzTest:test_Verify_ShoulReturnFalse_WhenSAboveP256_N_DIV_2() (gas: 429635068) 2 | WebAuthnFuzzTest:test_Verify_ShoulReturnFalse_WhenTheUpFlagIsNotSet() (gas: 435310864) 3 | WebAuthnFuzzTest:test_Verify_ShoulReturnFalse_WhenUserVerifictionIsRequiredButTestWasNotPerformed() (gas: 432573571) 4 | WebAuthnFuzzTest:test_Verify_ShoulReturnTrue_WhenSBelowP256_N_DIV_2() (gas: 456301488) 5 | WebAuthnTest:test_chrome() (gas: 225641) 6 | WebAuthnTest:test_safari() (gas: 221888) -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | env: 9 | FOUNDRY_PROFILE: ci 10 | 11 | jobs: 12 | check: 13 | strategy: 14 | fail-fast: true 15 | 16 | name: Foundry project 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | submodules: recursive 22 | 23 | - name: Install Foundry 24 | uses: foundry-rs/foundry-toolchain@v1 25 | with: 26 | version: nightly 27 | 28 | - name: Run Forge build 29 | run: | 30 | forge --version 31 | forge build --sizes 32 | id: build 33 | 34 | - name: Run Forge tests 35 | run: | 36 | forge test -vvv 37 | id: test 38 | 39 | - name: Check formatting 40 | run: | 41 | forge fmt --check 42 | id: fmt 43 | 44 | - name: Check snapshot 45 | run: | 46 | forge snapshot --check 47 | id: snapshot 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | 16 | # Python files 17 | __pycache__ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/FreshCryptoLib"] 5 | path = lib/FreshCryptoLib 6 | url = https://github.com/rdubois-crypto/FreshCryptoLib 7 | [submodule "lib/solady"] 8 | path = lib/solady 9 | url = https://github.com/vectorized/solady 10 | [submodule "lib/openzeppelin-contracts"] 11 | path = lib/openzeppelin-contracts 12 | url = https://github.com/openzeppelin/openzeppelin-contracts 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Base 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Solidity WebAuthn Authentication Assertion Verifier 2 | 3 | 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). 4 | 5 | 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). 6 | 7 | > [!IMPORTANT] 8 | > 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. 9 | 10 | Code excerpts 11 | 12 | ```solidity 13 | struct WebAuthnAuth { 14 | /// @dev https://www.w3.org/TR/webauthn-2/#dom-authenticatorassertionresponse-authenticatordata 15 | bytes authenticatorData; 16 | /// @dev https://www.w3.org/TR/webauthn-2/#dom-authenticatorresponse-clientdatajson 17 | string clientDataJSON; 18 | /// The index at which "challenge":"..." occurs in clientDataJSON 19 | uint256 challengeIndex; 20 | /// The index at which "type":"..." occurs in clientDataJSON 21 | uint256 typeIndex; 22 | /// @dev The r value of secp256r1 signature 23 | uint256 r; 24 | /// @dev The s value of secp256r1 signature 25 | uint256 s; 26 | } 27 | 28 | function verify( 29 | bytes memory challenge, 30 | bool requireUserVerification, 31 | WebAuthnAuth memory webAuthnAuth, 32 | uint256 x, 33 | uint256 y 34 | ) internal view returns (bool) 35 | ``` 36 | 37 | example usage 38 | ```solidity 39 | bytes challenge = abi.encode(0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf); 40 | uint256 x = 28573233055232466711029625910063034642429572463461595413086259353299906450061; 41 | uint256 y = 39367742072897599771788408398752356480431855827262528811857788332151452825281; 42 | WebAuthn.WebAuthnAuth memory auth = WebAuthn.WebAuthnAuth({ 43 | authenticatorData: hex"49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000101", 44 | clientDataJSON: string.concat( 45 | '{"type":"webauthn.get","challenge":"', Base64Url.encode(challenge), '","origin":"http://localhost:3005"}' 46 | ), 47 | challengeIndex: 23, 48 | typeIndex: 1, 49 | r: 43684192885701841787131392247364253107519555363555461570655060745499568693242, 50 | s: 22655632649588629308599201066602670461698485748654492451178007896016452673579 51 | }); 52 | ``` 53 | 54 | ### Developing 55 | After cloning the repo, run the tests using Forge, from [Foundry](https://github.com/foundry-rs/foundry?tab=readme-ov-file) 56 | ```bash 57 | forge test 58 | ``` -------------------------------------------------------------------------------- /audits/report-review-coinbase-webauthn.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/base/webauthn-sol/619f20ab0f074fef41066ee4ab24849a913263b2/audits/report-review-coinbase-webauthn.pdf -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | optimizer_runs = 999999 6 | fs_permissions = [{ access = "read", path = "test/fixtures" }] 7 | 8 | [fmt] 9 | sort_imports = true 10 | wrap_comments = true 11 | line_length = 140 -------------------------------------------------------------------------------- /src/WebAuthn.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {FCL_ecdsa} from "FreshCryptoLib/FCL_ecdsa.sol"; 5 | import {FCL_Elliptic_ZZ} from "FreshCryptoLib/FCL_elliptic.sol"; 6 | import {Base64} from "openzeppelin-contracts/contracts/utils/Base64.sol"; 7 | import {LibString} from "solady/utils/LibString.sol"; 8 | 9 | /// @title WebAuthn 10 | /// 11 | /// @notice A library for verifying WebAuthn Authentication Assertions, built off the work 12 | /// of Daimo. 13 | /// 14 | /// @dev Attempts to use the RIP-7212 precompile for signature verification. 15 | /// If precompile verification fails, it falls back to FreshCryptoLib. 16 | /// 17 | /// @author Coinbase (https://github.com/base-org/webauthn-sol) 18 | /// @author Daimo (https://github.com/daimo-eth/p256-verifier/blob/master/src/WebAuthn.sol) 19 | library WebAuthn { 20 | using LibString for string; 21 | 22 | struct WebAuthnAuth { 23 | /// @dev The WebAuthn authenticator data. 24 | /// See https://www.w3.org/TR/webauthn-2/#dom-authenticatorassertionresponse-authenticatordata. 25 | bytes authenticatorData; 26 | /// @dev The WebAuthn client data JSON. 27 | /// See https://www.w3.org/TR/webauthn-2/#dom-authenticatorresponse-clientdatajson. 28 | string clientDataJSON; 29 | /// @dev The index at which "challenge":"..." occurs in `clientDataJSON`. 30 | uint256 challengeIndex; 31 | /// @dev The index at which "type":"..." occurs in `clientDataJSON`. 32 | uint256 typeIndex; 33 | /// @dev The r value of secp256r1 signature 34 | uint256 r; 35 | /// @dev The s value of secp256r1 signature 36 | uint256 s; 37 | } 38 | 39 | /// @dev Bit 0 of the authenticator data struct, corresponding to the "User Present" bit. 40 | /// See https://www.w3.org/TR/webauthn-2/#flags. 41 | bytes1 private constant _AUTH_DATA_FLAGS_UP = 0x01; 42 | 43 | /// @dev Bit 2 of the authenticator data struct, corresponding to the "User Verified" bit. 44 | /// See https://www.w3.org/TR/webauthn-2/#flags. 45 | bytes1 private constant _AUTH_DATA_FLAGS_UV = 0x04; 46 | 47 | /// @dev Secp256r1 curve order / 2 used as guard to prevent signature malleability issue. 48 | uint256 private constant _P256_N_DIV_2 = FCL_Elliptic_ZZ.n / 2; 49 | 50 | /// @dev The precompiled contract address to use for signature verification in the “secp256r1” elliptic curve. 51 | /// See https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md. 52 | address private constant _VERIFIER = address(0x100); 53 | 54 | /// @dev The expected type (hash) in the client data JSON when verifying assertion signatures. 55 | /// See https://www.w3.org/TR/webauthn-2/#dom-collectedclientdata-type 56 | bytes32 private constant _EXPECTED_TYPE_HASH = keccak256('"type":"webauthn.get"'); 57 | 58 | /// 59 | /// @notice Verifies a Webauthn Authentication Assertion as described 60 | /// in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion. 61 | /// 62 | /// @dev We do not verify all the steps as described in the specification, only ones relevant to our context. 63 | /// Please carefully read through this list before usage. 64 | /// 65 | /// Specifically, we do verify the following: 66 | /// - Verify that authenticatorData (which comes from the authenticator, such as iCloud Keychain) indicates 67 | /// a well-formed assertion with the user present bit set. If `requireUV` is set, checks that the authenticator 68 | /// enforced user verification. User verification should be required if, and only if, options.userVerification 69 | /// is set to required in the request. 70 | /// - Verifies that the client JSON is of type "webauthn.get", i.e. the client was responding to a request to 71 | /// assert authentication. 72 | /// - Verifies that the client JSON contains the requested challenge. 73 | /// - Verifies that (r, s) constitute a valid signature over both the authenicatorData and client JSON, for public 74 | /// key (x, y). 75 | /// 76 | /// We make some assumptions about the particular use case of this verifier, so we do NOT verify the following: 77 | /// - Does NOT verify that the origin in the `clientDataJSON` matches the Relying Party's origin: tt is considered 78 | /// the authenticator's responsibility to ensure that the user is interacting with the correct RP. This is 79 | /// enforced by most high quality authenticators properly, particularly the iCloud Keychain and Google Password 80 | /// Manager were tested. 81 | /// - Does NOT verify That `topOrigin` in `clientDataJSON` is well-formed: We assume it would never be present, i.e. 82 | /// the credentials are never used in a cross-origin/iframe context. The website/app set up should disallow 83 | /// cross-origin usage of the credentials. This is the default behaviour for created credentials in common settings. 84 | /// - Does NOT verify that the `rpIdHash` in `authenticatorData` is the SHA-256 hash of the RP ID expected by the Relying 85 | /// Party: this means that we rely on the authenticator to properly enforce credentials to be used only by the correct RP. 86 | /// This is generally enforced with features like Apple App Site Association and Google Asset Links. To protect from 87 | /// edge cases in which a previously-linked RP ID is removed from the authorised RP IDs, we recommend that messages 88 | /// signed by the authenticator include some expiry mechanism. 89 | /// - Does NOT verify the credential backup state: this assumes the credential backup state is NOT used as part of Relying 90 | /// Party business logic or policy. 91 | /// - Does NOT verify the values of the client extension outputs: this assumes that the Relying Party does not use client 92 | /// extension outputs. 93 | /// - Does NOT verify the signature counter: signature counters are intended to enable risk scoring for the Relying Party. 94 | /// This assumes risk scoring is not used as part of Relying Party business logic or policy. 95 | /// - Does NOT verify the attestation object: this assumes that response.attestationObject is NOT present in the response, 96 | /// i.e. the RP does not intend to verify an attestation. 97 | /// 98 | /// @param challenge The challenge that was provided by the relying party. 99 | /// @param requireUV A boolean indicating whether user verification is required. 100 | /// @param webAuthnAuth The `WebAuthnAuth` struct. 101 | /// @param x The x coordinate of the public key. 102 | /// @param y The y coordinate of the public key. 103 | /// 104 | /// @return `true` if the authentication assertion passed validation, else `false`. 105 | function verify(bytes memory challenge, bool requireUV, WebAuthnAuth memory webAuthnAuth, uint256 x, uint256 y) 106 | internal 107 | view 108 | returns (bool) 109 | { 110 | if (webAuthnAuth.s > _P256_N_DIV_2) { 111 | // guard against signature malleability 112 | return false; 113 | } 114 | 115 | // 11. Verify that the value of C.type is the string webauthn.get. 116 | // bytes("type":"webauthn.get").length = 21 117 | string memory _type = webAuthnAuth.clientDataJSON.slice(webAuthnAuth.typeIndex, webAuthnAuth.typeIndex + 21); 118 | if (keccak256(bytes(_type)) != _EXPECTED_TYPE_HASH) { 119 | return false; 120 | } 121 | 122 | // 12. Verify that the value of C.challenge equals the base64url encoding of options.challenge. 123 | bytes memory expectedChallenge = bytes(string.concat('"challenge":"', Base64.encodeURL(challenge), '"')); 124 | string memory actualChallenge = 125 | webAuthnAuth.clientDataJSON.slice(webAuthnAuth.challengeIndex, webAuthnAuth.challengeIndex + expectedChallenge.length); 126 | if (keccak256(bytes(actualChallenge)) != keccak256(expectedChallenge)) { 127 | return false; 128 | } 129 | 130 | // Skip 13., 14., 15. 131 | 132 | // 16. Verify that the UP bit of the flags in authData is set. 133 | if (webAuthnAuth.authenticatorData[32] & _AUTH_DATA_FLAGS_UP != _AUTH_DATA_FLAGS_UP) { 134 | return false; 135 | } 136 | 137 | // 17. If user verification is required for this assertion, verify that the User Verified bit of the flags in 138 | // authData is set. 139 | if (requireUV && (webAuthnAuth.authenticatorData[32] & _AUTH_DATA_FLAGS_UV) != _AUTH_DATA_FLAGS_UV) { 140 | return false; 141 | } 142 | 143 | // skip 18. 144 | 145 | // 19. Let hash be the result of computing a hash over the cData using SHA-256. 146 | bytes32 clientDataJSONHash = sha256(bytes(webAuthnAuth.clientDataJSON)); 147 | 148 | // 20. Using credentialPublicKey, verify that sig is a valid signature over the binary concatenation of authData 149 | // and hash. 150 | bytes32 messageHash = sha256(abi.encodePacked(webAuthnAuth.authenticatorData, clientDataJSONHash)); 151 | bytes memory args = abi.encode(messageHash, webAuthnAuth.r, webAuthnAuth.s, x, y); 152 | // try the RIP-7212 precompile address 153 | (bool success, bytes memory ret) = _VERIFIER.staticcall(args); 154 | // staticcall will not revert if address has no code 155 | // check return length 156 | // note that even if precompile exists, ret.length is 0 when verification returns false 157 | // so an invalid signature will be checked twice: once by the precompile and once by FCL. 158 | // Ideally this signature failure is simulated offchain and no one actually pay this gas. 159 | bool valid = ret.length > 0; 160 | if (success && valid) return abi.decode(ret, (uint256)) == 1; 161 | 162 | return FCL_ecdsa.ecdsa_verify(messageHash, webAuthnAuth.r, webAuthnAuth.s, x, y); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /test/Utils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {FCL_Elliptic_ZZ} from "FreshCryptoLib/FCL_elliptic.sol"; 5 | import {Base64Url} from "FreshCryptoLib/utils/Base64Url.sol"; 6 | 7 | struct WebAuthnInfo { 8 | bytes authenticatorData; 9 | string clientDataJSON; 10 | bytes32 messageHash; 11 | } 12 | 13 | library Utils { 14 | uint256 constant P256_N_DIV_2 = FCL_Elliptic_ZZ.n / 2; 15 | 16 | function getWebAuthnStruct(bytes32 challenge) public pure returns (WebAuthnInfo memory) { 17 | string memory challengeb64url = Base64Url.encode(abi.encode(challenge)); 18 | string memory clientDataJSON = string( 19 | abi.encodePacked( 20 | '{"type":"webauthn.get","challenge":"', challengeb64url, '","origin":"https://sign.coinbase.com","crossOrigin":false}' 21 | ) 22 | ); 23 | 24 | // Authenticator data for Chrome Profile touchID signature 25 | bytes memory authenticatorData = hex"49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000"; 26 | 27 | bytes32 clientDataJSONHash = sha256(bytes(clientDataJSON)); 28 | bytes32 messageHash = sha256(abi.encodePacked(authenticatorData, clientDataJSONHash)); 29 | 30 | return WebAuthnInfo(authenticatorData, clientDataJSON, messageHash); 31 | } 32 | 33 | /// @dev normalizes the s value from a p256r1 signature so that 34 | /// it will pass malleability checks. 35 | function normalizeS(uint256 s) public pure returns (uint256) { 36 | if (s > P256_N_DIV_2) { 37 | return FCL_Elliptic_ZZ.n - s; 38 | } 39 | 40 | return s; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/WebAuthn.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {Base64Url} from "FreshCryptoLib/utils/Base64Url.sol"; 5 | import {Test, console2} from "forge-std/Test.sol"; 6 | 7 | import {WebAuthn} from "../src/WebAuthn.sol"; 8 | 9 | contract WebAuthnTest is Test { 10 | bytes challenge = abi.encode(0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf); 11 | 12 | function test_safari() public { 13 | uint256 x = 28573233055232466711029625910063034642429572463461595413086259353299906450061; 14 | uint256 y = 39367742072897599771788408398752356480431855827262528811857788332151452825281; 15 | WebAuthn.WebAuthnAuth memory auth = WebAuthn.WebAuthnAuth({ 16 | authenticatorData: hex"49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000101", 17 | clientDataJSON: string.concat( 18 | '{"type":"webauthn.get","challenge":"', Base64Url.encode(challenge), '","origin":"http://localhost:3005"}' 19 | ), 20 | challengeIndex: 23, 21 | typeIndex: 1, 22 | r: 43684192885701841787131392247364253107519555363555461570655060745499568693242, 23 | s: 22655632649588629308599201066602670461698485748654492451178007896016452673579 24 | }); 25 | assertTrue(WebAuthn.verify(challenge, false, auth, x, y)); 26 | } 27 | 28 | function test_chrome() public { 29 | uint256 x = 28573233055232466711029625910063034642429572463461595413086259353299906450061; 30 | uint256 y = 39367742072897599771788408398752356480431855827262528811857788332151452825281; 31 | WebAuthn.WebAuthnAuth memory auth = WebAuthn.WebAuthnAuth({ 32 | authenticatorData: hex"49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763050000010a", 33 | clientDataJSON: string.concat( 34 | '{"type":"webauthn.get","challenge":"', Base64Url.encode(challenge), '","origin":"http://localhost:3005","crossOrigin":false}' 35 | ), 36 | challengeIndex: 23, 37 | typeIndex: 1, 38 | r: 29739767516584490820047863506833955097567272713519339793744591468032609909569, 39 | s: 45947455641742997809691064512762075989493430661170736817032030660832793108102 40 | }); 41 | assertTrue(WebAuthn.verify(challenge, false, auth, x, y)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/Webauthn.fuzz.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {FCL_ecdsa} from "FreshCryptoLib/FCL_ecdsa.sol"; 5 | import {Test, Vm, console, stdJson} from "forge-std/Test.sol"; 6 | 7 | import {WebAuthn} from "../src/WebAuthn.sol"; 8 | 9 | import "./Utils.sol"; 10 | 11 | contract WebAuthnFuzzTest is Test { 12 | using stdJson for string; 13 | 14 | string constant testFile = "/test/fixtures/assertions_fixture.json"; 15 | 16 | /// @dev `WebAuthn.verify` should return `false` when `s` is above P256_N_DIV_2. 17 | function test_Verify_ShoulReturnFalse_WhenSAboveP256_N_DIV_2() public { 18 | string memory rootPath = vm.projectRoot(); 19 | string memory path = string.concat(rootPath, testFile); 20 | string memory json = vm.readFile(path); 21 | uint256 count = abi.decode(json.parseRaw(".count"), (uint256)); 22 | 23 | for (uint256 i; i < count; i++) { 24 | ( 25 | string memory jsonCaseSelector, 26 | bytes memory challenge, 27 | bool uv, 28 | WebAuthn.WebAuthnAuth memory webAuthnAuth, 29 | uint256 x, 30 | uint256 y 31 | ) = _parseJson({json: json, caseIndex: i}); 32 | 33 | console.log("Veryfing", jsonCaseSelector); 34 | 35 | // Only interested in s > P256_N_DIV_2 cases. 36 | if (webAuthnAuth.s <= Utils.P256_N_DIV_2) { 37 | webAuthnAuth.s = FCL_ecdsa.n - webAuthnAuth.s; 38 | } 39 | 40 | bool res = WebAuthn.verify({challenge: challenge, requireUV: uv, webAuthnAuth: webAuthnAuth, x: x, y: y}); 41 | 42 | // Assert the verification failed to guard against signature malleability. 43 | assertEq(res, false, string.concat("Failed on ", jsonCaseSelector)); 44 | 45 | console.log("------------------------------------"); 46 | } 47 | } 48 | 49 | /// @dev `WebAuthn.verify` should return `false` when the `up` flag is not set. 50 | function test_Verify_ShoulReturnFalse_WhenTheUpFlagIsNotSet() public { 51 | string memory rootPath = vm.projectRoot(); 52 | string memory path = string.concat(rootPath, testFile); 53 | string memory json = vm.readFile(path); 54 | uint256 count = abi.decode(json.parseRaw(".count"), (uint256)); 55 | 56 | for (uint256 i; i < count; i++) { 57 | ( 58 | string memory jsonCaseSelector, 59 | bytes memory challenge, 60 | bool uv, 61 | WebAuthn.WebAuthnAuth memory webAuthnAuth, 62 | uint256 x, 63 | uint256 y 64 | ) = _parseJson({json: json, caseIndex: i}); 65 | 66 | console.log("Veryfing", jsonCaseSelector); 67 | 68 | webAuthnAuth.s = Utils.normalizeS(webAuthnAuth.s); 69 | 70 | // Unset the `up` flag. 71 | webAuthnAuth.authenticatorData[32] = webAuthnAuth.authenticatorData[32] & bytes1(0xfe); 72 | 73 | bool res = WebAuthn.verify({challenge: challenge, requireUV: uv, webAuthnAuth: webAuthnAuth, x: x, y: y}); 74 | 75 | // Assert the verification failed because the `up` flag was not set. 76 | assertEq(res, false, string.concat("Failed on ", jsonCaseSelector)); 77 | 78 | console.log("------------------------------------"); 79 | } 80 | } 81 | 82 | /// @dev `WebAuthn.verify` should return `false` when `requireUV` is `true` but the 83 | /// authenticator did not set the `uv` flag. 84 | function test_Verify_ShoulReturnFalse_WhenUserVerifictionIsRequiredButTestWasNotPerformed() public { 85 | string memory rootPath = vm.projectRoot(); 86 | string memory path = string.concat(rootPath, testFile); 87 | string memory json = vm.readFile(path); 88 | uint256 count = abi.decode(json.parseRaw(".count"), (uint256)); 89 | 90 | for (uint256 i; i < count; i++) { 91 | ( 92 | string memory jsonCaseSelector, 93 | bytes memory challenge, 94 | bool uv, 95 | WebAuthn.WebAuthnAuth memory webAuthnAuth, 96 | uint256 x, 97 | uint256 y 98 | ) = _parseJson({json: json, caseIndex: i}); 99 | 100 | console.log("Veryfing", jsonCaseSelector); 101 | 102 | // Only interested in s > P256_N_DIV_2 cases with uv not performed. 103 | if (uv == true) { 104 | continue; 105 | } 106 | 107 | webAuthnAuth.s = Utils.normalizeS(webAuthnAuth.s); 108 | 109 | bool res = WebAuthn.verify({ 110 | challenge: challenge, 111 | requireUV: true, // Set UV to required to ensure false is returned 112 | webAuthnAuth: webAuthnAuth, 113 | x: x, 114 | y: y 115 | }); 116 | 117 | // Assert the verification failed because user verification was required but not performed by the 118 | // authenticator. 119 | assertEq(res, false, string.concat("Failed on ", jsonCaseSelector)); 120 | 121 | console.log("------------------------------------"); 122 | } 123 | } 124 | 125 | /// @dev `WebAuthn.verify` should return `true` when `s` is below `P256_N_DIV_2` and `requireUV` 126 | /// "matches" with the `uv` flag set by the authenticator. 127 | function test_Verify_ShoulReturnTrue_WhenSBelowP256_N_DIV_2() public { 128 | string memory rootPath = vm.projectRoot(); 129 | string memory path = string.concat(rootPath, testFile); 130 | string memory json = vm.readFile(path); 131 | 132 | uint256 count = abi.decode(json.parseRaw(".count"), (uint256)); 133 | 134 | for (uint256 i; i < count; i++) { 135 | ( 136 | string memory jsonCaseSelector, 137 | bytes memory challenge, 138 | bool uv, 139 | WebAuthn.WebAuthnAuth memory webAuthnAuth, 140 | uint256 x, 141 | uint256 y 142 | ) = _parseJson({json: json, caseIndex: i}); 143 | 144 | console.log("Veryfing", jsonCaseSelector); 145 | 146 | webAuthnAuth.s = Utils.normalizeS(webAuthnAuth.s); 147 | 148 | bool res = WebAuthn.verify({challenge: challenge, requireUV: uv, webAuthnAuth: webAuthnAuth, x: x, y: y}); 149 | 150 | // Assert the verification succeeded. 151 | assertEq(res, true, string.concat("Failed on ", jsonCaseSelector)); 152 | console.log("------------------------------------"); 153 | } 154 | } 155 | 156 | /// @dev Helper function to parse a test case fron the given json string. 157 | /// @param json The json string to parse. 158 | /// @param caseIndex The test case index to parse. 159 | function _parseJson(string memory json, uint256 caseIndex) 160 | private 161 | pure 162 | returns ( 163 | string memory jsonCaseSelector, 164 | bytes memory challenge, 165 | bool uv, 166 | WebAuthn.WebAuthnAuth memory webAuthnAuth, 167 | uint256 x, 168 | uint256 y 169 | ) 170 | { 171 | jsonCaseSelector = string.concat(".cases.[", string.concat(vm.toString(caseIndex), "]")); 172 | challenge = abi.decode(json.parseRaw(string.concat(jsonCaseSelector, ".challenge")), (bytes)); 173 | uv = abi.decode(json.parseRaw(string.concat(jsonCaseSelector, ".uv")), (bool)); 174 | 175 | webAuthnAuth = WebAuthn.WebAuthnAuth({ 176 | authenticatorData: abi.decode(json.parseRaw(string.concat(jsonCaseSelector, ".authenticator_data")), (bytes)), 177 | clientDataJSON: abi.decode(json.parseRaw(string.concat(jsonCaseSelector, ".client_data_json.json")), (string)), 178 | challengeIndex: abi.decode(json.parseRaw(string.concat(jsonCaseSelector, ".client_data_json.challenge_index")), (uint256)), 179 | typeIndex: abi.decode(json.parseRaw(string.concat(jsonCaseSelector, ".client_data_json.type_index")), (uint256)), 180 | r: abi.decode(json.parseRaw(string.concat(jsonCaseSelector, ".r")), (uint256)), 181 | s: abi.decode(json.parseRaw(string.concat(jsonCaseSelector, ".s")), (uint256)) 182 | }); 183 | 184 | x = abi.decode(json.parseRaw(string.concat(jsonCaseSelector, ".x")), (uint256)); 185 | y = abi.decode(json.parseRaw(string.concat(jsonCaseSelector, ".y")), (uint256)); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /test/helpers/app.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import Process 2 | import sys 3 | from flask import Flask, render_template_string 4 | 5 | _app = Flask(__name__) 6 | 7 | 8 | # HTML template with JavaScript for WebAuthn 9 | HTML_TEMPLATE = ''' 10 | 11 | 12 |
13 | 14 |