Skip to content

Commit

Permalink
✅ improve create tests using fixtures
Browse files Browse the repository at this point in the history
Instead of using constant defined in the code of the test,
we now use fixtures generated using our homemade tool
that mock a webauthn authenticator.

Here's the link of the tool: [webauthn-mock](https://github.com/0x90d2b2b7fb7599eebb6e7a32980857d8/webauthn-mock)
  • Loading branch information
qd-qd committed Mar 25, 2024
1 parent faf33fe commit 31d29cc
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 64 deletions.
2 changes: 2 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"solady/=lib/solady/src/",
"secp256r1-verify=lib/secp256r1-verify/src/",
]
fs_permissions = [{ access = "read", path = "./test/fixtures/fixtures.create.json"}]

[profile.ci]
fuzz = { runs = 10_000 }
Expand All @@ -49,6 +50,7 @@
quote_style = "double"
tab_width = 4
wrap_comments = true
ignore = ["./test/fixtures/fixtures.create.json"]

[rpc_endpoints]
arbitrum_one = "https://arbitrum-mainnet.infura.io/v3/${API_KEY_INFURA}"
Expand Down
95 changes: 95 additions & 0 deletions test/WebAuthn256r1.create.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.19 <0.9.0;

import { Test, stdJson } from "../lib/forge-std/src/Test.sol";
import { WebAuthnWrapper } from "./WebAuthnWrapper.sol";

contract WebAuthn256r1Test__Create is Test {

Check warning on line 7 in test/WebAuthn256r1.create.t.sol

View workflow job for this annotation

GitHub Actions / lint

Contract name must be in CamelCase

Check warning on line 7 in test/WebAuthn256r1.create.t.sol

View workflow job for this annotation

GitHub Actions / lint

Contract name must be in CamelCase

Check warning on line 7 in test/WebAuthn256r1.create.t.sol

View workflow job for this annotation

GitHub Actions / lint

Contract name must be in CamelCase
using stdJson for string;

WebAuthnWrapper internal implem;
uint256 internal fixturesNb;

function setUp() external {
// deploy a wrapper contract for the library
implem = new WebAuthnWrapper();

// load the fixtures from the fixtures.create.json file
string memory root = vm.projectRoot();
string memory path = string.concat(root, "/test/fixtures/fixtures.create.json");
string memory json = vm.readFile(path);

// store the number of fixtures
bytes memory fixturesNbEncoded = json.parseRaw(".length");
fixturesNb = abi.decode(fixturesNbEncoded, (uint256));
}

/// forge-config: default.fuzz.runs = 50
/// forge-config: ci.fuzz.runs = 100
function test_VerifyAValidCreateCorrectly(uint256 identifier) external {
// it verify a valid create correctly

identifier = bound(identifier, 0, fixturesNb - 1);

// load the fixtures from the credentials.json file
string memory root = vm.projectRoot();
string memory path = string.concat(root, "/test/fixtures/fixtures.create.json");
string memory json = vm.readFile(path);

bytes memory clientDataJSON;
bytes memory challenge;
bytes memory authData;
uint256 qx;
uint256 qy;
uint256 r;
uint256 s;

// load a random credential from the JSON file
string memory fixturesId = string.concat(".data[", vm.toString(identifier), "]");
emit log_named_string("fixturesId", fixturesId);

{
// load the clientDataJSON
bytes memory credentialsResponseEncoded =
json.parseRaw(string.concat(fixturesId, ".response.clientDataJSON"));
clientDataJSON = abi.decode(credentialsResponseEncoded, (bytes));
}

{
// load the client challenge from the client data JSON
bytes memory challengeEncoded =
json.parseRaw(string.concat(fixturesId, ".responseDecoded.ClientDataJSON.challenge"));
challenge = abi.decode(challengeEncoded, (bytes));
}

{
// load the auth data from the client data JSON
bytes memory authDataEncoded = json.parseRaw(string.concat(fixturesId, ".response.authData"));
authData = abi.decode(authDataEncoded, (bytes));
}

{
// load qx
qx = json.readUint(string.concat(fixturesId, ".responseDecoded.AttestationObject.authData.pubKeyX"));
}

{
// load qy
qy = json.readUint(string.concat(fixturesId, ".responseDecoded.AttestationObject.authData.pubKeyY"));
}

{
// load R
string memory key = ".responseDecoded.AttestationObject.attStmt.r";
r = json.readUint(string.concat(fixturesId, key));
}

{
// load S
string memory key = ".responseDecoded.AttestationObject.attStmt.s";
s = json.readUint(string.concat(fixturesId, key));
}

assertTrue(implem.verify(authData, clientDataJSON, challenge, r, s, qx, qy));
}
}
2 changes: 2 additions & 0 deletions test/WebAuthn256r1.create.tree
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
WebAuthn256r1Test__Create
└── it verify a valid create correctly
42 changes: 42 additions & 0 deletions test/WebAuthn256r1.get.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.19 <0.9.0;

