Skip to content

Commit

Permalink
Passkey support (#16)
Browse files Browse the repository at this point in the history
* Passkey support

* Fix passkey func

* fix import error

* Add P256Utils

* fix import error

* fix syntax error

* Add Passkey signature verification func

* add passkeyBinder

* fix bug: system context upgrade fail

* fix: constant init

* fix: validate passkey signature

* fix: spell error

* Revert "fix: spell error"

This reverts commit 526c076.

* fix: spell error

* fix: update contract hash

* optimise Passkey signature verification logic

* optimise PasskeyBinder

---------

Co-authored-by: zkbenny <[email protected]>
  • Loading branch information
zkJoaquin and zkbenny authored Sep 6, 2024
1 parent 353833e commit 50535b0
Show file tree
Hide file tree
Showing 8 changed files with 476 additions and 83 deletions.
6 changes: 3 additions & 3 deletions l1-contracts/scripts/upgrade-consistency-checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!;

Expand Down
8 changes: 4 additions & 4 deletions system-contracts/SystemContractsHashes.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
111 changes: 109 additions & 2 deletions system-contracts/contracts/DefaultAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
156 changes: 87 additions & 69 deletions system-contracts/contracts/PasskeyBinder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down
Loading

0 comments on commit 50535b0

Please sign in to comment.