Skip to content

Commit 5ec5f86

Browse files
committed
chore: add more comments to webauthn verify
Also move the test function to a test contract
1 parent ff8273d commit 5ec5f86

File tree

3 files changed

+75
-40
lines changed

3 files changed

+75
-40
lines changed

src/test/TestWebAuthValidator.sol

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
import { WebAuthValidator } from "../validators/WebAuthValidator.sol";
5+
6+
contract WebAuthValidatorTest is WebAuthValidator {
7+
/// @notice Verifies a message using the P256 curve.
8+
/// @dev Useful for testing the P256 precompile
9+
/// @param message The sha256 hash of the authenticator hash and hashed client data
10+
/// @param rs The signature to validate (r, s) from the signed message
11+
/// @param pubKey The public key to validate the signature against (x, y)
12+
/// @return valid true if the signature is valid
13+
function p256Verify(
14+
bytes32 message,
15+
bytes32[2] calldata rs,
16+
bytes32[2] calldata pubKey
17+
) external view returns (bool valid) {
18+
valid = rawVerify(message, rs, pubKey);
19+
}
20+
}

src/validators/WebAuthValidator.sol

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ contract WebAuthValidator is VerifierCaller, IModuleValidator {
2020
using JSONParserLib for string;
2121

2222
address private constant P256_VERIFIER = address(0x100);
23+
24+
// check for secure validation: bit 0 = 1 (user present), bit 2 = 1 (user verified)
2325
bytes1 private constant AUTH_DATA_MASK = 0x05;
2426
bytes32 private constant LOW_S_MAX = 0x7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a8;
2527
bytes32 private constant HIGH_R_MAX = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551;
@@ -103,17 +105,19 @@ contract WebAuthValidator is VerifierCaller, IModuleValidator {
103105
fatSignature
104106
);
105107

108+
// prevent signature replay https://yondon.blog/2019/01/01/how-not-to-use-ecdsa/
106109
if (rs[0] <= 0 || rs[0] > HIGH_R_MAX || rs[1] <= 0 || rs[1] > LOW_S_MAX) {
107110
return false;
108111
}
109112

110-
// check if the flags are set
113+
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API/Authenticator_data#attestedcredentialdata
111114
if (authenticatorData[32] & AUTH_DATA_MASK != AUTH_DATA_MASK) {
112115
return false;
113116
}
114117

115-
// parse out the important fields (type, challenge, origin, crossOrigin): https://goo.gl/yabPex
118+
// parse out the required fields (type, challenge, crossOrigin): https://goo.gl/yabPex
116119
JSONParserLib.Item memory root = JSONParserLib.parse(clientDataJSON);
120+
// challenge should contain the transaction hash, ensuring that the transaction is signed
117121
string memory challenge = root.at('"challenge"').value().decodeString();
118122
bytes memory challengeData = Base64.decode(challenge);
119123
if (challengeData.length != 32) {
@@ -123,11 +127,14 @@ contract WebAuthValidator is VerifierCaller, IModuleValidator {
123127
return false;
124128
}
125129

130+
// type ensures the signature was created from a validation request
126131
string memory type_ = root.at('"type"').value().decodeString();
127132
if (!Strings.equal("webauthn.get", type_)) {
128133
return false;
129134
}
130135

136+
// the origin determines which key to validate against
137+
// as passkeys are linked to domains, so the storage mapping reflects that
131138
string memory origin = root.at('"origin"').value().decodeString();
132139
bytes32[2] memory pubkey;
133140
pubkey[0] = lowerKeyHalf[origin][msg.sender];
@@ -137,6 +144,10 @@ contract WebAuthValidator is VerifierCaller, IModuleValidator {
137144
return false;
138145
}
139146

147+
// cross-origin validation is optional, but explicitly not supported.
148+
// cross-origin requests would be from embedding the auth request
149+
// from another domain. The current SSO setup uses a pop-up instead of
150+
// an i-frame, so we're rejecting these until the implemention supports it
140151
JSONParserLib.Item memory crossOriginItem = root.at('"crossOrigin"');
141152
if (!crossOriginItem.isUndefined()) {
142153
string memory crossOrigin = crossOriginItem.value();
@@ -181,7 +192,7 @@ contract WebAuthValidator is VerifierCaller, IModuleValidator {
181192
bytes32 message,
182193
bytes32[2] calldata rs,
183194
bytes32[2] calldata pubKey
184-
) external view returns (bool valid) {
195+
) internal view returns (bool valid) {
185196
valid = callVerifier(P256_VERIFIER, message, rs, pubKey);
186197
}
187198
}

test/PasskeyModule.ts

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ import { ECDSASigValue } from "@peculiar/asn1-ecc";
55
import { AsnParser } from "@peculiar/asn1-schema";
66
import { bigintToBuf, bufToBigint } from "bigint-conversion";
77
import { assert, expect } from "chai";
8+
import { randomBytes } from "crypto";
9+
import { parseEther, ZeroAddress } from "ethers";
810
import * as hre from "hardhat";
11+
import { encodeAbiParameters, Hex, hexToBytes, toHex } from "viem";
912
import { SmartAccount, Wallet } from "zksync-ethers";
13+
import { base64UrlToUint8Array } from "zksync-sso/utils";
1014

11-
import { SsoAccount__factory, WebAuthValidator, WebAuthValidator__factory } from "../typechain-types";
15+
import { SsoAccount__factory, WebAuthValidator, WebAuthValidator__factory, WebAuthValidatorTest, WebAuthValidatorTest__factory } from "../typechain-types";
1216
import { ContractFixtures, getProvider, getWallet, LOCAL_RICH_WALLETS, logInfo, RecordedResponse } from "./utils";
13-
import { base64UrlToUint8Array } from "zksync-sso/utils";
14-
import { encodeAbiParameters, Hex, hexToBytes, toHex } from "viem";
15-
import { randomBytes } from "crypto";
16-
import { parseEther, ZeroAddress } from "ethers";
1717

1818
/**
1919
* Decode from a Base64URL-encoded string to an ArrayBuffer. Best used when converting a
@@ -36,6 +36,14 @@ async function deployValidator(wallet: Wallet): Promise<WebAuthValidator> {
3636
return WebAuthValidator__factory.connect(await validator.getAddress(), wallet);
3737
}
3838

39+
async function deployP256Tester(wallet: Wallet): Promise<WebAuthValidatorTest> {
40+
const deployer: Deployer = new Deployer(hre, wallet);
41+
const passkeyValidatorArtifact = await deployer.loadArtifact("WebAuthValidatorTest");
42+
43+
const validator = await deployer.deploy(passkeyValidatorArtifact, []);
44+
return WebAuthValidatorTest__factory.connect(await validator.getAddress(), wallet);
45+
}
46+
3947
/**
4048
* COSE Keys
4149
*
@@ -338,9 +346,6 @@ function encodeFatSignature(
338346
clientDataJSON: string;
339347
signature: string;
340348
},
341-
contracts: {
342-
passkey: string;
343-
},
344349
) {
345350
const signature = unwrapEC2Signature(base64UrlToUint8Array(passkeyResponse.signature));
346351
return encodeAbiParameters(
@@ -358,7 +363,7 @@ function encodeFatSignature(
358363
}
359364

360365
async function rawVerify(
361-
passkeyValidator: WebAuthValidator,
366+
passkeyValidator: WebAuthValidatorTest,
362367
authenticatorData: string,
363368
clientData: string,
364369
b64SignedChallange: string,
@@ -370,7 +375,7 @@ async function rawVerify(
370375
const rs = unwrapEC2Signature(toBuffer(b64SignedChallange));
371376
const publicKeys = await getPublicKey(publicKeyEs256Bytes);
372377

373-
return await passkeyValidator.rawVerify(hashedData, rs, publicKeys);
378+
return await passkeyValidator.p256Verify(hashedData, rs, publicKeys);
374379
}
375380

376381
async function verifyKeyStorage(
@@ -390,11 +395,11 @@ function encodeKeyFromHex(hexStrings: [Hex, Hex], domain: string) {
390395
// the same as the ethers: new AbiCoder().encode(["bytes32[2]", "string"], [bytes, domain]);
391396
return encodeAbiParameters(
392397
[
393-
{ name: 'publicKeys', type: 'bytes32[2]' },
394-
{ name: 'domain', type: 'string' },
398+
{ name: "publicKeys", type: "bytes32[2]" },
399+
{ name: "domain", type: "string" },
395400
],
396-
[hexStrings, domain]
397-
)
401+
[hexStrings, domain],
402+
);
398403
}
399404

400405
function encodeKeyFromBytes(bytes: [Uint8Array, Uint8Array], domain: string) {
@@ -428,8 +433,8 @@ async function validateSignatureTest(
428433
{ name: "clientDataJson", type: "string" },
429434
{ name: "rs", type: "bytes32[2]" },
430435
],
431-
[toHex(authData), sampleClientString, [toHex(rNormalization(generatedSignature.r)), toHex(sNormalization(generatedSignature.s))]]
432-
)
436+
[toHex(authData), sampleClientString, [toHex(rNormalization(generatedSignature.r)), toHex(sNormalization(generatedSignature.s))]],
437+
);
433438
return await passkeyValidator.validateSignature(transactionHash, fatSignature);
434439
}
435440

@@ -480,7 +485,7 @@ describe("Passkey validation", function () {
480485
const receipt = await fundTx.wait();
481486
expect(receipt.status).to.eq(1, "send funds to proxy account");
482487

483-
return { passKeyModuleContract, sampleDomain, proxyAccountAddress, generatedR1Key, passKeyModuleAddress }
488+
return { passKeyModuleContract, sampleDomain, proxyAccountAddress, generatedR1Key, passKeyModuleAddress };
484489
}
485490

486491
it("should deploy proxy account via factory", async () => {
@@ -523,16 +528,16 @@ describe("Passkey validation", function () {
523528
], [
524529
toHex(authData),
525530
sampleClientString,
526-
[toHex(normalizeR(generatedSignature.r)), toHex(normalizeS(generatedSignature.s))]
527-
])
531+
[toHex(normalizeR(generatedSignature.r)), toHex(normalizeS(generatedSignature.s))],
532+
]);
528533

529534
const moduleSignature = encodeAbiParameters(
530535
[{ name: "signature", type: "bytes" }, { name: "moduleAddress", type: "address" }, { name: "validatorData", type: "bytes" }],
531536
[fatSignature, passKeyModuleAddress, "0x"]);
532537
return moduleSignature;
533538
},
534539
address: proxyAccountAddress,
535-
secret: wallet.privateKey, //generatedR1Key.privateKey,
540+
secret: wallet.privateKey, // generatedR1Key.privateKey,
536541
}, provider);
537542

538543
const aaTransaction = {
@@ -653,7 +658,6 @@ describe("Passkey validation", function () {
653658
clientDataJSON: ethersResponse.clientData,
654659
signature: ethersResponse.b64SignedChallenge,
655660
},
656-
{ passkey: publicKeys[0] },
657661
);
658662

659663
const initData = encodeKeyFromHex(publicKeys, "http://localhost:5173");
@@ -671,14 +675,14 @@ describe("Passkey validation", function () {
671675
// fully expand the raw validation to compare step by step
672676
describe("P256 precompile comparison", () => {
673677
it("should verify passkey", async function () {
674-
const passkeyValidator = await deployValidator(wallet);
678+
const passkeyValidator = await deployP256Tester(wallet);
675679

676680
// 37 bytes
677681
const authenticatorData = "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABQ";
678-
const clientData =
679-
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZFhPM3ctdWdycS00SkdkZUJLNDFsZFk1V2lNd0ZORDkiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUxNzMiLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2FuX2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ";
680-
const b64SignedChallenge =
681-
"MEUCIQCYrSUCR_QUPAhvRNUVfYiJC2JlOKuqf4gx7i129n9QxgIgaY19A9vAAObuTQNs5_V9kZFizwRpUFpiRVW_dglpR2A";
682+
const clientData
683+
= "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZFhPM3ctdWdycS00SkdkZUJLNDFsZFk1V2lNd0ZORDkiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUxNzMiLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2FuX2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ";
684+
const b64SignedChallenge
685+
= "MEUCIQCYrSUCR_QUPAhvRNUVfYiJC2JlOKuqf4gx7i129n9QxgIgaY19A9vAAObuTQNs5_V9kZFizwRpUFpiRVW_dglpR2A";
682686

683687
const verifyMessage = await rawVerify(
684688
passkeyValidator,
@@ -691,7 +695,7 @@ describe("Passkey validation", function () {
691695
assert(verifyMessage == true, "valid sig");
692696
});
693697
it("should sign with new data", async function () {
694-
const passkeyValidator = await deployValidator(wallet);
698+
const passkeyValidator = await deployP256Tester(wallet);
695699
// The precompile expects the fully hashed data
696700
const preHashedData = await toHash(
697701
concat([toBuffer(ethersResponse.authenticatorData), await toHash(toBuffer(ethersResponse.clientData))]),
@@ -718,7 +722,7 @@ describe("Passkey validation", function () {
718722
[generatedSignature.r, generatedSignature.s],
719723
[generatedX, generatedY],
720724
);
721-
const onChainGeneratedVerified = await passkeyValidator.rawVerify(
725+
const onChainGeneratedVerified = await passkeyValidator.p256Verify(
722726
preHashedData,
723727
[generatedSignature.r, generatedSignature.s],
724728
[generatedX, generatedY],
@@ -728,7 +732,7 @@ describe("Passkey validation", function () {
728732
[recordedR, recordedS],
729733
[recordedX, recordedY],
730734
);
731-
const onChainRecordedVerified = await passkeyValidator.rawVerify(
735+
const onChainRecordedVerified = await passkeyValidator.p256Verify(
732736
preHashedData,
733737
[recordedR, recordedS],
734738
[recordedX, recordedY],
@@ -741,7 +745,7 @@ describe("Passkey validation", function () {
741745
});
742746

743747
it("should verify other test passkey data", async function () {
744-
const passkeyValidator = await deployValidator(wallet);
748+
const passkeyValidator = await deployP256Tester(wallet);
745749

746750
const verifyMessage = await rawVerify(
747751
passkeyValidator,
@@ -755,10 +759,10 @@ describe("Passkey validation", function () {
755759
});
756760

757761
it("should fail when signature is bad", async function () {
758-
const passkeyValidator = await deployValidator(wallet);
762+
const passkeyValidator = await deployP256Tester(wallet);
759763

760-
const b64SignedChallenge =
761-
"MEUCIQCYrSUCR_QUPAhvRNUVfYiJC2JlOKuqf4gx7i129n9QxgIgaY19A9vAAObuTQNs5_V9kZFizwRpUFpiRVW_dglpR2A";
764+
const b64SignedChallenge
765+
= "MEUCIQCYrSUCR_QUPAhvRNUVfYiJC2JlOKuqf4gx7i129n9QxgIgaY19A9vAAObuTQNs5_V9kZFizwRpUFpiRVW_dglpR2A";
762766
const verifyMessage = await rawVerify(
763767
passkeyValidator,
764768
ethersResponse.authenticatorData,
@@ -899,10 +903,10 @@ describe("Passkey validation", function () {
899903
const partialClientObject = {
900904
challenge: "jBBiiOGt1aSBy1WAuRGxqU7YzRM5oWpMA9g8MKydjPI",
901905
};
902-
const duplicatedClientString =
903-
JSON.stringify(sampleClientObject).slice(0, -1) +
904-
"," +
905-
JSON.stringify(partialClientObject).slice(1);
906+
const duplicatedClientString
907+
= JSON.stringify(sampleClientObject).slice(0, -1)
908+
+ ","
909+
+ JSON.stringify(partialClientObject).slice(1);
906910
const authData = toBuffer(ethersResponse.authenticatorData);
907911
const transactionHash = Buffer.from(sampleClientObject.challenge, "base64url");
908912
const isValidSignature = await validateSignatureTest(

0 commit comments

Comments
 (0)