-
Notifications
You must be signed in to change notification settings - Fork 38
/
WebAuthnValidationModule.sol
184 lines (159 loc) · 7.58 KB
/
WebAuthnValidationModule.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
// This file is part of Modular Account.
//
// Copyright 2024 Alchemy Insights, Inc.
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General
// Public License as published by the Free Software Foundation, either version 3 of the License, or (at your
// option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with this program. If not, see
// <https://www.gnu.org/licenses/>.
pragma solidity ^0.8.26;
import {IModule} from "@erc6900/reference-implementation/interfaces/IModule.sol";
import {IValidationModule} from "@erc6900/reference-implementation/interfaces/IValidationModule.sol";
import {ReplaySafeWrapper} from "@erc6900/reference-implementation/modules/ReplaySafeWrapper.sol";
import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol";
import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol";
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import {WebAuthn} from "webauthn-sol/src/WebAuthn.sol";
import {ModuleBase} from "../ModuleBase.sol";
/// @title WebAuthn Validation Module
/// @author Alchemy
/// @dev Implementation referenced from WebAuthn + Coinbase Smart Wallet developed by Base.
/// @notice This validation module enables WebAuthn (secp256r1 curve) signature validation.
/// NOTE:
/// - Uninstallation will NOT disable all installed entity IDs of an account. It only uninstalls the entity ID that
/// is passed in. Account must remove access for each entity ID if want to disable all.
/// - None of the functions are installed on the account. Account states are to be retrieved from this global
/// singleton directly.
/// - This validation supports ERC-1271. The signature is valid if it is signed by the owner's private key.
/// - This validation supports composition that other validation can relay on entities in this validation to
/// validate partially or fully.
contract WebAuthnValidationModule is IValidationModule, ReplaySafeWrapper, ModuleBase {
using MessageHashUtils for bytes32;
using WebAuthn for WebAuthn.WebAuthnAuth;
struct PubKey {
uint256 x;
uint256 y;
}
uint256 internal constant _SIG_VALIDATION_PASSED = 0;
uint256 internal constant _SIG_VALIDATION_FAILED = 1;
// bytes4(keccak256("isValidSignature(bytes32,bytes)"))
bytes4 internal constant _1271_MAGIC_VALUE = 0x1626ba7e;
bytes4 internal constant _1271_INVALID = 0xffffffff;
mapping(uint32 entityId => mapping(address account => PubKey)) public signers;
/// @notice This event is emitted when signer of the account's validation changes.
/// @param account The account whose validation signer changed.
/// @param entityId The entityId for the account and the signer.
/// @param newX X coordinate of the new signer.
/// @param newY Y coordinate of the new signer.
/// @param oldX X coordinate of the old signer.
/// @param oldY Y coordinate of the old signer.
event SignerTransferred(
address indexed account,
uint32 indexed entityId,
uint256 indexed newX,
uint256 indexed newY,
uint256 oldX,
uint256 oldY
) anonymous;
error NotAuthorized();
/// @notice Updates the signer for an entityId.
/// @dev Used for key rotation or deleting a key
/// @param entityId The entityId to update the signer for.
/// @param x The x coordinate of the new signer.
/// @param y The y coordinate of the new signer.
function transferSigner(uint32 entityId, uint256 x, uint256 y) external {
_transferSigner(entityId, x, y);
}
/// @inheritdoc IModule
function onInstall(bytes calldata data) external override {
(uint32 entityId, uint256 x, uint256 y) = abi.decode(data, (uint32, uint256, uint256));
_transferSigner(entityId, x, y);
}
/// @inheritdoc IModule
function onUninstall(bytes calldata data) external override {
uint32 entityId = abi.decode(data, (uint32));
_transferSigner(entityId, 0, 0);
}
/// @inheritdoc IValidationModule
function validateUserOp(uint32 entityId, PackedUserOperation calldata userOp, bytes32 userOpHash)
external
view
override
returns (uint256)
{
if (_validateSignature(entityId, userOp.sender, userOpHash.toEthSignedMessageHash(), userOp.signature)) {
return _SIG_VALIDATION_PASSED;
}
return _SIG_VALIDATION_FAILED;
}
/// @inheritdoc IValidationModule
/// @dev The signature is valid if it is signed by the owner's private key
/// (if the owner is an EOA) or if it is a valid ERC-1271 signature from the
/// owner (if the owner is a contract).
/// Note that the digest is wrapped in an EIP-712 struct to prevent cross-account replay attacks. The
/// replay-safe hash may be retrieved by calling the public function `replaySafeHash`.
function validateSignature(address account, uint32 entityId, address, bytes32 digest, bytes calldata signature)
external
view
override
returns (bytes4)
{
bytes32 _replaySafeHash = replaySafeHash(account, digest);
if (_validateSignature(entityId, account, _replaySafeHash, signature)) {
return _1271_MAGIC_VALUE;
}
return _1271_INVALID;
}
/// @inheritdoc IValidationModule
function validateRuntime(address, uint32, address, uint256, bytes calldata, bytes calldata)
external
pure
override
{
revert NotAuthorized();
}
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Module interface functions ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
/// @inheritdoc IModule
function moduleId() external pure returns (string memory) {
return "alchemy.webauthn-validation-module.1.0.0";
}
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(ModuleBase, IERC165)
returns (bool)
{
return (interfaceId == type(IValidationModule).interfaceId || super.supportsInterface(interfaceId));
}
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Internal / Private functions ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
function _transferSigner(uint32 entityId, uint256 newX, uint256 newY) internal {
PubKey memory oldPubKey = signers[entityId][msg.sender];
signers[entityId][msg.sender] = PubKey(newX, newY);
emit SignerTransferred(msg.sender, entityId, newX, newY, oldPubKey.x, oldPubKey.y);
}
function _validateSignature(uint32 entityId, address account, bytes32 hash, bytes calldata signature)
internal
view
returns (bool)
{
WebAuthn.WebAuthnAuth memory webAuthnAuth = abi.decode(signature, (WebAuthn.WebAuthnAuth));
PubKey storage key = signers[entityId][account];
if (WebAuthn.verify(abi.encode(hash), false, webAuthnAuth, key.x, key.y)) {
return true;
}
return false;
}
}