Skip to content

Commit

Permalink
feat: add three basic stateless validators and update singleOwnerPlugin
Browse files Browse the repository at this point in the history
  • Loading branch information
fangting-alchemy committed Jun 21, 2024
1 parent c817ead commit 8aa1f09
Show file tree
Hide file tree
Showing 9 changed files with 292 additions and 40 deletions.
2 changes: 1 addition & 1 deletion src/plugins/owner/ISingleOwnerPlugin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity ^0.8.25;

import {IValidation} from "../../interfaces/IValidation.sol";
import {Signer} from "../../validators/ISignatureValidator.sol";
import {Signer} from "../../validators/IStatelessValidator.sol";

interface ISingleOwnerPlugin is IValidation {
enum FunctionId {
Expand Down
13 changes: 7 additions & 6 deletions src/plugins/owner/SingleOwnerPlugin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ pragma solidity ^0.8.25;

import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

import {
ManifestFunction,
Expand All @@ -17,7 +16,7 @@ import {IPlugin} from "../../interfaces/IPlugin.sol";
import {IPluginManager} from "../../interfaces/IPluginManager.sol";
import {IStandardExecutor} from "../../interfaces/IStandardExecutor.sol";
import {IValidation} from "../../interfaces/IValidation.sol";
import {Signer} from "../../validators/ISignatureValidator.sol";
import {Signer} from "../../validators/IStatelessValidator.sol";
import {BasePlugin, IERC165} from "../BasePlugin.sol";
import {ISingleOwnerPlugin} from "./ISingleOwnerPlugin.sol";

Expand Down Expand Up @@ -100,11 +99,13 @@ contract SingleOwnerPlugin is ISingleOwnerPlugin, BasePlugin {
{
if (functionId == uint8(FunctionId.VALIDATION_OWNER)) {
// Validate the user op signature against the owner.
(address signer,,) = (userOpHash.toEthSignedMessageHash()).tryRecover(userOp.signature);
if (signer == address(0) || signer != _owners[msg.sender]) {
Signer memory signer = _owners[msg.sender];

if (address(signer.validator) == address(0)) {
return _SIG_VALIDATION_FAILED;
}
return _SIG_VALIDATION_PASSED;
(bool isValid,) = signer.validator.validate(signer.data, userOpHash, userOp.signature);
return isValid ? _SIG_VALIDATION_PASSED : _SIG_VALIDATION_FAILED;
}
revert NotImplemented();
}
Expand All @@ -128,7 +129,7 @@ contract SingleOwnerPlugin is ISingleOwnerPlugin, BasePlugin {
{
if (functionId == uint8(FunctionId.SIG_VALIDATION)) {
Signer memory signer = _owners[msg.sender];
(bool isValid,) = signer.validator.validate(msg.sender, signer.data, digest, signature);
(bool isValid,) = signer.validator.validate(signer.data, digest, signature);
return isValid ? _1271_MAGIC_VALUE : _1271_INVALID;
}
revert NotImplemented();
Expand Down
32 changes: 32 additions & 0 deletions src/validators/Base64URL.sol
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

View workflow job for this annotation

GitHub Actions / Run Linters

global import of path openzeppelin-contracts/contracts/utils/Base64.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)

/// 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);
}
}
21 changes: 9 additions & 12 deletions src/validators/ERC1271Validator.sol
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.25;

import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";

import {ISignatureValidator} from "./ISignatureValidator.sol";
import {IStatelessValidator} from "./IStatelessValidator.sol";

contract ERC1271Validator is ISignatureValidator {
/// @dev Code inherited from function `isValidERC1271SignatureNow` in
/// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v5.0/contracts/utils/cryptography/SignatureChecker.sol
function validate(address, bytes memory signerData, bytes32 hash, bytes memory signature)
contract Erc1271Validator is IStatelessValidator {
using MessageHashUtils for bytes32;

function validate(bytes memory signerData, bytes32 hash, bytes memory signature)
external
view
returns (bool isValid, bytes memory result)
{
address signer = abi.decode(signerData, (address));
bytes32 messageHash = hash.toEthSignedMessageHash();
address expectedSigner = abi.decode(signerData, (address));

(isValid, result) = signer.staticcall(abi.encodeCall(IERC1271.isValidSignature, (hash, signature)));
isValid = (
isValid && result.length >= 32
&& abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector)
);
isValid = SignatureChecker.isValidERC1271SignatureNow(expectedSigner, messageHash, signature);
}

function encodeSignerData(address signer) external pure returns (bytes memory data) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ pragma solidity ^0.8.25;
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

import {ISignatureValidator} from "./ISignatureValidator.sol";
import {IStatelessValidator} from "./IStatelessValidator.sol";

contract Secp256k1Validator is ISignatureValidator {
using ECDSA for bytes32;
contract EcdsaValidator is IStatelessValidator {
using MessageHashUtils for bytes32;
using ECDSA for bytes32;

function validate(address, bytes memory signerData, bytes32 hash, bytes memory signature)
/// @dev result always returns the correct singer of the signature.
function validate(bytes memory signerData, bytes32 hash, bytes memory signature)
external
pure
view
returns (bool isValid, bytes memory result)
{
bytes32 messageHash = hash.toEthSignedMessageHash();
Expand Down
16 changes: 0 additions & 16 deletions src/validators/ISignatureValidator.sol

This file was deleted.

15 changes: 15 additions & 0 deletions src/validators/IStatelessValidator.sol
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);
}
171 changes: 171 additions & 0 deletions src/validators/WebAuthn.sol
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";

Check warning on line 4 in src/validators/WebAuthn.sol

View workflow job for this annotation

GitHub Actions / Run Linters

global import of path ./Base64URL.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)

/**
* 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

View workflow job for this annotation

GitHub Actions / Run Linters

Function order is incorrect, state variable declaration can not go after internal pure function (line 14)

Check warning on line 34 in src/validators/WebAuthn.sol

View workflow job for this annotation

GitHub Actions / Run Linters

Explicitly mark visibility of state
bytes1 constant _AUTH_DATA_FLAGS_UV = 0x04; // Bit 2

Check warning on line 35 in src/validators/WebAuthn.sol

View workflow job for this annotation

GitHub Actions / Run Linters

Explicitly mark visibility of state
bytes1 constant _AUTH_DATA_FLAGS_BE = 0x08; // Bit 3

Check warning on line 36 in src/validators/WebAuthn.sol

View workflow job for this annotation

GitHub Actions / Run Linters

Explicitly mark visibility of state
bytes1 constant _AUTH_DATA_FLAGS_BS = 0x10; // Bit 4

Check warning on line 37 in src/validators/WebAuthn.sol

View workflow job for this annotation

GitHub Actions / Run Linters

Explicitly mark visibility of state

/// @dev Secp256r1 curve order / 2 used as guard to prevent signature malleability issue.
/// 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551 / 2
uint256 constant _P256_N_DIV_2 = 57896044605178124381348723474703786764998477612067880171211129530534256022184;

Check warning on line 41 in src/validators/WebAuthn.sol

View workflow job for this annotation

GitHub Actions / Run Linters

Explicitly mark visibility of state
/// @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;

Check warning on line 44 in src/validators/WebAuthn.sol

View workflow job for this annotation

GitHub Actions / Run Linters

Explicitly mark visibility of state

/// 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;
}
}
51 changes: 51 additions & 0 deletions src/validators/WebAuthnValidator.sol
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);
}
}

0 comments on commit 8aa1f09

Please sign in to comment.