From 397ade6289d2a49280bbffb2dc66aaf475c39f16 Mon Sep 17 00:00:00 2001 From: Colin Bellmore Date: Thu, 6 Feb 2025 23:46:51 -0800 Subject: [PATCH] feat: dedicated revoke passkey Also allows checking if the passkey exists so you can avoid adding the same passkey to multiple accounts on the same domain --- src/validators/WebAuthValidator.sol | 30 ++++++++++++++++++++--------- test/PasskeyModule.ts | 21 +++++++++++++------- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/validators/WebAuthValidator.sol b/src/validators/WebAuthValidator.sol index 0c3f23ff..3e4d0319 100644 --- a/src/validators/WebAuthValidator.sol +++ b/src/validators/WebAuthValidator.sol @@ -33,6 +33,9 @@ contract WebAuthValidator is VerifierCaller, IModuleValidator { mapping(string originDomain => mapping(bytes credentialId => mapping(address accountAddress => bytes32 publicKey))) public upperKeyHalf; + // allow tracking of passkey existence + mapping(string originDomain => mapping(bytes credentialId => bool keyExists)) public passkeyExists; + struct PasskeyId { string domain; bytes credentialId; @@ -56,17 +59,26 @@ contract WebAuthValidator is VerifierCaller, IModuleValidator { PasskeyId[] memory passkeyIds = abi.decode(data, (PasskeyId[])); for (uint256 i = 0; i < passkeyIds.length; i++) { PasskeyId memory passkeyId = passkeyIds[i]; - lowerKeyHalf[passkeyId.domain][passkeyId.credentialId][msg.sender] = 0x0; - upperKeyHalf[passkeyId.domain][passkeyId.credentialId][msg.sender] = 0x0; - emit PasskeyRemoved(msg.sender, passkeyId.domain, passkeyId.credentialId); + _removeValidationKey(passkeyId.credentialId, passkeyId.domain); } } + function removeValidationKey(bytes calldata credentialId, string calldata domain) external { + return _removeValidationKey(credentialId, domain); + } + + function _removeValidationKey(bytes memory credentialId, string memory domain) internal { + lowerKeyHalf[domain][credentialId][msg.sender] = 0x0; + upperKeyHalf[domain][credentialId][msg.sender] = 0x0; + passkeyExists[domain][credentialId] = false; + emit PasskeyRemoved(msg.sender, domain, credentialId); + } + /// @notice Adds a WebAuthn passkey for the caller /// @param credentialId unique public identifier for the key /// @param rawPublicKey ABI-encoded WebAuthn public key to add /// @param originDomain the domain this associated with - /// @return true if the key was added, false if it was updated + /// @return true if the key was added, false if one already exists function addValidationKey( bytes memory credentialId, bytes32[2] memory rawPublicKey, @@ -74,17 +86,17 @@ contract WebAuthValidator is VerifierCaller, IModuleValidator { ) public returns (bool) { bytes32 initialLowerHalf = lowerKeyHalf[originDomain][credentialId][msg.sender]; bytes32 initialUpperHalf = upperKeyHalf[originDomain][credentialId][msg.sender]; + if (initialLowerHalf != 0 || initialUpperHalf != 0) { + return false; + } - // we might want to support multiple passkeys per domain lowerKeyHalf[originDomain][credentialId][msg.sender] = rawPublicKey[0]; upperKeyHalf[originDomain][credentialId][msg.sender] = rawPublicKey[1]; - - // we're returning true if this was a new key, false for update - bool keyExists = initialLowerHalf == 0 && initialUpperHalf == 0; + passkeyExists[originDomain][credentialId] = true; emit PasskeyCreated(msg.sender, originDomain, credentialId); - return keyExists; + return true; } /// @notice Validates a WebAuthn signature diff --git a/test/PasskeyModule.ts b/test/PasskeyModule.ts index 6bb2d232..febf219b 100644 --- a/test/PasskeyModule.ts +++ b/test/PasskeyModule.ts @@ -436,7 +436,7 @@ async function validateSignatureTest( return await passkeyValidator.validateSignature(transactionHash, fatSignature); } -describe("Passkey validation", function () { +describe.only("Passkey validation", function () { const wallet = getWallet(LOCAL_RICH_WALLETS[0].privateKey); const ethersResponse = new RecordedResponse("test/signed-challenge.json"); // this is a binary object formatted by @simplewebauthn that contains the alg type and public key @@ -601,7 +601,7 @@ describe("Passkey validation", function () { await verifyKeyStorage(passkeyValidator, secondDomain, publicKeys, credentialId, wallet, "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 credentialId = toHex(randomBytes(64)); @@ -617,11 +617,18 @@ describe("Passkey validation", function () { const nextR1Key = await generateES256R1Key(); assert(nextR1Key != null, "no second key was generated"); const [newX, newY] = await getRawPublicKeyFromCrpyto(nextR1Key); - const nextKeyAdded = await passkeyValidator.addValidationKey(credentialId, [newX, newY], keyDomain); + const keyUpdated = await passkeyValidator.addValidationKey(credentialId, [newX, newY], keyDomain); + const keyUpdatedReceipt = await keyUpdated.wait(); + assert(keyUpdatedReceipt?.status == 1, "return false instead of revert"); + await verifyKeyStorage(passkeyValidator, keyDomain, [toHex(generatedX), toHex(generatedY)], credentialId, wallet, "ensure it was untouched"); + + const newCredentialId = toHex(randomBytes(64)); + const nextKeyAdded = await passkeyValidator.addValidationKey(newCredentialId, [newX, newY], keyDomain); const newReceipt = await nextKeyAdded.wait(); - assert(newReceipt?.status == 1, "new generated key added"); + assert(newReceipt?.status == 1, "added new key, same domain"); - await verifyKeyStorage(passkeyValidator, keyDomain, [toHex(newX), toHex(newY)], credentialId, wallet, "updated key"); + await verifyKeyStorage(passkeyValidator, keyDomain, [toHex(newX), toHex(newY)], newCredentialId, wallet, "different key, same domain"); + await verifyKeyStorage(passkeyValidator, keyDomain, [toHex(generatedX), toHex(generatedY)], credentialId, wallet, "not overwritten"); }); it("should allow clearing existing key", async () => { @@ -636,11 +643,11 @@ describe("Passkey validation", function () { assert(receipt?.status == 1, "generated key added"); await verifyKeyStorage(passkeyValidator, keyDomain, [toHex(generatedX), toHex(generatedY)], credentialId, wallet, "added"); - const zeroKey = new Uint8Array(32).fill(0); - const emptyKeyAdded = await passkeyValidator.addValidationKey(credentialId, [zeroKey, zeroKey], keyDomain); + const emptyKeyAdded = await passkeyValidator.removeValidationKey(credentialId, keyDomain); const emptyReceipt = await emptyKeyAdded.wait(); assert(emptyReceipt?.status == 1, "empty key added"); + const zeroKey = new Uint8Array(32).fill(0); await verifyKeyStorage(passkeyValidator, keyDomain, [toHex(zeroKey), toHex(zeroKey)], credentialId, wallet, "key removed"); }); });