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

♻️ full assembly variant of the webauthn library #5

Closed
wants to merge 2 commits into from
Closed
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
87 changes: 25 additions & 62 deletions src/WebAuthn.sol
Original file line number Diff line number Diff line change
@@ -1,87 +1,50 @@
// 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 verify(
bytes1 authenticatorDataFlagMask,
bytes calldata authenticatorData,
bytes calldata clientData,
bytes32 clientChallenge,
uint256 clientChallengeOffset
bytes calldata clientChallengeBase64,
uint256 clientChallengeOffset,
uint256 r,
uint256 s,
uint256 qx,
uint256 qy
)
internal
pure
returns (bytes32 result)
returns (bool)
{
// 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 + clientChallengeBase64.length)]);

// hash the encoded challenge and check both challenges are equal
if (keccak256(clientChallengeBase64) != 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
)
}
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 clientData,
bytes32 clientChallenge,
uint256 clientChallengeOffset,
uint256 r,
uint256 s,
uint256 qx,
uint256 qy
)
internal
returns (bool)
{
bytes32 message =
format(authenticatorData, authenticatorDataFlagMask, clientData, clientChallenge, clientChallengeOffset);
// Verify the signature over sha256(authenticatorData || sha256(clientData))
bytes32 message = sha256(abi.encodePacked(authenticatorData, sha256(clientData)));

return ECDSA256r1.verify(message, r, s, qx, qy);
return ECDSA256r1.verify(message, r, s, qx, qy);
}
}
}
145 changes: 145 additions & 0 deletions src/WebAuthnTestAssembly.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.19 <0.9.0;

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

// error InvalidAuthenticatorData();
// error InvalidClientData();

// // equivelent to `clientData[clientChallengeOffset:clientChallengeOffset + bytes(challengeEncoded).length]`
// // (200 gas more expensive)
// // 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
// // )
// // }

// // equivalent to `keccak256(abi.encodePacked(challengeExtracted));` (350 gas more expensive)
// // bytes32 moreData;

// library WebAuthnImplementation {
// function format(
// bytes calldata authenticatorData,
// bytes1 authenticatorDataFlagMask,
// bytes calldata clientData,
// bytes32 clientChallenge,
// uint256 clientChallengeOffset
// )
// internal
// returns (bytes32 result)
// {
// {
// /// @DEV: Check if User Presence (0x01) or User Verification (0x04) are set
// if ((authenticatorData[32] & authenticatorDataFlagMask) != authenticatorDataFlagMask) {
// revert InvalidAuthenticatorData();
// }

// /// @DEV: Verify that clientData commits to the expected client challenge
// string memory challengeEncoded = Base64.encode(abi.encodePacked(clientChallenge), true, true);
// assembly ("memory-safe") {
// // equivelent to `clientData[clientChallengeOffset:clientChallengeOffset +
// // bytes(challengeEncoded).length]`
// // (200 gas more expensive)
// let challengeExtracted := mload(0x40)
// // store challengeExtracted length -- 1 slot expansion
// mstore(challengeExtracted, mload(challengeEncoded))

// calldatacopy( // copy from calldata to memory
// add(challengeExtracted, 0x20), // destOffset -- 1 slot expansion
// add(clientData.offset, clientChallengeOffset), // offset
// mload(challengeExtracted) // size
// )

// let challengeExtractedHash := keccak256(add(challengeExtracted, 32), mload(challengeExtracted))
// let challengeEncodedHash := keccak256(add(challengeEncoded, 32), mload(challengeEncoded))

// if iszero(eq(challengeExtractedHash, challengeEncodedHash)) {
// // override the first scratch space slot to not paying the gas expansion
// mstore(0x00, 0xebab5d29)
// revert(0x00, 0x20)
// }
// }
// }

// assembly ("memory-safe") {
// let verifyData := mload(0x40)
// // store authenticatorData length -- 1 slot expansion -- override challengeExtracted's length
// mstore(verifyData, add(authenticatorData.length, 0x20))

// calldatacopy( // copy from calldata to memory
// add(verifyData, 0x20), // destOffset -- n slot expansions -- override challengeExtracted's value
// authenticatorData.offset, // offset
// authenticatorData.length // size
// )

// // let clientDataHash := add(mload(0x40), 0x80)
// // assembly {
// // make sure not erase verifyData's length and value -- 1 slot expansion
// pop(
// staticcall(
// gas(),
// 0x02,
// clientData.offset,
// clientData.length,
// // add(verifyData, add(authenticatorData.length, 0x20)),
// add(verifyData, authenticatorData.length),
// 0x20
// )
// )
// // }

// // mstore(0x80, clientDataHash)

// // result := 0x01

// // TODO: check success
// // gas addr argsOffset argsSize retOffset retSize
// pop(staticcall(gas(), 0x02, add(verifyData, 0x20), mload(verifyData), 0x00, 0x20))
// result := mload(0x00)
// }

// // 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
// // )
// // }
// // 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 clientData,
// bytes32 clientChallenge,
// uint256 clientChallengeOffset,
// uint256 r,
// uint256 s,
// uint256 qx,
// uint256 qy
// )
// external
// returns (bool)
// {
// bytes32 message =
// format(authenticatorData, authenticatorDataFlagMask, clientData, clientChallenge, clientChallengeOffset);

// return ECDSA256r1.verify(message, r, s, qx, qy);
// }
// }
58 changes: 26 additions & 32 deletions test/WebAuthn.t.sol
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// 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(
bytes calldata authenticatorData,
bytes1 authenticatorDataFlagMask,
bytes calldata authenticatorData,
bytes calldata clientData,
bytes32 clientChallenge,
bytes calldata clientChallengeBase64,
uint256 clientChallengeOffset,
uint256 r,
uint256 s,
Expand All @@ -20,10 +20,10 @@ contract WebAuthnImplementation {
returns (bool)
{
return WebAuthn.verify(
authenticatorData,
authenticatorDataFlagMask,
authenticatorData,
clientData,
clientChallenge,
clientChallengeBase64,
clientChallengeOffset,
r,
s,
Expand All @@ -41,35 +41,29 @@ contract ContractTest is Test {
}

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;

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
)
);
}
Expand Down