Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/angry-waves-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`WebAuthn`: Add a library for verifying WebAuthn Authentication Assertions.
5 changes: 5 additions & 0 deletions .changeset/petite-seas-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`SignerWebAuthn`: Add an abstract signer that verifies WebAuthn signatures, with a P256 fallback.
5 changes: 5 additions & 0 deletions .changeset/tender-dolls-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`ERC7913WebAuthnVerifier`: Add an ERC-7913 verifier that verifies WebAuthn Authentication Assertions for P256 identities.
12 changes: 12 additions & 0 deletions contracts/mocks/account/AccountMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {AbstractSigner} from "../../utils/cryptography/signers/AbstractSigner.so
import {SignerECDSA} from "../../utils/cryptography/signers/SignerECDSA.sol";
import {SignerP256} from "../../utils/cryptography/signers/SignerP256.sol";
import {SignerRSA} from "../../utils/cryptography/signers/SignerRSA.sol";
import {SignerWebAuthn} from "../../utils/cryptography/signers/SignerWebAuthn.sol";
import {SignerERC7702} from "../../utils/cryptography/signers/SignerERC7702.sol";
import {SignerERC7913} from "../../utils/cryptography/signers/SignerERC7913.sol";
import {MultiSignerERC7913} from "../../utils/cryptography/signers/MultiSignerERC7913.sol";
Expand Down Expand Up @@ -70,6 +71,17 @@ abstract contract AccountRSAMock is Account, SignerRSA, ERC7739, ERC7821, ERC721
}
}

abstract contract AccountWebAuthnMock is Account, SignerWebAuthn, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
/// @inheritdoc ERC7821
function _erc7821AuthorizedExecutor(
address caller,
bytes32 mode,
bytes calldata executionData
) internal view virtual override returns (bool) {
return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
}
}

