-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add three basic stateless validators and update singleOwnerPlugin
- Loading branch information
1 parent
c817ead
commit 8aa1f09
Showing
9 changed files
with
292 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.25; | ||
|
||
import "openzeppelin-contracts/contracts/utils/Base64.sol"; | ||
Check warning on line 4 in src/validators/Base64URL.sol GitHub Actions / Run Linters
|
||
|
||
/// TODO this library is not necessary once Openzeppelin 5.0.2 Base64 is supported | ||
library Base64URL { | ||
function encode(bytes memory data) internal pure returns (string memory) { | ||
string memory strb64 = Base64.encode(data); | ||
bytes memory b64 = bytes(strb64); | ||
|
||
// Base64 can end with "=" or "=="; Base64URL has no padding. | ||
uint256 equalsCount = 0; | ||
if (b64.length > 2 && b64[b64.length - 2] == "=") equalsCount = 2; | ||
else if (b64.length > 1 && b64[b64.length - 1] == "=") equalsCount = 1; | ||
|
||
uint256 len = b64.length - equalsCount; | ||
bytes memory result = new bytes(len); | ||
|
||
for (uint256 i = 0; i < len; i++) { | ||
if (b64[i] == "+") { | ||
result[i] = "-"; | ||
} else if (b64[i] == "/") { | ||
result[i] = "_"; | ||
} else { | ||
result[i] = b64[i]; | ||
} | ||
} | ||
|
||
return string(result); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity ^0.8.25; | ||
|
||
struct Signer { | ||
IStatelessValidator validator; | ||
/// data is passed as signedData to the validator | ||
bytes data; | ||
} | ||
|
||
interface IStatelessValidator { | ||
function validate(bytes memory signerData, bytes32 hash, bytes memory signature) | ||
external | ||
view | ||
returns (bool isValid, bytes memory result); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.25; | ||
|
||
import "./Base64URL.sol"; | ||
|
||
/** | ||
* Helper library for external contracts to verify WebAuthn signatures. Only support RIP-7212 precompiled signature | ||
* check. | ||
* /// @author Alchemy | ||
* /// @author Daimo (https://github.com/daimo-eth/p256-verifier/blob/master/src/WebAuthn.sol) | ||
*/ | ||
library WebAuthn { | ||
/// Checks whether substr occurs in str starting at a given byte offset. | ||
function contains(string memory substr, string memory str, uint256 location) internal pure returns (bool) { | ||
bytes memory substrBytes = bytes(substr); | ||
bytes memory strBytes = bytes(str); | ||
|
||
uint256 substrLen = substrBytes.length; | ||
uint256 strLen = strBytes.length; | ||
|
||
for (uint256 i = 0; i < substrLen; i++) { | ||
if (location + i >= strLen) { | ||
return false; | ||
} | ||
|
||
if (substrBytes[i] != strBytes[location + i]) { | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
bytes1 constant _AUTH_DATA_FLAGS_UP = 0x01; // Bit 0 | ||
Check warning on line 34 in src/validators/WebAuthn.sol GitHub Actions / Run Linters
|
||
bytes1 constant _AUTH_DATA_FLAGS_UV = 0x04; // Bit 2 | ||
bytes1 constant _AUTH_DATA_FLAGS_BE = 0x08; // Bit 3 | ||
bytes1 constant _AUTH_DATA_FLAGS_BS = 0x10; // Bit 4 | ||
|
||
/// @dev Secp256r1 curve order / 2 used as guard to prevent signature malleability issue. | ||
/// 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551 / 2 | ||
uint256 constant _P256_N_DIV_2 = 57896044605178124381348723474703786764998477612067880171211129530534256022184; | ||
/// @dev The precompiled contract address to use for signature verification in the “secp256r1” elliptic curve. | ||
/// See https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md. | ||
address constant _PRECOMPILED_VERIFIER = 0x0000000000000000000000000000000000000100; | ||
|
||
/// Verifies the authFlags in authenticatorData. Numbers in inline comment | ||
/// correspond to the same numbered bullets in | ||
/// https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion. | ||
function checkAuthFlags(bytes1 flags, bool requireUserVerification) internal pure returns (bool) { | ||
// 17. Verify that the UP bit of the flags in authData is set. | ||
if (flags & _AUTH_DATA_FLAGS_UP != _AUTH_DATA_FLAGS_UP) { | ||
return false; | ||
} | ||
|
||
// 18. If user verification was determined to be required, verify that | ||
// the UV bit of the flags in authData is set. Otherwise, ignore the | ||
// value of the UV flag. | ||
if (requireUserVerification && (flags & _AUTH_DATA_FLAGS_UV) != _AUTH_DATA_FLAGS_UV) { | ||
return false; | ||
} | ||
|
||
// 19. If the BE bit of the flags in authData is not set, verify that | ||
// the BS bit is not set. | ||
if (flags & _AUTH_DATA_FLAGS_BE != _AUTH_DATA_FLAGS_BE) { | ||
if (flags & _AUTH_DATA_FLAGS_BS == _AUTH_DATA_FLAGS_BS) { | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
* Verifies a Webauthn P256 signature (Authentication Assertion) as described | ||
* in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion. We do not | ||
* verify all the steps as described in the specification, only ones relevant | ||
* to our context. Please carefully read through this list before usage. | ||
* Specifically, we do verify the following: | ||
* - Verify that authenticatorData (which comes from the authenticator, | ||
* such as iCloud Keychain) indicates a well-formed assertion. If | ||
* requireUserVerification is set, checks that the authenticator enforced | ||
* user verification. User verification should be required if, | ||
* and only if, options.userVerification is set to required in the request | ||
* - Verifies that the client JSON is of type "webauthn.get", i.e. the client | ||
* was responding to a request to assert authentication. | ||
* - Verifies that the client JSON contains the requested challenge. | ||
* - Finally, verifies that (r, s) constitute a valid signature over both | ||
* the authenicatorData and client JSON, for public key (x, y). | ||
* | ||
* We make some assumptions about the particular use case of this verifier, | ||
* so we do NOT verify the following: | ||
* - Does NOT verify that the origin in the clientDataJSON matches the | ||
* Relying Party's origin: It is considered the authenticator's | ||
* responsibility to ensure that the user is interacting with the correct | ||
* RP. This is enforced by most high quality authenticators properly, | ||
* particularly the iCloud Keychain and Google Password Manager were | ||
* tested. | ||
* - Does NOT verify That c.topOrigin is well-formed: We assume c.topOrigin | ||
* would never be present, i.e. the credentials are never used in a | ||
* cross-origin/iframe context. The website/app set up should disallow | ||
* cross-origin usage of the credentials. This is the default behaviour for | ||
* created credentials in common settings. | ||
* - Does NOT verify that the rpIdHash in authData is the SHA-256 hash of an | ||
* RP ID expected by the Relying Party: This means that we rely on the | ||
* authenticator to properly enforce credentials to be used only by the | ||
* correct RP. This is generally enforced with features like Apple App Site | ||
* Association and Google Asset Links. To protect from edge cases in which | ||
* a previously-linked RP ID is removed from the authorised RP IDs, | ||
* we recommend that messages signed by the authenticator include some | ||
* expiry mechanism. | ||
* - Does NOT verify the credential backup state: This assumes the credential | ||
* backup state is NOT used as part of Relying Party business logic or | ||
* policy. | ||
* - Does NOT verify the values of the client extension outputs: This assumes | ||
* that the Relying Party does not use client extension outputs. | ||
* - Does NOT verify the signature counter: Signature counters are intended | ||
* to enable risk scoring for the Relying Party. This assumes risk scoring | ||
* is not used as part of Relying Party business logic or policy. | ||
* - Does NOT verify the attestation object: This assumes that | ||
* response.attestationObject is NOT present in the response, i.e. the | ||
* RP does not intend to verify an attestation. | ||
*/ | ||
function verifySignature( | ||
bytes memory challenge, | ||
bytes memory authenticatorData, | ||
bool requireUserVerification, | ||
string memory clientDataJSON, | ||
uint256 challengeLocation, | ||
uint256 responseTypeLocation, | ||
uint256 r, | ||
uint256 s, | ||
uint256 x, | ||
uint256 y | ||
) internal view returns (bool) { | ||
// check for signature malleability | ||
if (s > _P256_N_DIV_2) { | ||
return false; | ||
} | ||
|
||
// Check that authenticatorData has good flags | ||
if (authenticatorData.length < 37 || !checkAuthFlags(authenticatorData[32], requireUserVerification)) { | ||
return false; | ||
} | ||
|
||
// Check that response is for an authentication assertion | ||
// solhint-disable-next-line quotes | ||
string memory responseType = '"type":"webauthn.get"'; | ||
if (!contains(responseType, clientDataJSON, responseTypeLocation)) { | ||
return false; | ||
} | ||
|
||
// Check that challenge is in the clientDataJSON | ||
string memory challengeB64url = Base64URL.encode(challenge); | ||
// solhint-disable-next-line quotes | ||
string memory challengeProperty = string.concat('"challenge":"', challengeB64url, '"'); | ||
|
||
if (!contains(challengeProperty, clientDataJSON, challengeLocation)) { | ||
return false; | ||
} | ||
|
||
// Check that the public key signed sha256(authenticatorData || sha256(clientDataJSON)) | ||
bytes32 clientDataJSONHash = sha256(bytes(clientDataJSON)); | ||
bytes32 messageHash = sha256(abi.encodePacked(authenticatorData, clientDataJSONHash)); | ||
bytes memory args = abi.encode(messageHash, r, s, x, y); | ||
(bool success, bytes memory ret) = _PRECOMPILED_VERIFIER.staticcall(args); | ||
if (success == false || ret.length == 0) { | ||
return false; | ||
} | ||
return abi.decode(ret, (uint256)) == 1; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity ^0.8.25; | ||
|
||
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; | ||
|
||
import {IStatelessValidator} from "./IStatelessValidator.sol"; | ||
import {WebAuthn} from "./WebAuthn.sol"; | ||
|
||
contract WebAuthnValidator is IStatelessValidator { | ||
using MessageHashUtils for bytes32; | ||
|
||
struct SignerData { | ||
/// The x coordinate of the public key. | ||
uint256 x; | ||
/// The y coordinate of the public key. | ||
uint256 y; | ||
} | ||
|
||
function validate(bytes memory signerData, bytes32 hash, bytes memory signature) | ||
external | ||
view | ||
returns (bool isValid, bytes memory result) | ||
{ | ||
( | ||
bytes memory authenticatorData, | ||
string memory clientDataJSON, | ||
uint256 challengeLocation, | ||
uint256 responseTypeLocation, | ||
uint256 r, | ||
uint256 s | ||
) = abi.decode(signature, (bytes, string, uint256, uint256, uint256, uint256)); | ||
SignerData memory signerDataDecoded = abi.decode(signerData, (SignerData)); | ||
|
||
isValid = WebAuthn.verifySignature( | ||
abi.encodePacked(hash), | ||
authenticatorData, | ||
true, | ||
clientDataJSON, | ||
challengeLocation, | ||
responseTypeLocation, | ||
r, | ||
s, | ||
signerDataDecoded.x, | ||
signerDataDecoded.y | ||
); | ||
} | ||
|
||
function encodeSignerData(SignerData calldata signerData) external pure returns (bytes memory data) { | ||
data = abi.encode(signerData); | ||
} | ||
} |