Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WebAuthN validator #68

Merged
merged 12 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
389 changes: 232 additions & 157 deletions .gas-snapshot

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
[submodule "lib/I4337"]
path = lib/I4337
url = https://github.com/leekt/I4337
[submodule "lib/FreshCryptoLib"]
path = lib/FreshCryptoLib
url = https://github.com/rdubois-crypto/FreshCryptoLib
1 change: 1 addition & 0 deletions lib/FreshCryptoLib
Submodule FreshCryptoLib added at 1a484c
2 changes: 1 addition & 1 deletion lib/forge-std
1 change: 0 additions & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
ds-test/=lib/forge-std/lib/ds-test/src/
forge-std/=lib/forge-std/src/
solady/=lib/solady/src/
p256-verifier/=lib/p256-verifier/src/
FreshCryptoLib/=lib/FreshCryptoLib/solidity/src/
24 changes: 24 additions & 0 deletions script/DeployWebAuthnFclValidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
pragma solidity ^0.8.0;

import "src/factory/KernelFactory.sol";
import "src/utils/P256VerifierWrapper.sol";
import "src/validator/webauthn//WebAuthnFclValidator.sol";
import "forge-std/Script.sol";
import "forge-std/console.sol";

contract DeployWebAuthnFclValidator is Script {

function run() public {
uint256 key = vm.envUint("DEPLOYER_PRIVATE_KEY");
vm.startBroadcast(key);

P256VerifierWrapper p256VerifierWrapper = new P256VerifierWrapper{salt:0}();
console.log("p256 wrapper address: %s", address(p256VerifierWrapper));

WebAuthnFclValidator validator = new WebAuthnFclValidator{salt:0}(address(p256VerifierWrapper));
console.log("validator address: %s", address(validator));

vm.stopBroadcast();
}
}

45 changes: 45 additions & 0 deletions src/utils/P256VerifierWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {FCL_ecdsa} from "FreshCryptoLib/FCL_ecdsa.sol";

/// @title P256VerifierWrapper
/// @author rdubois-crypto
/// @author KONFeature
/// @notice Wrapper arround the P256Verifier contract of @rdubois-crypto, using it to accept EIP-7212 compliant verification (p256 pre-compiled curve)
/// @dev This lib is only a wrapper around the P256Verifier contract.
/// It will call the verifySignature function of the P256Verifier contract.
/// Once the RIP-7212 will be deployed and effective, this contract will be useless.
/// Tracker on polygon: PR: https://github.com/maticnetwork/bor/pull/1069
/// Now waiting on the Napoli hardfork to be deployed
contract P256VerifierWrapper {
/**
* Precompiles don't use a function signature. The first byte of callldata
* is the first byte of an input argument. In this case:
*
* input[ 0: 32] = signed data hash
* input[ 32: 64] = signature r
* input[ 64: 96] = signature s
* input[ 96:128] = public key x
* input[128:160] = public key y
*
* result[ 0: 32] = 0x00..00 (invalid) or 0x00..01 (valid)
*
* For details, see https://eips.ethereum.org/EIPS/eip-7212
*/
fallback(bytes calldata input) external returns (bytes memory) {
if (input.length != 160) {
return abi.encodePacked(uint256(0));
}

bytes32 hash = bytes32(input[0:32]);
uint256 r = uint256(bytes32(input[32:64]));
uint256 s = uint256(bytes32(input[64:96]));
uint256 x = uint256(bytes32(input[96:128]));
uint256 y = uint256(bytes32(input[128:160]));

uint256 ret = FCL_ecdsa.ecdsa_verify(hash, r, s, x, y) ? 1 : 0;

return abi.encodePacked(ret);
}
}
124 changes: 124 additions & 0 deletions src/validator/webauthn/WebAuthnFclValidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {UserOperation} from "I4337/interfaces/UserOperation.sol";
import {ECDSA} from "solady/utils/ECDSA.sol";
import {IKernelValidator} from "../../interfaces/IKernelValidator.sol";
import {ValidationData} from "../../common/Types.sol";
import {SIG_VALIDATION_FAILED} from "../../common/Constants.sol";
import {WebAuthnFclVerifier} from "./WebAuthnFclVerifier.sol";

/// @dev Storage layout for a kernel in the WebAuthnValidator contract.
struct WebAuthnFclValidatorStorage {
/// @dev The `x` coord of the secp256r1 public key used to sign the user operation.
uint256 x;
/// @dev The `y` coord of the secp256r1 public key used to sign the user operation.
uint256 y;
}