abstract contract AccountERC7702Mock is Account, SignerERC7702, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
/// @inheritdoc ERC7821
function _erc7821AuthorizedExecutor(
Expand Down
8 changes: 7 additions & 1 deletion contracts/utils/cryptography/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ A collection of contracts and libraries that implement various signature validat
* {MerkleProof}: Functions for verifying https://en.wikipedia.org/wiki/Merkle_tree[Merkle Tree] proofs.
* {EIP712}: Contract with functions to allow processing signed typed structure data according to https://eips.ethereum.org/EIPS/eip-712[EIP-712].
* {ERC7739Utils}: Utilities library that implements a defensive rehashing mechanism to prevent replayability of smart contract signatures based on ERC-7739.
* {WebAuthn}: Library for verifying WebAuthn Authentication Assertions.
* {AbstractSigner}: Abstract contract for internal signature validation in smart contracts.
* {ERC7739}: An abstract contract to validate signatures following the rehashing scheme from {ERC7739Utils}.
* {SignerECDSA}, {SignerP256}, {SignerRSA}: Implementations of an {AbstractSigner} with specific signature validation algorithms.
* {SignerERC7702}: Implementation of {AbstractSigner} that validates signatures using the contract's own address as the signer, useful for delegated accounts following EIP-7702.
* {SignerWebAuthn}: Implementation of {SignerP256} that supports WebAuthn
* {SignerERC7913}, {MultiSignerERC7913}, {MultiSignerERC7913Weighted}: Implementations of {AbstractSigner} that validate signatures based on ERC-7913. Including a simple and weighted multisignature scheme.
* {ERC7913P256Verifier}, {ERC7913RSAVerifier}: Ready to use ERC-7913 signature verifiers for P256 and RSA keys.
* {ERC7913P256Verifier}, {ERC7913RSAVerifier}, {ERC7913WebAuthnVerifier}: Ready to use ERC-7913 signature verifiers for P256, RSA keys and WebAuthn.

== Utils

Expand All @@ -40,6 +42,8 @@ A collection of contracts and libraries that implement various signature validat

{{ERC7739Utils}}

{{WebAuthn}}

== Abstract Signers

{{AbstractSigner}}
Expand All @@ -65,3 +69,5 @@ A collection of contracts and libraries that implement various signature validat
{{ERC7913P256Verifier}}

{{ERC7913RSAVerifier}}

{{ERC7913WebAuthnVerifier}}
2 changes: 1 addition & 1 deletion contracts/utils/cryptography/SignatureChecker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pragma solidity ^0.8.24;
import {ECDSA} from "./ECDSA.sol";
import {IERC1271} from "../../interfaces/IERC1271.sol";
import {IERC7913SignatureVerifier} from "../../interfaces/IERC7913.sol";
import {Bytes} from "../../utils/Bytes.sol";
import {Bytes} from "../Bytes.sol";

/**
* @dev Signature verification helper that can be used instead of `ECDSA.recover` to seamlessly support:
Expand Down
260 changes: 260 additions & 0 deletions contracts/utils/cryptography/WebAuthn.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

import {P256} from "./P256.sol";
import {Base64} from "../Base64.sol";
import {Bytes} from "../Bytes.sol";
import {Strings} from "../Strings.sol";

/**
* @dev Library for verifying WebAuthn Authentication Assertions.
*
* WebAuthn enables strong authentication for smart contracts using
* https://docs.openzeppelin.com/contracts/5.x/api/utils#P256[P256]
* as an alternative to traditional secp256k1 ECDSA signatures. This library verifies
* signatures generated during WebAuthn authentication ceremonies as specified in the
* https://www.w3.org/TR/webauthn-2/[WebAuthn Level 2 standard].
*
* For blockchain use cases, the following WebAuthn validations are intentionally omitted:
*
* * Origin validation: Origin verification in `clientDataJSON` is omitted as blockchain
* contexts rely on authenticator and dapp frontend enforcement. Standard authenticators
* implement proper origin validation.
* * RP ID hash validation: Verification of `rpIdHash` in authenticatorData against expected
* RP ID hash is omitted. This is typically handled by platform-level security measures.
* Including an expiry timestamp in signed data is recommended for enhanced security.
* * Signature counter: Verification of signature counter increments is omitted. While
* useful for detecting credential cloning, on-chain operations typically include nonce
* protection, making this check redundant.
* * Extension outputs: Extension output value verification is omitted as these are not
* essential for core authentication security in blockchain applications.
* * Attestation: Attestation object verification is omitted as this implementation
* focuses on authentication (`webauthn.get`) rather than registration ceremonies.
*
* Inspired by:
*
* * https://github.com/daimo-eth/p256-verifier/blob/master/src/WebAuthn.sol[daimo-eth implementation]
* * https://github.com/base/webauthn-sol/blob/main/src/WebAuthn.sol[base implementation]
*/
library WebAuthn {
struct WebAuthnAuth {
bytes32 r; /// The r value of secp256r1 signature
bytes32 s; /// The s value of secp256r1 signature
uint256 challengeIndex; /// The index at which "challenge":"..." occurs in `clientDataJSON`.
uint256 typeIndex; /// The index at which "type":"..." occurs in `clientDataJSON`.
/// The WebAuthn authenticator data.
/// https://www.w3.org/TR/webauthn-2/#dom-authenticatorassertionresponse-authenticatordata
bytes authenticatorData;
/// The WebAuthn client data JSON.
/// https://www.w3.org/TR/webauthn-2/#dom-authenticatorresponse-clientdatajson
string clientDataJSON;
}

/// @dev Bit 0 of the authenticator data flags: "User Present" bit.
bytes1 internal constant AUTH_DATA_FLAGS_UP = 0x01;
/// @dev Bit 2 of the authenticator data flags: "User Verified" bit.
bytes1 internal constant AUTH_DATA_FLAGS_UV = 0x04;
/// @dev Bit 3 of the authenticator data flags: "Backup Eligibility" bit.
bytes1 internal constant AUTH_DATA_FLAGS_BE = 0x08;
/// @dev Bit 4 of the authenticator data flags: "Backup State" bit.
bytes1 internal constant AUTH_DATA_FLAGS_BS = 0x10;

/**
* @dev Performs standard verification of a WebAuthn Authentication Assertion.
*/
function verify(
bytes memory challenge,
WebAuthnAuth memory auth,
bytes32 qx,
bytes32 qy
) internal view returns (bool) {
return verify(challenge, auth, qx, qy, true);
}

/**
* @dev Performs verification of a WebAuthn Authentication Assertion. This variants allow the caller to select
* whether of not to require the UV flag (step 17).
*
* Verifies:
*
* 1. Type is "webauthn.get" (see {_validateExpectedTypeHash})
* 2. Challenge matches the expected value (see {_validateChallenge})
* 3. Cryptographic signature is valid for the given public key
* 4. confirming physical user presence during authentication
* 5. (if `requireUV` is true) confirming stronger user authentication (biometrics/PIN)
* 6. Backup Eligibility (`BE`) and Backup State (BS) bits relationship is valid
*/
function verify(
bytes memory challenge,
WebAuthnAuth memory auth,
bytes32 qx,
bytes32 qy,
bool requireUV
) internal view returns (bool) {
// Verify authenticator data has sufficient length (37 bytes minimum):
// - 32 bytes for rpIdHash
// - 1 byte for flags
// - 4 bytes for signature counter
return
auth.authenticatorData.length > 36 &&
_validateExpectedTypeHash(auth.clientDataJSON, auth.typeIndex) && // 11
_validateChallenge(auth.clientDataJSON, auth.challengeIndex, challenge) && // 12
_validateUserPresentBitSet(auth.authenticatorData[32]) && // 16
(!requireUV || _validateUserVerifiedBitSet(auth.authenticatorData[32])) && // 17
_validateBackupEligibilityAndState(auth.authenticatorData[32]) && // Consistency check
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO:
Do we want to completelly remove that check ? It could be argued that the WebAuthn devices should perfom that check offchain, and that we should not deal with it.

// P256.verify handles signature malleability internally
P256.verify(
sha256(
abi.encodePacked(
auth.authenticatorData,
sha256(bytes(auth.clientDataJSON)) // 19
)
),
auth.r,
auth.s,
qx,
qy
); // 20
}

/**
* @dev Validates that the https://www.w3.org/TR/webauthn-2/#type[Type] field in the client data JSON is set to
* "webauthn.get".
*
* Step 11 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion[verifying an assertion].
*/
function _validateExpectedTypeHash(
string memory clientDataJSON,
uint256 typeIndex
) private pure returns (bool success) {
assembly ("memory-safe") {
success := and(
// clientDataJson.length >= typeIndex + 21
gt(mload(clientDataJSON), add(typeIndex, 20)),
eq(
// get 32 bytes starting at index typexIndex in clientDataJSON, and keep the leftmost 21 bytes
and(mload(add(add(clientDataJSON, 0x20), typeIndex)), shl(88, not(0))),
// solhint-disable-next-line quotes
'"type":"webauthn.get"'
)
)
}
}

/**
* @dev Validates that the challenge in the client data JSON matches the `expectedChallenge`.
*
* Step 12 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion[verifying an assertion].
*/
function _validateChallenge(
string memory clientDataJSON,
uint256 challengeIndex,
bytes memory challenge
) private pure returns (bool) {
// solhint-disable-next-line quotes
string memory expectedChallenge = string.concat('"challenge":"', Base64.encodeURL(challenge), '"');
string memory actualChallenge = string(
Bytes.slice(bytes(clientDataJSON), challengeIndex, challengeIndex + bytes(expectedChallenge).length)
);

return Strings.equal(actualChallenge, expectedChallenge);
}

/**
* @dev Validates that the https://www.w3.org/TR/webauthn-2/#up[User Present (UP)] bit is set.
*
* Step 16 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion[verifying an assertion].
*
* NOTE: Required by WebAuthn spec but may be skipped for platform authenticators
* (Touch ID, Windows Hello) in controlled environments. Enforce for public-facing apps.
*/
function _validateUserPresentBitSet(bytes1 flags) private pure returns (bool) {
return (flags & AUTH_DATA_FLAGS_UP) == AUTH_DATA_FLAGS_UP;
}

/**
* @dev Validates that the https://www.w3.org/TR/webauthn-2/#uv[User Verified (UV)] bit is set.
*
* Step 17 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion[verifying an assertion].
*
* The UV bit indicates whether the user was verified using a stronger identification method
* (biometrics, PIN, password). While optional, requiring UV=1 is recommended for:
*
* * High-value transactions and sensitive operations
* * Account recovery and critical settings changes
* * Privileged operations
*
* NOTE: For routine operations or when using hardware authenticators without verification capabilities,
* `UV=0` may be acceptable. The choice of whether to require UV represents a security vs. usability
* tradeoff - for blockchain applications handling valuable assets, requiring UV is generally safer.
*/
function _validateUserVerifiedBitSet(bytes1 flags) private pure returns (bool) {
return (flags & AUTH_DATA_FLAGS_UV) == AUTH_DATA_FLAGS_UV;
}

/**
* @dev Validates the relationship between Backup Eligibility (`BE`) and Backup State (`BS`) bits
* according to the WebAuthn specification.
*
* The function enforces that if a credential is backed up (`BS=1`), it must also be eligible
* for backup (`BE=1`). This prevents unauthorized credential backup and ensures compliance
* with the WebAuthn spec.
*
* Returns true in these valid states:
*
* * `BE=1`, `BS=0`: Credential is eligible but not backed up
* * `BE=1`, `BS=1`: Credential is eligible and backed up
* * `BE=0`, `BS=0`: Credential is not eligible and not backed up
*
* Returns false only when `BE=0` and `BS=1`, which is an invalid state indicating
* a credential that's backed up but not eligible for backup.
*
* NOTE: While the WebAuthn spec defines this relationship between `BE` and `BS` bits,
* validating it is not explicitly required as part of the core verification procedure.
* Some implementations may choose to skip this check for broader authenticator
* compatibility or when the application's threat model doesn't consider credential
* syncing a major risk.
*/
function _validateBackupEligibilityAndState(bytes1 flags) private pure returns (bool) {
return (flags & AUTH_DATA_FLAGS_BE) == AUTH_DATA_FLAGS_BE || (flags & AUTH_DATA_FLAGS_BS) == 0;
}

/**
* @dev Verifies that calldata bytes (`input`) represents a valid `WebAuthnAuth` object. If encoding is valid,
* returns true and the calldata view at the object. Otherwise, returns false and an invalid calldata object.
*
* NOTE: The returned `auth` object should not be accessed if `success` is false. Trying to access the data may
* cause revert/panic.
*/
function tryDecodeAuth(bytes calldata input) internal pure returns (bool success, WebAuthnAuth calldata auth) {
assembly ("memory-safe") {
auth := input.offset
}

// Minimum length to hold 6 objects (32 bytes each)
if (input.length < 0xC0) return (false, auth);

// Get offset of non-value-type elements relative to the input buffer
uint256 authenticatorDataOffset = uint256(bytes32(input[0x80:]));
uint256 clientDataJSONOffset = uint256(bytes32(input[0xa0:]));

// The elements length (at the offset) should be 32 bytes long. We check that this is within the
// buffer bounds. Since we know input.length is at least 32, we can subtract with no overflow risk.
if (input.length - 0x20 < authenticatorDataOffset || input.length - 0x20 < clientDataJSONOffset)
return (false, auth);

// Get the lengths. offset + 32 is bounded by input.length so it does not overflow.
uint256 authenticatorDataLength = uint256(bytes32(input[authenticatorDataOffset:]));
uint256 clientDataJSONLength = uint256(bytes32(input[clientDataJSONOffset:]));

// Check that the input buffer is long enough to store the non-value-type elements
// Since we know input.length is at least xxxOffset + 32, we can subtract with no overflow risk.
if (
input.length - authenticatorDataOffset - 0x20 < authenticatorDataLength ||
input.length - clientDataJSONOffset - 0x20 < clientDataJSONLength
) return (false, auth);

return (true, auth);
}
}
50 changes: 50 additions & 0 deletions contracts/utils/cryptography/signers/SignerWebAuthn.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

import {SignerP256} from "./SignerP256.sol";
import {WebAuthn} from "../WebAuthn.sol";

/**
* @dev Implementation of {SignerP256} that supports WebAuthn authentication assertions.
*
* This contract enables signature validation using WebAuthn authentication assertions,
* leveraging the P256 public key stored in the contract. It allows for both WebAuthn
* and raw P256 signature validation, providing compatibility with both signature types.
*
* The signature is expected to be an abi-encoded {WebAuthn-WebAuthnAuth} struct.
*
* Example usage:
*
* ```solidity
* contract MyAccountWebAuthn is Account, SignerWebAuthn, Initializable {
* function initialize(bytes32 qx, bytes32 qy) public initializer {
* _setSigner(qx, qy);
* }
* }
* ```
*
* IMPORTANT: Failing to call {_setSigner} either during construction (if used standalone)
* or during initialization (if used as a clone) may leave the signer either front-runnable or unusable.
*/
abstract contract SignerWebAuthn is SignerP256 {
/**
* @dev Validates a raw signature using the WebAuthn authentication assertion.
*
* In case the signature can't be validated, it falls back to the
* {SignerP256-_rawSignatureValidation} method for raw P256 signature validation by passing
* the raw `r` and `s` values from the signature.
*/
function _rawSignatureValidation(
bytes32 hash,
bytes calldata signature
) internal view virtual override returns (bool) {
(bytes32 qx, bytes32 qy) = signer();
(bool decodeSuccess, WebAuthn.WebAuthnAuth calldata auth) = WebAuthn.tryDecodeAuth(signature);

return
decodeSuccess
? WebAuthn.verify(abi.encodePacked(hash), auth, qx, qy)
: super._rawSignatureValidation(hash, signature);
}
}
Loading