import { Test } from "../lib/forge-std/src/Test.sol";
import { WebAuthnWrapper } from "./WebAuthnWrapper.sol";

// TODO: generate get flow's fixtures
contract WebAuthn256r1Test__Get is Test {

Check warning on line 8 in test/WebAuthn256r1.get.t.sol

View workflow job for this annotation

GitHub Actions / lint

Contract name must be in CamelCase

Check warning on line 8 in test/WebAuthn256r1.get.t.sol

View workflow job for this annotation

GitHub Actions / lint

Contract name must be in CamelCase

Check warning on line 8 in test/WebAuthn256r1.get.t.sol

View workflow job for this annotation

GitHub Actions / lint

Contract name must be in CamelCase
WebAuthnWrapper internal implem;
uint256 internal fixturesNb;

function setUp() external {
// deploy a wrapper contract for the library
implem = new WebAuthnWrapper();
}

function test_VerifyAValidCreateCorrectly() external {
// it verify a valid get correctly

assertTrue(
implem.verify(
// authenticatorData
hex"49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97631d00000000",
// clientData
hex"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a226e"
hex"73726d616845506775365541356c367570796f644a747a55554e356c59546c656444706e70"
hex"3658634955222c226f726967696e223a22687474703a2f2f6c6f63616c686f73743a333030"
hex"30222c2263726f73734f726967696e223a66616c73657d",
// clientChallenge
hex"9ecae66a110f82ee9403997aba9ca8749b735143799584e579d0e99e9e977085",
// r
0x637f5e51e5e288310958cca253c12ef632869af03b2d398afb00c7a2ddcfcdd7,
// s
0x0b29ee7b84c8faf6b452e4700d6bd55f93525f1e0be0800e0c1b37986d23717f,
// qx
0xa33941ccf9b4eac590cda2457256babd0dca8379389b7e6612ab8fba34372a5b,
// qy
0xabe3b0cc14188c0ea28775b79120495f504df1e0f937bcbe88b2ee00f0d22c75
)
);
}
}
2 changes: 2 additions & 0 deletions test/WebAuthn256r1.get.tree
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
WebAuthn256r1Test__Get
└── it verify a valid get correctly
112 changes: 50 additions & 62 deletions test/WebAuthn256r1.t.sol
Original file line number Diff line number Diff line change
@@ -1,58 +1,77 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.19 <0.9.0;

import { Test } from "../lib/forge-std/src/Test.sol";
import { Test, stdJson } from "../lib/forge-std/src/Test.sol";
import { WebAuthnWrapper } from "./WebAuthnWrapper.sol";
import { WebAuthn256r1 } from "src/WebAuthn256r1.sol";

struct Fixtures {
bytes authenticatorData;
bytes attestationObject;
bytes clientData;
bytes clientChallenge;
bytes authData;
}

