-
Notifications
You must be signed in to change notification settings - Fork 0
/
WebAuthn256r1.sol
132 lines (121 loc) · 7.2 KB
/
WebAuthn256r1.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
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.19 <0.9.0;
import { ECDSA256r1 } from "../lib/secp256r1-verify/src/ECDSA256r1.sol";
import { Base64 } from "../lib/solady/src/utils/Base64.sol";
import {
UV_FLAG_MASK,
OFFSET_CLIENT_CHALLENGE_GET,
OFFSET_CLIENT_CHALLENGE_CREATE,
OFFSET_FLAG,
OFFSET_CLIENT_TYPE,
TYPE_GET_INDICATOR,
TYPE_CREATE_INDICATOR
} from "src/utils.sol";
/// @title WebAuthn256r1
/// @notice A library to verify ECDSA signature though WebAuthn on the secp256r1 curve
/// @custom:experimental This is an experimental library.
library WebAuthn256r1 {
error InvalidAuthenticatorData();
error InvalidClientData();
error InvalidChallenge();
/// @notice Validate the webauthn data and generate the signature message needed to recover
/// @param authenticatorData The authenticator data structure encodes contextual bindings made by the authenticator.
/// Described here: https://www.w3.org/TR/webauthn-2/#authenticator-data
/// @param clientData This is the client data that was signed. The client data represents the
/// contextual bindings of both the WebAuthn Relying Party and the client.
/// Described here: https://www.w3.org/TR/webauthn-2/#client-data
/// @param clientChallenge This is the challenge that was sent to the client to sign. It is
/// part of the client data. In a classic non-EVM flow, this challenge
/// is generated by the server and sent to the client to avoid replay
/// attack. In our context, as we already have the nonce for this purpose
/// we use this field to pass the arbitrary execution order.
/// This value is expected to not be encoded in Base64, the encoding is done
/// during the verification.
/// @return message The signature message needed to recover
/// @dev 1. The signature counter is not checked in this implementation because
/// we already have the nonce on-chain to prevent the anti-replay attack.
/// The counter is 4-bytes long and it is located at bytes 33 of the authenticator data.
/// 2. The RP.ID is not checked in this implementation as it is impossible to generate
/// the same keys for different RP.IDs with a well formed authenticator. The hash of the id
/// is 32-bytes long and it is located at bytes 0 of the authenticator data.
/// 3. The length of the authenticator data is not fixed. It is at least 37 bytes
/// (rpIdHash (32) + flags (1) + counter (4)) but it can be longer if there is an
/// attested credential data and/or some extensions data. As we do not consider
/// the counter in this implementation, we only require the authenticator data to be
/// at least 32 bytes long in order to save some calldata gas.
/// 4. You may probably ask why we encode the challenge in base64 on-chain instead of
/// of sending it already encoded to save some gas. This library is opinionated and
/// it assumes that it is used in the context of Account Abstraction. In this context,
/// valuable informations required to proceed the transaction will be stored in the
/// challenge meaning we need the challenge in clear to use it later in the flow.
/// That's why we decided to add an extra encoding step during the validation.
/// 5. It is assumed this is not the responsibility of this contract to check the value
/// of the `alg` parameter. It is expected this contract will be extended by another
/// contract that will redirect the message produced by this contract to the right
/// recovery function.
/// 6. Both extension data and attested credential data are out of scope of this implementation.
/// 7. It is not the responsibility of this contract to validate the attestation statement formats
///
/// This contract is based on the level 2 of the WebAuthn specification.
/// and until proven otherwise compliant with the level 3 of the specification.
function generateMessage(
bytes calldata authenticatorData,
bytes calldata clientData,
bytes calldata clientChallenge
)
internal
pure
returns (bytes32 message)
{
unchecked {
// Let the caller check the value of the flag in the authenticator data
// @dev: we don't need to manually check the length of the authenticator data
// here as the EVM will automatically revert if the length is lower than 32
if ((authenticatorData[OFFSET_FLAG] & UV_FLAG_MASK) == 0) {
revert InvalidAuthenticatorData();
}
// Ensure the client challenge is not null
if (clientChallenge.length == 0) revert InvalidChallenge();
// Encode the client challenge in base64 and explicitly convert it to bytes
bytes memory challengeEncoded = bytes(Base64.encode(clientChallenge, true, true));
// Extract the client challenge offset based on the client type
// By checking the indicator we can determine if we need to use the offset for the get or create flow
// @dev: we don't need to check the overflow here as the EVM will automatically revert if
// `OFFSET_CLIENT_TYPE` is out of bound.
uint256 clientChallengeOffset = clientData[OFFSET_CLIENT_TYPE] == TYPE_CREATE_INDICATOR
? OFFSET_CLIENT_CHALLENGE_CREATE
: OFFSET_CLIENT_CHALLENGE_GET;
// Extract the challenge from the client data and hash it
// @dev: we don't need to check the overflow here as the EVM will automatically revert if
// `clientChallengeOffset + challengeEncoded.length` overflow. This is because we will
// try to access a chunk of memory by passing an end index lower than the start index
bytes32 challengeHashed =
keccak256(clientData[clientChallengeOffset:(clientChallengeOffset + challengeEncoded.length)]);
// Hash the encoded challenge and check both challenges are equal
if (keccak256(challengeEncoded) != challengeHashed) {
revert InvalidClientData();
}
// Craft the signature message by hashing the client data, then concatenating
// it to the authenticator data without padding, before hashing the result
message = sha256(abi.encodePacked(authenticatorData, sha256(clientData)));
}
}
/// @notice Verify ECDSA signature though WebAuthn on the secp256r1 curve
function verify(
bytes calldata authData,
bytes calldata clientData,
bytes calldata clientChallenge,
uint256 r,
uint256 s,
uint256 qx,
uint256 qy
)
internal
returns (bool)
{
unchecked {
bytes32 message = generateMessage(authData, clientData, clientChallenge);
return ECDSA256r1.verify(message, r, s, qx, qy);
}
}
}