Skip to content
Draft
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: 4 additions & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@
url = https://github.com/vectorized/solady
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/openzeppelin/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
[submodule "lib/ozp256"]
path = lib/ozp256
url = https://github.com/OpenZeppelin/openzeppelin-contracts
20 changes: 20 additions & 0 deletions foundry.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"lib/FreshCryptoLib": {
"rev": "76f3f135b7b27d2aa519f265b56bfc49a2573ab5"
},
"lib/forge-std": {
"rev": "ae570fec082bfe1c1f45b0acca4a2b4f84d345ce"
},
"lib/openzeppelin-contracts": {
"rev": "5705e8208bc92cd82c7bcdfeac8dbc7377767d96"
},
"lib/ozp256": {
"tag": {
"name": "v5.5.0",
"rev": "fcbae5394ae8ad52d8e580a3477db99814b9d565"
}
},
"lib/solady": {
"rev": "e7024bee47b1623f436ee491ca9458a6dc8abce9"
}
}
1 change: 1 addition & 0 deletions lib/ozp256
Submodule ozp256 added at fcbae5
66 changes: 49 additions & 17 deletions src/WebAuthn.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ library WebAuthn {
/// See https://www.w3.org/TR/webauthn-2/#dom-collectedclientdata-type
bytes32 private constant _EXPECTED_TYPE_HASH = keccak256('"type":"webauthn.get"');

// Known-valid P-256 test vector used to disambiguate precompile presence vs invalid signature.
// Values are from Wycheproof and match OpenZeppelin's probe.
bytes32 private constant _PROBE_H = 0xbb5a52f42f9c9261ed4361f59422a1e30036e7c32b270c8807a419feca605023;
bytes32 private constant _PROBE_R = bytes32(uint256(5));
bytes32 private constant _PROBE_S = bytes32(uint256(1));
bytes32 private constant _PROBE_QX = 0xa71af64de5126a4a4e02b7922d66ce9415ce88a4c9d25514d91082c8725ac957;
bytes32 private constant _PROBE_QY = 0x5d47723c8fbe580bb369fec9c2665d8e30a435b9932645482e7c9f11e872296b;

///
/// @notice Verifies a Webauthn Authentication Assertion as described
/// in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion.
Expand Down Expand Up @@ -95,11 +103,11 @@ library WebAuthn {
/// - 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.
///
/// @param challenge The challenge that was provided by the relying party.
/// @param requireUV A boolean indicating whether user verification is required.
/// @param challenge The challenge that was provided by the relying party.
/// @param requireUV A boolean indicating whether user verification is required.
/// @param webAuthnAuth The `WebAuthnAuth` struct.
/// @param x The x coordinate of the public key.
/// @param y The y coordinate of the public key.
/// @param x The x coordinate of the public key.
/// @param y The y coordinate of the public key.
///
/// @return `true` if the authentication assertion passed validation, else `false`.
function verify(bytes memory challenge, bool requireUV, WebAuthnAuth memory webAuthnAuth, uint256 x, uint256 y)
Expand All @@ -113,7 +121,7 @@ library WebAuthn {
}

// 11. Verify that the value of C.type is the string webauthn.get.
// bytes("type":"webauthn.get").length = 21
// bytes("type":"webauthn.get").length = 21
string memory _type = webAuthnAuth.clientDataJSON.slice(webAuthnAuth.typeIndex, webAuthnAuth.typeIndex + 21);
if (keccak256(bytes(_type)) != _EXPECTED_TYPE_HASH) {
return false;
Expand All @@ -135,7 +143,7 @@ library WebAuthn {
}

// 17. If user verification is required for this assertion, verify that the User Verified bit of the flags in
// authData is set.
// authData is set.
if (requireUV && (webAuthnAuth.authenticatorData[32] & _AUTH_DATA_FLAGS_UV) != _AUTH_DATA_FLAGS_UV) {
return false;
}
Expand All @@ -146,19 +154,43 @@ library WebAuthn {
bytes32 clientDataJSONHash = sha256(bytes(webAuthnAuth.clientDataJSON));

// 20. Using credentialPublicKey, verify that sig is a valid signature over the binary concatenation of authData
// and hash.
// and hash.
bytes32 messageHash = sha256(abi.encodePacked(webAuthnAuth.authenticatorData, clientDataJSONHash));
bytes memory args = abi.encode(messageHash, webAuthnAuth.r, webAuthnAuth.s, x, y);
// try the RIP-7212 precompile address
(bool success, bytes memory ret) = _VERIFIER.staticcall(args);
// staticcall will not revert if address has no code
// check return length
// note that even if precompile exists, ret.length is 0 when verification returns false
// so an invalid signature will be checked twice: once by the precompile and once by FCL.
// Ideally this signature failure is simulated offchain and no one actually pay this gas.
bool valid = ret.length > 0;
if (success && valid) return abi.decode(ret, (uint256)) == 1;
// Attempt RIP-7212 precompile; if ambiguous/false, probe with known-valid vector to
// detect precompile presence and avoid unnecessary software fallback when present.
if (_rip7212(messageHash, webAuthnAuth.r, webAuthnAuth.s, x, y)) {
return true;
}

if (_rip7212(_PROBE_H, uint256(_PROBE_R), uint256(_PROBE_S), uint256(_PROBE_QX), uint256(_PROBE_QY))) {
// Precompile is present; original signature invalid.
return false;
}

// Precompile absent; fall back to FreshCryptoLib's software verifier.
return FCL_ecdsa.ecdsa_verify(messageHash, webAuthnAuth.r, webAuthnAuth.s, x, y);
Copy link
Author

Choose a reason for hiding this comment

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

continue to use FCL instead of OZ verifier because it has slightly better gas performance and was internally audited so we can be confident in the quality of the audit.

}

/// @dev RIP-7212 precompile call. Writes output to scratch space to distinguish
/// empty returns from zero words. Returns true if signature is valid.
function _rip7212(bytes32 h, uint256 r, uint256 s, uint256 qx, uint256 qy) private view returns (bool isValid) {
assembly {
let ptr := mload(0x40)
mstore(ptr, h)
mstore(add(ptr, 0x20), r)
mstore(add(ptr, 0x40), s)
mstore(add(ptr, 0x60), qx)
mstore(add(ptr, 0x80), qy)

// Zero scratch space. If the precompile returns nothing, it will remain zero.
mstore(0x00, 0)

// Call RIP-7212 precompile (address 0x100). Return data (32 bytes) is written to 0x00.
// staticcall returns success even if address has no code; output length may be zero.
// We do not branch on the success flag; instead we read the scratch word.
pop(staticcall(gas(), 0x100, ptr, 0xa0, 0x00, 0x20))

isValid := mload(0x00)
}
}
}
Loading