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 #720: Align user handle management with CTAP #730

Merged
merged 6 commits into from
Jan 3, 2018

Conversation

emlun
Copy link
Member

@emlun emlun commented Dec 20, 2017

As stated in #558 (comment). This resolves #720.


Preview | Diff

As stated in
#558 (comment) and
#558 (comment) the user
handle should not be returned when operating in 2nd factor mode (i.e.,
when given a non-empty `allowCredentials` list).
index.bs Outdated
@@ -392,7 +392,7 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S
* A [=Credential ID=].
* A [=credential private key=].
* The [=Relying Party Identifier=] for the [=[RP]=] that created this credential source.
* An optional [=user handle=] for the person who created this credential source.
* A [=user handle=] for the person who created this credential source.
Copy link
Member Author

Choose a reason for hiding this comment

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

This change is already merged into master via #721, but GitHub doesn't seem to detect it because I based this PR off the issue-720-user-handle-optional branch whose commits were rebased onto master instead of merging.

@emlun
Copy link
Member Author

emlun commented Dec 20, 2017

Assigned this to the CR milestone since the associated issue is assigned to CR.

@equalsJeffH equalsJeffH requested review from akshayku and jcjones and removed request for akshayku December 20, 2017 18:10
@emlun
Copy link
Member Author

emlun commented Dec 20, 2017

@akshayku pointed out on the 2017-12-20 WG call that the issues this was meant to solve are probably non-issues. We agreed but decided to ask @christiaanbrand to take one last look at this.

index.bs Outdated
- |authenticatorData|
- |signature|
- The [=user handle=] associated with |selectedCredential|.
- The [=user handle=] associated with |selectedCredential|, if no [=list=] of credentials was supplied by the client, or
no such [=list=] was supplied. Otherwise, do not return this value.
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see any reason on why not.

Its not a private information and RP may need it even if RP is supplying a set of credentialIDs as userHandle is helping RP for the lookup even in second factor case.

index.bs Outdated

Note: If the client supplies a [=list=] of exactly one credential and it was successfully employed, then its
[=credential ID=] and [=user handle=] are not returned since the [=[RP]=], having provided the [=credential ID=] to the
client, already knows the [=credential ID=] and the identity of the user. This saves transmitting these bytes over what
Copy link
Contributor

Choose a reason for hiding this comment

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

That's not true.

CredentialID and userHandle are providing different information and RP knowing CredentialID to information that is stored in userHandle is not accurate.

Copy link
Contributor

@akshayku akshayku left a comment

Choose a reason for hiding this comment

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

Returning userHandle even in second factor case provides a value to the RP.

As we have already established that userHandle is not private information, I don't see any reason on why we are directing authenticators to not return back that information.

@emlun
Copy link
Member Author

emlun commented Dec 21, 2017

I now agree that the user handle is not private information, but I don't see how the RP could not know the user's identity if the RP has already looked up a list of credential IDs for that user.

Either way, I see now that CTAP's getAssertion method always returns the user.id even in the 2nd factor case, so I support closing this. Sorry for wasting everyone's time on yesterday's call.

@akshayku
Copy link
Contributor

Let me give you our example.

We have a distributed server environment and is doing credentialID based lookup. We use user id as a helper for lookup in this distributed environment for scalability purposes. The response server is not the same as request server. And that is why, userId is a helpful to us even in a second factor case.

As this is not optional in CTAP spec, I see no reason for WebAuthN to hide this information.

I am closing this. Please reopen if someone does not agree.

@akshayku
Copy link
Contributor

I am reopening this PR.

The confusion is coming because 2nd factor != server credentials in all the cases.

CTAP spec needs clarification as there is no user information returned in authenticatorGetAssertion for server credentials and U2F devices as it is not available to the authenticator. For device resident keys, userID MUST be returned (irrespective of whether credentialID list is provided or not). I will open the clarification PR for CTAP spec.

Regarding this PR, it needs more work. Irrespective of allow credential ID list is present or not, if authenticator is giving userID back, it should be returned back to the RP.

So the sections in this PR which talks about whether user ID returned from authenticator is null or not is correct. Sections which talks about removing this information when credential ID list is present in not correct.

index.bs Outdated
@@ -1245,7 +1245,8 @@ When this method is invoked, the user agent MUST execute the following algorithm
:: whose value is the bytes of the signature value returned by the [=authenticator=].

