Skip to content

Commit

Permalink
Merge branch 'develop' into feat/sma-392-session-keys-v2
Browse files Browse the repository at this point in the history
  • Loading branch information
ankurdubey521 authored Jan 3, 2024
2 parents 1c524f9 + 224ef5c commit baf58f7
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 65 deletions.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface IPasskeyRegistryModule {

function isValidSignatureForAddress(
bytes32 signedDataHash,
bytes memory moduleSignature
bytes memory moduleSignature,
address smartAccount
) external view returns (bytes4);
}
101 changes: 64 additions & 37 deletions contracts/smart-account/modules/PasskeyRegistryModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,20 @@ contract PasskeyRegistryModule is
string public constant NAME = "PassKeys Ownership Registry Module";
string public constant VERSION = "1.1.0";

mapping(address => PassKeyId) public smartAccountPassKeys;
mapping(address => PassKeyId) public smartAccountPasskey;

/// @inheritdoc IPasskeyRegistryModule
function initForSmartAccount(
uint256 _pubKeyX,
uint256 _pubKeyY,
string calldata _keyId
) external override returns (address) {
PassKeyId storage passKeyId = smartAccountPassKeys[msg.sender];
PassKeyId storage passKeyId = smartAccountPasskey[msg.sender];

if (passKeyId.pubKeyX != 0 && passKeyId.pubKeyY != 0)
revert AlreadyInitedForSmartAccount(msg.sender);

smartAccountPassKeys[msg.sender] = PassKeyId(
smartAccountPasskey[msg.sender] = PassKeyId(
_pubKeyX,
_pubKeyY,
_keyId
Expand All @@ -47,6 +47,17 @@ contract PasskeyRegistryModule is
return address(this);
}

/**
* @dev Returns the owner of the Smart Account.
* @param smartAccount Smart Account address.
* @return PassKeyId The owner key of the Smart Account.
*/
function getOwner(
address smartAccount
) external view returns (PassKeyId memory) {
return smartAccountPasskey[smartAccount];
}

/// @inheritdoc IAuthorizationModule
function validateUserOp(
UserOperation calldata userOp,
Expand All @@ -56,49 +67,81 @@ contract PasskeyRegistryModule is
userOp.signature,
(bytes, address)
);
if (_verifySignature(userOpHash, passkeySignature)) {
if (_verifySignature(userOpHash, passkeySignature, userOp.sender)) {
return VALIDATION_SUCCESS;
}
return SIG_VALIDATION_FAILED;
}

/// @inheritdoc ISignatureValidator
/**
* @inheritdoc ISignatureValidator
* @dev Validates a signature for a message.
* @dev Appends smart account address to the hash to avoid replay attacks
* To be called from a Smart Account.
* @param signedDataHash Hash of the message that was signed.
* @param moduleSignature Signature to be validated.
* @return EIP1271_MAGIC_VALUE if signature is valid, 0xffffffff otherwise.
*/
function isValidSignature(
bytes32 signedDataHash,
bytes memory moduleSignature
) public view virtual override returns (bytes4) {
// TODO: @amanraj1608 make it safe
return isValidSignatureForAddress(signedDataHash, moduleSignature);
}

/// @inheritdoc ISignatureValidator
function isValidSignatureUnsafe(
bytes32 signedDataHash,
bytes memory moduleSignature
) public view virtual override returns (bytes4) {
return isValidSignatureForAddress(signedDataHash, moduleSignature);
return
isValidSignatureForAddress(
signedDataHash,
moduleSignature,
msg.sender
);
}

/// @inheritdoc IPasskeyRegistryModule
function isValidSignatureForAddress(
bytes32 signedDataHash,
bytes memory moduleSignature
) public view virtual override returns (bytes4) {
if (_verifySignature(signedDataHash, moduleSignature)) {
bytes memory moduleSignature,
address smartAccount
) public view virtual returns (bytes4) {
if (
_verifySignature(
keccak256(
abi.encodePacked(
"\x19Ethereum Signed Message:\n52",
signedDataHash,
smartAccount
)
),
moduleSignature,
smartAccount
)
) {
return EIP1271_MAGIC_VALUE;
}
return bytes4(0xffffffff);
}

/// @inheritdoc ISignatureValidator
function isValidSignatureUnsafe(
bytes32 signedDataHash,
bytes memory moduleSignature
) public view virtual returns (bytes4) {
return
isValidSignatureForAddress(
signedDataHash,
moduleSignature,
msg.sender
);
}

/**
* @dev Internal utility function to verify a signature.
* @param userOpDataHash The hash of the user operation data.
* @param moduleSignature The signature provided by the module.
* @param smartAccount The smart account address.
* @return True if the signature is valid, false otherwise.
*/
function _verifySignature(
bytes32 userOpDataHash,
bytes memory moduleSignature
bytes memory moduleSignature,
address smartAccount
) internal view returns (bool) {
(
bytes32 keyHash,
Expand All @@ -123,26 +166,10 @@ contract PasskeyRegistryModule is
bytes32 clientHash = sha256(bytes(clientDataJSON));
bytes32 sigHash = sha256(bytes.concat(authenticatorData, clientHash));

PassKeyId memory passKey = smartAccountPassKeys[msg.sender];
PassKeyId memory passKey = smartAccountPasskey[smartAccount];
if (passKey.pubKeyX == 0 && passKey.pubKeyY == 0) {
revert NoPassKeyRegisteredForSmartAccount(msg.sender);
revert NoPassKeyRegisteredForSmartAccount(smartAccount);
}
return Secp256r1.verify(passKey, sigx, sigy, uint256(sigHash));
}

/**
* @dev Internal function to validate a user operation signature.
* @param userOp The user operation to validate.
* @param userOpHash The hash of the user operation.
* @return sigValidationResult Returns 0 if the signature is valid, and SIG_VALIDATION_FAILED otherwise.
*/
function _validateSignature(
UserOperation calldata userOp,
bytes32 userOpHash
) internal view virtual returns (uint256 sigValidationResult) {
if (_verifySignature(userOpHash, userOp.signature)) {
return 0;
}
return SIG_VALIDATION_FAILED;
}
}
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,12 @@
"@account-abstraction/contracts": "^0.6.0",
"@ethersproject/abstract-signer": "^5.6.2",
"@ethersproject/constants": "^5.6.1",
"@types/elliptic": "^6.4.18",
"axios": "^1.4.0",
"ethereumjs-util": "^7.1.0",
"hardhat-tracer": "^2.7.0"
"bn.js": "^5.2.1",
"dotenv": "^16.0.3",
"elliptic": "^6.5.4",
}
}
108 changes: 90 additions & 18 deletions test/module/PasskeyFlow.module.specs.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { expect } from "chai";
import { Contract } from "ethers";
import { BigNumber, Contract } from "ethers";
import { deployments, ethers, waffle } from "hardhat";
import elliptic from "elliptic";
import BN = require("bn.js");
import {
getEntryPoint,
getSmartAccountFactory,
getSmartAccountWithModule,
} from "../utils/setupHelper";
import { fillAndSign } from "../utils/userOp";
import { fillAndSign, getUserOpHash } from "../utils/userOp";
import { keccak256 } from "ethers/lib/utils";

const keyId = "test";
const pubX =
"0xa736f00b7d22e878a2fe3836773219ddac3c9b2bdcb066b3c480232262b410ad";
const pubY =
"0xd238d6f412bbf0334a592d4cba3862d28853f9f27d4ff6a9546de355761eb0f8";
const EC = elliptic.ec;
const ec = new EC("p256");
const keyPair = ec.genKeyPair();
const pubX = "0x" + keyPair.getPublic().getX().toString("hex");
const pubY = "0x" + keyPair.getPublic().getY().toString("hex");

describe("Passkeys Registry Module:", function () {
const [deployer, offchainSigner, charlie] = waffle.provider.getWallets();
Expand Down Expand Up @@ -56,39 +60,107 @@ describe("Passkeys Registry Module:", function () {
it("Deploys Modular Smart Account with Passkey Validation Module", async () => {
const { userSA } = await setupTests();
expect(await userSA.isModuleEnabled(passKeyModule.address)).to.equal(true);

const passkeyOwner = await passKeyModule.getOwner(userSA.address);
// verify pubX and pubY is set or not
expect(
(await passKeyModule.smartAccountPassKeys(userSA.address))[0].valueOf()
).to.equal(pubX);
expect(
(await passKeyModule.smartAccountPassKeys(userSA.address))[1].valueOf()
).to.equal(pubY);
expect(
(await passKeyModule.smartAccountPassKeys(userSA.address))[2].valueOf()
).to.equal(keyId);
expect(passkeyOwner[0].valueOf()).to.equal(pubX);
expect(passkeyOwner[1].valueOf()).to.equal(pubY);
expect(passkeyOwner[2].valueOf()).to.equal(keyId);

expect(await ethers.provider.getBalance(userSA.address)).to.equal(
ethers.utils.parseEther("10")
);
});

it("Can send a userOp with a default validation module", async () => {
it("Can send a userOp with a passkey validation module", async () => {
const { userSA, entryPoint } = await setupTests();
const charlieBalanceBefore = await ethers.provider.getBalance(
charlie.address
);

const txnDataAA1 = userSA.interface.encodeFunctionData("execute", [
charlie.address,
ethers.utils.parseEther("1"),
"0x",
]);
const userOp1 = await fillAndSign(
const userOp = await fillAndSign(
{
sender: userSA.address,
callData: txnDataAA1,
callGasLimit: 1_000_000,
preVerificationGas: 50000,
verificationGasLimit: 400_000,
},
offchainSigner, // random eoa signing the transaction
entryPoint,
"nonce"
);
userOp1.signature = "";
userOp.signature = "";

const keyHash = keccak256(
ethers.utils.defaultAbiCoder.encode(["string"], ["test"])
);
const chainId = await entryPoint
.provider!.getNetwork()
.then((net) => net.chainId);
const userOpHash = getUserOpHash(userOp, entryPoint.address, chainId);
const clientDataJSONPre = '{"type":"webauthn.get","challenge":"';
const clientDataJSONPost =
'","origin":"https://webauthn.me","crossOrigin":false}';

const userOpDataHashBuffer = Buffer.from(ethers.utils.arrayify(userOpHash));
const opHashBase64 = userOpDataHashBuffer.toString("base64");

const clientDataJSON = `${clientDataJSONPre}${opHashBase64}${clientDataJSONPost}`;

const clientDataBuffer = Buffer.from(clientDataJSON);
const clientHash = ethers.utils.sha256(
ethers.utils.hexlify(clientDataBuffer)
);
// random data, on passkeys generated by the device
const authenticatorData =
"0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000"; // SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA
const authenticatorDataBuffer = Buffer.from(
authenticatorData.slice(2),
"hex"
);
const concatenatedBuffer = Buffer.concat([
authenticatorDataBuffer,
Buffer.from(ethers.utils.arrayify(clientHash)),
]);
// this is final data which gets reconstructed on the contract and verified
const sigHash = ethers.utils.sha256(
ethers.utils.hexlify(concatenatedBuffer)
);
const bn = new BN(BigNumber.from(sigHash).toString());
const sign = keyPair.sign(bn);

const signature = ethers.utils.defaultAbiCoder.encode(
["bytes32", "uint256", "uint256", "bytes", "string", "string"],
[
keyHash,
sign.r.toString(),
sign.s.toString(),
authenticatorData,
clientDataJSONPre,
clientDataJSONPost,
]
);
const signatureWithModuleAddress = ethers.utils.defaultAbiCoder.encode(
["bytes", "address"],
[signature, passKeyModule.address]
);
userOp.signature = signatureWithModuleAddress;

// console.log("userOp", userOp);

await entryPoint.handleOps([userOp], deployer.address);

const charlieBalanceAfter = await ethers.provider.getBalance(
charlie.address
);
expect(charlieBalanceAfter.sub(charlieBalanceBefore)).to.equal(
ethers.utils.parseEther("1")
);
});
});
36 changes: 27 additions & 9 deletions test/module/Secp256r1.specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,15 @@ describe("Secp256r1 tests:", function () {
const p1X = BigNumber.from("4"); // P as the point at infinity
const p1Y = BigNumber.from("8");
const p1Z = BigNumber.from("0");
const p2X = BigNumber.from("5"); // any point Q
const p2Y = BigNumber.from("5");
const p2Z = BigNumber.from("5");
const p2X = BigNumber.from(
"0x150c79a46d10a0a7b5977ce4b1e7f35dad727095655b33a38bfece87c66b5f07"
); // any point Q
const p2Y = BigNumber.from(
"0x7165fee1b0b7ff988cef7a9234527b89814cef0f95a4101ecbd98f1e84e086a9"
);
const p2Z = BigNumber.from(
"0x67f4336965ac19295a42860c44f2724595138978380e171631046a570fb257d8"
);

const [resultX, resultY, resultZ] = await secp256r1.jAdd(
p1X,
Expand All @@ -36,9 +42,15 @@ describe("Secp256r1 tests:", function () {

// px = c^2 * qx,py = c^3 * qy,andpz = c * qz
it("Point addition P + Q where Q is (0) -> (4, 8, 0)", async () => {
const p1X = BigNumber.from("5"); // P is any point
const p1Y = BigNumber.from("5");
const p1Z = BigNumber.from("5");
const p1X = BigNumber.from(
"0x150c79a46d10a0a7b5977ce4b1e7f35dad727095655b33a38bfece87c66b5f07"
); // P is any point
const p1Y = BigNumber.from(
"0x7165fee1b0b7ff988cef7a9234527b89814cef0f95a4101ecbd98f1e84e086a9"
);
const p1Z = BigNumber.from(
"0x67f4336965ac19295a42860c44f2724595138978380e171631046a570fb257d8"
);
const p2X = BigNumber.from("4"); // Q as the point at infinity
const p2Y = BigNumber.from("8");
const p2Z = BigNumber.from("0");
Expand Down Expand Up @@ -78,9 +90,15 @@ describe("Secp256r1 tests:", function () {
});

it("Point addition P + Q where P is equal to Q", async () => {
const pX = 5; // any point on curve if same
const pY = 1;
const pZ = 5;
const pX = BigNumber.from(
"0x150c79a46d10a0a7b5977ce4b1e7f35dad727095655b33a38bfece87c66b5f07"
);
const pY = BigNumber.from(
"0x7165fee1b0b7ff988cef7a9234527b89814cef0f95a4101ecbd98f1e84e086a9"
);
const pZ = BigNumber.from(
"0x67f4336965ac19295a42860c44f2724595138978380e171631046a570fb257d8"
);

const [resultX, resultY, resultZ] = await secp256r1.jAdd(
pX,
Expand Down
Loading

0 comments on commit baf58f7

Please sign in to comment.