/// @author @KONFeature
/// @title WebAuthnFclValidator
/// @notice Kernel validator used to validated user operations via WebAuthn signature (using P256 under the hood)
/// @notice Using the awesome FreshCryptoLib: https://github.com/rdubois-crypto/FreshCryptoLib/
/// @notice Inspired by the cometh Gnosis Safe signer: https://github.com/cometh-game/p256-signer
contract WebAuthnFclValidator is IKernelValidator {
/// @dev Event emitted when the public key signing the WebAuthN user operation is changed for a given `kernel`.
event WebAuthnPublicKeyChanged(address indexed kernel, uint256 x, uint256 y);

/// @dev Mapping of kernel address to each webAuthn specific storage
mapping(address kernel => WebAuthnFclValidatorStorage webAuthnStorage) private webAuthnValidatorStorage;

/// @dev The address of the p256 verifier contract (should be 0x100 on the RIP-7212 compliant chains)
/// @dev To follow up for the deployment: https://forum.polygon.technology/t/pip-27-precompiled-for-secp256r1-curve-support/13049
address public immutable P256_VERIFIER;

/// @dev Simple constructor, setting the P256 verifier address
constructor(address _p256Verifier) {
P256_VERIFIER = _p256Verifier;
}

/// @dev Disable this validator for a given `kernel` (msg.sender)
function disable(bytes calldata) external payable override {
delete webAuthnValidatorStorage[msg.sender];
}

/// @dev Enable this validator for a given `kernel` (msg.sender)
function enable(bytes calldata _data) external payable override {
// Extract the x & y coordinates of the public key from the `_data` bytes
(uint256 x, uint256 y) = abi.decode(_data, (uint256, uint256));
// Update the pub key data
WebAuthnFclValidatorStorage storage kernelValidatorStorage = webAuthnValidatorStorage[msg.sender];
kernelValidatorStorage.x = x;
kernelValidatorStorage.y = y;
// Emit the update event
emit WebAuthnPublicKeyChanged(msg.sender, x, y);
}

/// @dev Validate a `_userOp` using a WebAuthn Signature for the kernel account who is the `_userOp` sender
function validateUserOp(UserOperation calldata _userOp, bytes32 _userOpHash, uint256)
external
payable
override
returns (ValidationData validationData)
{
WebAuthnFclValidatorStorage memory kernelValidatorStorage = webAuthnValidatorStorage[_userOp.sender];

// Perform a check against the direct userOpHash, if ok consider the user op as validated
if (_checkSignature(kernelValidatorStorage, _userOpHash, _userOp.signature)) {
return ValidationData.wrap(0);
}

return SIG_VALIDATION_FAILED;
}

/// @dev Validate a `_signature` of the `_hash` ofor the given `kernel` (msg.sender)
function validateSignature(bytes32 _hash, bytes calldata _signature)
public
view
override
returns (ValidationData)
{
WebAuthnFclValidatorStorage memory kernelValidatorStorage = webAuthnValidatorStorage[msg.sender];

// Check the validity againt the hash directly
if (_checkSignature(kernelValidatorStorage, _hash, _signature)) {
return ValidationData.wrap(0);
}

// Otherwise, all good
return SIG_VALIDATION_FAILED;
}

/// @notice Validates the given `_signature` againt the `_hash` for the given `kernel` (msg.sender)
/// @param _kernelValidatorStorage The kernel storage replication (helping us to fetch the X & Y points of the public key)
/// @param _hash The hash signed
/// @param _signature The signature
function _checkSignature(
WebAuthnFclValidatorStorage memory _kernelValidatorStorage,
bytes32 _hash,
bytes calldata _signature
) private view returns (bool isValid) {
return WebAuthnFclVerifier._verifyWebAuthNSignature(
P256_VERIFIER, _hash, _signature, _kernelValidatorStorage.x, _kernelValidatorStorage.y
);
}

/// @dev Check if the caller is a valid signer, this don't apply to the WebAuthN validator, since it's using a public key
function validCaller(address, bytes calldata) external pure override returns (bool) {
revert NotImplemented();
}

/* -------------------------------------------------------------------------- */
/* Public view methods */
/* -------------------------------------------------------------------------- */

/// @dev Get the owner of a given `kernel`
function getPublicKey(address _kernel) public view returns (uint256 x, uint256 y) {
// Compute the storage slot
WebAuthnFclValidatorStorage storage kernelValidatorStorage = webAuthnValidatorStorage[_kernel];

// Access it for x and y
x = kernelValidatorStorage.x;
y = kernelValidatorStorage.y;
}
}
138 changes: 138 additions & 0 deletions src/validator/webauthn/WebAuthnFclVerifier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {Base64} from "solady/utils/Base64.sol";