: <code><dfn for="assertionCreationData">userHandleResult</dfn></code>
:: whose value is the bytes of the [=user handle=] returned by the [=authenticator=].
:: If |savedCredentialId| exists, set the value of [=userHandleResult=] to be the bytes of the [=user handle=]
Copy link
Contributor

Choose a reason for hiding this comment

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

Even if savedCredentialID exists or a list of credentialID exists, if authenticator is returning userID, it should be returned back to the RP.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed.

@@ -1278,7 +1279,9 @@ When this method is invoked, the user agent MUST execute the following algorithm
<code>|assertionCreationData|.[=assertionCreationData/signatureResult=]</code>.

: {{AuthenticatorAssertionResponse/userHandle}}
:: A new {{ArrayBuffer}}, created using |global|'s [=%ArrayBuffer%=], containing the bytes of
:: If <code>|assertionCreationData|.[=assertionCreationData/userHandleResult=]</code> is null, set this
Copy link
Contributor

Choose a reason for hiding this comment

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

This part is correct.

index.bs Outdated
- The [=user handle=] associated with |selectedCredential|, if no [=list=] of credentials was supplied by the client, or
no such [=list=] was supplied. Otherwise, do not return this value.

Note: If the client supplies a [=list=] of exactly one credential and it was successfully employed, then its
Copy link
Contributor

Choose a reason for hiding this comment

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

I would suggest this note section to be deleted. If user handle is returned from the authenticator, it should be returned back to the RP.

Regarding only one CredentialID being sent, not returning it back does not work for distributed server scenarios like ours where request processing server is not the same as response processing server.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed.

@emlun
Copy link
Member Author

emlun commented Dec 24, 2017

What do you mean by "server credentials"?

Ah yes, I see now that CTAP's authenticatorMakeCredential method stores the user data only for resident keys. However, even with that limitation I think your use case can be solved by embedding a user ID in the challenge parameter. Since challenge is an opaque byte array of unspecified length, and is returned in the assertion response, you could set challenge to the UTF-8 bytes of for example {"userId":"ff642b","random":"1/BjTenZNLAw9l03J2J2BcpgXP5Ic7gEuoVfVVrf7Bg="}. This should allow you to embed the user ID while maintaining challenge as a valid nonce, correct?

So I don't think changing CTAP is strictly necessary, but I'll update this PR either way just in case we need it.

@emlun
Copy link
Member Author

emlun commented Dec 24, 2017

Wait - actually, the response processing server (ResPS) needs to be able to verify that the returned challenge equals that sent to the client, so there needs to be some trusted communication path between the request processing server (ReqPS) and the ResPS so the ResPS can obtain the challenge from the ReqPS. Could that message then not also contain the user ID (if known, i.e. in 2nd factor mode)?

@emlun
Copy link
Member Author

emlun commented Dec 25, 2017

Summary of currently proposed changes:

  • AuthenticatorAssertionResponse.userHandle is now nullable.
  • The authenticator MAY now skip storing the user handle for credentials that do not have a client-side-resident credential private key. (CTAP currently does this)
  • The authenticator now always returns the user handle if it is available.
  • The client now returns userHandle: null if the authenticator did not return the user handle.

However it looks like CTAP is internally inconsistent: the user argument to authenticatorMakeCredential is stored only for resident keys, but the user attribute is required in the authenticatorGetAssertion response...

Copy link
Contributor

@akshayku akshayku left a comment

Choose a reason for hiding this comment

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

Looks good to me.

@akshayku
Copy link
Contributor

akshayku commented Dec 25, 2017

@emlun: Thanks for taking this on.

You are correct, even if response server is different request server (rare case), user information is still available in addition to credentialID as it is a second factor case and user has already entered user information in first step. I was incorrect earlier and when I relooked at the whole scenario, I realized that the confusion came because earlier we were equating second factor case with server credentials case which is not always correct.

For server credentials, it is of the same semantics as of U2F meaning authenticators MAY drop all user information while returning the credentialID. Some authenticators MAY also store this information in encrypted form in future however there is no need and I am not aware of any such authenticators as of now. In any case, this is authenticator specific. CTAP spec needs a clarification as it says user ID is required in authenticatorGetAssertion response which is inconsistent for U2F devices or for server credentials where there is no such information available. I will make that clarification.

