Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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 https://docs.openzeppelin.com/contracts/5.x/api/utils/cryptography#SignerP256[SignerP256] that supports WebAu
* {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