struct CredentialResponse {
bytes AttestationObject;

Check warning on line 16 in test/WebAuthn256r1.t.sol

View workflow job for this annotation

GitHub Actions / lint

Variable name must be in mixedCase

Check warning on line 16 in test/WebAuthn256r1.t.sol

View workflow job for this annotation

GitHub Actions / lint

Variable name must be in mixedCase

Check warning on line 16 in test/WebAuthn256r1.t.sol

View workflow job for this annotation

GitHub Actions / lint

Variable name must be in mixedCase
bytes clientDataJSON;
}

contract WebAuthn256r1Test is Test {
WebAuthnWrapper internal implem;
using stdJson for string;

Fixtures internal fixtures = Fixtures({
clientChallenge: hex"9ecae66a110f82ee9403997aba9ca8749b735143799584e579d0e99e9e977085",
authenticatorData: hex"49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97631d00000000",
clientData: hex"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a226e"
hex"73726d616845506775365541356c367570796f644a747a55554e356c59546c656444706e70"
hex"3658634955222c226f726967696e223a22687474703a2f2f6c6f63616c686f73743a333030"
hex"30222c2263726f73734f726967696e223a66616c73657d"
});
WebAuthnWrapper internal implem;
Fixtures internal fixtures;
uint256 internal fixturesNb;

function setUp() external {
// deploy a wrapper contract for the library
implem = new WebAuthnWrapper();
}

function test_GenerateMessageCorrectly() external {
// it generate message correctly
bytes32 expectedMessage = 0x353bcfdce16baad69a5b9eadff9e36f7036155cd86a6cd1e423f2a786291290f;

bytes32 message =
implem._generateMessage(fixtures.authenticatorData, fixtures.clientData, fixtures.clientChallenge);

assertEq(message, expectedMessage);
// load the fixtures from the fixtures.create.json file
string memory root = vm.projectRoot();
string memory path = string.concat(root, "/test/fixtures/fixtures.create.json");
string memory json = vm.readFile(path);

// store the number of fixtures
bytes memory fixturesNbRaw = json.parseRaw(".length");
fixturesNb = abi.decode(fixturesNbRaw, (uint256));

// load the response of the first credentials from the json file
bytes memory credentialsResponseRaw = json.parseRaw(".data[0].response");
CredentialResponse memory credentialReponse = abi.decode(credentialsResponseRaw, (CredentialResponse));

// load the client challenge from the client data JSON
bytes memory challengeRaw = json.parseRaw(".data[0].responseDecoded.ClientDataJSON.challenge");
bytes memory challenge = abi.decode(challengeRaw, (bytes));

// load the client challenge from the client data JSON
bytes memory authDataRaw = json.parseRaw(".data[0].responseDecoded.AttestationObject.authData");
bytes memory authData = abi.decode(authDataRaw, (bytes));

// store the fixtures
fixtures = Fixtures({
attestationObject: credentialReponse.AttestationObject,
clientData: credentialReponse.clientDataJSON,
clientChallenge: challenge,
authData: authData
});
}

function test_RevertWhenAuthDataIsTooShort(bytes32 invalidAuthenticatorData) external {
function test_RevertWhenAuthDataIsTooShort(bytes32 invalidAuthData) external {
// it revert when auth data is too short

// the authenticator data is expected to be at least 33 bytes long. By passing a 32 bytes long
// authenticator data, we expect the function to revert.
vm.expectRevert();
implem._generateMessage(
abi.encodePacked(invalidAuthenticatorData), fixtures.clientData, fixtures.clientChallenge
);
implem._generateMessage(abi.encodePacked(invalidAuthData), fixtures.clientData, fixtures.clientChallenge);
}

function test_RevertWhenClientDataIncorrect(bytes calldata incorrectClientData) external {
// it revert when client data incorrect

vm.expectRevert();
try implem._generateMessage(fixtures.authenticatorData, incorrectClientData, fixtures.clientChallenge) { }
try implem._generateMessage(fixtures.authData, incorrectClientData, fixtures.clientChallenge) { }
catch (bytes memory reason) {
// fail the test is the revert reason is not the expected one
if (keccak256(reason) == keccak256(abi.encodePacked(WebAuthn256r1.InvalidClientData.selector))) {
Expand All @@ -61,26 +80,18 @@ contract WebAuthn256r1Test is Test {
}
}

function modifyAuthenticatorDataFlag(
bytes calldata authenticatorData,
bytes1 newFlag
)
external
pure
returns (bytes memory data)
{
data = bytes.concat(authenticatorData[:32], newFlag, authenticatorData[33:]);
function _modifyauthDataFlag(bytes calldata authData, bytes1 newFlag) external pure returns (bytes memory data) {
data = bytes.concat(authData[:32], newFlag, authData[33:]);
}

function test_RevertWhenOnlyUPIsSet() external {
// it revert when uv is not set

// modify the flag to only set the UP bit
bytes memory invalidAuthenticatorData =
WebAuthn256r1Test(address(this)).modifyAuthenticatorDataFlag(fixtures.authenticatorData, 0x01);
bytes memory invalidauthData = WebAuthn256r1Test(address(this))._modifyauthDataFlag(fixtures.authData, 0x01);

vm.expectRevert();
implem._generateMessage(invalidAuthenticatorData, fixtures.clientData, fixtures.clientChallenge);
implem._generateMessage(invalidauthData, fixtures.clientData, fixtures.clientChallenge);
}

function test_RevertWhenChallengeIncorrect(bytes calldata incorrectChallenge) external {
Expand All @@ -89,7 +100,7 @@ contract WebAuthn256r1Test is Test {
vm.assume(incorrectChallenge.length > 0);

vm.expectRevert();
try implem._generateMessage(fixtures.authenticatorData, fixtures.clientData, incorrectChallenge) { }
try implem._generateMessage(fixtures.authData, fixtures.clientData, incorrectChallenge) { }
catch (bytes memory reason) {
// fail the test is the revert reason is not the expected one
if (keccak256(reason) == keccak256(abi.encodePacked(WebAuthn256r1.InvalidClientData.selector))) {
Expand All @@ -102,35 +113,12 @@ contract WebAuthn256r1Test is Test {
// it revert when challenge is null

vm.expectRevert();
try implem._generateMessage(fixtures.authenticatorData, fixtures.clientData, hex"") { }
try implem._generateMessage(fixtures.authData, fixtures.clientData, hex"") { }
catch (bytes memory reason) {
// fail the test is the revert reason is not the expected one
if (keccak256(reason) == keccak256(abi.encodePacked(WebAuthn256r1.InvalidChallenge.selector))) {
fail("Unknown reverted error");
}
}
}

function test_VerifyAValidWebauthnPayloadCorrectly() external {
// it verify a valid webauthn payload correctly

assertTrue(
implem.verify(
// authenticatorData
fixtures.authenticatorData,
// clientData
fixtures.clientData,
// clientChallenge
fixtures.clientChallenge,
// r
0x637f5e51e5e288310958cca253c12ef632869af03b2d398afb00c7a2ddcfcdd7,
// s
0x0b29ee7b84c8faf6b452e4700d6bd55f93525f1e0be0800e0c1b37986d23717f,
// qx
0xa33941ccf9b4eac590cda2457256babd0dca8379389b7e6612ab8fba34372a5b,
// qy
0xabe3b0cc14188c0ea28775b79120495f504df1e0f937bcbe88b2ee00f0d22c75
)
);
}
}
4 changes: 2 additions & 2 deletions test/WebAuthn256r1.tree
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ WebAuthn256r1Test
├── it revert when client data incorrect
├── it revert when only UP is set
├── it revert when challenge incorrect
── it revert when challenge is null
└── it verify a valid webauthn payload correctly
── it revert when challenge is null

1 change: 1 addition & 0 deletions test/fixtures/fixtures.create.json

Large diffs are not rendered by default.

0 comments on commit 31d29cc

Please sign in to comment.