Skip to content

Commit

Permalink
feat: dedicated revoke passkey
Browse files Browse the repository at this point in the history
Also allows checking if the passkey exists so you can avoid
adding the same passkey to multiple accounts on the same domain
  • Loading branch information
cpb8010 committed Feb 7, 2025
1 parent fc1f2a3 commit 397ade6
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 16 deletions.
30 changes: 21 additions & 9 deletions src/validators/WebAuthValidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -56,35 +59,44 @@ 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,
string memory originDomain
) 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
Expand Down
21 changes: 14 additions & 7 deletions test/PasskeyModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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));
Expand All @@ -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 () => {
Expand All @@ -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");
});
});
Expand Down

0 comments on commit 397ade6

Please sign in to comment.