-
Notifications
You must be signed in to change notification settings - Fork 36
Decode WebAuthn object from calldata buffers, with out-of-bound detection #191
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
Merged
Merged
Changes from 3 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
2e219d6
Decode WebAuthn object from calldata buffers, with out-of-bound detec…
Amxx 7c17ea3
doc
Amxx c454de7
minor optimisations/refactors
Amxx dfd84e3
Update WebAuthn.sol
Amxx 5171c6e
Update contracts/utils/cryptography/WebAuthn.sol
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 |
|---|---|---|
|
|
@@ -60,11 +60,6 @@ library WebAuthn { | |
| /// @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: | ||
|
|
@@ -89,18 +84,16 @@ library WebAuthn { | |
| // - 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 | ||
| auth.authenticatorData.length > 36 && | ||
| validateExpectedTypeHash(auth.clientDataJSON, auth.typeIndex) && // 11 | ||
| validateChallenge(auth.clientDataJSON, auth.challengeIndex, challenge) && // 12 | ||
| // Handles signature malleability internally | ||
| P256.verify( | ||
| sha256( | ||
| abi.encodePacked( | ||
| auth.authenticatorData, | ||
| sha256(clientDataJSON) // 19 | ||
| sha256(bytes(auth.clientDataJSON)) // 19 | ||
| ) | ||
| ), | ||
| auth.r, | ||
|
|
@@ -222,29 +215,64 @@ library WebAuthn { | |
| * @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) { | ||
| function validateExpectedTypeHash(string 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; | ||
| bytes memory typeValueBytes = Bytes.slice(bytes(clientDataJSON), typeIndex, typeIndex + 21); | ||
|
|
||
| // solhint-disable-next-line quotes | ||
| return bytes21(typeValueBytes) == bytes21('"type":"webauthn.get"'); | ||
| } | ||
|
|
||
| /// @dev Validates that the challenge in the client data JSON matches the `expectedChallenge`. | ||
| function validateChallenge( | ||
| bytes memory clientDataJSON, | ||
| string memory clientDataJSON, | ||
| uint256 challengeIndex, | ||
| bytes memory expectedChallenge | ||
| bytes memory challenge | ||
| ) 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 | ||
| // solhint-disable-next-line quotes | ||
| string memory expectedChallenge = string.concat('"challenge":"', Base64.encodeURL(challenge), '"'); | ||
| string memory actualChallenge = string( | ||
| Bytes.slice(bytes(clientDataJSON), challengeIndex, challengeIndex + bytes(expectedChallenge).length) | ||
| ); | ||
|
|
||
| return Strings.equal(string(actualChallengeBytes), string(expectedChallengeBytes)); | ||
| return Strings.equal(actualChallenge, expectedChallenge); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Verifies that calldata bytes (`input`) represents a valid `WebAuthnAuth` object. If encoding is valid, | ||
| * returns true and the calldata view at the object. Otherwise, returns false and an invalid calldata object. | ||
| * | ||
| * NOTE: The returned `auth` object should not be accessed if `success` is false. Trying to access the data may | ||
| * cause revert/panic. | ||
| */ | ||
| function tryDecodeAuth(bytes calldata input) internal pure returns (bool success, WebAuthnAuth calldata auth) { | ||
| // Default result (optimistic) | ||
| success = true; | ||
|
||
| assembly ("memory-safe") { | ||
| auth := input.offset | ||
| } | ||
|
|
||
| // Minimum length to hold 6 objects (32 bytes each) | ||
| if (input.length < 0xC0) return (false, auth); | ||
|
|
||
| // Get offset of non-value-type elements relative to the input buffer | ||
| uint256 authenticatorDataOffset = uint256(bytes32(input[0x80:])); | ||
| uint256 clientDataJSONOffset = uint256(bytes32(input[0xa0:])); | ||
|
|
||
| // The elements length (at the offset) should be 32 bytes long. We check that this is within the | ||
| // buffer bounds. Since we know input.length is at least 32, we can subtract with no overflow risk. | ||
| if (input.length - 0x20 < authenticatorDataOffset || input.length - 0x20 < clientDataJSONOffset) | ||
| return (false, auth); | ||
|
|
||
| // Get the lengths. offset + 32 is bounded by input.length so it does not overflow. | ||
| uint256 authenticatorDataLength = uint256(bytes32(input[authenticatorDataOffset:])); | ||
| uint256 clientDataJSONLength = uint256(bytes32(input[clientDataJSONOffset:])); | ||
|
|
||
| // Check that the input buffer is long enough to store the non-value-type elements | ||
| // Since we know input.length is at least xxxOffset + 32, we can subtract with no overflow risk. | ||
| if ( | ||
| input.length - authenticatorDataOffset - 0x20 < authenticatorDataLength || | ||
| input.length - clientDataJSONOffset - 0x20 < clientDataJSONLength | ||
| ) return (false, 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
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
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.