diff --git a/account-integrations/safe/README.md b/account-integrations/safe/README.md index 09c6384c..c5d10cbd 100644 --- a/account-integrations/safe/README.md +++ b/account-integrations/safe/README.md @@ -1,14 +1,20 @@ -# Webauthn plugin +# Safe plugins # Getting Started -1. (from this directory) +1. `cd account-integrations/safe` 2. Run `yarn` to install hardhat dependencies 3. Run `forge install` to install foundry dependencies -## Integration test +## Forge tests -To run the integration tests: +```bash +forge test +``` + +## Hardhat tests + +To run the hardhat tests, you'll need to run a node and a bundler as some of them are integration tests: 1. Start a geth node, fund accounts and deploy Safe contracts: @@ -25,7 +31,12 @@ cp .env.example .env 3. Setup and run an external bundler (make sure the values in `.env` match up with the bundler and node you're running). ```bash -# If using the eth-infinitism bundler (tested with commit #1b154c9) +# If using the eth-infinitism bundler, checkout to this commmit. The latest version of the bundler has started breaking the integration tests. This is a previous commit where the integration tests still pass +git checkout 1b154c9 +``` + +```bash +# If using the eth-infinitism bundler yarn run bundler ``` diff --git a/account-integrations/safe/src/SafeWebAuthnPlugin.sol b/account-integrations/safe/src/SafeWebAuthnPlugin.sol index 40ade6d0..bd018546 100644 --- a/account-integrations/safe/src/SafeWebAuthnPlugin.sol +++ b/account-integrations/safe/src/SafeWebAuthnPlugin.sol @@ -69,6 +69,7 @@ contract SafeWebAuthnPlugin is BaseAccount { struct LocalVarStruct { bytes1 authenticatorDataFlagMask; + bytes32 clientChallenge; uint256 clientChallengeDataOffset; } @@ -87,20 +88,22 @@ contract SafeWebAuthnPlugin is BaseAccount { uint i = 0; uint dataLen = 32; uint256 paramLen = abi.decode(userOp.signature[i:i+dataLen], (uint256)); - // Fixed-length params (bytes1, (uint256?), uint256, uint256[2], uint256[2]). Expect 8 slots = 256 bytes + // Fixed-length params (bytes1, (uint256?), bytes32, uint256, uint256[2], uint256[2]). Expect 9 slots = 256 bytes i += dataLen; // advance index // decode fixed length params (values to memory) - dataLen = 3 * 32; //lenFixedParams - 32; // -32 already read length + dataLen = 4 * 32; //lenFixedParams - 32; // -32 already read length ( s.authenticatorDataFlagMask, , // some number + s.clientChallenge, s.clientChallengeDataOffset ) = abi.decode( userOp.signature[i:i+dataLen], ( bytes1, uint256, //not sure what is encoded here + bytes32, uint256 ) ); @@ -146,7 +149,7 @@ contract SafeWebAuthnPlugin is BaseAccount { authenticatorData, s.authenticatorDataFlagMask, clientData, - userOpHash, + s.clientChallenge, s.clientChallengeDataOffset, signature, pubKey diff --git a/account-integrations/safe/src/lib/Base64URL.sol b/account-integrations/safe/src/lib/Base64URL.sol new file mode 100644 index 00000000..3b46b7c1 --- /dev/null +++ b/account-integrations/safe/src/lib/Base64URL.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT +// from OpenZeppelin Contracts (last updated v4.7.0) (utils/Base64.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Provides a set of functions to operate with Base64 strings. + * + * _Available since v4.5._ + */ +library Base64URL { + /** + * @dev Base64 Encoding/Decoding Table + */ + string internal constant _TABLE = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + + /** + * @dev Converts a `bytes` to its Bytes64 `string` representation. + */ + function encode32(bytes memory data) internal pure returns (string memory) { + /** + * Inspired by Brecht Devos (Brechtpd) implementation - MIT licence + * https://github.com/Brechtpd/base64/blob/e78d9fd951e7b0977ddca77d92dc85183770daf4/base64.sol + */ + if (data.length == 0) return ""; + + // Loads the table into memory + string memory table = _TABLE; + + // Encoding takes 3 bytes chunks of binary data from `bytes` data parameter + // and split into 4 numbers of 6 bits. + // The final Base64 length should be `bytes` data length multiplied by 4/3 rounded up + // - `data.length + 2` -> Round up + // - `/ 3` -> Number of 3-bytes chunks + // - `4 *` -> 4 characters for each chunk + //string memory result = new string(4 * ((data.length + 2) / 3)); + string memory result = new string(4 * ((data.length + 2) / 3) - 1); + + /// @solidity memory-safe-assembly + assembly { + // Prepare the lookup table (skip the first "length" byte) + let tablePtr := add(table, 1) + + // Prepare result pointer, jump over length + let resultPtr := add(result, 32) + + // Run over the input, 3 bytes at a time + for { + let dataPtr := data + let endPtr := add(data, mload(data)) + } lt(dataPtr, endPtr) { + + } { + // Advance 3 bytes + dataPtr := add(dataPtr, 3) + let input := mload(dataPtr) + + // To write each character, shift the 3 bytes (18 bits) chunk + // 4 times in blocks of 6 bits for each character (18, 12, 6, 0) + // and apply logical AND with 0x3F which is the number of + // the previous character in the ASCII table prior to the Base64 Table + // The result is then added to the table to get the character to write, + // and finally write it in the result pointer but with a left shift + // of 256 (1 byte) - 8 (1 ASCII char) = 248 bits + + mstore8( + resultPtr, + mload(add(tablePtr, and(shr(18, input), 0x3F))) + ) + resultPtr := add(resultPtr, 1) // Advance + + mstore8( + resultPtr, + mload(add(tablePtr, and(shr(12, input), 0x3F))) + ) + resultPtr := add(resultPtr, 1) // Advance + + mstore8( + resultPtr, + mload(add(tablePtr, and(shr(6, input), 0x3F))) + ) + resultPtr := add(resultPtr, 1) // Advance + + mstore8(resultPtr, mload(add(tablePtr, and(input, 0x3F)))) + resultPtr := add(resultPtr, 1) // Advance + } + + /* + // When data `bytes` is not exactly 3 bytes long + // it is padded with `=` characters at the end + switch mod(mload(data), 3) + case 1 { + mstore8(sub(resultPtr, 1), 0x3d) + mstore8(sub(resultPtr, 2), 0x3d) + } + case 2 { + mstore8(sub(resultPtr, 1), 0x3d) + } +*/ + } + + return result; + } +} diff --git a/account-integrations/safe/src/lib/FCL_Webauthn.sol b/account-integrations/safe/src/lib/FCL_Webauthn.sol index c4bfe9d6..7a12fa76 100644 --- a/account-integrations/safe/src/lib/FCL_Webauthn.sol +++ b/account-integrations/safe/src/lib/FCL_Webauthn.sol @@ -20,7 +20,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.12; -import {Base64} from "openzeppelin-contracts/contracts/utils/Base64.sol"; +import {Base64URL} from "./Base64URL.sol"; import {FCL_Elliptic_ZZ} from "./FCL_elliptic.sol"; library FCL_WebAuthn { @@ -28,14 +28,13 @@ library FCL_WebAuthn { error InvalidClientData(); error InvalidSignature(); - /** @notice Modified from original Fresh Crypto Lib code to use memory instead of calldata */ function WebAuthn_format( - bytes memory authenticatorData, + bytes calldata authenticatorData, bytes1 authenticatorDataFlagMask, - bytes memory clientData, + bytes calldata clientData, bytes32 clientChallenge, uint256 clientChallengeDataOffset, - uint256[2] memory // rs + uint256[2] calldata // rs ) internal pure returns (bytes32 result) { // Let the caller check if User Presence (0x01) or User Verification (0x04) are set { @@ -43,28 +42,27 @@ library FCL_WebAuthn { revert InvalidAuthenticatorData(); } // Verify that clientData commits to the expected client challenge - string memory challengeEncoded = Base64.encode(abi.encodePacked(clientChallenge)); + string memory challengeEncoded = Base64URL.encode32(abi.encodePacked(clientChallenge)); bytes memory challengeExtracted = new bytes( bytes(challengeEncoded).length ); - // TODO: wax#44 Extract webauthn challenge from clientData and compare to clientChallenge - // assembly { - // calldatacopy( - // add(challengeExtracted, 32), - // add(clientData.offset, clientChallengeDataOffset), - // mload(challengeExtracted) - // ) - // } - - // bytes32 moreData; //=keccak256(abi.encodePacked(challengeExtracted)); - // assembly { - // moreData := keccak256(add(challengeExtracted, 32), mload(challengeExtracted)) - // } - - // if (keccak256(abi.encodePacked(bytes(challengeEncoded))) != moreData) { - // revert InvalidClientData(); - // } + assembly { + calldatacopy( + add(challengeExtracted, 32), + add(clientData.offset, clientChallengeDataOffset), + mload(challengeExtracted) + ) + } + + bytes32 moreData; //=keccak256(abi.encodePacked(challengeExtracted)); + assembly { + moreData := keccak256(add(challengeExtracted, 32), mload(challengeExtracted)) + } + + if (keccak256(abi.encodePacked(bytes(challengeEncoded))) != moreData) { + revert InvalidClientData(); + } } //avoid stack full // Verify the signature over sha256(authenticatorData || sha256(clientData)) @@ -74,7 +72,6 @@ library FCL_WebAuthn { return sha256(verifyData); } - /** @notice Modified from original Fresh Crypto Lib code to use memory instead of calldata */ function checkSignature( bytes calldata authenticatorData, bytes1 authenticatorDataFlagMask, @@ -135,4 +132,4 @@ library FCL_WebAuthn { return result; } -} +} \ No newline at end of file diff --git a/account-integrations/safe/test/forge/SafeWebAuthnPlugin.t.sol b/account-integrations/safe/test/forge/SafeWebAuthnPlugin.t.sol index 5cd2eb72..b66aa95e 100644 --- a/account-integrations/safe/test/forge/SafeWebAuthnPlugin.t.sol +++ b/account-integrations/safe/test/forge/SafeWebAuthnPlugin.t.sol @@ -52,7 +52,7 @@ contract SafeWebAuthnPluginTest is TestHelper { bytes memory authenticatorData, bytes1 authenticatorDataFlagMask, bytes memory clientData, - , + bytes32 clientChallenge, uint256 clientChallengeDataOffset, ) = getWebAuthnSignatureValues(); @@ -63,6 +63,7 @@ contract SafeWebAuthnPluginTest is TestHelper { authenticatorData, authenticatorDataFlagMask, clientData, + clientChallenge, clientChallengeDataOffset, invalidSignature, publicKey diff --git a/account-integrations/safe/test/forge/WebAuthn.t.sol b/account-integrations/safe/test/forge/WebAuthn.t.sol index 3b3acbbc..28040b11 100644 --- a/account-integrations/safe/test/forge/WebAuthn.t.sol +++ b/account-integrations/safe/test/forge/WebAuthn.t.sol @@ -22,7 +22,7 @@ contract WebauthnTest is TestHelper { bytes memory authenticatorData, bytes1 authenticatorDataFlagMask, bytes memory clientData, - bytes32 messageHash, + bytes32 clientChallenge, uint256 clientChallengeDataOffset, uint256[2] memory signature ) = getWebAuthnSignatureValues(); @@ -33,7 +33,7 @@ contract WebauthnTest is TestHelper { authenticatorData, authenticatorDataFlagMask, clientData, - messageHash, + clientChallenge, clientChallengeDataOffset, signature, publicKey diff --git a/account-integrations/safe/test/forge/utils/TestHelper.sol b/account-integrations/safe/test/forge/utils/TestHelper.sol index b77feaa0..f37d0072 100644 --- a/account-integrations/safe/test/forge/utils/TestHelper.sol +++ b/account-integrations/safe/test/forge/utils/TestHelper.sol @@ -67,8 +67,8 @@ abstract contract TestHelper is Test { returns (uint256[2] memory publicKey) { publicKey = [ - 84983235508986227069118519878575107221347312419117152995971180204793547896817, - 65342283463016373417889909198331793507107976344099657471582098851386861908802 + 114874632398302156264159990279427641021947882640101801130664833947273521181002, + 32136952818958550240756825111900051564117520891182470183735244184006536587423 ]; } @@ -79,19 +79,19 @@ abstract contract TestHelper is Test { bytes memory authenticatorData, bytes1 authenticatorDataFlagMask, bytes memory clientData, - bytes32 messageHash, + bytes32 clientChallenge, uint256 clientChallengeDataOffset, uint256[2] memory signature ) { - authenticatorData = hex"1584482fdf7a4d0b7eb9d45cf835288cb59e55b8249fff356e33be88ecc546d11d00000000"; + authenticatorData = hex"f8e4b678e1c62f7355266eaa4dc1148573440937063a46d848da1e25babbd20b010000004d"; authenticatorDataFlagMask = 0x01; - clientData = hex"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a22efbfbd22efbfbd5f21efbfbd1b113e63efbfbdefbfbd6defbfbd4fefbfbdefbfbd11efbfbd11efbfbd40efbfbdefbfbdefbfbd64efbfbdefbfbd3cefbfbd58222c226f726967696e223a2268747470733a2f2f646576656c6f706d656e742e666f72756d64616f732e636f6d227d"; - messageHash = keccak256("test"); + clientData = hex"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a224e546f2d3161424547526e78786a6d6b61544865687972444e5833697a6c7169316f776d4f643955474a30222c226f726967696e223a2268747470733a2f2f66726573682e6c65646765722e636f6d222c2263726f73734f726967696e223a66616c73657d"; + clientChallenge = hex"353a3ed5a0441919f1c639a46931de872ac3357de2ce5aa2d68c2639df54189d"; clientChallengeDataOffset = 36; signature = [ - 36788204816852931931532076736929768488646494203674172515272861180041446565109, - 60595451626159535380360537025565143491223093262105891867977188941268073626113 + 45847212378479006099766816358861726414873720355505495069909394794949093093607, + 55835259151215769394881684156457977412783812617123006733908193526332337539398 ]; } @@ -104,7 +104,7 @@ abstract contract TestHelper is Test { bytes memory authenticatorData, bytes1 authenticatorDataFlagMask, bytes memory clientData, - , + bytes32 clientChallenge, uint256 clientChallengeDataOffset, uint256[2] memory signature ) = getWebAuthnSignatureValues(); @@ -114,6 +114,7 @@ abstract contract TestHelper is Test { authenticatorData, authenticatorDataFlagMask, clientData, + clientChallenge, clientChallengeDataOffset, signature, publicKey diff --git a/account-integrations/safe/test/hardhat/integration/SafeWebAuthnPlugin.test.ts b/account-integrations/safe/test/hardhat/integration/SafeWebAuthnPlugin.test.ts index a4c4044f..e82001f5 100644 --- a/account-integrations/safe/test/hardhat/integration/SafeWebAuthnPlugin.test.ts +++ b/account-integrations/safe/test/hardhat/integration/SafeWebAuthnPlugin.test.ts @@ -62,35 +62,46 @@ describe("SafeWebAuthnPlugin", () => { const getPublicKeyAndSignature = () => { const publicKey: [BigNumberish, BigNumberish] = [ BigInt( - "84983235508986227069118519878575107221347312419117152995971180204793547896817" + "114874632398302156264159990279427641021947882640101801130664833947273521181002" ), BigInt( - "65342283463016373417889909198331793507107976344099657471582098851386861908802" + "32136952818958550240756825111900051564117520891182470183735244184006536587423" ), ]; const authenticatorData = - "0x1584482fdf7a4d0b7eb9d45cf835288cb59e55b8249fff356e33be88ecc546d11d00000000"; + "0xf8e4b678e1c62f7355266eaa4dc1148573440937063a46d848da1e25babbd20b010000004d"; const authenticatorDataFlagMask = "0x01"; const clientData = - "0x7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a22efbfbd22efbfbd5f21efbfbd1b113e63efbfbdefbfbd6defbfbd4fefbfbdefbfbd11efbfbd11efbfbd40efbfbdefbfbdefbfbd64efbfbdefbfbd3cefbfbd58222c226f726967696e223a2268747470733a2f2f646576656c6f706d656e742e666f72756d64616f732e636f6d227d"; + "0x7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a224e546f2d3161424547526e78786a6d6b61544865687972444e5833697a6c7169316f776d4f643955474a30222c226f726967696e223a2268747470733a2f2f66726573682e6c65646765722e636f6d222c2263726f73734f726967696e223a66616c73657d"; + const clientChallenge = + "0x353a3ed5a0441919f1c639a46931de872ac3357de2ce5aa2d68c2639df54189d"; const clientChallengeDataOffset = 36; const signature: [BigNumberish, BigNumberish] = [ BigInt( - "36788204816852931931532076736929768488646494203674172515272861180041446565109" + "45847212378479006099766816358861726414873720355505495069909394794949093093607" ), BigInt( - "60595451626159535380360537025565143491223093262105891867977188941268073626113" + "55835259151215769394881684156457977412783812617123006733908193526332337539398" ), ]; const encoder = new ethers.AbiCoder(); const userOpSignature = encoder.encode( - ["bytes", "bytes1", "bytes", "uint256", "uint256[2]", "uint256[2]"], + [ + "bytes", + "bytes1", + "bytes", + "bytes32", + "uint256", + "uint256[2]", + "uint256[2]", + ], [ authenticatorData, authenticatorDataFlagMask, clientData, + clientChallenge, clientChallengeDataOffset, signature, publicKey,