From 68a0ed8cb17bf658f4339b9a0c23634fb54e7709 Mon Sep 17 00:00:00 2001 From: Wilson Cusack Date: Mon, 22 Apr 2024 09:58:54 -0400 Subject: [PATCH 1/7] update to latest FCL + fmt --- .gas-snapshot | 6 ++--- foundry.toml | 5 ++-- lib/FreshCryptoLib | 2 +- src/WebAuthn.sol | 57 ++++++++++++++++++++++++++-------------- test/Utils.sol | 2 +- test/WebAuthn.t.sol | 6 ++--- test/Webauthn.fuzz.t.sol | 7 ++--- 7 files changed, 53 insertions(+), 32 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 57b900b..fc9414b 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,6 +1,6 @@ WebAuthnFuzzTest:test_Verify_ShoulReturnFalse_WhenSAboveP256_N_DIV_2() (gas: 429635068) WebAuthnFuzzTest:test_Verify_ShoulReturnFalse_WhenTheUpFlagIsNotSet() (gas: 435310864) WebAuthnFuzzTest:test_Verify_ShoulReturnFalse_WhenUserVerifictionIsRequiredButTestWasNotPerformed() (gas: 432573571) -WebAuthnFuzzTest:test_Verify_ShoulReturnTrue_WhenSBelowP256_N_DIV_2() (gas: 456300088) -WebAuthnTest:test_chrome() (gas: 225627) -WebAuthnTest:test_safari() (gas: 221874) \ No newline at end of file +WebAuthnFuzzTest:test_Verify_ShoulReturnTrue_WhenSBelowP256_N_DIV_2() (gas: 456301488) +WebAuthnTest:test_chrome() (gas: 225641) +WebAuthnTest:test_safari() (gas: 221888) \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index b8e794e..3f53772 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,5 +4,6 @@ out = "out" libs = ["lib"] optimizer_runs = 999999 -# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options -fs_permissions = [{ access = "read", path = "test/fixtures" }] +[fmt] +sort_imports = true +wrap_comments = true \ No newline at end of file diff --git a/lib/FreshCryptoLib b/lib/FreshCryptoLib index 027cb87..76f3f13 160000 --- a/lib/FreshCryptoLib +++ b/lib/FreshCryptoLib @@ -1 +1 @@ -Subproject commit 027cb877dc78640b62e25fce63eccee102ac828d +Subproject commit 76f3f135b7b27d2aa519f265b56bfc49a2573ab5 diff --git a/src/WebAuthn.sol b/src/WebAuthn.sol index bd2e73c..f49fa14 100644 --- a/src/WebAuthn.sol +++ b/src/WebAuthn.sol @@ -64,35 +64,52 @@ library WebAuthn { /// /// Specifically, we do verify the following: /// - Verify that authenticatorData (which comes from the authenticator, such as iCloud Keychain) indicates - /// a well-formed assertion with the user present bit set. If `requireUV` is set, checks that the authenticator - /// enforced user verification. User verification should be required if, and only if, options.userVerification + /// a well-formed assertion with the user present bit set. If `requireUV` is set, checks that the + /// authenticator + /// enforced user verification. User verification should be required if, and only if, + /// options.userVerification /// is set to required in the request. - /// - Verifies that the client JSON is of type "webauthn.get", i.e. the client was responding to a request to + /// - Verifies that the client JSON is of type "webauthn.get", i.e. the client was responding to a request + /// to /// assert authentication. /// - Verifies that the client JSON contains the requested challenge. - /// - Verifies that (r, s) constitute a valid signature over both the authenicatorData and client JSON, for public + /// - Verifies that (r, s) constitute a valid signature over both the authenicatorData and client JSON, for + /// public /// key (x, y). /// /// We make some assumptions about the particular use case of this verifier, so we do NOT verify the following: - /// - Does NOT verify that the origin in the `clientDataJSON` matches the Relying Party's origin: tt is considered + /// - Does NOT verify that the origin in the `clientDataJSON` matches the Relying Party's origin: tt is + /// considered /// the authenticator's responsibility to ensure that the user is interacting with the correct RP. This is - /// enforced by most high quality authenticators properly, particularly the iCloud Keychain and Google Password + /// enforced by most high quality authenticators properly, particularly the iCloud Keychain and Google + /// Password /// Manager were tested. - /// - Does NOT verify That `topOrigin` in `clientDataJSON` is well-formed: We assume it would never be present, i.e. - /// the credentials are never used in a cross-origin/iframe context. The website/app set up should disallow - /// cross-origin usage of the credentials. This is the default behaviour for created credentials in common settings. - /// - Does NOT verify that the `rpIdHash` in `authenticatorData` is the SHA-256 hash of the RP ID expected by the Relying - /// Party: this means that we rely on the authenticator to properly enforce credentials to be used only by the correct RP. - /// This is generally enforced with features like Apple App Site Association and Google Asset Links. To protect from - /// edge cases in which a previously-linked RP ID is removed from the authorised RP IDs, we recommend that messages + /// - Does NOT verify That `topOrigin` in `clientDataJSON` is well-formed: We assume it would never be + /// present, i.e. + /// the credentials are never used in a cross-origin/iframe context. The website/app set up should + /// disallow + /// cross-origin usage of the credentials. This is the default behaviour for created credentials in common + /// settings. + /// - Does NOT verify that the `rpIdHash` in `authenticatorData` is the SHA-256 hash of the RP ID expected + /// by the Relying + /// Party: this means that we rely on the authenticator to properly enforce credentials to be used only by + /// the correct RP. + /// This is generally enforced with features like Apple App Site Association and Google Asset Links. To + /// protect from + /// edge cases in which a previously-linked RP ID is removed from the authorised RP IDs, we recommend that + /// messages /// signed by the authenticator include some expiry mechanism. - /// - Does NOT verify the credential backup state: this assumes the credential backup state is NOT used as part of Relying + /// - Does NOT verify the credential backup state: this assumes the credential backup state is NOT used as + /// part of Relying /// Party business logic or policy. - /// - Does NOT verify the values of the client extension outputs: this assumes that the Relying Party does not use client + /// - Does NOT verify the values of the client extension outputs: this assumes that the Relying Party does + /// not use client /// extension outputs. - /// - Does NOT verify the signature counter: signature counters are intended to enable risk scoring for the Relying Party. + /// - Does NOT verify the signature counter: signature counters are intended to enable risk scoring for the + /// Relying Party. /// This assumes risk scoring is not used as part of Relying Party business logic or policy. - /// - Does NOT verify the attestation object: this assumes that response.attestationObject is NOT present in the response, + /// - 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. @@ -135,7 +152,8 @@ library WebAuthn { return false; } - // 17. If user verification is required for this assertion, verify that the User Verified bit of the flags in authData is set. + // 17. If user verification is required for this assertion, verify that the User Verified bit of the flags in + // authData is set. if (requireUV && (webAuthnAuth.authenticatorData[32] & _AUTH_DATA_FLAGS_UV) != _AUTH_DATA_FLAGS_UV) { return false; } @@ -145,7 +163,8 @@ library WebAuthn { // 19. Let hash be the result of computing a hash over the cData using SHA-256. bytes32 clientDataJSONHash = sha256(bytes(webAuthnAuth.clientDataJSON)); - // 20. Using credentialPublicKey, verify that sig is a valid signature over the binary concatenation of authData and hash. + // 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 diff --git a/test/Utils.sol b/test/Utils.sol index 461a7a5..4a680ba 100644 --- a/test/Utils.sol +++ b/test/Utils.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {Base64Url} from "FreshCryptoLib/utils/Base64Url.sol"; import {FCL_Elliptic_ZZ} from "FreshCryptoLib/FCL_elliptic.sol"; +import {Base64Url} from "FreshCryptoLib/utils/Base64Url.sol"; struct WebAuthnInfo { bytes authenticatorData; diff --git a/test/WebAuthn.t.sol b/test/WebAuthn.t.sol index 056934a..df01604 100644 --- a/test/WebAuthn.t.sol +++ b/test/WebAuthn.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {Test, console2} from "forge-std/Test.sol"; import {WebAuthn} from "../src/WebAuthn.sol"; import {Base64Url} from "FreshCryptoLib/utils/Base64Url.sol"; +import {Test, console2} from "forge-std/Test.sol"; contract WebAuthnTest is Test { bytes challenge = abi.encode(0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf); @@ -15,7 +15,7 @@ contract WebAuthnTest is Test { authenticatorData: hex"49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000101", clientDataJSON: string.concat( '{"type":"webauthn.get","challenge":"', Base64Url.encode(challenge), '","origin":"http://localhost:3005"}' - ), + ), challengeIndex: 23, typeIndex: 1, r: 43684192885701841787131392247364253107519555363555461570655060745499568693242, @@ -33,7 +33,7 @@ contract WebAuthnTest is Test { '{"type":"webauthn.get","challenge":"', Base64Url.encode(challenge), '","origin":"http://localhost:3005","crossOrigin":false}' - ), + ), challengeIndex: 23, typeIndex: 1, r: 29739767516584490820047863506833955097567272713519339793744591468032609909569, diff --git a/test/Webauthn.fuzz.t.sol b/test/Webauthn.fuzz.t.sol index a9b8357..87c80b5 100644 --- a/test/Webauthn.fuzz.t.sol +++ b/test/Webauthn.fuzz.t.sol @@ -5,8 +5,8 @@ import {FCL_ecdsa} from "FreshCryptoLib/FCL_ecdsa.sol"; import {WebAuthn} from "../src/WebAuthn.sol"; -import "forge-std/Test.sol"; import "./Utils.sol"; +import "forge-std/Test.sol"; contract WebAuthnFuzzTest is Test { using stdJson for string; @@ -114,7 +114,8 @@ contract WebAuthnFuzzTest is Test { y: y }); - // Assert the verification failed because user verification was required but not performed by the authenticator. + // Assert the verification failed because user verification was required but not performed by the + // authenticator. assertEq(res, false, string.concat("Failed on ", jsonCaseSelector)); console.log("------------------------------------"); @@ -176,7 +177,7 @@ contract WebAuthnFuzzTest is Test { clientDataJSON: abi.decode(json.parseRaw(string.concat(jsonCaseSelector, ".client_data_json.json")), (string)), challengeIndex: abi.decode( json.parseRaw(string.concat(jsonCaseSelector, ".client_data_json.challenge_index")), (uint256) - ), + ), typeIndex: abi.decode(json.parseRaw(string.concat(jsonCaseSelector, ".client_data_json.type_index")), (uint256)), r: abi.decode(json.parseRaw(string.concat(jsonCaseSelector, ".r")), (uint256)), s: abi.decode(json.parseRaw(string.concat(jsonCaseSelector, ".s")), (uint256)) From 593e2b80cef2fbc72bbc5b9d2ca39d0786f5ee27 Mon Sep 17 00:00:00 2001 From: Wilson Cusack Date: Mon, 22 Apr 2024 09:59:30 -0400 Subject: [PATCH 2/7] add snapshot to workflow --- .github/workflows/test.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7061b60..d3ae07f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,3 +40,8 @@ jobs: run: | forge fmt --check id: fmt + + - name: Check snapshot + run: | + forge snapshot --check + id: snapshot From 286cb3112c7ec9c9147cd24074d78901d72be568 Mon Sep 17 00:00:00 2001 From: Wilson Cusack Date: Mon, 22 Apr 2024 10:03:33 -0400 Subject: [PATCH 3/7] allow longer line --- foundry.toml | 3 ++- src/WebAuthn.sol | 56 ++++++++++++++-------------------------- test/Utils.sol | 4 +-- test/WebAuthn.t.sol | 4 +-- test/Webauthn.fuzz.t.sol | 4 +-- 5 files changed, 24 insertions(+), 47 deletions(-) diff --git a/foundry.toml b/foundry.toml index 3f53772..5a7dca1 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,4 +6,5 @@ optimizer_runs = 999999 [fmt] sort_imports = true -wrap_comments = true \ No newline at end of file +wrap_comments = true +line_length = 140 \ No newline at end of file diff --git a/src/WebAuthn.sol b/src/WebAuthn.sol index f49fa14..0d468fb 100644 --- a/src/WebAuthn.sol +++ b/src/WebAuthn.sol @@ -64,52 +64,35 @@ library WebAuthn { /// /// Specifically, we do verify the following: /// - Verify that authenticatorData (which comes from the authenticator, such as iCloud Keychain) indicates - /// a well-formed assertion with the user present bit set. If `requireUV` is set, checks that the - /// authenticator - /// enforced user verification. User verification should be required if, and only if, - /// options.userVerification + /// a well-formed assertion with the user present bit set. If `requireUV` is set, checks that the authenticator + /// enforced user verification. User verification should be required if, and only if, options.userVerification /// is set to required in the request. - /// - Verifies that the client JSON is of type "webauthn.get", i.e. the client was responding to a request - /// to + /// - Verifies that the client JSON is of type "webauthn.get", i.e. the client was responding to a request to /// assert authentication. /// - Verifies that the client JSON contains the requested challenge. - /// - Verifies that (r, s) constitute a valid signature over both the authenicatorData and client JSON, for - /// public + /// - Verifies that (r, s) constitute a valid signature over both the authenicatorData and client JSON, for public /// key (x, y). /// /// We make some assumptions about the particular use case of this verifier, so we do NOT verify the following: - /// - Does NOT verify that the origin in the `clientDataJSON` matches the Relying Party's origin: tt is - /// considered + /// - Does NOT verify that the origin in the `clientDataJSON` matches the Relying Party's origin: tt is considered /// the authenticator's responsibility to ensure that the user is interacting with the correct RP. This is - /// enforced by most high quality authenticators properly, particularly the iCloud Keychain and Google - /// Password + /// enforced by most high quality authenticators properly, particularly the iCloud Keychain and Google Password /// Manager were tested. - /// - Does NOT verify That `topOrigin` in `clientDataJSON` is well-formed: We assume it would never be - /// present, i.e. - /// the credentials are never used in a cross-origin/iframe context. The website/app set up should - /// disallow - /// cross-origin usage of the credentials. This is the default behaviour for created credentials in common - /// settings. - /// - Does NOT verify that the `rpIdHash` in `authenticatorData` is the SHA-256 hash of the RP ID expected - /// by the Relying - /// Party: this means that we rely on the authenticator to properly enforce credentials to be used only by - /// the correct RP. - /// This is generally enforced with features like Apple App Site Association and Google Asset Links. To - /// protect from - /// edge cases in which a previously-linked RP ID is removed from the authorised RP IDs, we recommend that - /// messages + /// - Does NOT verify That `topOrigin` in `clientDataJSON` is well-formed: We assume it would never be present, i.e. + /// the credentials are never used in a cross-origin/iframe context. The website/app set up should disallow + /// cross-origin usage of the credentials. This is the default behaviour for created credentials in common settings. + /// - Does NOT verify that the `rpIdHash` in `authenticatorData` is the SHA-256 hash of the RP ID expected by the Relying + /// Party: this means that we rely on the authenticator to properly enforce credentials to be used only by the correct RP. + /// This is generally enforced with features like Apple App Site Association and Google Asset Links. To protect from + /// edge cases in which a previously-linked RP ID is removed from the authorised RP IDs, we recommend that messages /// signed by the authenticator include some expiry mechanism. - /// - Does NOT verify the credential backup state: this assumes the credential backup state is NOT used as - /// part of Relying + /// - Does NOT verify the credential backup state: this assumes the credential backup state is NOT used as part of Relying /// Party business logic or policy. - /// - Does NOT verify the values of the client extension outputs: this assumes that the Relying Party does - /// not use client + /// - Does NOT verify the values of the client extension outputs: this assumes that the Relying Party does not use client /// extension outputs. - /// - Does NOT verify the signature counter: signature counters are intended to enable risk scoring for the - /// Relying Party. + /// - Does NOT verify the signature counter: signature counters are intended to enable risk scoring for the Relying Party. /// This assumes risk scoring is not used as part of Relying Party business logic or policy. - /// - Does NOT verify the attestation object: this assumes that response.attestationObject is NOT present in - /// the response, + /// - 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. @@ -138,9 +121,8 @@ library WebAuthn { // 12. Verify that the value of C.challenge equals the base64url encoding of options.challenge. bytes memory expectedChallenge = bytes(string.concat('"challenge":"', Base64.encodeURL(challenge), '"')); - string memory actualChallenge = webAuthnAuth.clientDataJSON.slice( - webAuthnAuth.challengeIndex, webAuthnAuth.challengeIndex + expectedChallenge.length - ); + string memory actualChallenge = + webAuthnAuth.clientDataJSON.slice(webAuthnAuth.challengeIndex, webAuthnAuth.challengeIndex + expectedChallenge.length); if (keccak256(bytes(actualChallenge)) != keccak256(expectedChallenge)) { return false; } diff --git a/test/Utils.sol b/test/Utils.sol index 4a680ba..2313d99 100644 --- a/test/Utils.sol +++ b/test/Utils.sol @@ -17,9 +17,7 @@ library Utils { string memory challengeb64url = Base64Url.encode(abi.encode(challenge)); string memory clientDataJSON = string( abi.encodePacked( - '{"type":"webauthn.get","challenge":"', - challengeb64url, - '","origin":"https://sign.coinbase.com","crossOrigin":false}' + '{"type":"webauthn.get","challenge":"', challengeb64url, '","origin":"https://sign.coinbase.com","crossOrigin":false}' ) ); diff --git a/test/WebAuthn.t.sol b/test/WebAuthn.t.sol index df01604..b41b4a3 100644 --- a/test/WebAuthn.t.sol +++ b/test/WebAuthn.t.sol @@ -30,9 +30,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":"', Base64Url.encode(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 87c80b5..67f7ea3 100644 --- a/test/Webauthn.fuzz.t.sol +++ b/test/Webauthn.fuzz.t.sol @@ -175,9 +175,7 @@ contract WebAuthnFuzzTest is Test { webAuthnAuth = WebAuthn.WebAuthnAuth({ authenticatorData: abi.decode(json.parseRaw(string.concat(jsonCaseSelector, ".authenticator_data")), (bytes)), clientDataJSON: abi.decode(json.parseRaw(string.concat(jsonCaseSelector, ".client_data_json.json")), (string)), - challengeIndex: abi.decode( - json.parseRaw(string.concat(jsonCaseSelector, ".client_data_json.challenge_index")), (uint256) - ), + challengeIndex: abi.decode(json.parseRaw(string.concat(jsonCaseSelector, ".client_data_json.challenge_index")), (uint256)), typeIndex: abi.decode(json.parseRaw(string.concat(jsonCaseSelector, ".client_data_json.type_index")), (uint256)), r: abi.decode(json.parseRaw(string.concat(jsonCaseSelector, ".r")), (uint256)), s: abi.decode(json.parseRaw(string.concat(jsonCaseSelector, ".s")), (uint256)) From 8bffd6fb53b3298baa6fe02e321c4b31aa35cc49 Mon Sep 17 00:00:00 2001 From: Wilson Cusack Date: Mon, 22 Apr 2024 10:04:35 -0400 Subject: [PATCH 4/7] fix fs --- foundry.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/foundry.toml b/foundry.toml index 5a7dca1..400cfb0 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,6 +3,7 @@ src = "src" out = "out" libs = ["lib"] optimizer_runs = 999999 +fs_permissions = [{ access = "read", path = "test/fixtures" }] [fmt] sort_imports = true From 76da324f05f24d1a0ba642ce00c10c5944a8a4b3 Mon Sep 17 00:00:00 2001 From: Wilson Cusack Date: Mon, 22 Apr 2024 10:06:22 -0400 Subject: [PATCH 5/7] comment clean up --- src/WebAuthn.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/WebAuthn.sol b/src/WebAuthn.sol index 0d468fb..0df8506 100644 --- a/src/WebAuthn.sol +++ b/src/WebAuthn.sol @@ -113,7 +113,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 +135,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,7 +146,7 @@ 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 From 242817f60c5d95c59d7916e7b1c3fa207a31da40 Mon Sep 17 00:00:00 2001 From: Wilson Cusack Date: Mon, 22 Apr 2024 10:13:38 -0400 Subject: [PATCH 6/7] fix import style --- test/WebAuthn.t.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/WebAuthn.t.sol b/test/WebAuthn.t.sol index b41b4a3..2b5a398 100644 --- a/test/WebAuthn.t.sol +++ b/test/WebAuthn.t.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {WebAuthn} from "../src/WebAuthn.sol"; import {Base64Url} from "FreshCryptoLib/utils/Base64Url.sol"; import {Test, console2} from "forge-std/Test.sol"; +import {WebAuthn} from "../src/WebAuthn.sol"; + contract WebAuthnTest is Test { bytes challenge = abi.encode(0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf); From 37cee182ea0775be7ac9e332804edba08fa28896 Mon Sep 17 00:00:00 2001 From: Wilson Cusack Date: Mon, 22 Apr 2024 10:19:39 -0400 Subject: [PATCH 7/7] fix other import --- test/Webauthn.fuzz.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Webauthn.fuzz.t.sol b/test/Webauthn.fuzz.t.sol index 67f7ea3..f05b77f 100644 --- a/test/Webauthn.fuzz.t.sol +++ b/test/Webauthn.fuzz.t.sol @@ -2,11 +2,11 @@ pragma solidity ^0.8.0; import {FCL_ecdsa} from "FreshCryptoLib/FCL_ecdsa.sol"; +import {Test, Vm, console, stdJson} from "forge-std/Test.sol"; import {WebAuthn} from "../src/WebAuthn.sol"; import "./Utils.sol"; -import "forge-std/Test.sol"; contract WebAuthnFuzzTest is Test { using stdJson for string;