-
Notifications
You must be signed in to change notification settings - Fork 78
/
SafeWebAuthnSharedSigner.sol
167 lines (148 loc) · 7.66 KB
/
SafeWebAuthnSharedSigner.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
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.26;
import {SignatureValidator} from "../base/SignatureValidator.sol";
import {ISafe} from "../interfaces/ISafe.sol";
import {P256, WebAuthn} from "../libraries/WebAuthn.sol";
/**
* @title Safe WebAuthn Shared Signer
* @dev A contract for verifying WebAuthn signatures shared by all Safe accounts. This contract uses
* storage from the Safe account itself for full ERC-4337 compatibility.
*/
contract SafeWebAuthnSharedSigner is SignatureValidator {
/**
* @notice Data associated with a WebAuthn signer. It represents the X and Y coordinates of the
* signer's public key as well as the P256 verifiers to use. This is stored in account storage
* starting at the storage slot {SIGNER_SLOT}.
*/
struct Signer {
uint256 x;
uint256 y;
P256.Verifiers verifiers;
}
/**
* @notice The storage slot of the mapping from shared WebAuthn signer address to signer data.
* @custom:computed-as keccak256("SafeWebAuthnSharedSigner.signer") - 1
* @dev This value is intentionally computed to be a hash -1 as a precaution to avoid any
* potential issues from unintended hash collisions, and have enough space for all the signer
* fields. Also, this is the slot of a `mapping(address self => Signer)` to ensure that multiple
* {SafeWebAuthnSharedSigner} instances can coexist with the same account.
*/
uint256 private constant _SIGNER_MAPPING_SLOT = 0x2e0aed53485dc2290ceb5ce14725558ad3e3a09d38c69042410ad15c2b4ea4e8;
/**
* @notice Address of the shared signer contract itself.
* @dev This is used for determining whether or not the contract is being `DELEGATECALL`-ed when
* setting signer data.
*/
address private immutable _SELF;
/**
* @notice The starting storage slot on the account containing the signer data.
*/
uint256 public immutable SIGNER_SLOT;
/**
* @notice Emitted when the shared signer is configured for an account.
* @dev Note that the configured account is not included in the event data. Since configuration
* is done as a `DELEGATECALL`, the contract emitting the event is the configured account. This
* is also why the event name is prefixed with `SafeWebAuthnSharedSigner`, in order to avoid
* event `topic0` collisions with other contracts (seeing as "configured" is a common term).
* @param publicKeyHash The Keccak-256 hash of the public key coordinates.
* @param x The x-coordinate of the public key.
* @param y The y-coordinate of the public key.
* @param verifiers The P-256 verifiers to use.
*/
event SafeWebAuthnSharedSignerConfigured(bytes32 indexed publicKeyHash, uint256 x, uint256 y, P256.Verifiers verifiers);
/**
* @notice An error indicating a `CALL` to a function that should only be `DELEGATECALL`-ed.
*/
error NotDelegateCalled();
/**
* @notice Create a new shared WebAuthn signer instance.
*/
constructor() {
_SELF = address(this);
SIGNER_SLOT = uint256(keccak256(abi.encode(address(this), _SIGNER_MAPPING_SLOT)));
}
/**
* @notice Validates the call is done via `DELEGATECALL`.
*/
modifier onlyDelegateCall() {
if (address(this) == _SELF) {
revert NotDelegateCalled();
}
_;
}
/**
* @notice Return the signer configuration for the specified account.
* @dev The calling account must be a Safe, as the signer data is stored in the Safe's storage
* and must be read with the {StorageAccessible} support from the Safe.
* @param account The account to request signer data for.
*/
function getConfiguration(address account) public view returns (Signer memory signer) {
bytes memory getStorageAtData = abi.encodeCall(ISafe(account).getStorageAt, (SIGNER_SLOT, 3));
// Call the {StorageAccessible.getStorageAt} with assembly. This allows us to return a
// zeroed out signer configuration instead of reverting for `account`s that are not Safes.
// We also, expect the implementation to behave **exactly** like the Safe's - that is it
// should encode the return data using a standard ABI encoding:
// - The first 32 bytes is the offset of the values bytes array, always `0x20`
// - The second 32 bytes is the length of the values bytes array, always `0x60`
// - the following 3 words (96 bytes) are the values of the signer configuration.
// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
// Note that Yul expressions are evaluated in reverse order, so the `staticcall` is the
// first thing to be evaluated in the nested `and` expression.
if and(
and(
// The offset of the ABI encoded bytes is 0x20, this should always be the case
// for standard ABI encoding of `(bytes)` tuple that `getStorageAt` returns.
eq(mload(0x00), 0x20),
// The length of the encoded bytes is exactly 0x60 bytes (i.e. 3 words, which is
// exactly how much we read from the Safe's storage in the `getStorageAt` call).
eq(mload(0x20), 0x60)
),
and(
// The length of the return data should be exactly 0xa0 bytes, which should
// always be the case for the Safe's `getStorageAt` implementation.
eq(returndatasize(), 0xa0),
// The call succeeded. We write the first two words of the return data into the
// scratch space, as we need to inspect them before copying the signer
// signer configuration to our `signer` memory pointer.
staticcall(gas(), account, add(getStorageAtData, 0x20), mload(getStorageAtData), 0x00, 0x40)
)
) {
// Copy only the storage values from the return data to our `signer` memory address.
// This only happens on success, so the `signer` value will be zeroed out if any of
// the above conditions fail, indicating that no signer is configured.
returndatacopy(signer, 0x40, 0x60)
}
}
}
/**
* @notice Sets the signer configuration for the calling account.
* @dev The Safe must call this function with a `DELEGATECALL`, as the signer data is stored in
* the Safe account's storage.
* @param signer The new signer data to set for the calling account.
*/
function configure(Signer memory signer) external onlyDelegateCall {
uint256 signerSlot = SIGNER_SLOT;
Signer storage signerStorage;
// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
signerStorage.slot := signerSlot
}
signerStorage.x = signer.x;
signerStorage.y = signer.y;
signerStorage.verifiers = signer.verifiers;
bytes32 publicKeyHash = keccak256(abi.encode(signer.x, signer.y));
emit SafeWebAuthnSharedSignerConfigured(publicKeyHash, signer.x, signer.y, signer.verifiers);
}
/**
* @inheritdoc SignatureValidator
*/
function _verifySignature(bytes32 message, bytes calldata signature) internal view virtual override returns (bool isValid) {
Signer memory signer = getConfiguration(msg.sender);
// Make sure that the signer is configured in the first place.
if (P256.Verifiers.unwrap(signer.verifiers) == 0) {
return false;
}
isValid = WebAuthn.verifySignature(message, signature, WebAuthn.USER_VERIFICATION, signer.x, signer.y, signer.verifiers);
}
}