Platform logic is simple, if it returned from authenticator (for resident keys, irrespective of whether credential list if present or not), return it to the RP, otherwise set it to NULL.

Thanks

@emlun emlun changed the title Fix #720: Don't return user handle in 2nd factor mode Fix #720: Align user handle management with CTAP Dec 25, 2017
@emlun
Copy link
Member Author

emlun commented Dec 25, 2017

@akshayku Ok, thanks!

@equalsJeffH
Copy link
Contributor

@akshayku please clarify what you mean by "server credentials"? thx.

@akshayku
Copy link
Contributor

@equalsJeffH : By "server credentials", I meant credentials which are resident on the server instead of FIDO device. Or in other words, credentials which are created via authenticatorMakeCredential API where "rk" is not set to TRUE and credentialID returned by the API contains encrypted private credential.

These credentials created by FIDO 2 device has the same characteristics as credentials which would have been created via U2F devices.

@agl
Copy link
Contributor

agl commented Jan 2, 2018

I asked internally about cbrand's concerns here and I understand them to be centered around the case where a 2nd-factor token is found by someone. It would be good for that not to essentially disclose their username in the 2nd-factor case.

Such a concern would thus have to be focused on CTAP2 rather than webauthn. So I don't think we have any unexpected concerns here.

@ve7jtb
Copy link
Contributor

ve7jtb commented Jan 2, 2018

all

the concern seems more around how resident credentials are protected. It shouldn't matter if the resident credential is being used as first or second factor. Both need to be protected.
I think any sort of resident credential needs to be protected with at least a device pin.

I am not sure that a resident credential with no userID is sufficient protection, without some device unlock. I think from a CTAP point of view a resident credential is resident credential.

John B.

Copy link
Contributor

@equalsJeffH equalsJeffH left a comment

Choose a reason for hiding this comment

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

this seems nominally OK IIUC, modulo results of discussing @ve7jtb's #730 (comment)

index.bs Outdated
@@ -1245,7 +1245,8 @@ When this method is invoked, the user agent MUST execute the following algorithm
:: whose value is the bytes of the signature value returned by the [=authenticator=].

: <code><dfn for="assertionCreationData">userHandleResult</dfn></code>
:: whose value is the bytes of the [=user handle=] returned by the [=authenticator=].
:: If the authenticator returned a [=user handle=], set the value of [=userHandleResult=] to be the bytes of that
[=user handle=] returned by the [=authenticator=]. Otherwise, set the value of [=userHandleResult=] to null.
Copy link
Contributor

Choose a reason for hiding this comment

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

suggest:
If the [=authenticator=] returned a [=user handle=], set the value of [=userHandleResult=] to be the bytes of the returned [=user handle=]. Otherwise, ...

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks!

@@ -1278,7 +1279,9 @@ When this method is invoked, the user agent MUST execute the following algorithm
<code>|assertionCreationData|.[=assertionCreationData/signatureResult=]</code>.

: {{AuthenticatorAssertionResponse/userHandle}}
:: A new {{ArrayBuffer}}, created using |global|'s [=%ArrayBuffer%=], containing the bytes of
:: If <code>|assertionCreationData|.[=assertionCreationData/userHandleResult=]</code> is null, set this
field to null. Otherwise, set this field to a new {{ArrayBuffer}}, created using |global|'s
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I'm not sure these two sentences are technically correct given that the dfn of ArrayBuffer is: "An object that holds a pointer (which may be null) to a buffer of a fixed number of bytes". I.e., may need to only instantiate the arraybuffer and assign the bytes of userHandleResult to it, and if userHandleResult is null, then the buffer pointer is automagically set to null.

However, the present language may be close enough to pass muster with reviewers.

Copy link
Member Author

Choose a reason for hiding this comment

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

As far as I could tell when I looked into it, it seemed like ArrayBuffer is a nullable type, if that's what you mean. Did I get that wrong?

Copy link
Member Author

Choose a reason for hiding this comment

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

Or, rather, it looked to me making a field of type ArrayBuffer nullable is legal. In that case I suppose the value could be null in two ways, so you may be right in that it's more correct to keep it not nullable, but I feel like making the field itself explicitly nullable better communicates the intent. What do you think?

@emlun emlun merged commit 5948f3b into master Jan 3, 2018
@emlun emlun deleted the issue-720-user-handle-optional branch January 3, 2018 19:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Contradiction in whether user handle is required
7 participants