Skip to content

Commit

Permalink
Merge pull request #6 from base-org/wilson/fix
Browse files Browse the repository at this point in the history
Fix for variable clientDataJSON serialization
  • Loading branch information
wilsoncusack authored Mar 5, 2024
2 parents 8932223 + 88d6749 commit 003b01f
Show file tree
Hide file tree
Showing 8 changed files with 62 additions and 316 deletions.
11 changes: 2 additions & 9 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,9 +1,2 @@
WebAuthnTest:test_CB() (gas: 294338)
WebAuthnTest:test_CBCalldataSize() (gas: 354149)
WebAuthnTest:test_CBCalldataSize2() (gas: 370966)
WebAuthnTest:test_Daimo() (gas: 424627)
WebAuthnTest:test_DaimoCalldataSize() (gas: 432582)
WebAuthnTest:test_FCL() (gas: 301259)
WebAuthnTest:test_FCLCalldataSize() (gas: 415293)
WebAuthnTest:test_chrome() (gas: 249746)
WebAuthnTest:test_safari() (gas: 245050)
WebAuthnTest:test_chrome() (gas: 252308)
WebAuthnTest:test_safari() (gas: 247931)
6 changes: 3 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
[submodule "lib/FreshCryptoLib"]
path = lib/FreshCryptoLib
url = https://github.com/rdubois-crypto/FreshCryptoLib
[submodule "lib/p256-verifier"]
path = lib/p256-verifier
url = https://github.com/daimo-eth/p256-verifier
[submodule "lib/solady"]
path = lib/solady
url = https://github.com/vectorized/solady
24 changes: 5 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ Webauthn-sol is a Solidity library for verifying WebAuthn authentication asserti

