-
Notifications
You must be signed in to change notification settings - Fork 18
Add WebAuthn library and SignerWebAuthn #117
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
Open
ernestognw
wants to merge
54
commits into
master
Choose a base branch
from
feature/webauthn
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+827
−0
Open
Changes from all commits
Commits
Show all changes
54 commits
Select commit
Hold shift + click to select a range
13cc239
Add WebAuthn
ernestognw 2d2912e
Pragma up
ernestognw 7519e27
remove version header
ernestognw bb25a17
Improve docs
ernestognw 14a54af
Nits
ernestognw 5f6bab3
Iterate
ernestognw 4866998
Partially fix tests
ernestognw 8016d33
Fuzz test instead
ernestognw 19daf08
Merge branch 'master' into feature/webauthn
ernestognw 8573738
Add SignerWebAuthN
ernestognw ac7d01b
up
ernestognw c871241
Up
ernestognw fbab0d5
Merge branch 'master' into feature/webauthn
ernestognw 0059aad
up
ernestognw ab9f79b
Remove signer
ernestognw 9090f60
Add signer
ernestognw 60ff832
Remove unnecessary P256 library
ernestognw 9f34d45
Merge branch 'master' into feature/webauthn
ernestognw a13903d
Add to docs
ernestognw 6790c6b
Add AccountWebAuthn test
ernestognw 7c5f45e
Reorder webauth auth
ernestognw 4482164
nit
ernestognw 95f3c0a
Update SignerWebAuthn.sol
ernestognw 3eea8a3
Apply suggestions from code review
ernestognw 3c50fe7
Update contracts/utils/cryptography/WebAuthn.sol
ernestognw 50aa7d0
Update contracts/utils/cryptography/WebAuthn.sol
arr00 d6136c4
Make use of dangling `_decodeKey` in ERC7913ZKEmailVerifier (#145)
ernestognw d19afd9
Update node modules and remove unnecessary npm dependencies (#146)
arr00 eca5513
Treat subsequent onInstall calls as no-ops in ERC7579SignatureValidat…
ernestognw 3aa481c
Vendor dependencies in `vendored` branch (#149)
ernestognw e8492a4
Revert "Vendor dependencies in `vendored` branch" (#150)
ernestognw 30f2f61
Add documentation for crosschain message passing (#138)
luiz-lvj 4b8b87f
Move npm dependencies to git submodules (#153)
ernestognw e6be8f1
Add `view` modifier to `ERC20Allowlist.allowed` (#154)
felipelincoln 4662a31
Add ERC7579 Executor modules (#121)
ernestognw c878724
Fix documentation nits and add missing Changelog entries (#155)
ernestognw 8bc5b33
Move Axelar audit to audits folder (#156)
ernestognw a023191
Simplify ERC7579 Executors validation pattern (#158)
ernestognw 9b73593
Simplify ERC-7579 validator modules (#159)
ernestognw c86c9ef
Add ERC-7579 modules docs and EIP-7702 note in accounts docs (#157)
ernestognw dc94c40
Add `husky` to devDependencies (#160)
ernestognw 680d249
Fix bug in docs CI (#161)
arr00 63b3467
Refactor of the MultiSignerERC7913 abstract signer (#163)
Amxx 131b611
Refactor ERC7579 Multisig modules (#165)
ernestognw ef537ba
Reorganise utils/cryptography folder (#164)
Amxx 9527637
Merge branch 'master' into feature/webauthn
ernestognw f7a97dd
Move Signer
ernestognw 051ae83
up
ernestognw 797cf7a
Reapply Aryeh's suggestion
ernestognw 0e8a700
Fix import
ernestognw 1bfa073
Fix another import
ernestognw 3b53c2b
Fix compilation
ernestognw f72340d
Revert libraries
ernestognw 586b06d
Merge branch 'master' into feature/webauthn
ernestognw File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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,25 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.24; | ||
|
||
import {Account} from "../../account/Account.sol"; | ||
import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; | ||
import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; | ||
import {ERC7739} from "../../utils/cryptography/signers/ERC7739.sol"; | ||
import {ERC7821} from "../../account/extensions/ERC7821.sol"; | ||
import {SignerWebAuthn} from "../../utils/cryptography/signers/SignerWebAuthn.sol"; | ||
|
||
abstract contract AccountWebAuthnMock is Account, SignerWebAuthn, ERC7739, ERC7821, ERC721Holder, ERC1155Holder { | ||
constructor(bytes32 qx, bytes32 qy) { | ||
_setSigner(qx, qy); | ||
} | ||
|
||
/// @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); | ||
} | ||
} |
This file contains hidden or 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 hidden or 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,250 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.24; | ||
|
||
import {Base64} from "@openzeppelin/contracts/utils/Base64.sol"; | ||
import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; | ||
import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; | ||
import {Strings} from "@openzeppelin/contracts/utils/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 private constant AUTH_DATA_FLAGS_UP = 0x01; | ||
/// @dev Bit 2 of the authenticator data flags: "User Verified" bit. | ||
bytes1 private constant AUTH_DATA_FLAGS_UV = 0x04; | ||
/// @dev Bit 3 of the authenticator data flags: "Backup Eligibility" bit. | ||
bytes1 private constant AUTH_DATA_FLAGS_BE = 0x08; | ||
/// @dev Bit 4 of the authenticator data flags: "Backup State" bit. | ||
bytes1 private constant AUTH_DATA_FLAGS_BS = 0x10; | ||
|
||
/// @dev The expected type string in the client data JSON when verifying assertion signatures. | ||
/// https://www.w3.org/TR/webauthn-2/#dom-collectedclientdata-type | ||
// solhint-disable-next-line quotes | ||
bytes32 private constant EXPECTED_TYPE_HASH = keccak256('"type":"webauthn.get"'); | ||
|
||
/** | ||
* @dev Performs the absolute minimal verification of a WebAuthn Authentication Assertion. | ||
* This function includes only the essential checks required for basic WebAuthn security: | ||
* | ||
* 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 | ||
* | ||
* For most applications, use {verify} or {verifyStrict} instead. | ||
* | ||
* NOTE: This function intentionally omits User Presence (UP), User Verification (UV), | ||
* and Backup State/Eligibility checks. Use this only when broader compatibility with | ||
* authenticators is required or in constrained environments. | ||
*/ | ||
function verifyMinimal( | ||
bytes memory challenge, | ||
WebAuthnAuth memory auth, | ||
bytes32 qx, | ||
bytes32 qy | ||
) 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 | ||
if (auth.authenticatorData.length < 37) return false; | ||
bytes memory clientDataJSON = bytes(auth.clientDataJSON); | ||
|
||
return | ||
validateExpectedTypeHash(clientDataJSON, auth.typeIndex) && // 11 | ||
validateChallenge(clientDataJSON, auth.challengeIndex, challenge) && // 12 | ||
// Handles signature malleability internally | ||
P256.verify( | ||
sha256( | ||
abi.encodePacked( | ||
auth.authenticatorData, | ||
sha256(clientDataJSON) // 19 | ||
) | ||
), | ||
auth.r, | ||
auth.s, | ||
qx, | ||
qy | ||
); // 20 | ||
} | ||
|
||
/** | ||
* @dev Performs standard verification of a WebAuthn Authentication Assertion. | ||
* | ||
* Same as {verifyMinimal}, but also verifies: | ||
* | ||
* [start=4] | ||
* 4. {validateUserPresentBitSet} - confirming physical user presence during authentication | ||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* | ||
* This compliance level satisfies the core WebAuthn verification requirements while | ||
* maintaining broad compatibility with authenticators. For higher security requirements, | ||
* consider using {verifyStrict}. | ||
*/ | ||
function verify( | ||
bytes memory challenge, | ||
WebAuthnAuth memory auth, | ||
bytes32 qx, | ||
bytes32 qy | ||
) internal view returns (bool) { | ||
// 16 && rest | ||
return validateUserPresentBitSet(auth.authenticatorData[32]) && verifyMinimal(challenge, auth, qx, qy); | ||
} | ||
|
||
/** | ||
* @dev Performs strict verification of a WebAuthn Authentication Assertion. | ||
* | ||
* Same as {verify}, but also also verifies: | ||
* | ||
* [start=5] | ||
* 5. {validateUserVerifiedBitSet} - confirming stronger user authentication (biometrics/PIN) | ||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* 6. {validateBackupEligibilityAndState}- Backup Eligibility (`BE`) and Backup State (BS) bits | ||
* relationship is valid | ||
* | ||
* This strict verification is recommended for: | ||
* | ||
* * High-value transactions | ||
* * Privileged operations | ||
* * Account recovery or critical settings changes | ||
* * Applications where security takes precedence over broad authenticator compatibility | ||
*/ | ||
function verifyStrict( | ||
bytes memory challenge, | ||
WebAuthnAuth memory auth, | ||
bytes32 qx, | ||
bytes32 qy | ||
) internal view returns (bool) { | ||
return | ||
validateUserVerifiedBitSet(auth.authenticatorData[32]) && // 17 | ||
validateBackupEligibilityAndState(auth.authenticatorData[32]) && // Consistency check | ||
verify(challenge, auth, qx, qy); | ||
} | ||
|
||
/** | ||
* @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) internal 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) internal 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) internal pure returns (bool) { | ||
return (flags & AUTH_DATA_FLAGS_BE) != 0 || (flags & AUTH_DATA_FLAGS_BS) == 0; | ||
} | ||
Comment on lines
+217
to
+219
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would prefer consistency on how we are checking the value of a bit
Otherwise looks good |
||
|
||
/** | ||
* @dev Validates that the https://www.w3.org/TR/webauthn-2/#type[Type] field in the client data JSON | ||
* is set to "webauthn.get". | ||
*/ | ||
function validateExpectedTypeHash(bytes memory clientDataJSON, uint256 typeIndex) internal pure returns (bool) { | ||
// 21 = length of '"type":"webauthn.get"' | ||
bytes memory typeValueBytes = Bytes.slice(clientDataJSON, typeIndex, typeIndex + 21); | ||
return keccak256(typeValueBytes) == EXPECTED_TYPE_HASH; | ||
} | ||
|
||
/// @dev Validates that the challenge in the client data JSON matches the `expectedChallenge`. | ||
function validateChallenge( | ||
bytes memory clientDataJSON, | ||
uint256 challengeIndex, | ||
bytes memory expectedChallenge | ||
) internal pure returns (bool) { | ||
bytes memory expectedChallengeBytes = bytes( | ||
// solhint-disable-next-line quotes | ||
string.concat('"challenge":"', Base64.encodeURL(expectedChallenge), '"') | ||
); | ||
if (challengeIndex + expectedChallengeBytes.length > clientDataJSON.length) return false; | ||
bytes memory actualChallengeBytes = Bytes.slice( | ||
clientDataJSON, | ||
challengeIndex, | ||
challengeIndex + expectedChallengeBytes.length | ||
); | ||
|
||
return Strings.equal(string(actualChallengeBytes), string(expectedChallengeBytes)); | ||
} | ||
} |
This file contains hidden or 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,60 @@ | ||
// 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(); | ||
|
||
return | ||
WebAuthn.verifyMinimal(abi.encodePacked(hash), _toWebAuthnSignature(signature), qx, qy) || | ||
super._rawSignatureValidation(hash, signature); | ||
} | ||
|
||
/// @dev Non-reverting version of signature decoding. | ||
function _toWebAuthnSignature(bytes calldata signature) private pure returns (WebAuthn.WebAuthnAuth memory auth) { | ||
bool decodable; | ||
assembly ("memory-safe") { | ||
let offset := calldataload(signature.offset) | ||
// Validate the offset is within bounds and makes sense for a WebAuthnAuth struct | ||
// A valid offset should be 32 and point to data within the signature bounds | ||
decodable := and(eq(offset, 32), lt(add(offset, 0x80), signature.length)) | ||
arr00 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
return decodable ? abi.decode(signature, (WebAuthn.WebAuthnAuth)) : auth; | ||
} | ||
} |
This file contains hidden or 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
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.