From 118428f9cabef6e89aae8f302da59028eb1cd30d Mon Sep 17 00:00:00 2001 From: Akshay Date: Mon, 11 Mar 2024 15:46:22 +0100 Subject: [PATCH 01/11] [#289] Use assembly in WebAuthnVerifier for Base64 encoding --- .../verifiers/WebAuthnVerifier.sol | 60 ++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/modules/4337/contracts/experimental/verifiers/WebAuthnVerifier.sol b/modules/4337/contracts/experimental/verifiers/WebAuthnVerifier.sol index 989158da6..8c2f66e9c 100644 --- a/modules/4337/contracts/experimental/verifiers/WebAuthnVerifier.sol +++ b/modules/4337/contracts/experimental/verifiers/WebAuthnVerifier.sol @@ -3,7 +3,6 @@ pragma solidity >=0.8.0; import {P256Wrapper} from "./P256Wrapper.sol"; -import {Base64Url} from "../../vendor/FCL/utils/Base64Url.sol"; /** * @title WebAuthnConstants @@ -90,6 +89,9 @@ interface IWebAuthnVerifier { contract WebAuthnVerifier is IWebAuthnVerifier, P256Wrapper { constructor(address verifier) P256Wrapper(verifier) {} + string internal constant ENCODING_TABLE = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + /** * @dev Generates a signing message based on the authenticator data, challenge, and client data fields. * @param authenticatorData Authenticator data. @@ -102,7 +104,61 @@ contract WebAuthnVerifier is IWebAuthnVerifier, P256Wrapper { bytes32 challenge, bytes calldata clientDataFields ) internal pure returns (bytes32 message) { - string memory encodedChallenge = Base64Url.encode(abi.encodePacked(challenge)); + + // Result is 44 bytes long because the encoded challenge is 32 bytes long + // 4*(32 + 2)/3 = 44 after rounding. + string memory table = ENCODING_TABLE; + string memory encodedChallenge = new string(44); + + assembly { + // Skip first 32 bytes of the table containing the length + let tablePtr := add(table, 1) + // Skip first 32 bytes of the encodedChallenge containing the length + let resultPtr := add(encodedChallenge, 32) + + // Temporarily stores 3 bytes of challenge + let buffer + for{let i:= 0} lt(i, 10) + { + i := add(i, 1) + }{ + // Calculate the shift value to get the 3 bytes of challenge + let shift := sub(256, mul(add(i, 1), 24)) + buffer := and(shr(shift, challenge), 0xFFFFFF) + + mstore8(resultPtr, mload(add(tablePtr, and(shr(18, buffer), 0x3F)))) + resultPtr := add(resultPtr, 1) + + mstore8(resultPtr, mload(add(tablePtr, and(shr(12, buffer), 0x3F)))) + resultPtr := add(resultPtr, 1) + + mstore8(resultPtr, mload(add(tablePtr, and(shr(6, buffer), 0x3F)))) + resultPtr := add(resultPtr, 1) + + mstore8(resultPtr, mload(add(tablePtr, and(buffer, 0x3F)))) + resultPtr := add(resultPtr, 1) + } + + // As 32 bytes input is not divisible by 3, process last 2 bytes of challenge separately + buffer := shl(8, and(challenge, 0xFFFF)) + + mstore8(resultPtr, mload(add(tablePtr, and(shr(18, buffer), 0x3F)))) + resultPtr := add(resultPtr, 1) + + mstore8(resultPtr, mload(add(tablePtr, and(shr(12, buffer), 0x3F)))) + resultPtr := add(resultPtr, 1) + + mstore8(resultPtr, mload(add(tablePtr, and(shr(6, buffer), 0x3F)))) + resultPtr := add(resultPtr, 1) + + mstore8(resultPtr, mload(add(tablePtr, and(buffer, 0x3F)))) + resultPtr := add(resultPtr, 1) + + // Because the input is fixed 32 bytes long + resultPtr := sub(resultPtr, 1) + mstore(encodedChallenge, sub(resultPtr, add(encodedChallenge, 32))) + } + /* solhint-disable quotes */ bytes memory clientDataJson = abi.encodePacked( '{"type":"webauthn.get","challenge":"', From f4c2e88d1d7c757b63ad85cc89b93f1e70a0a6f5 Mon Sep 17 00:00:00 2001 From: Akshay Date: Tue, 12 Mar 2024 18:54:23 +0000 Subject: [PATCH 02/11] [#289] Store Base64 encoded value without allocating string variable --- .../verifiers/WebAuthnVerifier.sol | 80 +++++++------------ 1 file changed, 28 insertions(+), 52 deletions(-) diff --git a/modules/4337/contracts/experimental/verifiers/WebAuthnVerifier.sol b/modules/4337/contracts/experimental/verifiers/WebAuthnVerifier.sol index 8c2f66e9c..defb50a3e 100644 --- a/modules/4337/contracts/experimental/verifiers/WebAuthnVerifier.sol +++ b/modules/4337/contracts/experimental/verifiers/WebAuthnVerifier.sol @@ -105,70 +105,46 @@ contract WebAuthnVerifier is IWebAuthnVerifier, P256Wrapper { bytes calldata clientDataFields ) internal pure returns (bytes32 message) { - // Result is 44 bytes long because the encoded challenge is 32 bytes long - // 4*(32 + 2)/3 = 44 after rounding. - string memory table = ENCODING_TABLE; - string memory encodedChallenge = new string(44); + /* solhint-disable quotes */ + // AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA is placeholder for the encoded challenge + bytes memory clientDataJson = abi.encodePacked( + '{"type":"webauthn.get","challenge":"', + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + '",', + clientDataFields, + "}" + ); + string memory table = ENCODING_TABLE; + + // solhint-disable-next-line no-inline-assembly assembly { // Skip first 32 bytes of the table containing the length let tablePtr := add(table, 1) - // Skip first 32 bytes of the encodedChallenge containing the length - let resultPtr := add(encodedChallenge, 32) + // Skip first 36 bytes of the clientDataJson containing '{"type":"webauthn.get","challenge":"' + let resultPtr := add(clientDataJson, 68) - // Temporarily stores 3 bytes of challenge - let buffer - for{let i:= 0} lt(i, 10) - { - i := add(i, 1) - }{ - // Calculate the shift value to get the 3 bytes of challenge - let shift := sub(256, mul(add(i, 1), 24)) - buffer := and(shr(shift, challenge), 0xFFFFFF) - - mstore8(resultPtr, mload(add(tablePtr, and(shr(18, buffer), 0x3F)))) - resultPtr := add(resultPtr, 1) - - mstore8(resultPtr, mload(add(tablePtr, and(shr(12, buffer), 0x3F)))) - resultPtr := add(resultPtr, 1) - - mstore8(resultPtr, mload(add(tablePtr, and(shr(6, buffer), 0x3F)))) - resultPtr := add(resultPtr, 1) + // Store group of 6 bits from the challenge to be encoded. Storing of 6 bits group can be removed but, kept here for readability. + let sixBitGroup - mstore8(resultPtr, mload(add(tablePtr, and(buffer, 0x3F)))) - resultPtr := add(resultPtr, 1) - } - - // As 32 bytes input is not divisible by 3, process last 2 bytes of challenge separately - buffer := shl(8, and(challenge, 0xFFFF)) - - mstore8(resultPtr, mload(add(tablePtr, and(shr(18, buffer), 0x3F)))) - resultPtr := add(resultPtr, 1) - - mstore8(resultPtr, mload(add(tablePtr, and(shr(12, buffer), 0x3F)))) - resultPtr := add(resultPtr, 1) - - mstore8(resultPtr, mload(add(tablePtr, and(shr(6, buffer), 0x3F)))) - resultPtr := add(resultPtr, 1) - - mstore8(resultPtr, mload(add(tablePtr, and(buffer, 0x3F)))) + // Iterate over challenge in group of 6 bits, for each 6 bits lookup the ENCODING_TABLE, transform it and store it in the result + for {let i := 0} lt(i, 252) + { + i := add(i, 6) + } { + sixBitGroup := and(shr(sub(250, i), challenge), 0x3F) + mstore8(resultPtr, mload(add(tablePtr, sixBitGroup))) resultPtr := add(resultPtr, 1) + } - // Because the input is fixed 32 bytes long - resultPtr := sub(resultPtr, 1) - mstore(encodedChallenge, sub(resultPtr, add(encodedChallenge, 32))) + // Load the remaining last 4 bits of challenge that are yet to be encoded and then shift left to add 2 bits at the end to make it a group of 6 bits. + sixBitGroup := shl(2, and(challenge, 0x0F)) + mstore8(resultPtr, mload(add(tablePtr, sixBitGroup))) } - /* solhint-disable quotes */ - bytes memory clientDataJson = abi.encodePacked( - '{"type":"webauthn.get","challenge":"', - encodedChallenge, - '",', - clientDataFields, - "}" - ); /* solhint-enable quotes */ message = sha256(abi.encodePacked(authenticatorData, sha256(clientDataJson))); + } /** From 74ded1ff23de24b4ce23ae5af435fbda610075cb Mon Sep 17 00:00:00 2001 From: Akshay Date: Wed, 13 Mar 2024 10:25:39 +0000 Subject: [PATCH 03/11] [#289] Update WebAuthnVerifier in passkey package --- .../contracts/verifiers/WebAuthnVerifier.sol | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/modules/passkey/contracts/verifiers/WebAuthnVerifier.sol b/modules/passkey/contracts/verifiers/WebAuthnVerifier.sol index 1b3c7a512..b62509a06 100644 --- a/modules/passkey/contracts/verifiers/WebAuthnVerifier.sol +++ b/modules/passkey/contracts/verifiers/WebAuthnVerifier.sol @@ -3,7 +3,6 @@ pragma solidity >=0.8.0; import {IP256Verifier, P256VerifierLib} from "./IP256Verifier.sol"; -import {Base64Url} from "../vendor/FCL/utils/Base64Url.sol"; /** * @title WebAuthnConstants @@ -90,6 +89,8 @@ interface IWebAuthnVerifier { contract WebAuthnVerifier is IWebAuthnVerifier { IP256Verifier internal immutable P256_VERIFIER; + string internal constant ENCODING_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + constructor(IP256Verifier verifier) { P256_VERIFIER = verifier; } @@ -106,15 +107,44 @@ contract WebAuthnVerifier is IWebAuthnVerifier { bytes32 challenge, bytes calldata clientDataFields ) internal pure returns (bytes32 message) { - string memory encodedChallenge = Base64Url.encode(abi.encodePacked(challenge)); /* solhint-disable quotes */ + // AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA is placeholder for the encoded challenge bytes memory clientDataJson = abi.encodePacked( '{"type":"webauthn.get","challenge":"', - encodedChallenge, + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", '",', clientDataFields, "}" ); + + string memory table = ENCODING_TABLE; + + // solhint-disable-next-line no-inline-assembly + assembly { + // Skip first 32 bytes of the table containing the length + let tablePtr := add(table, 1) + // Skip first 36 bytes of the clientDataJson containing '{"type":"webauthn.get","challenge":"' + let resultPtr := add(clientDataJson, 68) + + // Store group of 6 bits from the challenge to be encoded. Storing of 6 bits group can be removed but, kept here for readability. + let sixBitGroup + + // Iterate over challenge in group of 6 bits, for each 6 bits lookup the ENCODING_TABLE, transform it and store it in the result + for { + let i := 0 + } lt(i, 252) { + i := add(i, 6) + } { + sixBitGroup := and(shr(sub(250, i), challenge), 0x3F) + mstore8(resultPtr, mload(add(tablePtr, sixBitGroup))) + resultPtr := add(resultPtr, 1) + } + + // Load the remaining last 4 bits of challenge that are yet to be encoded and then shift left to add 2 bits at the end to make it a group of 6 bits. + sixBitGroup := shl(2, and(challenge, 0x0F)) + mstore8(resultPtr, mload(add(tablePtr, sixBitGroup))) + } + /* solhint-enable quotes */ message = sha256(abi.encodePacked(authenticatorData, sha256(clientDataJson))); } From e3faff8d284568afbbd5083ce36a050e6c89ef8d Mon Sep 17 00:00:00 2001 From: Akshay Date: Wed, 13 Mar 2024 16:21:16 +0100 Subject: [PATCH 04/11] Update modules/4337/contracts/experimental/verifiers/WebAuthnVerifier.sol --- .../contracts/experimental/verifiers/WebAuthnVerifier.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/4337/contracts/experimental/verifiers/WebAuthnVerifier.sol b/modules/4337/contracts/experimental/verifiers/WebAuthnVerifier.sol index defb50a3e..79610234d 100644 --- a/modules/4337/contracts/experimental/verifiers/WebAuthnVerifier.sol +++ b/modules/4337/contracts/experimental/verifiers/WebAuthnVerifier.sol @@ -128,11 +128,11 @@ contract WebAuthnVerifier is IWebAuthnVerifier, P256Wrapper { let sixBitGroup // Iterate over challenge in group of 6 bits, for each 6 bits lookup the ENCODING_TABLE, transform it and store it in the result - for {let i := 0} lt(i, 252) + for {let i := 250} lt(i, 251) { - i := add(i, 6) + i := sub(i, 6) } { - sixBitGroup := and(shr(sub(250, i), challenge), 0x3F) + sixBitGroup := and(shr(i, challenge), 0x3F) mstore8(resultPtr, mload(add(tablePtr, sixBitGroup))) resultPtr := add(resultPtr, 1) } From 70999e198ffb0343665a0f25b93f089abdf2a791 Mon Sep 17 00:00:00 2001 From: Akshay Date: Wed, 13 Mar 2024 16:21:21 +0100 Subject: [PATCH 05/11] Update modules/passkey/contracts/verifiers/WebAuthnVerifier.sol --- modules/passkey/contracts/verifiers/WebAuthnVerifier.sol | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/modules/passkey/contracts/verifiers/WebAuthnVerifier.sol b/modules/passkey/contracts/verifiers/WebAuthnVerifier.sol index b62509a06..f5b94f8d1 100644 --- a/modules/passkey/contracts/verifiers/WebAuthnVerifier.sol +++ b/modules/passkey/contracts/verifiers/WebAuthnVerifier.sol @@ -130,12 +130,11 @@ contract WebAuthnVerifier is IWebAuthnVerifier { let sixBitGroup // Iterate over challenge in group of 6 bits, for each 6 bits lookup the ENCODING_TABLE, transform it and store it in the result - for { - let i := 0 - } lt(i, 252) { - i := add(i, 6) + for {let i := 250} lt(i, 251) + { + i := sub(i, 6) } { - sixBitGroup := and(shr(sub(250, i), challenge), 0x3F) + sixBitGroup := and(shr(i, challenge), 0x3F) mstore8(resultPtr, mload(add(tablePtr, sixBitGroup))) resultPtr := add(resultPtr, 1) } From 5ab7fac5fc6890864a57dbe3fc6f082b75c77b13 Mon Sep 17 00:00:00 2001 From: Akshay Date: Wed, 13 Mar 2024 17:10:14 +0100 Subject: [PATCH 06/11] Fix lint issue --- modules/passkey/contracts/verifiers/WebAuthnVerifier.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/passkey/contracts/verifiers/WebAuthnVerifier.sol b/modules/passkey/contracts/verifiers/WebAuthnVerifier.sol index f5b94f8d1..1b6c2c36f 100644 --- a/modules/passkey/contracts/verifiers/WebAuthnVerifier.sol +++ b/modules/passkey/contracts/verifiers/WebAuthnVerifier.sol @@ -130,8 +130,9 @@ contract WebAuthnVerifier is IWebAuthnVerifier { let sixBitGroup // Iterate over challenge in group of 6 bits, for each 6 bits lookup the ENCODING_TABLE, transform it and store it in the result - for {let i := 250} lt(i, 251) - { + for { + let i := 250 + } lt(i, 251) { i := sub(i, 6) } { sixBitGroup := and(shr(i, challenge), 0x3F) From 43882d025411b8a1021ca20e0c7e13d8e066d788 Mon Sep 17 00:00:00 2001 From: Mikhail Mikheev <16622558+mmv08@users.noreply.github.com> Date: Mon, 25 Mar 2024 11:58:08 +0100 Subject: [PATCH 07/11] Remove vendored library --- .../passkey/contracts/libraries/WebAuthn.sol | 1 - .../contracts/vendor/FCL/utils/Base64Url.sol | 76 ------------------- 2 files changed, 77 deletions(-) delete mode 100644 modules/passkey/contracts/vendor/FCL/utils/Base64Url.sol diff --git a/modules/passkey/contracts/libraries/WebAuthn.sol b/modules/passkey/contracts/libraries/WebAuthn.sol index 3211bc1a2..7dd2807f2 100644 --- a/modules/passkey/contracts/libraries/WebAuthn.sol +++ b/modules/passkey/contracts/libraries/WebAuthn.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.0; -import {Base64Url} from "../vendor/FCL/utils/Base64Url.sol"; import {IP256Verifier, P256} from "./P256.sol"; /** diff --git a/modules/passkey/contracts/vendor/FCL/utils/Base64Url.sol b/modules/passkey/contracts/vendor/FCL/utils/Base64Url.sol deleted file mode 100644 index 0d9ac2d0a..000000000 --- a/modules/passkey/contracts/vendor/FCL/utils/Base64Url.sol +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -/** - * @dev Encode (without '=' padding) - * @author evmbrahmin, adapted from hiromin's Base64URL libraries - */ -library Base64Url { - /** - * @dev Base64Url Encoding Table - */ - string internal constant ENCODING_TABLE = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; - - function encode(bytes memory data) internal pure returns (string memory) { - if (data.length == 0) return ""; - - // Load the table into memory - string memory table = ENCODING_TABLE; - - string memory result = new string(4 * ((data.length + 2) / 3)); - - // @solidity memory-safe-assembly - assembly { - let tablePtr := add(table, 1) - let resultPtr := add(result, 32) - - for { - let dataPtr := data - let endPtr := add(data, mload(data)) - } lt(dataPtr, endPtr) { - - } { - dataPtr := add(dataPtr, 3) - let input := mload(dataPtr) - - mstore8( - resultPtr, - mload(add(tablePtr, and(shr(18, input), 0x3F))) - ) - resultPtr := add(resultPtr, 1) - - mstore8( - resultPtr, - mload(add(tablePtr, and(shr(12, input), 0x3F))) - ) - resultPtr := add(resultPtr, 1) - - mstore8( - resultPtr, - mload(add(tablePtr, and(shr(6, input), 0x3F))) - ) - resultPtr := add(resultPtr, 1) - - mstore8(resultPtr, mload(add(tablePtr, and(input, 0x3F)))) - resultPtr := add(resultPtr, 1) - } - - // Remove the padding adjustment logic - switch mod(mload(data), 3) - case 1 { - // Adjust for the last byte of data - resultPtr := sub(resultPtr, 2) - } - case 2 { - // Adjust for the last two bytes of data - resultPtr := sub(resultPtr, 1) - } - - // Set the correct length of the result string - mstore(result, sub(resultPtr, add(result, 32))) - } - - return result; - } -} From a8928b704b9552b2043b6964b21af0ad0e2c0499 Mon Sep 17 00:00:00 2001 From: Mikhail Mikheev <16622558+mmv08@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:41:55 +0100 Subject: [PATCH 08/11] Add optimized Base64URL implementation for bytes32 --- .../passkey/contracts/libraries/Base64Url.sol | 91 +++++++++++++++++++ .../passkey/contracts/libraries/WebAuthn.sol | 38 +------- .../contracts/test/TestBase64UrlLib.sol | 10 ++ .../passkey/test/libraries/Base64Url.spec.ts | 30 ++++++ package-lock.json | 4 +- 5 files changed, 139 insertions(+), 34 deletions(-) create mode 100644 modules/passkey/contracts/libraries/Base64Url.sol create mode 100644 modules/passkey/contracts/test/TestBase64UrlLib.sol create mode 100644 modules/passkey/test/libraries/Base64Url.spec.ts diff --git a/modules/passkey/contracts/libraries/Base64Url.sol b/modules/passkey/contracts/libraries/Base64Url.sol new file mode 100644 index 000000000..7a18025c0 --- /dev/null +++ b/modules/passkey/contracts/libraries/Base64Url.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/** + * @title Base64Url Encoding Library + * @dev Provides a function to encode `bytes32` data into a Base64 URL string representation without '=' padding. + * @notice This library is adapted from solady's Base64 library and optimized for bytes32 encoding, which is useful for + * WebAuthn-based cryptographic operations. + * @author Modified from Solady (https://github.com/Vectorized/solady/blob/e4a14a5b365b353352f7c38e699a2bc9363d6576/src/utils/Base64.sol) + */ +library Base64Url { + /** + * @dev Encodes `bytes32` data into a Base64 URL string without '=' padding. + * @param data The `bytes32` input data to be encoded. + * @return result The encoded string in Base64 URL format. + */ + function encode(bytes32 data) internal pure returns (string memory result) { + // solhint-disable-next-line no-inline-assembly + assembly ("memory-safe") { + // The length of the encoded string (43 characters for bytes32 input). + // (32 bytes * 8 bits) / 6 bits base64 groups = 43 characters rounded. + let encodedLength := 43 + + // Set `result` to point to the start of the free memory. + // This is where the encoded string will be stored. + result := mload(0x40) + + // Store the Base64 URL character table into the scratch space. + // The table is split into two parts and stored at memory locations 0x1f and 0x3f. + // Offsetted by -1 byte so that the `mload` will load the correct character. + // We will rewrite the free memory pointer at `0x40` later with the allocated size. + mstore(0x1f, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef") + mstore(0x3f, "ghijklmnopqrstuvwxyz0123456789-_") + + // Initialize pointers for writing the encoded data. + // `ptr` points to the start of the encoded string (skipping the first slot that stores the length). + // `end` points to the end of the encoded string. + let ptr := add(result, 0x20) + let end := add(ptr, encodedLength) + + // To minimize stack operations, we unroll the loop. + // With full 6 iterations of the loop, we need to encode seven 6-bit groups and one 4-bit group. + // In total, it encodes 6 iterations * 7 groups * 6 bits = 252 bits. + // The remaining 4-bit group is encoded after the loop. + // `i` is initialized to 250, which is the number of bits by which we need to shift the data + // to get the first 6-bit group, and then we subtract 6 to get the next 6-bit group. + let i := 250 + for { + + } 1 { + + } { + // Encode 6-bit groups into characters by looking them up in the character table. + // The encoded characters are written to the scratch space at memory locations 0 to 6. + // 0x3F is a mask to get the last 6 bits. + mstore8(0, mload(and(shr(i, data), 0x3F))) + mstore8(1, mload(and(shr(sub(i, 6), data), 0x3F))) + mstore8(2, mload(and(shr(sub(i, 12), data), 0x3F))) + mstore8(3, mload(and(shr(sub(i, 18), data), 0x3F))) + mstore8(4, mload(and(shr(sub(i, 24), data), 0x3F))) + mstore8(5, mload(and(shr(sub(i, 30), data), 0x3F))) + mstore8(6, mload(and(shr(sub(i, 36), data), 0x3F))) + + // Write the encoded characters to the result string. + // The characters are loaded from memory locations 0 to 6 and stored at `ptr`. + mstore(ptr, mload(0x00)) + // Advance the pointer by the number of bytes written (7 bytes in this case). + ptr := add(ptr, 0x7) + // Move the data pointer to the next 6-bit group. + // 42 = 6 bits * 7 (number of groups processed in each iteration). + i := sub(i, 42) + + // Break the loop when the end of the encoded string is reached. + if iszero(sgt(i, 0)) { + break + } + } + + // Encode the final 4-bit group. + // 0x0F is a mask to get the last 4 bits. + // The encoded character is stored at memory location 74 (result + 74): + // <32byte string length><43byte encoded string>. 74 is the penultimate byte. + mstore8(add(result, 74), mload(add(0, shl(2, and(data, 0x0F))))) + + // Update the free memory pointer to point to the end of the encoded string. + // Store the length of the encoded string at the beginning of `result`. + mstore(0x40, end) + mstore(result, encodedLength) + } + } +} diff --git a/modules/passkey/contracts/libraries/WebAuthn.sol b/modules/passkey/contracts/libraries/WebAuthn.sol index 7dd2807f2..17af88c34 100644 --- a/modules/passkey/contracts/libraries/WebAuthn.sol +++ b/modules/passkey/contracts/libraries/WebAuthn.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.0; +import {Base64Url} from "../libraries/Base64Url.sol"; import {IP256Verifier, P256} from "./P256.sol"; /** @@ -12,8 +13,6 @@ import {IP256Verifier, P256} from "./P256.sol"; library WebAuthn { using P256 for IP256Verifier; - string internal constant ENCODING_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; - /** * @notice The WebAuthn signature data format. * @dev WebAuthn signatures are expected to be the ABI-encoded bytes of the following structure. @@ -103,45 +102,18 @@ library WebAuthn { bytes calldata authenticatorData, string calldata clientDataFields ) internal pure returns (bytes32 message) { + string memory encodedChallenge = Base64Url.encode(challenge); + /* solhint-disable quotes */ - // AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA is placeholder for the encoded challenge bytes memory clientDataJson = abi.encodePacked( '{"type":"webauthn.get","challenge":"', - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + encodedChallenge, '",', clientDataFields, "}" ); - - string memory table = ENCODING_TABLE; - - // solhint-disable-next-line no-inline-assembly - assembly { - // Skip first 32 bytes of the table containing the length - let tablePtr := add(table, 1) - // Skip first 36 bytes of the clientDataJson containing '{"type":"webauthn.get","challenge":"' - let resultPtr := add(clientDataJson, 68) - - // Store group of 6 bits from the challenge to be encoded. Storing of 6 bits group can be removed but, kept here for readability. - let sixBitGroup - - // Iterate over challenge in group of 6 bits, for each 6 bits lookup the ENCODING_TABLE, transform it and store it in the result - for { - let i := 250 - } lt(i, 251) { - i := sub(i, 6) - } { - sixBitGroup := and(shr(i, challenge), 0x3F) - mstore8(resultPtr, mload(add(tablePtr, sixBitGroup))) - resultPtr := add(resultPtr, 1) - } - - // Load the remaining last 4 bits of challenge that are yet to be encoded and then shift left to add 2 bits at the end to make it a group of 6 bits. - sixBitGroup := shl(2, and(challenge, 0x0F)) - mstore8(resultPtr, mload(add(tablePtr, sixBitGroup))) - } - /* solhint-enable quotes */ + message = sha256(abi.encodePacked(authenticatorData, sha256(clientDataJson))); } diff --git a/modules/passkey/contracts/test/TestBase64UrlLib.sol b/modules/passkey/contracts/test/TestBase64UrlLib.sol new file mode 100644 index 000000000..f71d13d47 --- /dev/null +++ b/modules/passkey/contracts/test/TestBase64UrlLib.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +import {Base64Url} from "../libraries/Base64Url.sol"; + +contract TestBase64UrlLib { + function encode(bytes32 data) public pure returns (string memory) { + return Base64Url.encode(data); + } +} diff --git a/modules/passkey/test/libraries/Base64Url.spec.ts b/modules/passkey/test/libraries/Base64Url.spec.ts new file mode 100644 index 000000000..cb89dcd86 --- /dev/null +++ b/modules/passkey/test/libraries/Base64Url.spec.ts @@ -0,0 +1,30 @@ +import { expect } from 'chai' +import { deployments, ethers } from 'hardhat' + +const base64 = { + encodeFromHex: (h: string) => { + const normalized = h.startsWith('0x') ? h.slice(2) : h + + return Buffer.from(normalized, 'hex').toString('base64url') + }, +} + +describe('WebAuthn Library', () => { + const setupTests = deployments.createFixture(async () => { + const Base64UrlLibFactory = await ethers.getContractFactory('TestBase64UrlLib') + const base64UrlLib = await Base64UrlLibFactory.deploy() + + return { base64UrlLib } + }) + + it('Encode: Should correctly base64 encode a bytes32 hash', async () => { + const { base64UrlLib } = await setupTests() + + for (let i = 0; i < 50; i++) { + const input = ethers.keccak256(ethers.randomBytes(32)) + const base64Encoded = base64.encodeFromHex(input) + + expect(await base64UrlLib.encode(input)).to.be.equal(base64Encoded) + } + }) +}) diff --git a/package-lock.json b/package-lock.json index c05c845ca..c2a34d9b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4289,6 +4289,7 @@ }, "node_modules/@parcel/watcher-wasm/node_modules/napi-wasm": { "version": "1.1.0", + "extraneous": true, "inBundle": true, "license": "MIT" }, @@ -19312,7 +19313,8 @@ "dependencies": { "napi-wasm": { "version": "1.1.0", - "bundled": true + "bundled": true, + "extraneous": true } } }, From 1952444f1195340d3e98cb6828d976a002119178 Mon Sep 17 00:00:00 2001 From: Mikhail <16622558+mmv08@users.noreply.github.com> Date: Tue, 26 Mar 2024 17:11:01 +0100 Subject: [PATCH 09/11] Add codesize task --- modules/passkey/hardhat.config.ts | 1 + modules/passkey/package.json | 1 + modules/passkey/src/tasks/codesize.ts | 37 +++++++++++++++++++++++++++ modules/passkey/src/types/solc.d.ts | 9 +++++++ modules/passkey/src/utils/solc.ts | 15 +++++++++++ modules/passkey/tsconfig.json | 3 ++- 6 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 modules/passkey/src/tasks/codesize.ts create mode 100644 modules/passkey/src/types/solc.d.ts create mode 100644 modules/passkey/src/utils/solc.ts diff --git a/modules/passkey/hardhat.config.ts b/modules/passkey/hardhat.config.ts index 0c549747a..2ec206cab 100644 --- a/modules/passkey/hardhat.config.ts +++ b/modules/passkey/hardhat.config.ts @@ -3,6 +3,7 @@ import dotenv from 'dotenv' import type { HardhatUserConfig } from 'hardhat/config' import 'hardhat-deploy' import { HttpNetworkUserConfig } from 'hardhat/types' +import './src/tasks/codesize' dotenv.config() const { CUSTOM_NODE_URL, MNEMONIC, ETHERSCAN_API_KEY, PK } = process.env diff --git a/modules/passkey/package.json b/modules/passkey/package.json index 4975f5817..87845bc14 100644 --- a/modules/passkey/package.json +++ b/modules/passkey/package.json @@ -31,6 +31,7 @@ "build:sol": "rimraf build typechain-types && hardhat compile", "build:ts": "rimraf dist && tsc", "coverage": "hardhat coverage", + "codesize": "hardhat codesize", "fmt": "prettier --write .", "fmt:check": "prettier --check .", "lint": "npm run lint:sol && npm run lint:ts", diff --git a/modules/passkey/src/tasks/codesize.ts b/modules/passkey/src/tasks/codesize.ts new file mode 100644 index 000000000..7e97daa16 --- /dev/null +++ b/modules/passkey/src/tasks/codesize.ts @@ -0,0 +1,37 @@ +import { task, types } from 'hardhat/config' +import { loadSolc } from '../utils/solc' + +task('codesize', 'Displays the codesize of the contracts') + .addParam('skipcompile', 'should not compile before printing size', false, types.boolean, true) + .addParam('contractname', 'name of the contract', undefined, types.string, true) + .setAction(async (taskArgs, hre) => { + if (!taskArgs.skipcompile) { + await hre.run('compile') + } + const contracts = await hre.artifacts.getAllFullyQualifiedNames() + for (const contract of contracts) { + const artifact = await hre.artifacts.readArtifact(contract) + if (taskArgs.contractname && taskArgs.contractname !== artifact.contractName) continue + console.log(artifact.contractName, hre.ethers.dataLength(artifact.deployedBytecode), 'bytes (limit is 24576)') + } + }) + +task('yulcode', 'Outputs yul code for contracts') + .addParam('contractname', 'name of the contract', undefined, types.string, true) + .setAction(async (taskArgs, hre) => { + const contracts = await hre.artifacts.getAllFullyQualifiedNames() + for (const contract of contracts) { + if (taskArgs.contractname && !contract.endsWith(taskArgs.contractname)) continue + const buildInfo = await hre.artifacts.getBuildInfo(contract) + if (!buildInfo) return + console.log({ buildInfo }) + buildInfo.input.settings.outputSelection['*']['*'].push('ir', 'evm.assembly') + const solcjs = await loadSolc(buildInfo.solcLongVersion) + const compiled = solcjs.compile(JSON.stringify(buildInfo.input)) + const output = JSON.parse(compiled) + console.log(output.contracts[contract.split(':')[0]]) + console.log(output.errors) + } + }) + +export {} diff --git a/modules/passkey/src/types/solc.d.ts b/modules/passkey/src/types/solc.d.ts new file mode 100644 index 000000000..68c8e5c7e --- /dev/null +++ b/modules/passkey/src/types/solc.d.ts @@ -0,0 +1,9 @@ +declare module 'solc' { + export type Compiler = { + compile: (input: string) => string + } + + export function compile(input: string): string + + export function loadRemoteVersion(version: string, callback: (err: Error, solc: Compiler) => void): void +} diff --git a/modules/passkey/src/utils/solc.ts b/modules/passkey/src/utils/solc.ts new file mode 100644 index 000000000..0e130e101 --- /dev/null +++ b/modules/passkey/src/utils/solc.ts @@ -0,0 +1,15 @@ +import solc from 'solc' +import type { Compiler } from 'solc' + +const solcCache: Record = {} + +export const loadSolc = async (version: string): Promise => { + return await new Promise((resolve, reject) => { + if (solcCache[version] !== undefined) resolve(solcCache[version]) + else + solc.loadRemoteVersion(`v${version}`, (error, solcjs) => { + solcCache[version] = solcjs + return error ? reject(error) : resolve(solcjs) + }) + }) +} diff --git a/modules/passkey/tsconfig.json b/modules/passkey/tsconfig.json index 5032b2fe0..e9c02a72c 100644 --- a/modules/passkey/tsconfig.json +++ b/modules/passkey/tsconfig.json @@ -8,5 +8,6 @@ "strict": true, "skipLibCheck": true, "resolveJsonModule": true - } + }, + "include": ["src/**/*.ts", "hardhat.config.ts", "test"] } From 063de789cabe1dece6423fd32790ea86649b6087 Mon Sep 17 00:00:00 2001 From: Mikhail <16622558+mmv08@users.noreply.github.com> Date: Wed, 27 Mar 2024 14:28:16 +0100 Subject: [PATCH 10/11] Update modules/passkey/contracts/libraries/Base64Url.sol Co-authored-by: Nicholas Rodrigues Lordello --- modules/passkey/contracts/libraries/Base64Url.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/passkey/contracts/libraries/Base64Url.sol b/modules/passkey/contracts/libraries/Base64Url.sol index 7a18025c0..5f52bc70f 100644 --- a/modules/passkey/contracts/libraries/Base64Url.sol +++ b/modules/passkey/contracts/libraries/Base64Url.sol @@ -80,7 +80,7 @@ library Base64Url { // 0x0F is a mask to get the last 4 bits. // The encoded character is stored at memory location 74 (result + 74): // <32byte string length><43byte encoded string>. 74 is the penultimate byte. - mstore8(add(result, 74), mload(add(0, shl(2, and(data, 0x0F))))) + mstore8(add(result, 74), mload(shl(2, and(data, 0x0F)))) // Update the free memory pointer to point to the end of the encoded string. // Store the length of the encoded string at the beginning of `result`. From e526a354acfda8129921a521764ef2fcd54a93ee Mon Sep 17 00:00:00 2001 From: Mikhail <16622558+mmv08@users.noreply.github.com> Date: Wed, 27 Mar 2024 14:35:45 +0100 Subject: [PATCH 11/11] Improve the comment on exiting the loop --- modules/passkey/contracts/libraries/Base64Url.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/passkey/contracts/libraries/Base64Url.sol b/modules/passkey/contracts/libraries/Base64Url.sol index 5f52bc70f..f775fe0b7 100644 --- a/modules/passkey/contracts/libraries/Base64Url.sol +++ b/modules/passkey/contracts/libraries/Base64Url.sol @@ -70,7 +70,8 @@ library Base64Url { // 42 = 6 bits * 7 (number of groups processed in each iteration). i := sub(i, 42) - // Break the loop when the end of the encoded string is reached. + // We want to exit when all full 6 bits groups are encoded. After 6 iterations, + // I will be -2 and we need to make a signed(!) comparison with 0 to break the loop. if iszero(sgt(i, 0)) { break }