/// @title WebAuthnFclVerifier
/// @author rdubois-crypto
/// @author obatirou
/// @author KONFeature
/// @notice A library used to format webauthn stuff into verifiable p256 messages msg
/// From https://github.com/cometh-hq/p256-signer/blob/09319213276da69aad6d96fa75cd339726f78bb9/contracts/P256Signer.sol
/// And https://github.com/rdubois-crypto/FreshCryptoLib/blob/master/solidity/src/FCL_Webauthn.sol
library WebAuthnFclVerifier {
/// @dev Error thrown when the webauthn data is invalid
error InvalidWebAuthNData();

/// @dev 'bytes4(keccak256("InvalidWebAuthNData()"))'
uint256 private constant _INVALID_WEBAUTHN_DATA_SELECTOR = 0x81177746;

/// @dev the data flag mask we will use to verify the signature
/// @dev Always 0x01 for user presence flag -> https://www.w3.org/TR/webauthn-2/#concept-user-present
bytes1 private constant AUTHENTICATOR_DATA_FLAG_MASK = 0x01;

/// @dev layout of a signature (used to extract the reauired payload from the initial calldata)
struct FclSignatureLayout {
bytes authenticatorData;
bytes clientData;
uint256 challengeOffset;
uint256[2] rs;
}

/// @dev Format the webauthn challenge into a p256 message
/// @dev return the raw message that has been signed by the user on the p256 curve
/// @dev Logic from https://github.com/rdubois-crypto/FreshCryptoLib/blob/master/solidity/src/FCL_Webauthn.sol
/// @param _hash The hash that has been signed via WebAuthN
/// @param _signature The signature that has been provided with the userOp
/// @return p256Message The message that has been signed on the p256 curve
function _formatWebAuthNChallenge(bytes32 _hash, FclSignatureLayout calldata _signature)
internal
pure
returns (bytes32 p256Message)
{
// Extract a few calldata pointer we will use to format / verify our msg
bytes calldata authenticatorData = _signature.authenticatorData;
bytes calldata clientData = _signature.clientData;
uint256 challengeOffset = _signature.challengeOffset;

// If the challenge offset is uint256 max, it's mean that we are in the case of a dummy sig, so we can skip the check and just return the hash
if (challengeOffset == type(uint256).max) {
return _hash;
}

// Otherwise, perform the complete format and checks of the data
{
// Let the caller check if User Presence (0x01) or User Verification (0x04) are set
if ((authenticatorData[32] & AUTHENTICATOR_DATA_FLAG_MASK) != AUTHENTICATOR_DATA_FLAG_MASK) {
revert InvalidWebAuthNData();
}
// Verify that clientData commits to the expected client challenge
// Use the Base64Url encoding which omits padding characters to match WebAuthn Specification
bytes memory challengeEncoded = bytes(Base64.encode(abi.encodePacked(_hash), true, true));

// The part that will old the challenge extracted from the clientData
bytes memory challengeExtracted = new bytes(challengeEncoded.length);

assembly {
// Extract the challenge from the clientData
calldatacopy(
add(challengeExtracted, 32), add(clientData.offset, challengeOffset), mload(challengeExtracted)
)

// Check that the challenge extracted from the clientData is the same as the one provided in the userOp
if iszero(eq(
// Hash of the challenge exracted from the `clientData`
keccak256(add(challengeExtracted, 32), mload(challengeExtracted)),
// Hash of the provided challenge, encoded in Base64Url (to match the clientData encoding)
keccak256(add(challengeEncoded, 32), mload(challengeEncoded))
)) {
mstore(0x00, _INVALID_WEBAUTHN_DATA_SELECTOR)
revert(0x1c, 0x04)
}
}
}

// Verify the signature over sha256(authenticatorData || sha256(clientData))
bytes memory verifyData = new bytes(authenticatorData.length + 32);

assembly {
// Add the authenticator data at the start of the verifyData
calldatacopy(add(verifyData, 32), authenticatorData.offset, authenticatorData.length)
}

bytes32 clientDataHashed = sha256(clientData);
assembly {
// Add the client data hash at the end of the verifyData
mstore(add(verifyData, add(authenticatorData.length, 32)), clientDataHashed)
}

// Return the sha256 of the verifyData
return sha256(verifyData);
}

/// @dev Proceed to the full webauth verification
/// @param _p256Verifier The p256 verifier contract
/// @param _hash The hash that has been signed via WebAuthN
/// @param _signature The signature that has been provided with the userOp
/// @param _x The X point of the public key
/// @param _y The Y point of the public key
/// @return isValid True if the signature is valid, false otherwise
function _verifyWebAuthNSignature(
address _p256Verifier,
bytes32 _hash,
bytes calldata _signature,
uint256 _x,
uint256 _y
) internal view returns (bool isValid) {
// Extract the signature
FclSignatureLayout calldata signature;
// This code should precalculate the offsets of variables as defined in the layout
// From: https://twitter.com/k06a/status/1706934230779883656
assembly {
signature := _signature.offset
}

// Format the webauthn challenge into a p256 message
bytes32 challenge = _formatWebAuthNChallenge(_hash, signature);

// Prepare the argument we will use to verify the signature
bytes memory args = abi.encode(challenge, signature.rs[0], signature.rs[1], _x, _y);

// Send the call the the p256 verifier
(bool success, bytes memory ret) = _p256Verifier.staticcall(args);
assert(success); // never reverts, always returns 0 or 1

// Ensure that it has returned 1
return abi.decode(ret, (uint256)) == 1;
}
}
Loading