Skip to content

Commit 46d291e

Browse files
googyiabergs
authored andcommitted
tokenbindig, AppId, UVP
Back to 100% conformance. TokenBinding logic readded. AppId: prevent serialization in a nicer way. UV flags are verified differently for conformance testing, otherwise as described in the RFC.
1 parent de20e66 commit 46d291e

File tree

8 files changed

+93
-27
lines changed

8 files changed

+93
-27
lines changed

Src/Fido2.Models/Objects/AuthenticationExtensionsClientInputs.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,8 @@ public sealed class AuthenticationExtensionsClientInputs
1919
/// https://www.w3.org/TR/webauthn/#sctn-appid-extension
2020
/// </summary>
2121
[JsonPropertyName("appid")]
22-
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
23-
public string AppID { private get; set; }
24-
25-
public string GetAppID()
26-
{
27-
return AppID;
28-
}
22+
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
23+
public string AppID { get; set; }
2924

3025
/// <summary>
3126
/// This extension enables the WebAuthn Relying Party to determine which extensions the authenticator supports.

Src/Fido2/AuthenticatorAssertionResponse.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,10 @@ public async Task<VerifyAssertionResult> VerifyAsync(
6161
uint storedSignatureCounter,
6262
IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredId,
6363
IMetadataService? metadataService,
64+
byte[]? requestTokenBindingId,
6465
CancellationToken cancellationToken = default)
6566
{
66-
BaseVerify(config.FullyQualifiedOrigins, options.Challenge);
67+
BaseVerify(config.FullyQualifiedOrigins, options.Challenge, requestTokenBindingId);
6768

6869
if (Raw.Type != PublicKeyCredentialType.PublicKey)
6970
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, Fido2ErrorMessages.AssertionResponseNotPublicKey);
@@ -116,10 +117,11 @@ public async Task<VerifyAssertionResult> VerifyAsync(
116117
// FIDO AppID Extension:
117118
// If true, the AppID was used and thus, when verifying an assertion, the Relying Party MUST expect the rpIdHash to be the hash of the AppID, not the RP ID.
118119

119-
var rpid = Raw.ClientExtensionResults?.AppID ?? false ? options.Extensions?.GetAppID() : options.RpId;
120+
var rpid = Raw.ClientExtensionResults?.AppID ?? false ? options.Extensions?.AppID : options.RpId;
120121

121122
byte[] hashedRpId = SHA256.HashData(Encoding.UTF8.GetBytes(rpid ?? string.Empty));
122123
byte[] hash = SHA256.HashData(Raw.Response.ClientDataJson);
124+
bool conformanceTesting = metadataService != null && metadataService.ConformanceTesting();
123125

124126
if (!authData.RpIdHash.SequenceEqual(hashedRpId))
125127
throw new Fido2VerificationException(Fido2ErrorCode.InvalidRpidHash, Fido2ErrorMessages.InvalidRpidHash);
@@ -134,6 +136,15 @@ public async Task<VerifyAssertionResult> VerifyAsync(
134136
if (!authData.UserVerified)
135137
throw new Fido2VerificationException(Fido2ErrorCode.UserVerificationRequirementNotMet, Fido2ErrorMessages.UserVerificationRequirementNotMet);
136138
}
139+
// =====
140+
// // 14. Verify that the UP bit of the flags in authData is set.
141+
// if (!authData.UserPresent && (!conformanceTesting || options.UserVerification is UserVerificationRequirement.Required))
142+
// throw new Fido2VerificationException(Fido2ErrorCode.UserPresentFlagNotSet, Fido2ErrorMessages.UserPresentFlagNotSet);
143+
//
144+
// // 15. If the Relying Party requires user verification for this assertion, verify that the UV bit of the flags in authData is set.
145+
// if (options.UserVerification is UserVerificationRequirement.Required && !authData.UserVerified)
146+
// throw new Fido2VerificationException(Fido2ErrorCode.UserVerificationRequirementNotMet, Fido2ErrorMessages.UserVerificationRequirementNotMet);
147+
137148

138149
// 16. If the credential backup state is used as part of Relying Party business logic or policy, let currentBe and currentBs be the values of the BE and BS bits, respectively, of the flags in authData.
139150
// Compare currentBe and currentBs with credentialRecord.BE and credentialRecord.BS and apply Relying Party policy, if any.

Src/Fido2/AuthenticatorAttestationResponse.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public async Task<RegisteredPublicKeyCredential> VerifyAsync(
6060
Fido2Configuration config,
6161
IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser,
6262
IMetadataService? metadataService,
63+
byte[]? requestTokenBindingId,
6364
CancellationToken cancellationToken = default)
6465
{
6566
// https://www.w3.org/TR/webauthn/#registering-a-new-credential
@@ -74,7 +75,10 @@ public async Task<RegisteredPublicKeyCredential> VerifyAsync(
7475

7576
// 8. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the create() call.
7677
// 9. Verify that the value of C.origin matches the Relying Party's origin.
77-
BaseVerify(config.FullyQualifiedOrigins, originalOptions.Challenge);
78+
// 9.5. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection over which the attestation was obtained.
79+
// If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection.
80+
// Validated in BaseVerify.
81+
BaseVerify(config.FullyQualifiedOrigins, originalOptions.Challenge, requestTokenBindingId);
7882

7983
if (Raw.Id is null || Raw.Id.Length == 0)
8084
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestationResponse, Fido2ErrorMessages.AttestationResponseIdMissing);

Src/Fido2/AuthenticatorResponse.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Text;
45
using System.Text.Json;
56
using System.Text.Json.Serialization;
67

@@ -48,6 +49,7 @@ protected AuthenticatorResponse(ReadOnlySpan<byte> utf8EncodedJson)
4849
Type = response.Type;
4950
Challenge = response.Challenge;
5051
Origin = response.Origin;
52+
TokenBinding = response.TokenBinding;
5153
}
5254

5355
public const int MAX_ORIGINS_TO_PRINT = 5;
@@ -62,7 +64,10 @@ protected AuthenticatorResponse(ReadOnlySpan<byte> utf8EncodedJson)
6264
[JsonPropertyName("origin")]
6365
public string Origin { get; }
6466

65-
protected void BaseVerify(IReadOnlySet<string> fullyQualifiedExpectedOrigins, ReadOnlySpan<byte> originalChallenge)
67+
[JsonPropertyName("tokenBinding")]
68+
public TokenBindingDto? TokenBinding { get; set; }
69+
70+
protected void BaseVerify(IReadOnlySet<string> fullyQualifiedExpectedOrigins, ReadOnlySpan<byte> originalChallenge, byte[]? requestTokenBindingId)
6671
{
6772
if (Type is not "webauthn.create" && Type is not "webauthn.get")
6873
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAuthenticatorResponse, $"Type must be 'webauthn.create' or 'webauthn.get'. Was '{Type}'");
@@ -79,6 +84,13 @@ protected void BaseVerify(IReadOnlySet<string> fullyQualifiedExpectedOrigins, Re
7984
// 12. Verify that the value of C.origin matches the Relying Party's origin.
8085
if (!fullyQualifiedExpectedOrigins.Contains(fullyQualifiedOrigin))
8186
throw new Fido2VerificationException($"Fully qualified origin {fullyQualifiedOrigin} of {Origin} not equal to fully qualified original origin {string.Join(", ", fullyQualifiedExpectedOrigins.Take(MAX_ORIGINS_TO_PRINT))} ({fullyQualifiedExpectedOrigins.Count})");
87+
88+
// 13?. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection over which the assertion was obtained.
89+
// If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection.
90+
if (TokenBinding != null)
91+
{
92+
TokenBinding.Verify(requestTokenBindingId);
93+
}
8294
}
8395

8496
/*

Src/Fido2/Fido2.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,18 @@ public CredentialCreateOptions RequestNewCredential(
6565
/// <param name="attestationResponse">The attestation response from the authenticator.</param>
6666
/// <param name="originalOptions">The original options that was sent to the client.</param>
6767
/// <param name="isCredentialIdUniqueToUser">The delegate used to validate that the CredentialID is unique to this user.</param>
68+
/// <param name="requestTokenBindingId">deprecated ===</param>
6869
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
6970
/// <returns></returns>
7071
public async Task<RegisteredPublicKeyCredential> MakeNewCredentialAsync(
7172
AuthenticatorAttestationRawResponse attestationResponse,
7273
CredentialCreateOptions originalOptions,
7374
IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser,
75+
byte[]? requestTokenBindingId = null,
7476
CancellationToken cancellationToken = default)
7577
{
7678
var parsedResponse = AuthenticatorAttestationResponse.Parse(attestationResponse);
77-
var credential = await parsedResponse.VerifyAsync(originalOptions, _config, isCredentialIdUniqueToUser, _metadataService, cancellationToken);
79+
var credential = await parsedResponse.VerifyAsync(originalOptions, _config, isCredentialIdUniqueToUser, _metadataService, requestTokenBindingId, cancellationToken);
7880

7981
return credential;
8082
}
@@ -105,6 +107,7 @@ public AssertionOptions GetAssertionOptions(
105107
/// <param name="storedDevicePublicKeys">The stored device public keys.</param>
106108
/// <param name="storedSignatureCounter">The stored value of the signature counter.</param>
107109
/// <param name="isUserHandleOwnerOfCredentialIdCallback">The delegate used to validate that the user handle is indeed owned of the CredentialId.</param>
110+
/// <param name="requestTokenBindingId">Deprecated ===</param>
108111
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
109112
/// <returns></returns>
110113
public async Task<VerifyAssertionResult> MakeAssertionAsync(
@@ -114,6 +117,7 @@ public async Task<VerifyAssertionResult> MakeAssertionAsync(
114117
IReadOnlyList<byte[]> storedDevicePublicKeys,
115118
uint storedSignatureCounter,
116119
IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredentialIdCallback,
120+
byte[]? requestTokenBindingId = null,
117121
CancellationToken cancellationToken = default)
118122
{
119123
var parsedResponse = AuthenticatorAssertionResponse.Parse(assertionResponse);
@@ -125,6 +129,7 @@ public async Task<VerifyAssertionResult> MakeAssertionAsync(
125129
storedSignatureCounter,
126130
isUserHandleOwnerOfCredentialIdCallback,
127131
_metadataService,
132+
requestTokenBindingId,
128133
cancellationToken);
129134

130135
return result;

Src/Fido2/IFido2.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ Task<VerifyAssertionResult> MakeAssertionAsync(
2020
IReadOnlyList<byte[]> storedDevicePublicKeys,
2121
uint storedSignatureCounter,
2222
IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredentialIdCallback,
23+
byte[]? requestTokenBindingId = null,
2324
CancellationToken cancellationToken = default);
2425

2526
Task<RegisteredPublicKeyCredential> MakeNewCredentialAsync(
2627
AuthenticatorAttestationRawResponse attestationResponse,
2728
CredentialCreateOptions originalOptions,
2829
IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser,
30+
byte[]? requestTokenBindingId = null,
2931
CancellationToken cancellationToken = default);
3032

3133
CredentialCreateOptions RequestNewCredential(

Src/Fido2/TokenBindingDto.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
namespace Fido2NetLib
2+
{
3+
public class TokenBindingDto
4+
{
5+
/// <summary>
6+
/// Either "present" or "supported". https://www.w3.org/TR/webauthn/#enumdef-tokenbindingstatus
7+
/// supported: Indicates the client supports token binding, but it was not negotiated when communicating with the Relying Party.
8+
/// present: Indicates token binding was used when communicating with the Relying Party. In this case, the id member MUST be present
9+
/// </summary>
10+
public string? Status { get; set; }
11+
12+
/// <summary>
13+
/// This member MUST be present if status is present, and MUST a base64url encoding of the Token Binding ID that was used when communicating with the Relying Party.
14+
/// </summary>
15+
public string? Id { get; set; }
16+
17+
public void Verify(byte[]? requestTokenbinding)
18+
{
19+
// validation by the FIDO conformance tool (more than spec says)
20+
switch (Status)
21+
{
22+
case "present":
23+
if (string.IsNullOrEmpty(Id))
24+
throw new Fido2VerificationException("TokenBinding status was present but Id is missing");
25+
var b64 = Base64Url.Encode(requestTokenbinding);
26+
if (Id != b64)
27+
throw new Fido2VerificationException("Tokenbinding Id does not match");
28+
break;
29+
case "supported":
30+
case "not-supported":
31+
break;
32+
default:
33+
throw new Fido2VerificationException("Malformed tokenbinding status field");
34+
}
35+
}
36+
}
37+
}

0 commit comments

Comments
 (0)