Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support multiple passkey per domain #259

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 61 additions & 23 deletions src/validators/WebAuthValidator.sol
Original file line number Diff line number Diff line change
@@ -25,10 +25,13 @@ contract WebAuthValidator is VerifierCaller, IModuleValidator {
bytes32 private constant HIGH_R_MAX = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551;

event PasskeyCreated(address indexed keyOwner, string originDomain);
event PasskeyRemoved(address indexed keyOwner, string originDomain);

// The layout is unusual due to EIP-7562 storage read restrictions for validation phase.
mapping(string originDomain => mapping(address accountAddress => bytes32)) public lowerKeyHalf;
mapping(string originDomain => mapping(address accountAddress => bytes32)) public upperKeyHalf;
mapping(string originDomain => mapping(uint8 index => mapping(address accountAddress => bytes32 keyBytes) keyByteMapping) keyIndexMapping)
public lowerKeyHalf;
mapping(string originDomain => mapping(uint8 index => mapping(address accountAddress => bytes32 keyBytes) keyByteMapping) keyIndexMapping)
public upperKeyHalf;

/// @notice Runs on module install
/// @param data ABI-encoded WebAuthn passkey to add immediately, or empty if not needed
@@ -44,8 +47,8 @@ contract WebAuthValidator is VerifierCaller, IModuleValidator {
string[] memory domains = abi.decode(data, (string[]));
for (uint256 i = 0; i < domains.length; i++) {
string memory domain = domains[i];
lowerKeyHalf[domain][msg.sender] = 0x0;
upperKeyHalf[domain][msg.sender] = 0x0;
lowerKeyHalf[domain][0][msg.sender] = 0x0;
upperKeyHalf[domain][0][msg.sender] = 0x0;
}
}

@@ -54,19 +57,44 @@ contract WebAuthValidator is VerifierCaller, IModuleValidator {
/// @return true if the key was added, false if it was updated
function addValidationKey(bytes calldata key) public returns (bool) {
(bytes32[2] memory key32, string memory originDomain) = abi.decode(key, (bytes32[2], string));
bytes32 initialLowerHalf = lowerKeyHalf[originDomain][msg.sender];
bytes32 initialUpperHalf = upperKeyHalf[originDomain][msg.sender];

// we might want to support multiple passkeys per domain
lowerKeyHalf[originDomain][msg.sender] = key32[0];
upperKeyHalf[originDomain][msg.sender] = key32[1];
bool keyAdded = false;
for (uint8 index = 0; index < 255; index++) {
bytes32 initialLowerHalf = lowerKeyHalf[originDomain][index][msg.sender];
bytes32 initialUpperHalf = upperKeyHalf[originDomain][index][msg.sender];
bool keyExists = initialLowerHalf != 0 && initialUpperHalf != 0;
if (keyExists) {
continue;
}

// we're returning true if this was a new key, false for update
bool keyExists = initialLowerHalf == 0 && initialUpperHalf == 0;
lowerKeyHalf[originDomain][index][msg.sender] = key32[0];
upperKeyHalf[originDomain][index][msg.sender] = key32[1];
keyAdded = true;
break;
}

emit PasskeyCreated(msg.sender, originDomain);

return keyExists;
return keyAdded;
}

/// @notice Removes a WebAuthn passkey for the caller
/// @param originDomain string domain to remove the key from
/// @param index domain to remove the key from
/// @return true if the key was removed, false if it failed
function removeValidationKey(string calldata originDomain, uint8 index) public returns (bool) {
bytes32 initialLowerHalf = lowerKeyHalf[originDomain][index][msg.sender];
bytes32 initialUpperHalf = upperKeyHalf[originDomain][index][msg.sender];
bool noKey = initialLowerHalf == 0 && initialUpperHalf == 0;
if (noKey) {
return false;
}

lowerKeyHalf[originDomain][index][msg.sender] = 0x0;
upperKeyHalf[originDomain][index][msg.sender] = 0x0;

emit PasskeyRemoved(msg.sender, originDomain);

return true;
}

/// @notice Validates a WebAuthn signature
@@ -128,15 +156,6 @@ contract WebAuthValidator is VerifierCaller, IModuleValidator {
return false;
}

string memory origin = root.at('"origin"').value().decodeString();
bytes32[2] memory pubkey;
pubkey[0] = lowerKeyHalf[origin][msg.sender];
pubkey[1] = upperKeyHalf[origin][msg.sender];
// This really only validates the origin is set
if (pubkey[0] == 0 || pubkey[1] == 0) {
return false;
}

JSONParserLib.Item memory crossOriginItem = root.at('"crossOrigin"');
if (!crossOriginItem.isUndefined()) {
string memory crossOrigin = crossOriginItem.value();
@@ -145,8 +164,27 @@ contract WebAuthValidator is VerifierCaller, IModuleValidator {
}
}

string memory origin = root.at('"origin"').value().decodeString();
bytes32[2] memory pubkey;
bytes32 message = _createMessage(authenticatorData, bytes(clientDataJSON));
return callVerifier(P256_VERIFIER, message, rs, pubkey);

// try all possible passkeys for this domain
for (uint8 index = 0; index < 255; index++) {
pubkey[0] = lowerKeyHalf[origin][index][msg.sender];
pubkey[1] = upperKeyHalf[origin][index][msg.sender];
// This really only validates the origin is set
if (pubkey[0] == 0 || pubkey[1] == 0) {
continue;
}

bool keyVerified = callVerifier(P256_VERIFIER, message, rs, pubkey);
if (keyVerified) {
return true;
}
}

// no keys matched
return false;
}

/// @inheritdoc IERC165
256 changes: 163 additions & 93 deletions test/PasskeyModule.ts
Original file line number Diff line number Diff line change
@@ -5,15 +5,15 @@ import { ECDSASigValue } from "@peculiar/asn1-ecc";
import { AsnParser } from "@peculiar/asn1-schema";
import { bigintToBuf, bufToBigint } from "bigint-conversion";
import { assert, expect } from "chai";
import { randomBytes } from "crypto";
import { parseEther, ZeroAddress } from "ethers";
import * as hre from "hardhat";
import { SmartAccount, Wallet } from "zksync-ethers";
import { encodeAbiParameters, Hex, hexToBytes, toHex } from "viem";
import { Provider, SmartAccount, Wallet } from "zksync-ethers";
import { base64UrlToUint8Array } from "zksync-sso/utils";

import { SsoAccount__factory, WebAuthValidator, WebAuthValidator__factory } from "../typechain-types";
import { ContractFixtures, getProvider, getWallet, LOCAL_RICH_WALLETS, logInfo, RecordedResponse } from "./utils";
import { base64UrlToUint8Array } from "zksync-sso/utils";
import { encodeAbiParameters, Hex, hexToBytes, toHex } from "viem";
import { randomBytes } from "crypto";
import { parseEther, ZeroAddress } from "ethers";

/**
* Decode from a Base64URL-encoded string to an ArrayBuffer. Best used when converting a
@@ -337,11 +337,7 @@ function encodeFatSignature(
authenticatorData: string;
clientDataJSON: string;
signature: string;
},
contracts: {
passkey: string;
},
) {
}) {
const signature = unwrapEC2Signature(base64UrlToUint8Array(passkeyResponse.signature));
return encodeAbiParameters(
[
@@ -376,12 +372,13 @@ async function rawVerify(
async function verifyKeyStorage(
passkeyValidator: WebAuthValidator,
domain: string,
index: number,
publicKeys,
wallet: Wallet,
accountAddress: string,
error: string,
) {
const lowerKey = await passkeyValidator.lowerKeyHalf(domain, wallet.address);
const upperKey = await passkeyValidator.upperKeyHalf(domain, wallet.address);
const lowerKey = await passkeyValidator.lowerKeyHalf(domain, index, accountAddress);
const upperKey = await passkeyValidator.upperKeyHalf(domain, index, accountAddress);
expect(lowerKey).to.eq(publicKeys[0], `lower key ${error}`);
expect(upperKey).to.eq(publicKeys[1], `upper key ${error}`);
}
@@ -390,17 +387,30 @@ function encodeKeyFromHex(hexStrings: [Hex, Hex], domain: string) {
// the same as the ethers: new AbiCoder().encode(["bytes32[2]", "string"], [bytes, domain]);
return encodeAbiParameters(
[
{ name: 'publicKeys', type: 'bytes32[2]' },
{ name: 'domain', type: 'string' },
{ name: "publicKeys", type: "bytes32[2]" },
{ name: "domain", type: "string" },
],
[[hexStrings[0], hexStrings[1]], domain]
)
[[hexStrings[0], hexStrings[1]], domain],
);
}

function encodeKeyFromBytes(bytes: [Uint8Array, Uint8Array], domain: string) {
return encodeKeyFromHex([toHex(bytes[0]), toHex(bytes[1])], domain);
}

async function addNewWebAuthnKey(wallet: Wallet, keyDomain: string) {
const passkeyValidator = await deployValidator(wallet);
const generatedR1Key = await generateES256R1Key();
assert(generatedR1Key != null, "no key was generated");
const [generatedX, generatedY] = await getRawPublicKeyFromCrpyto(generatedR1Key);
const generatedKey = encodeKeyFromBytes([generatedX, generatedY], keyDomain);
const addingKey = await passkeyValidator.addValidationKey(generatedKey);
const addingKeyResult = await addingKey.wait();
expect(addingKeyResult?.status).to.eq(1, "failed to add key during setup");

return { generatedR1Key, passkeyValidator };
}

async function validateSignatureTest(
wallet: Wallet,
keyDomain: string,
@@ -410,14 +420,7 @@ async function validateSignatureTest(
sampleClientString: string,
transactionHash: Buffer,
) {
const passkeyValidator = await deployValidator(wallet);
const generatedR1Key = await generateES256R1Key();
assert(generatedR1Key != null, "no key was generated");
const [generatedX, generatedY] = await getRawPublicKeyFromCrpyto(generatedR1Key);
const generatedKey = encodeKeyFromBytes([generatedX, generatedY], keyDomain);
const addingKey = await passkeyValidator.addValidationKey(generatedKey);
const addingKeyResult = await addingKey.wait();
expect(addingKeyResult?.status).to.eq(1, "failed to add key during setup");
const { generatedR1Key, passkeyValidator } = await addNewWebAuthnKey(wallet, keyDomain);

const sampleClientBuffer = Buffer.from(sampleClientString);
const partiallyHashedData = concat([authData, await toHash(sampleClientBuffer)]);
@@ -428,11 +431,64 @@ async function validateSignatureTest(
{ name: "clientDataJson", type: "string" },
{ name: "rs", type: "bytes32[2]" },
],
[toHex(authData), sampleClientString, [toHex(rNormalization(generatedSignature.r)), toHex(sNormalization(generatedSignature.s))]]
)
[toHex(authData), sampleClientString, [toHex(rNormalization(generatedSignature.r)), toHex(sNormalization(generatedSignature.s))]],
);
return await passkeyValidator.validateSignature(transactionHash, fatSignature);
}

async function addPasskey(index: number, passKeyModuleAddress: string, proxyAccountAddress: string, r1Key: CryptoKeyPair, sampleDomain: string, wallet: Wallet, provider: Provider) {
const passkeyValidator = WebAuthValidator__factory.connect(passKeyModuleAddress, new SmartAccount({ address: proxyAccountAddress, secret: wallet.privateKey }, provider));
assert(r1Key != null, "no key was generated");
const [generatedX, generatedY] = await getRawPublicKeyFromCrpyto(r1Key);
const initPasskeyData = encodeKeyFromBytes([generatedX, generatedY], sampleDomain);
const addedSecondPasskey = await passkeyValidator.addValidationKey(initPasskeyData);
const secondRecipt = await addedSecondPasskey.wait();
expect(secondRecipt?.status).to.eq(1, "second key added");
await verifyKeyStorage(passkeyValidator, sampleDomain, index, [toHex(generatedX), toHex(generatedY)], proxyAccountAddress, "second key");
}

async function createSmartAccount(
sampleDomain: string,
r1Key: CryptoKeyPair,
authData: Uint8Array,
passKeyModuleAddress: Hex,
proxyAccountAddress: string,
wallet: Wallet,
provider: Provider,
) {
return new SmartAccount({
payloadSigner: async (hash: Hex) => {
const sampleClientObject = {
type: "webauthn.get",
challenge: fromBuffer(hexToBytes(hash)),
origin: sampleDomain,
crossOrigin: false,
};
const sampleClientString = JSON.stringify(sampleClientObject);
const sampleClientBuffer = Buffer.from(sampleClientString);
const partiallyHashedData = concat([authData, await toHash(sampleClientBuffer)]);
const generatedSignature = await signStringWithR1Key(r1Key.privateKey, partiallyHashedData);
assert(generatedSignature != null, "no signature generated");
const fatSignature = encodeAbiParameters([
{ name: "authData", type: "bytes" },
{ name: "clientDataJson", type: "string" },
{ name: "rs", type: "bytes32[2]" },
], [
toHex(authData),
sampleClientString,
[toHex(normalizeR(generatedSignature.r)), toHex(normalizeS(generatedSignature.s))],
]);

const moduleSignature = encodeAbiParameters(
[{ name: "signature", type: "bytes" }, { name: "moduleAddress", type: "address" }, { name: "validatorData", type: "bytes" }],
[fatSignature, passKeyModuleAddress, "0x"]);
return moduleSignature;
},
address: proxyAccountAddress,
secret: wallet.privateKey, // generatedR1Key.privateKey,
}, provider);
}

describe("Passkey validation", function () {
const wallet = getWallet(LOCAL_RICH_WALLETS[0].privateKey);
const ethersResponse = new RecordedResponse("test/signed-challenge.json");
@@ -446,6 +502,19 @@ describe("Passkey validation", function () {
describe("account integration", () => {
const fixtures = new ContractFixtures();
const provider = getProvider();
async function createTx(proxyAccountAddress: string) {
return {
to: wallet.address,
type: 113,
from: proxyAccountAddress,
data: "0x",
value: 0,
chainId: (await provider.getNetwork()).chainId,
nonce: await provider.getTransactionCount(proxyAccountAddress),
gasPrice: await provider.getGasPrice(),
gasLimit: 100_000_000n,
};
}

async function deployAccount() {
const factoryContract = await fixtures.getAaFactory();
@@ -480,17 +549,17 @@ describe("Passkey validation", function () {
const receipt = await fundTx.wait();
expect(receipt.status).to.eq(1, "send funds to proxy account");

return { passKeyModuleContract, sampleDomain, proxyAccountAddress, generatedR1Key, passKeyModuleAddress }
return { passKeyModuleContract, sampleDomain, proxyAccountAddress, generatedR1Key, passKeyModuleAddress };
}

it("should deploy proxy account via factory", async () => {
const { passKeyModuleContract, sampleDomain, proxyAccountAddress, generatedR1Key, passKeyModuleAddress } = await deployAccount();

const [generatedX, generatedY] = await getRawPublicKeyFromCrpyto(generatedR1Key);

const initLowerKey = await passKeyModuleContract.lowerKeyHalf(sampleDomain, proxyAccountAddress);
const initLowerKey = await passKeyModuleContract.lowerKeyHalf(sampleDomain, 0, proxyAccountAddress);
expect(initLowerKey).to.equal(toHex(generatedX), "initial lower key should exist");
const initUpperKey = await passKeyModuleContract.upperKeyHalf(sampleDomain, proxyAccountAddress);
const initUpperKey = await passKeyModuleContract.upperKeyHalf(sampleDomain, 0, proxyAccountAddress);
expect(initUpperKey).to.equal(toHex(generatedY), "initial upper key should exist");

const account = SsoAccount__factory.connect(proxyAccountAddress, provider);
@@ -503,55 +572,33 @@ describe("Passkey validation", function () {
const authData = toBuffer(ethersResponse.authenticatorData);
const { sampleDomain, proxyAccountAddress, generatedR1Key, passKeyModuleAddress } = await deployAccount();

const sessionAccount = new SmartAccount({
payloadSigner: async (hash: Hex) => {
const sampleClientObject = {
type: "webauthn.get",
challenge: fromBuffer(hexToBytes(hash)),
origin: sampleDomain,
crossOrigin: false,
};
const sampleClientString = JSON.stringify(sampleClientObject);
const sampleClientBuffer = Buffer.from(sampleClientString);
const partiallyHashedData = concat([authData, await toHash(sampleClientBuffer)]);
const generatedSignature = await signStringWithR1Key(generatedR1Key.privateKey, partiallyHashedData);
assert(generatedSignature != null, "no signature generated");
const fatSignature = encodeAbiParameters([
{ name: "authData", type: "bytes" },
{ name: "clientDataJson", type: "string" },
{ name: "rs", type: "bytes32[2]" },
], [
toHex(authData),
sampleClientString,
[toHex(normalizeR(generatedSignature.r)), toHex(normalizeS(generatedSignature.s))]
])

const moduleSignature = encodeAbiParameters(
[{ name: "signature", type: "bytes" }, { name: "moduleAddress", type: "address" }, { name: "validatorData", type: "bytes" }],
[fatSignature, passKeyModuleAddress, "0x"]);
return moduleSignature;
},
address: proxyAccountAddress,
secret: wallet.privateKey, //generatedR1Key.privateKey,
}, provider);
const sessionAccount = await createSmartAccount(sampleDomain, generatedR1Key, authData, passKeyModuleAddress, proxyAccountAddress, wallet, provider);
const aaTransaction = await createTx(proxyAccountAddress);

const aaTransaction = {
to: wallet.address,
type: 113,
from: proxyAccountAddress,
data: "0x",
value: 0,
chainId: (await provider.getNetwork()).chainId,
nonce: await provider.getTransactionCount(proxyAccountAddress),
gasPrice: await provider.getGasPrice(),
gasLimit: 100_000_000n,
};
const signedTransaction = await sessionAccount.signTransaction(aaTransaction);
const transactionResponse = await provider.broadcastTransaction(signedTransaction);
const transactionReceipt = await transactionResponse.wait();
expect(transactionReceipt.status).to.eq(1, "transaction should be successful");
logInfo(`passkey transaction gas used: ${transactionReceipt.gasUsed.toString()}`);
});

it("should sign transaction with second passkey", async () => {
const authData = toBuffer(ethersResponse.authenticatorData);
const { sampleDomain, proxyAccountAddress, passKeyModuleAddress } = await deployAccount();

// can add more passkeys here to measure per-gas increase
const lastPasskey = await generateES256R1Key();
await addPasskey(1, passKeyModuleAddress, proxyAccountAddress, lastPasskey, sampleDomain, wallet, provider);

const sessionAccount = await createSmartAccount(sampleDomain, lastPasskey, authData, passKeyModuleAddress, proxyAccountAddress, wallet, provider);

const aaTransaction = await createTx(proxyAccountAddress);

const signedTransaction = await sessionAccount.signTransaction(aaTransaction);
const transactionResponse = await provider.broadcastTransaction(signedTransaction);
const transactionReceipt = await transactionResponse.wait();
expect(transactionReceipt.status).to.eq(1, "transaction should be successful");
logInfo(`passkey transaction gas used: ${transactionReceipt?.gasUsed.toString()}`);
logInfo(`passkey transaction gas used: ${transactionReceipt.gasUsed.toString()}`);
});
});

@@ -592,11 +639,11 @@ describe("Passkey validation", function () {
const keyReceipt = await secondCreatedKey.wait();
assert(keyReceipt?.status == 1, "second key was saved");

await verifyKeyStorage(passkeyValidator, firstDomain, publicKeys, wallet, "first domain");
await verifyKeyStorage(passkeyValidator, secondDomain, publicKeys, wallet, "second domain");
await verifyKeyStorage(passkeyValidator, firstDomain, 0, publicKeys, wallet.address, "first domain");
await verifyKeyStorage(passkeyValidator, secondDomain, 0, publicKeys, wallet.address, "second domain");
});

it("should update existing key", async () => {
it("should add second key to the same domain", async () => {
const passkeyValidator = await deployValidator(wallet);
const keyDomain = randomBytes(32).toString("hex");
const generatedR1Key = await generateES256R1Key();
@@ -607,7 +654,7 @@ describe("Passkey validation", function () {
const receipt = await generatedKeyAdded.wait();
assert(receipt?.status == 1, "generated key added");

await verifyKeyStorage(passkeyValidator, keyDomain, [toHex(generatedX), toHex(generatedY)], wallet, "first key");
await verifyKeyStorage(passkeyValidator, keyDomain, 0, [toHex(generatedX), toHex(generatedY)], wallet.address, "first key");

const nextR1Key = await generateES256R1Key();
assert(nextR1Key != null, "no second key was generated");
@@ -617,7 +664,8 @@ describe("Passkey validation", function () {
const newReceipt = await nextKeyAdded.wait();
assert(newReceipt?.status == 1, "new generated key added");

await verifyKeyStorage(passkeyValidator, keyDomain, [toHex(newX), toHex(newY)], wallet, "updated key");
await verifyKeyStorage(passkeyValidator, keyDomain, 0, [toHex(generatedX), toHex(generatedY)], wallet.address, "first key");
await verifyKeyStorage(passkeyValidator, keyDomain, 1, [toHex(newX), toHex(newY)], wallet.address, "updated key");
});

it("should allow clearing existing key", async () => {
@@ -630,15 +678,14 @@ describe("Passkey validation", function () {
const generatedKeyAdded = await passkeyValidator.addValidationKey(generatedKey);
const receipt = await generatedKeyAdded.wait();
assert(receipt?.status == 1, "generated key added");
await verifyKeyStorage(passkeyValidator, keyDomain, [toHex(generatedX), toHex(generatedY)], wallet, "added");
await verifyKeyStorage(passkeyValidator, keyDomain, 0, [toHex(generatedX), toHex(generatedY)], wallet.address, "added");

const zeroKey = new Uint8Array(32).fill(0);
const emptyKey = encodeKeyFromBytes([zeroKey, zeroKey], keyDomain);
const emptyKeyAdded = await passkeyValidator.addValidationKey(emptyKey);
const emptyKeyAdded = await passkeyValidator.removeValidationKey(keyDomain, 0);
const emptyReceipt = await emptyKeyAdded.wait();
assert(emptyReceipt?.status == 1, "empty key added");

await verifyKeyStorage(passkeyValidator, keyDomain, [toHex(zeroKey), toHex(zeroKey)], wallet, "key removed");
await verifyKeyStorage(passkeyValidator, keyDomain, 0, [toHex(zeroKey), toHex(zeroKey)], wallet.address, "key removed");
});
});

@@ -653,7 +700,6 @@ describe("Passkey validation", function () {
clientDataJSON: ethersResponse.clientData,
signature: ethersResponse.b64SignedChallenge,
},
{ passkey: publicKeys[0] },
);

const initData = encodeKeyFromHex(publicKeys, "http://localhost:5173");
@@ -675,10 +721,10 @@ describe("Passkey validation", function () {

// 37 bytes
const authenticatorData = "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABQ";
const clientData =
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZFhPM3ctdWdycS00SkdkZUJLNDFsZFk1V2lNd0ZORDkiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUxNzMiLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2FuX2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ";
const b64SignedChallenge =
"MEUCIQCYrSUCR_QUPAhvRNUVfYiJC2JlOKuqf4gx7i129n9QxgIgaY19A9vAAObuTQNs5_V9kZFizwRpUFpiRVW_dglpR2A";
const clientData
= "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZFhPM3ctdWdycS00SkdkZUJLNDFsZFk1V2lNd0ZORDkiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUxNzMiLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2FuX2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ";
const b64SignedChallenge
= "MEUCIQCYrSUCR_QUPAhvRNUVfYiJC2JlOKuqf4gx7i129n9QxgIgaY19A9vAAObuTQNs5_V9kZFizwRpUFpiRVW_dglpR2A";

const verifyMessage = await rawVerify(
passkeyValidator,
@@ -757,8 +803,8 @@ describe("Passkey validation", function () {
it("should fail when signature is bad", async function () {
const passkeyValidator = await deployValidator(wallet);

const b64SignedChallenge =
"MEUCIQCYrSUCR_QUPAhvRNUVfYiJC2JlOKuqf4gx7i129n9QxgIgaY19A9vAAObuTQNs5_V9kZFizwRpUFpiRVW_dglpR2A";
const b64SignedChallenge
= "MEUCIQCYrSUCR_QUPAhvRNUVfYiJC2JlOKuqf4gx7i129n9QxgIgaY19A9vAAObuTQNs5_V9kZFizwRpUFpiRVW_dglpR2A";
const verifyMessage = await rawVerify(
passkeyValidator,
ethersResponse.authenticatorData,
@@ -795,6 +841,30 @@ describe("Passkey validation", function () {
assert(isValidSignature, "valid signature");
});

it("should verify signature from the 2nd passkey", async () => {
const keyDomain = randomBytes(32).toString("hex");
const sampleClientObject = {
type: "webauthn.get",
challenge: "iBBiiOGt1aSBy1WAuRGxqU7YzRM5oWpMA9g8MKydjPI",
origin: keyDomain,
crossOrigin: false,
};
const sampleClientString = JSON.stringify(sampleClientObject);
const authData = toBuffer(ethersResponse.authenticatorData);
const transactionHash = Buffer.from(sampleClientObject.challenge, "base64url");
await addNewWebAuthnKey(wallet, keyDomain);
const isValidSignature = await validateSignatureTest(
wallet,
keyDomain,
authData,
normalizeS,
normalizeR,
sampleClientString,
transactionHash,
);
assert(isValidSignature, "valid signature");
});

it("should verify a signature without cross-origin set", async () => {
const keyDomain = randomBytes(32).toString("hex");
const sampleClientObject = {
@@ -899,10 +969,10 @@ describe("Passkey validation", function () {
const partialClientObject = {
challenge: "jBBiiOGt1aSBy1WAuRGxqU7YzRM5oWpMA9g8MKydjPI",
};
const duplicatedClientString =
JSON.stringify(sampleClientObject).slice(0, -1) +
"," +
JSON.stringify(partialClientObject).slice(1);
const duplicatedClientString
= JSON.stringify(sampleClientObject).slice(0, -1)
+ ","
+ JSON.stringify(partialClientObject).slice(1);
const authData = toBuffer(ethersResponse.authenticatorData);
const transactionHash = Buffer.from(sampleClientObject.challenge, "base64url");
const isValidSignature = await validateSignatureTest(
26 changes: 12 additions & 14 deletions test/utils.ts
Original file line number Diff line number Diff line change
@@ -6,31 +6,29 @@ import { ethers, parseEther, randomBytes } from "ethers";
import { readFileSync } from "fs";
import { promises } from "fs";
import * as hre from "hardhat";
import { Address, isHex, toHex } from "viem";
import { ContractFactory, Provider, utils, Wallet } from "zksync-ethers";
import { base64UrlToUint8Array, getPublicKeyBytesFromPasskeySignature, unwrapEC2Signature } from "zksync-sso/utils";
import { Address, isHex, toHex } from "viem";

import type {
AAFactory,
AccountProxy,
ERC20,
ExampleAuthServerPaymaster,
SessionKeyValidator,
SsoAccount,
WebAuthValidator,
SsoBeacon,
AccountProxy
} from "../typechain-types";
WebAuthValidator } from "../typechain-types";
import {
AAFactory__factory,
AccountProxy__factory,
ERC20__factory,
ExampleAuthServerPaymaster__factory,
SessionKeyValidator__factory,
SsoAccount__factory,
WebAuthValidator__factory,
SsoBeacon__factory,
TestPaymaster__factory
} from "../typechain-types";
TestPaymaster__factory,
WebAuthValidator__factory } from "../typechain-types";

export const ethersStaticSalt = new Uint8Array([
205, 241, 161, 186, 101, 105, 79,
@@ -95,7 +93,7 @@ export class ContractFixtures {

async getPasskeyModuleAddress(): Promise<Address> {
const webAuthnVerifierContract = await this.getWebAuthnVerifierContract();
const contractAddress = await webAuthnVerifierContract.getAddress()
const contractAddress = await webAuthnVerifierContract.getAddress();
return isHex(contractAddress) ? contractAddress : toHex(contractAddress);
}

@@ -309,13 +307,13 @@ const masterWallet = ethers.Wallet.fromPhrase("stuff slice staff easily soup par
export const LOCAL_RICH_WALLETS = [
hre.network.name == "dockerizedNode"
? {
address: masterWallet.address,
privateKey: masterWallet.privateKey,
}
address: masterWallet.address,
privateKey: masterWallet.privateKey,
}
: {
address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
},
address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
},
{
address: "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049",
privateKey: "0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110",