WebAuthn Library
Description
webauthn
is a library designed to validate passkeys. Currently, it supports only the ECDSA signature scheme utilizing the secp256r1 curve. This functionality is provided by the secp256r1 library. Support for additional algorithms is planned for future releases.
Installation
Foundry
To install the webauthn
package in a Foundry project, execute the following command:
forge install https://github.com/0x90d2b2b7fb7599eebb6e7a32980857d8/webauthn
This command will install the latest version of the package in your lib directory. To install a specific version of the library, follow the instructions in the official Foundry documentation.
Hardhat or Truffle
To install the webauthn
package in a Hardhat or Truffle project, use npm
to run the following command:
npm install @0x90d2b2b7fb7599eebb6e7a32980857d8/webauthn
After the installation, import the package into your project and use it to validate a Web Authentication signature.
⚠️ Note: This package is not published on the npm registry, and is only available on GitHub Packages. You need to be authenticated with GitHub Packages to install it. For more information, please refer to the troubleshooting section. We are willing to deploy it on the npm registry if there is a need. Please open an issue if you would like to see this package on the npm registry.
Usage
This repository supports the verification of Web Authentication signatures using the ECDSA signature scheme with the secp256r1 curve. The non-experimental implementations from the secp256r1 library are all supported. After you've integrated this library into your project, you can freely import and use the implementation that best suits your specific use cases and requirements. Let's take a more detailed look at each one.
🚨 None of the implementations have been audited. DO NOT USE THEM IN PRODUCTION.
Scripts
This repository includes a script directory containing a set of scripts that can be used to deploy the
different implementations on-chain. Each script contains a set of instructions and an example of how to use it. The
scripts are expected to be run using the forge script
command.
Gas reports
This gas report was produced using the 0.8.19
version of the Solidity compiler (with 100k optimizer runs), specifically for the 0.3.0
version of the library.
test/WebAuthnWrapper.sol:WebAuthnWrapper contract | |||||
---|---|---|---|---|---|
Deployment Cost | Deployment Size | ||||
1321776 | 6634 | ||||
Function Name | min | avg | median | max | # calls |
_generateMessage | 5230 | 5261 | 5230 | 5421 | 6 |
verify | 209257 | 209257 | 209257 | 209257 | 1 |
These costs can be considered end-to-end as they include the cost of the secp256r1 library. Note the library is wrapped in a contract that exposes the different functions of the library, impacting the gas cost of the benchmark.
ℹ️ Tests expected to revert are excluded from the gas report
Contributing
To contribute to the project, you must have Foundry and Node.js installed on your system. You can download them from their official websites:
- Node.js: https://nodejs.org/
- Foundry: https://book.getfoundry.sh/getting-started/installation
ℹ️ We recommend using nvm to manage your Node.js versions. Nvm is a flexible node version manager that allows you to switch between different versions of Node.js effortlessly. This repository includes a
.nvmrc
file at the root of the project. If you have nvm installed, you can runnvm use
at the root of the project to automatically switch to the appropriate version of Node.js.
Following the installation of Foundry and Node.js, there's an additional dependency called make
that needs to be
addressed.
make
is a build automation tool that employs a file known as a makefile to automate the construction of executable
programs and libraries. The makefile details the process of deriving the target program from the source files and other
dependencies. This allows developers to automate repetitive tasks and manage complex build processes efficiently. make
is our primary tool in a multi-environment repository. It enables us to centralize all commands into a single file
(the makefile), eliminating the need to deal with npm
scripts defined in a package.json or remembering
the various commands provided by the foundry
cli. If you're unfamiliar with make
, you can read more about it
here.
make
is automatically included in all modern Linux distributions. If you're using Linux, you should be able to use
make
without any additional steps. If not, you can likely find it in the package tool you usually use. MacOS users can
install make
using Homebrew with the following command:
brew install make
At this point, you should have all the required dependencies installed on your system.
💡 Running make at the root of the project will display a list of all the available commands. This can be useful to know what you can do
Installing the dependencies
To install the project dependencies, you can run the following command:
make install
This command will install the forge dependencies in the lib/
directory, the npm dependencies in the node_modules
directory and the git hooks defined in the project (refer to the Git hooks sections to learn more about
them). These dependencies aren't shipped in production; they're utility dependencies used to build, test, lint, format,
and more, for the project.
⚠️ This package uses a dependency installed on the Github package registry, meaning you need to authenticate with GitHub Packages to install it. For more information, refer to the troubleshooting section. We're open to deploying it on the npm registry if there's a demand for it. Please open an issue if you'd like to see this package on the npm registry.
Next, let's set up the git hooks.
Git hooks
This project uses Lefthook
to manage Git hooks, which are scripts that run automatically when certain Git events
occur, such as committing code or pushing changes to a remote repository. Lefthook
simplifies the management and
execution of these scripts.
After installing the dependencies, you can configure the Git hooks by running the following command in the project directory:
make hooks-i
This command installs a Git hook that runs Lefthook before pushing code to a remote repository. If Lefthook fails, the push is aborted.
If you wish to run Lefthook manually, you can use the following command:
make hooks
This will run all the Git hooks defined in the lefthook file.
Skipping git hooks
Should you need to intentionally skip Lefthook, you can pass the --no-verify
flag to the git push command. To bypass
Lefthook when pushing code, use the following command:
git push origin --no-verify
Testing
Unit tests
The unit tests are stored in the test
directory. They test individual functions of the package in isolation. These
tests are automatically run by GitHub Actions with every push to the main
branch and on every pull request targeting
this branch. They are also automatically run by the git hook on every push to a remote repository if you have installed
it (refer to the Git hooks section). Alternatively, you can run them locally by executing the following
command in the project directory:
make test
ℹ️ By adding the sufix
-v
the test command will run in verbose mode, displaying valuable output for debugging.
For your information, these tests are written using forge, and some employ the property-based testing pattern (fuzzing) to generate random inputs for the functions under test.
The tests use one cheat code` you should be aware of:
vm.ffi
: This cheat code allows us to execute an arbitrary command during the test suite. This cheat code is not enabled by default when creating a new foundry project, but in our case, it's enabled in our configuration (foundry configuration) for all tests. This cheat code is used to run the computation library that calculates 256 points on the secp256r1 elliptic curve from a public key. This is required for the variants that need these points to be deployed on-chain. Therefore, even if it's not explicit, every time you run the test suite, a Node.js script is executed at least one time. You can learn more about the library we use here.
📖 Cheatcodes are special instructions exposed by Foundry to enhance the developer experience. Learn more about them here.
💡 Run
make
to learn how to run the test in verbose mode, or display the coverage, or even generate a gas report.
Quality
This repository uses forge-fmt
, solhint
, and prettier
to enforce code quality. These tools are automatically run by
the GitHub Actions on every push to the main
branch and on every pull request targeting this branch. They are also
automatically run by the git hook on every push to a remote repository if you have installed it
(refer to the Git hooks section). Alternatively, you can run them locally by executing the following
command in the project directory:
make lint # run the linter
make format # run the formatter
make quality # run both
ℹ️ By adding the suffix
-fix
the linter and the formatter will try to fix the issues automatically.
Troubleshootings
Setup Github registry
You need to configure npm to use the Github registry. You can do so using the following command in your terminal:
npm config set @0x90d2b2b7fb7599eebb6e7a32980857d8:registry=https://npm.pkg.github.com
This will instruct npm to use the Github registry for packages deployed by @0x90d2b2b7fb7599eebb6e7a32980857d8
.
Once the Github registry is configured, you have to create a classic token on Github. To do so, go to your Github settings. The token must have the read:packages scope. Once you have created the token, use the following command in your terminal to authenticate to the Github registry:
npm login --auth-type=legacy --registry=https://npm.pkg.github.com
Your Github username is the username, and the password is the token you just created. At this point, your git should be configured to use the Github Package Registry for our packages.
⚠️ For more information, please refer to the GitHub documentation
Acknowledgements
Special thanks to btchip for developing the reference implementation here and for the invaluable guidance through the WebAuthn specification.
IWebAuthn256r1
Functions
verify
function verify(
bytes calldata authenticatorData,
bytes calldata clientData,
bytes calldata clientChallenge,
uint256 r,
uint256 s,
uint256 qx,
uint256 qy
)
external
returns (bool);
WebAuthn256r1
A library to verify ECDSA signature though WebAuthn on the secp256r1 curve
Functions
generateMessage
Validate the webauthn data and generate the signature message needed to recover
1. The signature counter is not checked in this implementation because
we already have the nonce on-chain to prevent the anti-replay attack.
The counter is 4-bytes long and it is located at bytes 33 of the authenticator data.
2. The RP.ID is not checked in this implementation as it is impossible to generate
the same keys for different RP.IDs with a well formed authenticator. The hash of the id
is 32-bytes long and it is located at bytes 0 of the authenticator data.
3. The length of the authenticator data is not fixed. It is at least 37 bytes
(rpIdHash (32) + flags (1) + counter (4)) but it can be longer if there is an
attested credential data and/or some extensions data. As we do not consider
the counter in this implementation, we only require the authenticator data to be
at least 32 bytes long in order to save some calldata gas.
4. You may probably ask why we encode the challenge in base64 on-chain instead of
of sending it already encoded to save some gas. This library is opinionated and
it assumes that it is used in the context of Account Abstraction. In this context,
valuable informations required to proceed the transaction will be stored in the
challenge meaning we need the challenge in clear to use it later in the flow.
That's why we decided to add an extra encoding step during the validation.
5. It is assumed this is not the responsibility of this contract to check the value
of the alg
parameter. It is expected this contract will be extended by another
contract that will redirect the message produced by this contract to the right
recovery function.
6. Both extension data and attested credential data are out of scope of this implementation.
7. It is not the responsibility of this contract to validate the attestation statement formats
This contract is based on the level 2 of the WebAuthn specification.
and until proven otherwise compliant with the level 3 of the specification.
function generateMessage(
bytes calldata authenticatorData,
bytes calldata clientData,
bytes calldata clientChallenge
)
internal
pure
returns (bytes32 message);
Parameters
Name | Type | Description |
---|---|---|
authenticatorData | bytes | The authenticator data structure encodes contextual bindings made by the authenticator. Described here: https://www.w3.org/TR/webauthn-2/#authenticator-data |
clientData | bytes | This is the client data that was signed. The client data represents the contextual bindings of both the WebAuthn Relying Party and the client. Described here: https://www.w3.org/TR/webauthn-2/#client-data |
clientChallenge | bytes | This is the challenge that was sent to the client to sign. It is part of the client data. In a classic non-EVM flow, this challenge is generated by the server and sent to the client to avoid replay attack. In our context, as we already have the nonce for this purpose we use this field to pass the arbitrary execution order. This value is expected to not be encoded in Base64, the encoding is done during the verification. |
Returns
Name | Type | Description |
---|---|---|
message | bytes32 | The signature message needed to recover |
verify
Verify ECDSA signature though WebAuthn on the secp256r1 curve
function verify(
bytes calldata authData,
bytes calldata clientData,
bytes calldata clientChallenge,
uint256 r,
uint256 s,
uint256 qx,
uint256 qy
)
internal
returns (bool);
Errors
InvalidAuthenticatorData
error InvalidAuthenticatorData();
InvalidClientData
error InvalidClientData();
InvalidChallenge
error InvalidChallenge();
Constants
UP_FLAG_MASK
bytes1 constant UP_FLAG_MASK = 0x01;
UV_FLAG_MASK
bytes1 constant UV_FLAG_MASK = 0x04;
BOTH_FLAG_MASK
bytes1 constant BOTH_FLAG_MASK = 0x05;
OFFSET_CLIENT_TYPE
uint256 constant OFFSET_CLIENT_TYPE = 0x12;
TYPE_GET_INDICATOR
bytes1 constant TYPE_GET_INDICATOR = 0x67;
TYPE_CREATE_INDICATOR
bytes1 constant TYPE_CREATE_INDICATOR = 0x63;
OFFSET_CLIENT_CHALLENGE_GET
uint256 constant OFFSET_CLIENT_CHALLENGE_GET = 0x24;
OFFSET_CLIENT_CHALLENGE_CREATE
uint256 constant OFFSET_CLIENT_CHALLENGE_CREATE = 0x27;
OFFSET_CREDID_LENGTH
uint256 constant OFFSET_CREDID_LENGTH = 0x35;
CREDID_LENGTH_LENGTH
uint256 constant CREDID_LENGTH_LENGTH = 0x02;
OFFSET_CREDID
uint256 constant OFFSET_CREDID = 0x37;
OFFSET_FLAG
uint256 constant OFFSET_FLAG = 0x20;
P256R1_PUBKEY_COORD_LENGTH
uint256 constant P256R1_PUBKEY_COORD_LENGTH = 0x20;