diff --git a/l1-contracts/scripts/upgrade-consistency-checker.ts b/l1-contracts/scripts/upgrade-consistency-checker.ts index 932c68ea0..16661e6a9 100644 --- a/l1-contracts/scripts/upgrade-consistency-checker.ts +++ b/l1-contracts/scripts/upgrade-consistency-checker.ts @@ -58,16 +58,16 @@ const maxNumberOfHyperchains = 100; const expectedStoredBatchHashZero = "0x1574fa776dec8da2071e5f20d71840bfcbd82c2bca9ad68680edfedde1710bc4"; const expectedL2BridgeAddress = "0x11f943b2c77b743AB90f4A0Ae7d5A4e7FCA3E102"; const expectedL1LegacyBridge = "0x57891966931Eb4Bb6FB81430E6cE0A03AAbDe063"; -const expectedGenesisBatchCommitment = "0x667177606c5d72ce5988172b151b0a97e6cd67de002f86ec66c3899cd9ce7d4c"; +const expectedGenesisBatchCommitment = "0xbac9e5a16fb537337fdd23693eef715c18349a695505580ace203c0ca1bd342f"; const expectedIndexRepeatedStorageChanges = BigNumber.from(56); const expectedProtocolVersion = BigNumber.from(2).pow(32).mul(24); -const expectedGenesisRoot = "0x2e86468e2aa39e313daed4f4ea1865ef11876cc700fea35a1695de22af99915b"; +const expectedGenesisRoot = "0x7692f38725c1969ab55613dab4e74e12be95e66493528531144107870a6921fa"; const expectedRecursionNodeLevelVkHash = "0xf520cd5b37e74e19fdb369c8d676a04dce8a19457497ac6686d2bb95d94109c8"; const expectedRecursionLeafLevelVkHash = "0xf9664f4324c1400fa5c3822d667f30e873f53f1b8033180cd15fe41c1e2355c6"; const expectedRecursionCircuitsSetVksHash = "0x0000000000000000000000000000000000000000000000000000000000000000"; const expectedBootloaderHash = "0x010008e742608b21bf7eb23c1a9d0602047e3618b464c9b59c0fba3b3d7ab66e"; -const expectedDefaultAccountHash = "0x01000567eb1d0eac3e32d1a5b5a0ececcbaf7a0b38b3fd7ce1eb8ff8296ef544"; +const expectedDefaultAccountHash = "0x010008af0fe0bacedf23ca0e881ca8554e4d879a81249c55fed6006740eae70c"; const validatorOne = process.env.ETH_SENDER_SENDER_OPERATOR_COMMIT_ETH_ADDR!; diff --git a/system-contracts/SystemContractsHashes.json b/system-contracts/SystemContractsHashes.json index d70a272a8..38202a1fd 100644 --- a/system-contracts/SystemContractsHashes.json +++ b/system-contracts/SystemContractsHashes.json @@ -45,8 +45,8 @@ "contractName": "DefaultAccount", "bytecodePath": "artifacts-zk/contracts-preprocessed/DefaultAccount.sol/DefaultAccount.json", "sourceCodePath": "contracts-preprocessed/DefaultAccount.sol", - "bytecodeHash": "0x01000567eb1d0eac3e32d1a5b5a0ececcbaf7a0b38b3fd7ce1eb8ff8296ef544", - "sourceCodeHash": "0x1a601a1c617c81daf95a03933b436987a03d6984ed6a49e71310dc706449e2fc" + "bytecodeHash": "0x010008af0fe0bacedf23ca0e881ca8554e4d879a81249c55fed6006740eae70c", + "sourceCodeHash": "0x87b5bd0fbcf98e9d7dc4b2ece48e410ef9adbfaf3d7dc226f6a21709ac070a7f" }, { "contractName": "EmptyContract", @@ -101,8 +101,8 @@ "contractName": "PasskeyBinder", "bytecodePath": "artifacts-zk/contracts-preprocessed/PasskeyBinder.sol/PasskeyBinder.json", "sourceCodePath": "contracts-preprocessed/PasskeyBinder.sol", - "bytecodeHash": "0x010001ddd2d9e5935a6fff10d41e305a9ff33340cdcf15536546332efa3e3c68", - "sourceCodeHash": "0xa331e26de173330a95f7f6eec7a2ad66379d783c996d12eea3d5d6bb9efce6a4" + "bytecodeHash": "0x010001159907d8c069d2b90b03a32980e5a063c1f1006585c5c1113b587d399e", + "sourceCodeHash": "0xd7343fc706c5ef7490419b655f4ebb27074dc6560a9f2d5faba9c8d9d433a8b8" }, { "contractName": "PubdataChunkPublisher", diff --git a/system-contracts/contracts/DefaultAccount.sol b/system-contracts/contracts/DefaultAccount.sol index 6f78ad3ef..f1a9a7645 100644 --- a/system-contracts/contracts/DefaultAccount.sol +++ b/system-contracts/contracts/DefaultAccount.sol @@ -5,10 +5,11 @@ pragma solidity 0.8.20; import {IAccount, ACCOUNT_VALIDATION_SUCCESS_MAGIC} from "./interfaces/IAccount.sol"; import {IPasskeyBinder} from "./interfaces/IPasskeyBinder.sol"; import {TransactionHelper, Transaction} from "./libraries/TransactionHelper.sol"; +import {PasskeyHelper, WebAuthnSignatureStruct, SINGLE_TX_R1_TYPE, MULTI_TX_R1_TYPE, MULTI_TX_K1_TYPE} from "./libraries/PasskeyHelper.sol"; import {SystemContractsCaller} from "./libraries/SystemContractsCaller.sol"; import {SystemContractHelper} from "./libraries/SystemContractHelper.sol"; import {EfficientCall} from "./libraries/EfficientCall.sol"; -import {BOOTLOADER_FORMAL_ADDRESS, NONCE_HOLDER_SYSTEM_CONTRACT, DEPLOYER_SYSTEM_CONTRACT, INonceHolder, SYSTEM_CONTRACTS_OFFSET} from "./Constants.sol"; +import {BOOTLOADER_FORMAL_ADDRESS, NONCE_HOLDER_SYSTEM_CONTRACT, DEPLOYER_SYSTEM_CONTRACT, SYSTEM_CONTRACTS_OFFSET, INonceHolder} from "./Constants.sol"; import {Utils} from "./libraries/Utils.sol"; /** @@ -22,8 +23,23 @@ import {Utils} from "./libraries/Utils.sol"; contract DefaultAccount is IAccount { using TransactionHelper for *; + /// @notice Structure used to represent a zkSync's EIP-712 type transaction hash. + struct TransactionHashStruct { + // The hash of zkSync's EIP-712-signed transaction. + bytes32 txHash; + } + IPasskeyBinder public constant PASSKEY_BINDER = IPasskeyBinder(address(SYSTEM_CONTRACTS_OFFSET + 0xff)); + /// @dev The EIP-712 typehash for the TransactionHash. + bytes32 constant TRANSACTION_HASH_TYPEHASH = keccak256("TransactionHash(bytes32 txHash)"); + + /// @dev The EIP-712 typehash for the multi transaction. + bytes32 constant MULTI_TRANSACTION_TYPEHASH = + keccak256( + "MultiTransaction(TransactionHash[] transactionHashes,UserOperationHash[] userOpHashes,ChainDomain[] userOpDomains)ChainDomain(string name,string version,uint256 chainId,address verifyingContract)TransactionHash(bytes32 txHash)UserOperationHash(bytes32 txHash)" + ); + /** * @dev Simulate the behavior of the EOA if the caller is not the bootloader. * Essentially, for all non-bootloader callers halt the execution with empty return data. @@ -104,11 +120,98 @@ contract DefaultAccount is IAccount { uint256 totalRequiredBalance = _transaction.totalRequiredBalance(); require(totalRequiredBalance <= address(this).balance, "Not enough balance for fee + value"); - if (_isValidSignature(txHash, _transaction.signature)) { + if (_validateSignature(txHash, _transaction.signature)) { magic = ACCOUNT_VALIDATION_SUCCESS_MAGIC; } } + /// @notice Verify signatures for different types of transactions. + /// @param _hash The hash of the current transaction to be signed. + /// @param _signature The signature for different types of transactions. + function _validateSignature(bytes32 _hash, bytes memory _signature) internal returns (bool) { + if (_signature.length == 65) { + return _isValidSignature(_hash, _signature); + } else if (_signature.length > 65) { + (bytes2 magicNum, bytes memory encodedSignature) = abi.decode(_signature, (bytes2, bytes)); + + if (magicNum == SINGLE_TX_R1_TYPE) { + (bytes32 credentialIdHash, WebAuthnSignatureStruct memory decodedSignature) = PasskeyHelper + .decodeWebAuthnP256Signature(encodedSignature); + (uint256 x, uint256 y) = _getPasskeyPublicKey(credentialIdHash); + return PasskeyHelper.verifyByP256Contract(_hash, decodedSignature, x, y); + } else if (magicNum == MULTI_TX_R1_TYPE) { + (bytes32 rootHash, bytes memory passkeySignature) = _decodeMultiTxRootHashAndSignature( + _hash, + encodedSignature + ); + + (bytes32 credentialIdHash, WebAuthnSignatureStruct memory decodedSignature) = PasskeyHelper + .decodeWebAuthnP256Signature(passkeySignature); + (uint256 x, uint256 y) = _getPasskeyPublicKey(credentialIdHash); + + return PasskeyHelper.verifyByP256Contract(rootHash, decodedSignature, x, y); + } else if (magicNum == MULTI_TX_K1_TYPE) { + (bytes32 rootHash, bytes memory signature) = _decodeMultiTxRootHashAndSignature( + _hash, + encodedSignature + ); + return _isValidSignature(rootHash, signature); + } + } + + return false; + } + + function _getPasskeyPublicKey(bytes32 _credentialIdHash) internal returns (uint256, uint256) { + bytes memory returnData = SystemContractsCaller.systemCallWithPropagatedRevert( + uint32(gasleft()), + address(PASSKEY_BINDER), + 0, + abi.encodeCall(IPasskeyBinder.getAuthorizedKey, (_credentialIdHash)) + ); + (address passkeyOwner, uint256 x, uint256 y) = abi.decode(returnData, (address, uint256, uint256)); + require(passkeyOwner == address(this), "Passkey is not owned by the account"); + require(x != 0 && y != 0, "Passkey is not set"); + return (x, y); + } + + /// @notice Decode the root hash and signature for the multi transaction. + /// @param _hash The hash of the current transaction to be signed. + /// @param _encodedSignature The encoded signature. + function _decodeMultiTxRootHashAndSignature( + bytes32 _hash, + bytes memory _encodedSignature + ) internal view returns (bytes32, bytes memory) { + ( + bytes32 userOpsRootHash, + bytes32 userOpDomainsRootHash, + bytes memory txHashPrefix, + bytes memory txHashSuffix, + bytes memory signature + ) = abi.decode(_encodedSignature, (bytes32, bytes32, bytes, bytes, bytes)); + + bytes32 txRootHash = keccak256( + abi.encodePacked(txHashPrefix, hash(TransactionHashStruct({txHash: _hash})), txHashSuffix) + ); + + bytes32 rootHashWithNonPrefix = keccak256( + abi.encode(MULTI_TRANSACTION_TYPEHASH, txRootHash, userOpsRootHash, userOpDomainsRootHash) + ); + + bytes32 domainSeparator = keccak256( + abi.encode( + TransactionHelper.EIP712_DOMAIN_TYPEHASH, + keccak256("ZKLink Nova Multi Transaction Validator"), + keccak256("0.1.0"), + block.chainid + ) + ); + + bytes32 rootHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, rootHashWithNonPrefix)); + + return (rootHash, signature); + } + /// @notice Method called by the bootloader to execute the transaction. /// @param _transaction The transaction to execute. /// @dev It also accepts unused _txHash and _suggestedSignedHash parameters: @@ -199,6 +302,10 @@ contract DefaultAccount is IAccount { return recoveredAddress == address(this) && recoveredAddress != address(0); } + function hash(TransactionHashStruct memory txHashStruct) internal pure returns (bytes32) { + return keccak256(abi.encode(TRANSACTION_HASH_TYPEHASH, txHashStruct.txHash)); + } + /// @notice Method for paying the bootloader for the transaction. /// @param _transaction The transaction for which the fee is paid. /// @dev It also accepts unused _txHash and _suggestedSignedHash parameters: diff --git a/system-contracts/contracts/PasskeyBinder.sol b/system-contracts/contracts/PasskeyBinder.sol index 301453cde..17e3b3079 100644 --- a/system-contracts/contracts/PasskeyBinder.sol +++ b/system-contracts/contracts/PasskeyBinder.sol @@ -2,7 +2,12 @@ pragma solidity 0.8.20; -contract PasskeyBinder { +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {IPasskeyBinder} from "./interfaces/IPasskeyBinder.sol"; + +contract PasskeyBinder is IPasskeyBinder { + using EnumerableSet for EnumerableSet.Bytes32Set; + //curve prime field modulus uint256 private constant p = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF; //short weierstrass second coefficient @@ -15,110 +20,123 @@ contract PasskeyBinder { uint256 y; } - mapping(bytes32 keyIdHash => P256PublicKey) private authorizedKeys; - mapping(bytes32 keyIdHash => address account) private keyIdHashToAccount; - mapping(address account => string[] keyIds) private accountToKeyIdList; + struct AuthorizedKey { + address owner; + P256PublicKey publicKey; + } + + mapping(address account => EnumerableSet.Bytes32Set credentialIdHashSet) private accountToCredentialIdHashSet; + mapping(bytes32 credentialIdHash => AuthorizedKey) private authorizedKeys; /// @dev Event emitted when a P256 key is added - event AddedP256Key(bytes32 indexed keyIdHash, string keyId, uint256 x, uint256 y); + event AddedP256PublicKey(bytes32 indexed credentialIdHash, address indexed owner, uint256 x, uint256 y); /// @dev Event emitted when a P256 key is removed - event RemovedP256Key(bytes32 indexed keyIdHash, uint256 x, uint256 y); + event RemovedP256PublicKey(bytes32 indexed credentialIdHash, address indexed owner, uint256 x, uint256 y); /// @dev Error emitted when a P256 key is not on the curve error KeyNotOnCurve(uint256 x, uint256 y); - /// @dev Error emitted when an empty key is attempted to be added - error InvalidEmptyKey(); + /// @dev Error emitted when an empty credential id hash is attempted to be added + error InvalidCredentialIdHash(); /// @dev Error emitted when a P256 key is already stored and attempted to be added - error KeyAlreadyExists(string keyId); + error KeyAlreadyExists(bytes32 credentialIdHash); /// @dev Error emitted when a P256 key is not stored and attempted to be removed - error KeyDoesNotExist(string keyId); + error KeyDoesNotExist(bytes32 credentialIdHash); /// @dev Error emitted when a P256 key is not owned by the caller - error DoesNotOwner(string keyId); + error DoesNotOwner(bytes32 credentialIdHash); + /// @dev Error emitted when a P256 key is attempted to be add by not EOA + error DoesNotEOA(); - function addKey(string calldata _keyId, uint256 _x, uint256 _y) external { + /** + * @notice Adds a P256 public key to the contract + * @param _credentialIdHash The ID Hash of the credential to add + * @param _x The X value of the public key + * @param _y The Y value of the public key + */ + function addP256PublicKey(bytes32 _credentialIdHash, uint256 _x, uint256 _y) external { + address sender = msg.sender; // slither-disable-next-line tx-origin - require(msg.sender == tx.origin, "Not authorized"); - _addKey(_keyId, _x, _y); + if (sender != tx.origin) revert DoesNotEOA(); + _addP256PublicKey(_credentialIdHash, sender, _x, _y); } - function _addKey(string calldata _keyId, uint256 _x, uint256 _y) internal { + function _addP256PublicKey(bytes32 _credentialIdHash, address sender, uint256 _x, uint256 _y) internal { if (!isValidPublicKey(_x, _y)) revert KeyNotOnCurve(_x, _y); - bytes32 keyIdHash_ = keccak256(abi.encodePacked(_keyId)); - if (bytes(_keyId).length == 0) revert InvalidEmptyKey(); + if (_credentialIdHash == bytes32(0)) revert InvalidCredentialIdHash(); - P256PublicKey storage publicKey_ = authorizedKeys[keyIdHash_]; + AuthorizedKey storage authorizedKey = authorizedKeys[_credentialIdHash]; - // update key - if (publicKey_.x != 0 || publicKey_.y != 0) { - revert KeyAlreadyExists(_keyId); - } + if (authorizedKey.owner != address(0)) revert KeyAlreadyExists(_credentialIdHash); - authorizedKeys[keyIdHash_] = P256PublicKey(_x, _y); - keyIdHashToAccount[keyIdHash_] = msg.sender; - accountToKeyIdList[msg.sender].push(_keyId); + authorizedKeys[_credentialIdHash] = AuthorizedKey({owner: sender, publicKey: P256PublicKey({x: _x, y: _y})}); + accountToCredentialIdHashSet[sender].add(_credentialIdHash); - emit AddedP256Key(keyIdHash_, _keyId, _x, _y); + emit AddedP256PublicKey(_credentialIdHash, sender, _x, _y); } - function removeKey(string calldata _keyId) external { - bytes32 keyIdHash_ = keccak256(abi.encodePacked(_keyId)); - if (keyIdHashToAccount[keyIdHash_] != msg.sender) revert DoesNotOwner(_keyId); - P256PublicKey memory publicKey_ = authorizedKeys[keyIdHash_]; - uint256 x_ = publicKey_.x; - uint256 y_ = publicKey_.y; - - if (x_ == 0 && y_ == 0) revert KeyDoesNotExist(_keyId); - - delete authorizedKeys[keyIdHash_]; - delete keyIdHashToAccount[keyIdHash_]; - uint256 length = accountToKeyIdList[msg.sender].length; - for (uint256 i = 0; i < length; i++) { - if (keccak256(abi.encodePacked(accountToKeyIdList[msg.sender][i])) == keyIdHash_) { - accountToKeyIdList[msg.sender][i] = accountToKeyIdList[msg.sender][length - 1]; - accountToKeyIdList[msg.sender].pop(); - break; - } - } - - emit RemovedP256Key(keyIdHash_, x_, y_); - } /** - * @notice Returns the P256 public key coordinates of a given key ID if it is a signer - * @param keyIdHash The ID Hash of the key to get - * @return x_ The X value of the public key - * @return y_ The Y value of the public key + * @notice Removes a P256 public key from the contract + * @param _credentialIdHash The ID Hash of the credential to remove */ - function getKey(bytes32 keyIdHash) external view returns (uint256 x_, uint256 y_) { - P256PublicKey memory publicKey_ = authorizedKeys[keyIdHash]; - x_ = publicKey_.x; - y_ = publicKey_.y; - } + function removeP256PublicKey(bytes32 _credentialIdHash) external { + address sender = msg.sender; + AuthorizedKey memory authorizedKey = authorizedKeys[_credentialIdHash]; + address publicKeyOwner = authorizedKey.owner; - function getKeyIdLength(address _account) external view returns (uint256) { - return accountToKeyIdList[_account].length; + if (publicKeyOwner == address(0)) revert KeyDoesNotExist(_credentialIdHash); + if (publicKeyOwner != sender) revert DoesNotOwner(_credentialIdHash); + + uint256 x = authorizedKey.publicKey.x; + uint256 y = authorizedKey.publicKey.y; + + delete authorizedKeys[_credentialIdHash]; + accountToCredentialIdHashSet[sender].remove(_credentialIdHash); + + emit RemovedP256PublicKey(_credentialIdHash, sender, x, y); } - function getKeyIdByIndex(address _account, uint256 _index) external view returns (string memory) { - return accountToKeyIdList[_account][_index]; + /** + * @notice Returns authorized key infos by credential id hash + * @param _credentialIdHash The ID Hash of the credential to get + * @return owner The owner of the public key + * @return x The X value of the public key + * @return y The Y value of the public key + */ + function getAuthorizedKey(bytes32 _credentialIdHash) external view returns (address owner, uint256 x, uint256 y) { + AuthorizedKey memory authorizedKey = authorizedKeys[_credentialIdHash]; + + owner = authorizedKey.owner; + x = authorizedKey.publicKey.x; + y = authorizedKey.publicKey.y; } - function getAccountByKeyIdHash(bytes32 keyIdHash) external view returns (address) { - return keyIdHashToAccount[keyIdHash]; + /** + * @notice Returns the number of credential id hash set length + * @param _account The account to get the credential id hash set length + * @return The number of credential id hash set length + */ + function getCredentialIdHashSetLength(address _account) external view returns (uint256) { + return accountToCredentialIdHashSet[_account].length(); } - function getP256PublicKey(bytes32 keyIdHash) external view returns (P256PublicKey memory) { - return authorizedKeys[keyIdHash]; + /** + * @notice Returns the credential id hash by index + * @param _account The account to get the credential id hash + * @param _index The index to get the credential id hash + * @return The credential id hash by index + */ + function getCredentialIdHashByIndex(address _account, uint256 _index) external view returns (bytes32) { + return accountToCredentialIdHashSet[_account].at(_index); } - function isValidPublicKey(uint256 x, uint256 y) internal pure returns (bool) { - if (x >= p || y >= p || ((x == 0) && (y == 0))) { + function isValidPublicKey(uint256 _x, uint256 _y) internal pure returns (bool) { + if (_x >= p || _y >= p || ((_x == 0) && (_y == 0))) { return false; } unchecked { - uint256 LHS = mulmod(y, y, p); // y^2 - uint256 RHS = addmod(mulmod(mulmod(x, x, p), x, p), mulmod(x, a, p), p); // x^3+ax + uint256 LHS = mulmod(_y, _y, p); // y^2 + uint256 RHS = addmod(mulmod(mulmod(_x, _x, p), _x, p), mulmod(_x, a, p), p); // x^3+ax RHS = addmod(RHS, b, p); // x^3 + a*x + b return LHS == RHS; diff --git a/system-contracts/contracts/interfaces/IPasskeyBinder.sol b/system-contracts/contracts/interfaces/IPasskeyBinder.sol index accf6adaa..5b1ffab81 100644 --- a/system-contracts/contracts/interfaces/IPasskeyBinder.sol +++ b/system-contracts/contracts/interfaces/IPasskeyBinder.sol @@ -3,9 +3,27 @@ pragma solidity 0.8.20; interface IPasskeyBinder { - /// @dev Returns the public key associated with the given keyIdHash. - /// @param keyIdHash The hash of the keyId. - /// @return x_ The x-coordinate of the public key. - /// @return y_ The y-coordinate of the public key. - function getKey(bytes32 keyIdHash) external view returns (uint256 x_, uint256 y_); + /** + * @notice Returns authorized key infos by credential id hash + * @param credentialIdHash The ID Hash of the credential to get + * @return owner The owner of the public key + * @return x The X value of the public key + * @return y The Y value of the public key + */ + function getAuthorizedKey(bytes32 credentialIdHash) external view returns (address owner, uint256 x, uint256 y); + + /** + * @notice Returns the number of credential id hash set length + * @param _account The account to get the credential id hash set length + * @return The number of credential id hash set length + */ + function getCredentialIdHashSetLength(address _account) external view returns (uint256); + + /** + * @notice Returns the credential id hash by index + * @param _account The account to get the credential id hash + * @param _index The index to get the credential id hash + * @return The credential id hash by index + */ + function getCredentialIdHashByIndex(address _account, uint256 _index) external view returns (bytes32); } diff --git a/system-contracts/contracts/libraries/Base64URL.sol b/system-contracts/contracts/libraries/Base64URL.sol new file mode 100644 index 000000000..bbe002ce3 --- /dev/null +++ b/system-contracts/contracts/libraries/Base64URL.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Base64} from "@openzeppelin/contracts/utils/Base64.sol"; + +library Base64URL { + function encode(bytes memory data) internal pure returns (string memory) { + string memory strb64 = Base64.encode(data); + bytes memory b64 = bytes(strb64); + + // Base64 can end with "=" or "=="; Base64URL has no padding. + uint256 equalsCount = 0; + if (b64.length > 2 && b64[b64.length - 2] == "=") equalsCount = 2; + else if (b64.length > 1 && b64[b64.length - 1] == "=") equalsCount = 1; + + uint256 len = b64.length - equalsCount; + bytes memory result = new bytes(len); + + for (uint256 i = 0; i < len; i++) { + if (b64[i] == "+") { + result[i] = "-"; + } else if (b64[i] == "/") { + result[i] = "_"; + } else { + result[i] = b64[i]; + } + } + + return string(result); + } +} diff --git a/system-contracts/contracts/libraries/PasskeyHelper.sol b/system-contracts/contracts/libraries/PasskeyHelper.sol new file mode 100644 index 000000000..58b9e16d8 --- /dev/null +++ b/system-contracts/contracts/libraries/PasskeyHelper.sol @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.20; + +import {Base64URL} from "./Base64URL.sol"; + +/// @dev The type id of a single transaction signed by r1 +bytes2 constant SINGLE_TX_R1_TYPE = 0xA000; +/// @dev The type id of the multi transactions signed by a single r1 key +bytes2 constant MULTI_TX_R1_TYPE = 0xB000; +/// @dev The type id of the multi transactions signed by a single k1 key +bytes2 constant MULTI_TX_K1_TYPE = 0xB001; + +/// @dev The passkey types +enum PasskeyTypes { + // 0: WebAuthn P-256 + Legacy, + // 1: WebAuthn P-256 with turnkey payload + Turnkey +} + +/// @dev The struct from a WebAuthn P-256 signature +struct WebAuthnSignatureStruct { + uint256 r; + uint256 s; + uint8 v; + uint8 passkeyType; + bool requireUserVerification; + bytes authenticatorData; + string clientDataJSONPrefix; + string clientDataJSONSuffix; + uint256 responseTypeLocation; + string turnkeyPayloadPrefix; + string turnkeyPayloadSuffix; +} + +library PasskeyHelper { + /// @dev Address of the system contract used to validate the p256 algorithm + address private constant REAL_P256VERIFY_CONTRACT_ADDRESS = 0x0000000000000000000000000000000000000100; + /// @dev The prime number used in the P-256 curve + uint256 private constant n = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551; + /// @dev n / 2 + uint256 private constant P256_N_DIV_2 = n / 2; + + /// @dev Decode a WebAuthn P-256 signature + /// @param _signature The signature to decode + function decodeWebAuthnP256Signature( + bytes memory _signature + ) internal pure returns (bytes32 credentialIdHash, WebAuthnSignatureStruct memory decodedWebAuthnSig) { + ( + credentialIdHash, + decodedWebAuthnSig.r, + decodedWebAuthnSig.s, + decodedWebAuthnSig.passkeyType, + decodedWebAuthnSig.authenticatorData, + decodedWebAuthnSig.requireUserVerification, + decodedWebAuthnSig.clientDataJSONPrefix, + decodedWebAuthnSig.clientDataJSONSuffix, + decodedWebAuthnSig.responseTypeLocation, + decodedWebAuthnSig.turnkeyPayloadPrefix, + decodedWebAuthnSig.turnkeyPayloadSuffix + ) = abi.decode( + _signature, + (bytes32, uint256, uint256, uint8, bytes, bool, string, string, uint256, string, string) + ); + } + + /// @notice Verify a WebAuthn P-256 signature + /// @param _hash The hash to verify + /// @param decodedWebAuthnSig The decoded webauthn signature + /// @param x The x coordinate of the public key + /// @param y The y coordinate of the public key + /// @return Whether the signature is valid + function verifyByP256Contract( + bytes32 _hash, + WebAuthnSignatureStruct memory decodedWebAuthnSig, + uint256 x, + uint256 y + ) internal view returns (bool) { + bytes memory authenticatorData = decodedWebAuthnSig.authenticatorData; + bool requireUserVerification = decodedWebAuthnSig.requireUserVerification; + string memory clientDataJSONPrefix = decodedWebAuthnSig.clientDataJSONPrefix; + string memory clientDataJSONSuffix = decodedWebAuthnSig.clientDataJSONSuffix; + string memory turnkeyPayloadPrefix = decodedWebAuthnSig.turnkeyPayloadPrefix; + string memory turnkeyPayloadSuffix = decodedWebAuthnSig.turnkeyPayloadSuffix; + PasskeyTypes passkeyType = PasskeyTypes(decodedWebAuthnSig.passkeyType); + + bytes memory challenge; + if (passkeyType == PasskeyTypes.Legacy) { + challenge = bytesToHex(abi.encodePacked(_hash)); + } else if (passkeyType == PasskeyTypes.Turnkey) { + string memory hashString = string(abi.encodePacked("0x", bytesToHex(abi.encodePacked(_hash)))); + challenge = bytesToHex( + abi.encodePacked( + sha256((abi.encodePacked(string.concat(turnkeyPayloadPrefix, hashString, turnkeyPayloadSuffix)))) + ) + ); + } + uint256 responseTypeLocation = decodedWebAuthnSig.responseTypeLocation; + uint256 r = decodedWebAuthnSig.r; + uint256 s = decodedWebAuthnSig.s; + + if (authenticatorData.length < 37 || !checkAuthFlags(authenticatorData[32], requireUserVerification)) { + return false; + } + + bytes memory clientDataJSON = abi.encodePacked( + clientDataJSONPrefix, + Base64URL.encode(challenge), + clientDataJSONSuffix + ); + + // Check that response is for an authentication assertion + string memory responseType = '"type":"webauthn.get"'; + if (!contains(responseType, string(clientDataJSON), responseTypeLocation)) { + return false; + } + + // Check that the public key signed sha256(authenticatorData || sha256(clientDataJSON)) + bytes32 clientDataJSONHash = sha256(bytes(clientDataJSON)); + bytes32 messageHash = sha256(abi.encodePacked(authenticatorData, clientDataJSONHash)); + + return verifySignature(messageHash, r, s, x, y); + } + + /** + * + * @param message_hash The hash to be verified + * @param r The r component of the signature + * @param s The s component of the signature + * @param x The X coordinate of the public key that signed the message + * @param y The Y coordinate of the public key that signed the message + */ + function verifySignature( + bytes32 message_hash, + uint256 r, + uint256 s, + uint256 x, + uint256 y + ) internal view returns (bool) { + // check for signature malleability + if (s > P256_N_DIV_2) { + return false; + } + + bytes memory args = abi.encode(message_hash, r, s, x, y); + + (bool success, bytes memory ret) = REAL_P256VERIFY_CONTRACT_ADDRESS.staticcall(args); + + if (success) return abi.decode(ret, (bool)) == true; + + return false; + } + + function bytesToHex(bytes memory buffer) internal pure returns (bytes memory) { + // Fixed buffer size for hexadecimal conversion + bytes memory converted = new bytes(buffer.length * 2); + + bytes memory _base = "0123456789abcdef"; + + for (uint256 i = 0; i < buffer.length; i++) { + converted[i * 2] = _base[uint8(buffer[i] >> 4)]; + converted[i * 2 + 1] = _base[uint8(buffer[i] & 0x0f)]; + } + return converted; + } + + bytes1 private constant AUTH_DATA_FLAGS_UP = 0x01; // Bit 0 + bytes1 private constant AUTH_DATA_FLAGS_UV = 0x04; // Bit 2 + bytes1 private constant AUTH_DATA_FLAGS_BE = 0x08; // Bit 3 + bytes1 private constant AUTH_DATA_FLAGS_BS = 0x10; // Bit 4 + + /// Verifies the authFlags in authenticatorData. Numbers in inline comment + /// correspond to the same numbered bullets in + /// https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion. + function checkAuthFlags(bytes1 flags, bool requireUserVerification) internal pure returns (bool) { + // 17. Verify that the UP bit of the flags in authData is set. + if (flags & AUTH_DATA_FLAGS_UP != AUTH_DATA_FLAGS_UP) { + return false; + } + // 18. If user verification was determined to be required, verify that + // the UV bit of the flags in authData is set. Otherwise, ignore the + // value of the UV flag. + if (requireUserVerification && (flags & AUTH_DATA_FLAGS_UV) != AUTH_DATA_FLAGS_UV) { + return false; + } + + // 19. If the BE bit of the flags in authData is not set, verify that + // the BS bit is not set. + if (flags & AUTH_DATA_FLAGS_BE != AUTH_DATA_FLAGS_BE) { + if (flags & AUTH_DATA_FLAGS_BS == AUTH_DATA_FLAGS_BS) { + return false; + } + } + + return true; + } + + function contains(string memory substr, string memory str, uint256 location) internal pure returns (bool) { + bytes memory substrBytes = bytes(substr); + bytes memory strBytes = bytes(str); + + uint256 substrLen = substrBytes.length; + uint256 strLen = strBytes.length; + + for (uint256 i = 0; i < substrLen; i++) { + if (location + i >= strLen) { + return false; + } + + if (substrBytes[i] != strBytes[location + i]) { + return false; + } + } + + return true; + } +} diff --git a/system-contracts/package.json b/system-contracts/package.json index d7d5ad1b2..58b24e16f 100644 --- a/system-contracts/package.json +++ b/system-contracts/package.json @@ -7,6 +7,7 @@ "@matterlabs/hardhat-zksync-deploy": "^0.6.5", "@matterlabs/hardhat-zksync-solc": "^1.1.4", "@matterlabs/hardhat-zksync-verify": "^1.4.3", + "@openzeppelin/contracts": "4.9.5", "commander": "^9.4.1", "eslint": "^8.51.0", "eslint-plugin-import": "^2.29.0",