Skip to content

Commit

Permalink
Webauthn initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Agusx1211 committed Jan 10, 2024
1 parent dd9ce56 commit 1b3d7f1
Show file tree
Hide file tree
Showing 3 changed files with 284 additions and 0 deletions.
147 changes: 147 additions & 0 deletions contracts/libs/p256-verifier/WebAuthn.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import "../../utils/LibString.sol";
import "./P256.sol";

/**
* Helper library for external contracts to verify WebAuthn signatures.
* based on p256 verifier https://github.com/daimo-eth/p256-verifier/blob/master/src/WebAuthn.sol
* MIT License
**/
library WebAuthn {
bytes1 private constant AUTH_DATA_FLAGS_UP = 0x01; // Bit 0
bytes1 private constant AUTH_DATA_FLAGS_UV = 0x04; // Bit 2
bytes1 private constant AUTH_DATA_FLAGS_BE = 0x08; // Bit 3
bytes1 private constant AUTH_DATA_FLAGS_BS = 0x10; // Bit 4

/// Verifies the authFlags in authenticatorData. Numbers in inline comment
/// correspond to the same numbered bullets in
/// https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion.
function checkAuthFlags(
bytes1 flags,
bool requireUserVerification
) internal pure returns (bool) {
// 17. Verify that the UP bit of the flags in authData is set.
if (flags & AUTH_DATA_FLAGS_UP != AUTH_DATA_FLAGS_UP) {
return false;
}

// 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 &&
(flags & AUTH_DATA_FLAGS_UV) != AUTH_DATA_FLAGS_UV
) {
return false;
}

// 19. If the BE bit of the flags in authData is not set, verify that
// the BS bit is not set.
if (flags & AUTH_DATA_FLAGS_BE != AUTH_DATA_FLAGS_BE) {
if (flags & AUTH_DATA_FLAGS_BS == AUTH_DATA_FLAGS_BS) {
return false;
}
}

return true;
}

