Skip to content
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

Fix for variable clientDataJSON serialization #6

Merged
merged 7 commits into from
Mar 5, 2024
Merged

Conversation

wilsoncusack
Copy link
Collaborator

Our key assumption and optimization here turned out to be wrong: there are clients that have varied serialization of clientDataJSON.

E.g. in Mozilla the fields are ordered in the order they appear here.

With this change, our implementation basically mirrors Daimo and FreshCryptoLib, with the addition of the precompile call.

@@ -122,47 +109,41 @@ 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.
string memory _type = webAuthnAuth.clientDataJSON.slice(webAuthnAuth.typeIndex, webAuthnAuth.typeIndex + 21);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@mdehoog @rholterhus @xenoliss given all the steps we skip from the official spec, I am wondering if we care to keep this. Do we really care if it is webauthn.get vs. .create not sure.

Copy link

Choose a reason for hiding this comment

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

Two questions:

  • what's the worse that could happen if we skip?
  • how much gas do we save by skipping (seems negligible at first glance)?

Copy link
Collaborator

@xenoliss xenoliss Mar 3, 2024

Choose a reason for hiding this comment

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

For gas saving, I won't comment as I haven't tested it.

What's the worst that could happen if we skip?

The specification is quite clear about it.

I guess whether to skip it or not depends on whether we want to restrict its usage to our use case only or make it more generic:

  • For our usage only, I think skipping it is ok. In our specific case, we are not really interested in its value and simply assume that if the user signed it, then they must agree with whatever they just signed.

  • For public usage (or other usages not related to our ERC4337 implementation), it might make sense to keep this verification to avoid "certain types of signature confusion attacks."

I'm always in favor of security, so if the gas is negligible, I would keep it. If it breaks at some point because an authenticator delivers assertion signatures with a type different than "webauthn.get", we can still upgrade the contract.

Copy link
Collaborator

Choose a reason for hiding this comment

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

My opinion would also be to keep the check. I agree with the above analysis - the worst-case scenario would be if someone could reuse a signature from a registration ceremony. For the context of ERC4337, this is very unlikely to matter. Because the challenge in the registration ceremony has no reason to be equal to a hash of a UserOp, so there shouldn't ever be a collision. But for public usage it'd be good to keep the check.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah I guess the only thought I had was whether we wanted to allow webauthn.create to be used for signing. But I am ok with the conclusion here. Will keep.

Comment on lines +121 to +126
string memory actualChallenge = webAuthnAuth.clientDataJSON.slice(
webAuthnAuth.challengeIndex, webAuthnAuth.challengeIndex + expectedChallenge.length
);
if (keccak256(bytes(actualChallenge)) != keccak256(expectedChallenge)) {
return false;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

One thing I found interesting is that the slice() function doesn't revert if the start or end indices are greater than the length of the string. Instead of reverting, it'll just return a shorter string, for example see this part of the Solady code:

let subjectLength := mload(subject)
if iszero(gt(subjectLength, end)) { end := subjectLength }
if iszero(gt(subjectLength, start)) { start := subjectLength }

However I think this is fine. Because the only way that hash(a) == hash(b) is if a and b are equal. So if the user provides some weird values for challengeIndex or typeIndex such that it results in a shorter string, then the hashes aren't going to match, and verification will fail as expected.

So I think this is all okay, just leaving this note

Comment on lines +21 to +26
/// @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;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I have one thing I've been thinking about recently. Technically, compared to the previous implementation, there's less of a guarantee that the "challenge":"..." and "type":"..." fields are actually occurring in the top level of the JSON structure. For example if a future version of WebAuthn has a field called arbitraryData in the clientDataJSON, then I'm imagining a JSON like this:

{
    "type":"webauthn.get",
    "challenge": "<challenge1>",
    "arbitraryData": "\"challenge\": \"<challenge2>\""
}

With this example, the arbitrary data contains a string that looks like a challenge field in the JSON, but really it's just a random string. In this scenario it might be possible to point the challengeIndex to the arbitrary string to prove a challenge the user wasn't expecting.

I think the implication here is that users need to be careful of everything they're signing, and they need to avoid clientDataJSON that have obvious red flags like the above. Maybe adding a note about this in the comments would be useful.

WDYT? The rest of the PR looks good to me pending this last note

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the note. And this sort of gets to why I question verify the type at all. Ultimately, anyone with a p256 key can format the data to get this test to pass. We don't know for sure that what is passed is the result of a proper webauthn authentication. Of course it's also possible that a wallet lies about what the transaction will be, and so the challenge is correct and it was a proper webauthn authentication, but the user is still surprised. We can't cover everything. I think the best we can do is: Did the data that you signed include the userOpHash? And, I guess per other discussion, was type: webauthn.get also present.

Copy link

@stevieraykatz stevieraykatz left a comment

Choose a reason for hiding this comment

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

One small nit, otherwise looks good.

@@ -122,47 +109,41 @@ 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.
string memory _type = webAuthnAuth.clientDataJSON.slice(webAuthnAuth.typeIndex, webAuthnAuth.typeIndex + 21);

Choose a reason for hiding this comment

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

NIT: magic number 21 here should be described in a comment. I.E.

// bytes of string "type":"webauthn.get"

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

thanks, updated!

@cb-heimdall
Copy link
Collaborator

Review Error for stevieraykatz @ 2024-03-04 21:57:08 UTC
User must have write permissions to review

@cb-heimdall
Copy link
Collaborator

Review Error for stevieraykatz @ 2024-03-04 23:12:46 UTC
User must have write permissions to review

@wilsoncusack wilsoncusack merged commit 003b01f into main Mar 5, 2024
1 of 3 checks passed
@wilsoncusack wilsoncusack deleted the wilson/fix branch March 5, 2024 15:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants