Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

♻️ Refactor the WebAuthn library #4

Merged
merged 1 commit into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions script/DeployWebAuthn.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { WebAuthn } from "../src/WebAuthn.sol";

contract LibraryWrapper {
function verify(
bytes calldata authData,
bytes1 authDataFlagMask,
bytes1 authenticatorDataFlagMask,
bytes calldata authenticatorData,
bytes calldata clientData,
bytes32 clientChallenge,
bytes calldata clientChallengeBase64,
uint256 clientChallengeOffset,
uint256 r,
uint256 s,
Expand All @@ -20,7 +20,15 @@ contract LibraryWrapper {
returns (bool)
{
return WebAuthn.verify(
authData, authDataFlagMask, clientData, clientChallenge, clientChallengeOffset, r, s, qx, qy
authenticatorDataFlagMask,
authenticatorData,
clientData,
clientChallengeBase64,
clientChallengeOffset,
r,
s,
qx,
qy
);
}
}
Expand Down Expand Up @@ -56,13 +64,13 @@ contract MyScript is BaseScript {

example:
cast call 0x387ca8d38f379710a3d24d710ba2940787f7b4a1 \
"verify(bytes,bytes1,bytes,bytes32,uint256,uint256,uint256,uint256,uint256)(bool)" \
0xf8e4b678e1c62f7355266eaa4dc1148573440937063a46d848da1e25babbd20b010000004d \
"verify(bytes1,bytes,bytes,bytes,uint256,uint256,uint256,uint256,uint256)(bool)" \
0x01 \
0xf8e4b678e1c62f7355266eaa4dc1148573440937063a46d848da1e25babbd20b010000004d \
0x7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a224e546f2d316\
1424547526e78786a6d6b61544865687972444e5833697a6c7169316f776d4f643955474a30222c226f7269\
67696e223a2268747470733a2f2f66726573682e6c65646765722e636f6d222c2263726f73734f726967696e223a66616c73657d\
0x353a3ed5a0441919f1c639a46931de872ac3357de2ce5aa2d68c2639df54189d \
4e546f2d3161424547526e78786a6d6b61544865687972444e5833697a6c7169316f776d4f643955474a30 \
0x24 \
45847212378479006099766816358861726414873720355505495069909394794949093093607 \
55835259151215769394881684156457977412783812617123006733908193526332337539398 \
Expand Down
72 changes: 27 additions & 45 deletions src/WebAuthn.sol
Original file line number Diff line number Diff line change
@@ -1,75 +1,53 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.19 <0.9.0;

import { Base64 } from "../lib/solady/src/utils/Base64.sol";
// import { Base64 } from "../lib/solady/src/utils/Base64.sol";
import { ECDSA256r1 } from "../lib/secp256r1-verify/src/ECDSA256r1.sol";

error InvalidAuthenticatorData();
error InvalidClientData();

/// dev: this implementation assumes the caller check if User Presence (0x01) or User Verification (0x04) are set
library WebAuthn {
function format(
bytes calldata authenticatorData,
function generateMessage(
bytes1 authenticatorDataFlagMask,
bytes calldata authenticatorData,
bytes calldata clientData,
bytes32 clientChallenge,
bytes calldata clientChallenge,
uint256 clientChallengeOffset
)
internal
pure
returns (bytes32 result)
returns (bytes32)
{
// Let the caller check if User Presence (0x01) or User Verification (0x04) are set
{
unchecked {
// Let the caller check if User Presence (0x01) or User Verification (0x04) are set
if ((authenticatorData[32] & authenticatorDataFlagMask) != authenticatorDataFlagMask) {
revert InvalidAuthenticatorData();
}
// Verify that clientData commits to the expected client challenge
string memory challengeEncoded = Base64.encode(abi.encodePacked(clientChallenge), true, true);
bytes memory challengeExtracted = new bytes(
bytes(challengeEncoded).length
);

assembly {
calldatacopy( // copy from calldata to memory
add(challengeExtracted, 32), // destOffset
add(clientData.offset, clientChallengeOffset), // offset
mload(challengeExtracted) // size
)
}
// TODO: Pass non-encode challenge? Convert the challenge to `bytes memory` and encode it to Base64
// bytes memory challengeEncoded = bytes(Base64.encode(abi.encodePacked(clientChallenge), true, true));

bytes32 moreData; //=keccak256(abi.encodePacked(challengeExtracted));
assembly {
moreData := keccak256(add(challengeExtracted, 32), mload(challengeExtracted))
}
if (keccak256(abi.encodePacked(bytes(challengeEncoded))) != moreData) {
// Extract the challenge from the client data and hash it
bytes32 challengeHashed =
keccak256(clientData[clientChallengeOffset:(clientChallengeOffset + clientChallenge.length)]);

// hash the encoded challenge and check both challenges are equal
if (keccak256(clientChallenge) != challengeHashed) {
revert InvalidClientData();
}
}

// Verify the signature over sha256(authenticatorData || sha256(clientData))
bytes memory verifyData = new bytes(authenticatorData.length + 32);
assembly {
calldatacopy( // copy from calldata to memory
add(verifyData, 32), // destOffset
authenticatorData.offset, // offset
authenticatorData.length // size
)
// Verify the signature over sha256(authenticatorData || sha256(clientData))
return sha256(abi.encodePacked(authenticatorData, sha256(clientData)));
}
bytes32 more = sha256(clientData);
assembly {
mstore(add(verifyData, add(authenticatorData.length, 32)), more)
}

return sha256(verifyData);
}

/// note: this implementation assumes the caller check if User Presence (0x01) or User Verification (0x04) are set
function verify(
bytes calldata authenticatorData,
bytes1 authenticatorDataFlagMask,
bytes calldata authenticatorData,
bytes calldata clientData,
bytes32 clientChallenge,
bytes calldata clientChallenge,
uint256 clientChallengeOffset,
uint256 r,
uint256 s,
Expand All @@ -79,9 +57,13 @@ library WebAuthn {
internal
returns (bool)
{
bytes32 message =
format(authenticatorData, authenticatorDataFlagMask, clientData, clientChallenge, clientChallengeOffset);
unchecked {
// Verify the signature over sha256(authenticatorData || sha256(clientData))
bytes32 message = generateMessage(
authenticatorDataFlagMask, authenticatorData, clientData, clientChallenge, clientChallengeOffset
);

return ECDSA256r1.verify(message, r, s, qx, qy);
return ECDSA256r1.verify(message, r, s, qx, qy);
}
}
}
96 changes: 63 additions & 33 deletions test/WebAuthn.t.sol
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.19 <0.9.0;

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

contract WebAuthnImplementation {
function verify(
function generateMessage(
bytes1 authenticatorDataFlagMask,
bytes calldata authenticatorData,
bytes calldata clientData,
bytes calldata clientChallengeBase64,
uint256 clientChallengeOffset
)
external
pure
returns (bytes32)
{
return WebAuthn.generateMessage(
authenticatorDataFlagMask, authenticatorData, clientData, clientChallengeBase64, clientChallengeOffset
);
}

function verify(
bytes1 authenticatorDataFlagMask,
bytes calldata authenticatorData,
bytes calldata clientData,
bytes32 clientChallenge,
bytes calldata clientChallengeBase64,
uint256 clientChallengeOffset,
uint256 r,
uint256 s,
Expand All @@ -20,10 +36,10 @@ contract WebAuthnImplementation {
returns (bool)
{
return WebAuthn.verify(
authenticatorData,
authenticatorDataFlagMask,
authenticatorData,
clientData,
clientChallenge,
clientChallengeBase64,
clientChallengeOffset,
r,
s,
Expand All @@ -40,37 +56,51 @@ contract ContractTest is Test {
implem = new WebAuthnImplementation();
}

function test_Webauthn() public {
bytes memory authenticatorData =
hex"f8e4b678e1c62f7355266eaa4dc1148573440937063a46d848da1e25babbd20b" hex"010000004d";
bytes1 authenticatorDataFlagMask = 0x01;
bytes memory clientData = hex"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e67"
hex"65223a224e546f2d3161424547526e78786a6d6b61544865687972444e583369"
hex"7a6c7169316f776d4f643955474a30222c226f726967696e223a226874747073"
hex"3a2f2f66726573682e6c65646765722e636f6d222c2263726f73734f726967696e223a66616c73657d";
bytes32 clientChallenge = hex"353a3ed5a0441919f1c639a46931de872ac3357de2ce5aa2d68c2639df54189d";
uint256 clientChallengeDataOffset = 0x24; // 36
uint256 r =
45_847_212_378_479_006_099_766_816_358_861_726_414_873_720_355_505_495_069_909_394_794_949_093_093_607;
uint256 s =
55_835_259_151_215_769_394_881_684_156_457_977_412_783_812_617_123_006_733_908_193_526_332_337_539_398;
uint256 qx =
114_874_632_398_302_156_264_159_990_279_427_641_021_947_882_640_101_801_130_664_833_947_273_521_181_002;
uint256 qy =
32_136_952_818_958_550_240_756_825_111_900_051_564_117_520_891_182_470_183_735_244_184_006_536_587_423;

function test_Verify() public {
assertTrue(
implem.verify(
authenticatorData,
authenticatorDataFlagMask,
clientData,
clientChallenge,
clientChallengeDataOffset,
r,
s,
qx,
qy
// authenticatorDataFlagMask
0x01,
// authenticatorData
hex"f8e4b678e1c62f7355266eaa4dc1148573440937063a46d848da1e25babbd20b010000004d",
// clientData
hex"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e67"
hex"65223a224e546f2d3161424547526e78786a6d6b61544865687972444e583369"
hex"7a6c7169316f776d4f643955474a30222c226f726967696e223a226874747073"
hex"3a2f2f66726573682e6c65646765722e636f6d222c2263726f73734f726967696e223a66616c73657d",
// clientChallengeBase64
hex"4e546f2d3161424547526e78786a6d6b61544865687972444e5833697a6c7169316f776d4f643955474a30",
// clientChallengeOffset
0x24,
// r
45_847_212_378_479_006_099_766_816_358_861_726_414_873_720_355_505_495_069_909_394_794_949_093_093_607,
// s
55_835_259_151_215_769_394_881_684_156_457_977_412_783_812_617_123_006_733_908_193_526_332_337_539_398,
// qx
114_874_632_398_302_156_264_159_990_279_427_641_021_947_882_640_101_801_130_664_833_947_273_521_181_002,
// qy
32_136_952_818_958_550_240_756_825_111_900_051_564_117_520_891_182_470_183_735_244_184_006_536_587_423
)
);
}

function test_GenerateMessage() public {
bytes32 message = implem.generateMessage(
// authenticatorDataFlagMask
0x01,
// authenticatorData
hex"f8e4b678e1c62f7355266eaa4dc1148573440937063a46d848da1e25babbd20b010000004d",
// clientData
hex"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e67"
hex"65223a224e546f2d3161424547526e78786a6d6b61544865687972444e583369"
hex"7a6c7169316f776d4f643955474a30222c226f726967696e223a226874747073"
hex"3a2f2f66726573682e6c65646765722e636f6d222c2263726f73734f726967696e223a66616c73657d",
// clientChallengeBase64
hex"4e546f2d3161424547526e78786a6d6b61544865687972444e5833697a6c7169316f776d4f643955474a30",
// clientChallengeOffset
0x24
);

assertEq(message, 0x4bfd0c06e1609b41d94e18b705de3163f6bf61fa44dcb8127c94bab7ba55fb9c);
}
}