/**
* Verifies a Webauthn P256 signature (Authentication Assertion) as described
* in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion. We do not
* verify all the steps as described in the specification, only ones relevant
* to our context. Please carefully read through this list before usage.
* Specifically, we do verify the following:
* - Verify that authenticatorData (which comes from the authenticator,
* such as iCloud Keychain) indicates a well-formed assertion. If
* requireUserVerification is set, checks that the authenticator enforced
* user verification. User verification should be required if,
* and only if, options.userVerification is set to required in the request
* - Verifies that the client JSON is of type "webauthn.get", i.e. the client
* was responding to a request to assert authentication.
* - Verifies that the client JSON contains the requested challenge.
* - Finally, verifies that (r, s) constitute a valid signature over both
* the authenicatorData and client JSON, for public key (x, y).
*
* We make some assumptions about the particular use case of this verifier,
* so we do NOT verify the following:
* - Does NOT verify that the origin in the clientDataJSON matches the
* Relying Party's origin: It is considered the authenticator's
* responsibility to ensure that the user is interacting with the correct
* RP. This is enforced by most high quality authenticators properly,
* particularly the iCloud Keychain and Google Password Manager were
* tested.
* - Does NOT verify That c.topOrigin is well-formed: We assume c.topOrigin
* would never be present, i.e. the credentials are never used in a
* cross-origin/iframe context. The website/app set up should disallow
* cross-origin usage of the credentials. This is the default behaviour for
* created credentials in common settings.
* - Does NOT verify that the rpIdHash in authData is the SHA-256 hash of an
* RP ID expected by the Relying Party: This means that we rely on the
* authenticator to properly enforce credentials to be used only by the
* correct RP. This is generally enforced with features like Apple App Site
* Association and Google Asset Links. To protect from edge cases in which
* a previously-linked RP ID is removed from the authorised RP IDs,
* we recommend that messages signed by the authenticator include some
* expiry mechanism.
* - Does NOT verify the credential backup state: This assumes the credential
* backup state is NOT used as part of Relying Party business logic or
* policy.
* - Does NOT verify the values of the client extension outputs: This assumes
* that the Relying Party does not use client extension outputs.
* - Does NOT verify the signature counter: Signature counters are intended
* to enable risk scoring for the Relying Party. This assumes risk scoring
* is not used as part of Relying Party business logic or policy.
* - 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.
*/
function verifySignature(
bytes memory challenge,
bytes memory authenticatorData,
bool requireUserVerification,
string memory clientDataJSON,
uint256 challengeLocation,
uint256 responseTypeLocation,
uint256 r,
uint256 s,
uint256 x,
uint256 y
) internal view returns (bool) {
// Check that authenticatorData has good flags
if (
authenticatorData.length < 37 ||
!checkAuthFlags(authenticatorData[32], requireUserVerification)
) {
return false;
}

// Check that response is for an authentication assertion
string memory responseType = '"type":"webauthn.get"';
if (!LibString.contains(clientDataJSON, responseType, responseTypeLocation)) {
return false;
}

// Check that challenge is in the clientDataJSON
string memory challengeB64url = LibString.bytesToBase64URL(challenge);
string memory challengeProperty = string.concat(
'"challenge":"',
challengeB64url,
'"'
);

if (!LibString.contains(clientDataJSON, challengeProperty, challengeLocation)) {
return false;
}

// Check that the public key signed sha256(authenticatorData || sha256(clientDataJSON))
bytes32 clientDataJSONHash = sha256(bytes(clientDataJSON));
bytes32 messageHash = sha256(
abi.encodePacked(authenticatorData, clientDataJSONHash)
);

return P256.verifySignature(messageHash, r, s, x, y);
}
}
104 changes: 104 additions & 0 deletions contracts/utils/LibString.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,37 @@ pragma solidity 0.8.19;
library LibString {
bytes private constant ALPHABET_HEX_16 = '0123456789abcdef';
bytes private constant ALPHABET_32 = 'abcdefghijklmnopqrstuvwxyz234567';
bytes private constant ALPHABET_64_URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";

/**
* @notice Checks if `_str` contains `_substr` starting at `_location`.
* @param _substr Substring to search for.
* @param _str String to search within.
* @param _location Starting index in `_str` for the search.
* @return True if `_substr` is found at `_location` in `_str`, otherwise false.
*/
function contains(
string memory _str,
string memory _substr,
uint256 _location
) internal pure returns (bool) {
if (_location >= bytes(_str).length) {
return false;
}

uint256 strLen = bytes(_substr).length;

bytes32 result1;
bytes32 result2;

/// @solidity memory-safe-assembly
assembly {
result1 := keccak256(add(_substr, 32), strLen)
result2 := keccak256(add(_str, add(32, _location)), strLen)
}

return result1 == result2;
}

/**
* @notice Prefixes a hexadecimal string with "0x".
Expand Down Expand Up @@ -84,4 +115,77 @@ library LibString {
return string(bytesArray);
}
}

/**
* @notice Converts a byte array to a base64 URL string.
* @param _bytes The byte array to convert.
* @return The resulting base64 URL string.
* @dev This function is based on the implementation provided by OpenZeppelin:
* - MIT license - OpenZeppelin Contracts (last updated v5.0.0) (utils/Base64.sol)
*/
function bytesToBase64URL(bytes memory _bytes) internal pure returns (string memory) {
/**
* Inspired by Brecht Devos (Brechtpd) implementation - MIT licence
* https://github.com/Brechtpd/base64/blob/e78d9fd951e7b0977ddca77d92dc85183770daf4/base64.sol
*/
if (_bytes.length == 0) return "";

// Loads the table into memory
bytes memory table = ALPHABET_64_URL;

// Encoding takes 3-byte chunks of binary data from the `_bytes` parameter
// and splits them into 4 numbers of 6 bits each.
// For no-padding Base64 URL, the final length should be the exact number of required characters,
// without rounding up to the nearest multiple of 4.
// - `4 * _bytes.length / 3` computes the base length (ignoring leftover bytes)
// - The conditional adds extra characters for leftover bytes (if any):
// - If there's 1 leftover byte, 2 extra characters are needed.
// - If there are 2 leftover bytes, 3 extra characters are needed.
uint256 encodedLen = 4 * _bytes.length / 3;
if (_bytes.length % 3 > 0) {
encodedLen += 2 - (_bytes.length % 3 - 1);
}
string memory result = new string(encodedLen);

/// @solidity memory-safe-assembly
assembly {
// Prepare the lookup table (skip the first "length" byte)
let tablePtr := add(table, 1)

// Prepare result pointer, jump over length
let resultPtr := add(result, 32)

// Run over the input, 3 bytes at a time
for {
let dataPtr := _bytes
let endPtr := add(_bytes, mload(_bytes))
} lt(dataPtr, endPtr) { } {
// Advance 3 bytes
dataPtr := add(dataPtr, 3)
let input := mload(dataPtr)

// To write each character, shift the 3 bytes (18 bits) chunk
// 4 times in blocks of 6 bits for each character (18, 12, 6, 0)
// and apply logical AND with 0x3F which is the number of
// the previous character in the ASCII table prior to the Base64 Table
// The result is then added to the table to get the character to write,
// and finally write it in the result pointer but with a left shift
// of 256 (1 byte) - 8 (1 ASCII char) = 248 bits

mstore8(resultPtr, mload(add(tablePtr, and(shr(18, input), 0x3F))))
resultPtr := add(resultPtr, 1) // Advance

mstore8(resultPtr, mload(add(tablePtr, and(shr(12, input), 0x3F))))
resultPtr := add(resultPtr, 1) // Advance

mstore8(resultPtr, mload(add(tablePtr, and(shr(6, input), 0x3F))))
resultPtr := add(resultPtr, 1) // Advance

mstore8(resultPtr, mload(add(tablePtr, and(input, 0x3F))))
resultPtr := add(resultPtr, 1) // Advance
}
}

return result;
}
}
33 changes: 33 additions & 0 deletions foundry_test/utils/LibString.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.19;

import "contracts/utils/LibString.sol";

import "foundry_test/base/AdvTest.sol";


contract LibStringImp {
function contains(string memory _a, string memory _b, uint256 _l) external pure returns (bool) {
return LibString.contains(_a, _b, _l);
}

function prefixHexadecimal(string memory _a) external pure returns (bool) {
return LibString.prefixHexadecimal(_a);
}

function prefixBase32(string memory _a) external pure returns (bool) {
return LibString.prefixBase32(_a);
}

function bytesToHexadecimal(bytes memory _a) external pure returns (string memory) {
return LibString.bytesToHexadecimal(_a);
}

function bytesToBase32(bytes memory _a) external pure returns (string memory) {
return LibString.bytesToBase32(_a);
}

function bytesToBase64URL(bytes memory _a) external pure returns (string memory) {
return LibString.bytesToBase64URL(_a);
}
}

0 comments on commit 1b3d7f1

Please sign in to comment.