This library is optimized for Ethereum layer 2 rollup chains but will work on all EVM chains. Signature verification always attempts to use the [RIP-7212 precompile](https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md) and, if this fails, falls back to using [FreshCryptoLib](https://github.com/rdubois-crypto/FreshCryptoLib/blob/master/solidity/src/FCL_ecdsa.sol#L40).

As L1 calldata is the main cost driver of L2 transactions, this library is designed to minimize calldata. Rather than requiring the full clientDataJSON to be passed, we use a template to verify against what a well formed response *should* be, leveraging the [serialization specification](https://www.w3.org/TR/webauthn/#clientdatajson-serialization).

Code excerpts

```solidity
Expand Down Expand Up @@ -49,28 +47,16 @@ uint256 x = 28573233055232466711029625910063034642429572463461595413086259353299
uint256 y = 39367742072897599771788408398752356480431855827262528811857788332151452825281;
WebAuthn.WebAuthnAuth memory auth = WebAuthn.WebAuthnAuth({
authenticatorData: hex"49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000101",
origin: "http://localhost:3005",
crossOriginAndRemainder: "",
clientDataJSON: string.concat(
'{"type":"webauthn.get","challenge":"', Base64Url.encode(challenge), '","origin":"http://localhost:3005"}'
),
challengeIndex: 23,
typeIndex: 1,
r: 43684192885701841787131392247364253107519555363555461570655060745499568693242,
s: 22655632649588629308599201066602670461698485748654492451178007896016452673579
});
assert(
WebAuthn.verify(
challenge, false, auth, x, y
)
);
```

### Calldata fee comparison
A comparison with some other WebAuthn verifiers.
Numbers from Base mainnet as of February 26, 2024.

| Library | Calldata size (bytes) | L1 fee wei | L1 fee cents |
|--------|---------------|------------|--------------|
| WebAuthn-sol | 576 | 212990146162662 | 63 |
| [Daimo's WebAuthn.sol](https://github.com/daimo-eth/p256-verifier/blob/master/src/WebAuthn.sol) | 672 | 262592374578294 | 78 |
| [FCL_WebAuthn.sol](https://github.com/rdubois-crypto/FreshCryptoLib/blob/master/solidity/src/FCL_Webauthn.sol) | 640 | 258426308149685 | 77 |

### Developing
After cloning the repo, run the tests using Forge, from [Foundry](https://github.com/foundry-rs/foundry?tab=readme-ov-file)
```bash
Expand Down
1 change: 0 additions & 1 deletion lib/p256-verifier
Submodule p256-verifier deleted from 29475a
1 change: 1 addition & 0 deletions lib/solady
Submodule solady added at e7024b
86 changes: 34 additions & 52 deletions src/WebAuthn.sol
Original file line number Diff line number Diff line change
@@ -1,32 +1,29 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {Base64Url} from "FreshCryptoLib/utils/Base64Url.sol";
import {Base64} from "solady/utils/Base64.sol";
import {FCL_ecdsa} from "FreshCryptoLib/FCL_ecdsa.sol";
import {LibString} from "solady/utils/LibString.sol";

/// @title WebAuthn
/// @notice A library for verifying WebAuthn Authentication Assertions, built off the work
/// of Daimo. This library is optimized for calldata,
/// and attempts to use the RIP-7212 precompile for signature verification.
/// of Daimo.
/// @dev Attempts to use the RIP-7212 precompile for signature verification.
/// If precompile verification fails, it falls back to FreshCryptoLib.
/// @author Coinbase (https://github.com/base-org/webauthn-sol)
/// @author Daimo (https://github.com/daimo-eth/p256-verifier/blob/master/src/WebAuthn.sol)
library WebAuthn {
using LibString for string;

struct WebAuthnAuth {
/// @dev https://www.w3.org/TR/webauthn-2/#dom-authenticatorassertionresponse-authenticatordata
bytes authenticatorData;
/// @dev https://www.w3.org/TR/webauthn-2/#dom-collectedclientdata-origin
string origin;
/// @dev https://www.w3.org/TR/webauthn-2/#dom-collectedclientdata-crossorigin
/// @dev 13. https://www.w3.org/TR/webauthn/#clientdatajson-serialization
/// crossOrigin should always be present, re https://www.w3.org/TR/webauthn/#clientdatajson-serialization
/// but in practice is sometimes not. For this reason we include with remainder. String may be empty.
/// e.g.
/// ''
/// '"crossOrigin":false'
/// '"tokenBinding":{"status":"present","id":"TbId"}'
/// '"crossOrigin":false,"tokenBinding":{"status":"present","id":"TbId"}'
string crossOriginAndRemainder;
/// @dev https://www.w3.org/TR/webauthn-2/#dom-authenticatorresponse-clientdatajson
string clientDataJSON;
/// The index at which "challenge":"..." occurs in clientDataJSON
uint256 challengeIndex;
/// The index at which "type":"..." occurs in clientDataJSON
uint256 typeIndex;
/// @dev The r value of secp256r1 signature
uint256 r;
/// @dev The s value of secp256r1 signature
Expand All @@ -40,6 +37,7 @@ library WebAuthn {
/// @dev secp256r1 curve order / 2 for malleability check
uint256 constant P256_N_DIV_2 = 57896044605178124381348723474703786764998477612067880171211129530534256022184;
address constant VERIFIER = address(0x100);
bytes32 constant EXPECTED_TYPE_HASH = keccak256('"type":"webauthn.get"');

/**
* @notice Verifies a Webauthn Authentication Assertion as described
Expand Down Expand Up @@ -92,17 +90,6 @@ library WebAuthn {
* response.attestationObject is NOT present in the response, i.e. the
* RP does not intend to verify an attestation.
*
* Our verification does not use full JSON parsing but leverages the serialization spec
* https://www.w3.org/TR/webauthn/#clientdatajson-serialization
* which is depended on by the limited verification algorithm
* https://www.w3.org/TR/webauthn/#clientdatajson-verification.
* We believe our templating approach is robust to future changes because the spec states
* "...future versions of this specification must not remove any of the fields
* type, challenge, origin, or crossOrigin from CollectedClientData.
* They also must not change the serialization algorithm to change the order
* in which those fields are serialized."
* https://www.w3.org/TR/webauthn/#clientdatajson-development
*
* @param challenge The challenge that was provided by the relying party
* @param requireUserVerification A boolean indicating whether user verification is required
* @param webAuthnAuth The WebAuthnAuth struct containing the authenticatorData, origin, crossOriginAndRemainder, r, and s
Expand All @@ -122,47 +109,42 @@ library WebAuthn {
return false;
}

// 11. and 12. will be verified by the signature check
// 11. Verify that the value of C.type is the string webauthn.get.
// 21 = bytes("type":"webauthn.get").length
string memory _type = webAuthnAuth.clientDataJSON.slice(webAuthnAuth.typeIndex, webAuthnAuth.typeIndex + 21);
if (keccak256(bytes(_type)) != EXPECTED_TYPE_HASH) {
return false;
}

// 12. Verify that the value of C.challenge equals the base64url encoding of options.challenge.
string memory challengeB64url = Base64Url.encode(challenge);
string memory remainder = bytes(webAuthnAuth.crossOriginAndRemainder).length == 0
? ""
: string.concat(",", webAuthnAuth.crossOriginAndRemainder);
string memory clientDataJSON = string.concat(
// A well formed clientDataJSON will always begin with
// {"type":"webauthn.get","challenge":"
// and so we can save calldata and use this by default
// https://www.w3.org/TR/webauthn/#clientdatajson-serialization
'{"type":"webauthn.get","challenge":"',
challengeB64url,
'",',
'"origin":"',
webAuthnAuth.origin,
'"',
remainder,
"}"
string memory challengeB64url = Base64.encode(challenge, true, true);
// 13. Verify that the value of C.challenge equals the base64url encoding of options.challenge.
bytes memory expectedChallenge = bytes(string.concat('"challenge":"', challengeB64url, '"'));
string memory actualChallenge = webAuthnAuth.clientDataJSON.slice(
webAuthnAuth.challengeIndex, webAuthnAuth.challengeIndex + expectedChallenge.length
);
if (keccak256(bytes(actualChallenge)) != keccak256(expectedChallenge)) {
return false;
}

// Skip 13., 14., and 15.
// Skip 15., 16., and 16.

// 16. Verify that the User Present bit of the flags in authData is set.
// 17. Verify that the UP bit of the flags in authData is set.
if (webAuthnAuth.authenticatorData[32] & AUTH_DATA_FLAGS_UP != AUTH_DATA_FLAGS_UP) {
return false;
}

// 17. If user verification is required for this assertion, verify that the User Verified bit of the flags in authData is set.
// 18. If user verification was determined to be required, verify that the UV bit of the flags in authData is set. Otherwise, ignore the value of the UV flag.
if (requireUserVerification && (webAuthnAuth.authenticatorData[32] & AUTH_DATA_FLAGS_UV) != AUTH_DATA_FLAGS_UV)
{
return false;
}

// skip 18.
// skip 19., 20., and 21.

// 19. Let hash be the result of computing a hash over the cData using SHA-256.
bytes32 clientDataJSONHash = sha256(bytes(clientDataJSON));
// 22. Let hash be the result of computing a hash over the cData using SHA-256.
bytes32 clientDataJSONHash = sha256(bytes(webAuthnAuth.clientDataJSON));

// 20. Using credentialPublicKey, verify that sig is a valid signature over the binary concatenation of authData and hash.
// 23. Using credentialPublicKey, verify that sig is a valid signature over the binary concatenation of authData 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
Expand Down
Loading

0 comments on commit 003b01f

Please sign in to comment.