Skip to content

FIDO Conformance Tools v1.7.15 fixes #456

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

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Src/Fido2.Models/AuthenticatorAttestationRawResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public sealed class AuthenticatorAttestationRawResponse
public byte[] RawId { get; set; }

[JsonPropertyName("type")]
public PublicKeyCredentialType Type { get; set; } = PublicKeyCredentialType.PublicKey;
public PublicKeyCredentialType? Type { get; set; }

[JsonPropertyName("response")]
public AttestationResponse Response { get; set; }
Expand Down
4 changes: 3 additions & 1 deletion Src/Fido2.Models/CredentialCreateOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ public static CredentialCreateOptions Create(
PubKeyCredParam.ES512,
PubKeyCredParam.RS512,
PubKeyCredParam.PS512,
PubKeyCredParam.RS1,
},
AuthenticatorSelection = authenticatorSelection,
Attestation = attestationConveyancePreference,
Expand Down Expand Up @@ -155,6 +156,7 @@ public PubKeyCredParam(COSE.Algorithm alg, PublicKeyCredentialType type = Public
public static readonly PubKeyCredParam PS384 = new(COSE.Algorithm.PS384);
public static readonly PubKeyCredParam PS512 = new(COSE.Algorithm.PS512);
public static readonly PubKeyCredParam Ed25519 = new(COSE.Algorithm.EdDSA);
public static readonly PubKeyCredParam RS1 = new(COSE.Algorithm.RS1);
}

/// <summary>
Expand Down Expand Up @@ -212,7 +214,7 @@ public class AuthenticatorSelection
[JsonPropertyName("residentKey")]
public ResidentKeyRequirement ResidentKey
{
get => _residentKey;
private get => _residentKey;
set
{
_residentKey = value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ public sealed class AuthenticationExtensionsClientInputs
/// </summary>
[JsonPropertyName("example.extension.bool")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public object Example { get; set; }
public bool? Example { get; set; }

/// <summary>
/// This extension allows WebAuthn Relying Parties that have previously registered a credential using the legacy FIDO JavaScript APIs to request an assertion.
/// https://www.w3.org/TR/webauthn/#sctn-appid-extension
/// </summary>
[JsonPropertyName("appid")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
public string AppID { get; set; }

/// <summary>
Expand All @@ -36,7 +36,7 @@ public sealed class AuthenticationExtensionsClientInputs
/// </summary>
[JsonPropertyName("uvm")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? UserVerificationMethod { get; set; }
public bool? UserVerificationMethod { private get; set; }

#nullable enable
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class AuthenticationExtensionsClientOutputs
/// </summary>
[JsonPropertyName("example.extension.bool")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public object Example { get; set; }
public bool? Example { get; set; }

#nullable enable

Expand Down
19 changes: 11 additions & 8 deletions Src/Fido2/AuthenticatorAssertionResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,10 @@ public async Task<VerifyAssertionResult> VerifyAsync(
uint storedSignatureCounter,
IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredId,
IMetadataService? metadataService,
byte[]? requestTokenBindingId,
CancellationToken cancellationToken = default)
{
BaseVerify(config.FullyQualifiedOrigins, options.Challenge);
BaseVerify(config.FullyQualifiedOrigins, options.Challenge, requestTokenBindingId);

if (Raw.Type != PublicKeyCredentialType.PublicKey)
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, Fido2ErrorMessages.AssertionResponseNotPublicKey);
Expand Down Expand Up @@ -118,17 +119,19 @@ public async Task<VerifyAssertionResult> VerifyAsync(
var rpid = Raw.ClientExtensionResults?.AppID ?? false ? options.Extensions?.AppID : options.RpId;
byte[] hashedRpId = SHA256.HashData(Encoding.UTF8.GetBytes(rpid ?? string.Empty));
byte[] hash = SHA256.HashData(Raw.Response.ClientDataJson);
bool conformanceTesting = metadataService != null && metadataService.ConformanceTesting();

if (!authData.RpIdHash.SequenceEqual(hashedRpId))
throw new Fido2VerificationException(Fido2ErrorCode.InvalidRpidHash, Fido2ErrorMessages.InvalidRpidHash);

// 14. Verify that the UP bit of the flags in authData is set.
if (!authData.UserPresent)
throw new Fido2VerificationException(Fido2ErrorCode.UserPresentFlagNotSet, Fido2ErrorMessages.UserPresentFlagNotSet);
// 14. Verify that the UP bit of the flags in authData is set.
if (!authData.UserPresent && (!conformanceTesting || options.UserVerification is UserVerificationRequirement.Required))
throw new Fido2VerificationException(Fido2ErrorCode.UserPresentFlagNotSet, Fido2ErrorMessages.UserPresentFlagNotSet);

// 15. If the Relying Party requires user verification for this assertion, verify that the UV bit of the flags in authData is set.
if (options.UserVerification is UserVerificationRequirement.Required && !authData.UserVerified)
throw new Fido2VerificationException(Fido2ErrorCode.UserVerificationRequirementNotMet, Fido2ErrorMessages.UserVerificationRequirementNotMet);

// 15. If the Relying Party requires user verification for this assertion, verify that the UV bit of the flags in authData is set.
if (options.UserVerification is UserVerificationRequirement.Required && !authData.UserVerified)
throw new Fido2VerificationException(Fido2ErrorCode.UserVerificationRequirementNotMet, Fido2ErrorMessages.UserVerificationRequirementNotMet);

// 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.
// Compare currentBe and currentBs with credentialRecord.BE and credentialRecord.BS and apply Relying Party policy, if any.
Expand Down Expand Up @@ -211,7 +214,7 @@ public async Task<VerifyAssertionResult> VerifyAsync(
if (metadataService?.ConformanceTesting() is true && metadataEntry is null && attType != AttestationType.None && fmt is not "fido-u2f")
throw new Fido2VerificationException(Fido2ErrorCode.AaGuidNotFound, "AAGUID not found in MDS test metadata");

TrustAnchor.Verify(metadataEntry, trustPath);
TrustAnchor.Verify(metadataEntry, trustPath, metadataService?.ConformanceTesting() is true);
}

return new VerifyAssertionResult
Expand Down
12 changes: 8 additions & 4 deletions Src/Fido2/AuthenticatorAttestationResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public async Task<RegisteredPublicKeyCredential> VerifyAsync(
Fido2Configuration config,
IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser,
IMetadataService? metadataService,
byte[]? requestTokenBindingId,
CancellationToken cancellationToken = default)
{
// https://www.w3.org/TR/webauthn/#registering-a-new-credential
Expand All @@ -74,7 +75,10 @@ public async Task<RegisteredPublicKeyCredential> VerifyAsync(

// 8. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the create() call.
// 9. Verify that the value of C.origin matches the Relying Party's origin.
BaseVerify(config.FullyQualifiedOrigins, originalOptions.Challenge);
// 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.
// 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.
// Validated in BaseVerify.
BaseVerify(config.FullyQualifiedOrigins, originalOptions.Challenge, requestTokenBindingId);

if (Raw.Id is null || Raw.Id.Length == 0)
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestationResponse, Fido2ErrorMessages.AttestationResponseIdMissing);
Expand Down Expand Up @@ -149,7 +153,7 @@ public async Task<RegisteredPublicKeyCredential> VerifyAsync(
if (metadataService?.ConformanceTesting() is true && metadataEntry is null && attType != AttestationType.None && AttestationObject.Fmt is not "fido-u2f")
throw new Fido2VerificationException(Fido2ErrorCode.AaGuidNotFound, "AAGUID not found in MDS test metadata");

TrustAnchor.Verify(metadataEntry, trustPath);
TrustAnchor.Verify(metadataEntry, trustPath, metadataService?.ConformanceTesting() is true);

// 22. Assess the attestation trustworthiness using the outputs of the verification procedure in step 14, as follows:
// If self attestation was used, check if self attestation is acceptable under Relying Party policy.
Expand Down Expand Up @@ -186,7 +190,7 @@ public async Task<RegisteredPublicKeyCredential> VerifyAsync(

return new RegisteredPublicKeyCredential
{
Type = Raw.Type,
Type = Raw.Type.Value,
Id = authData.AttestedCredentialData.CredentialId,
PublicKey = authData.AttestedCredentialData.CredentialPublicKey.GetBytes(),
SignCount = authData.SignCount,
Expand Down Expand Up @@ -250,7 +254,7 @@ private async Task<byte[]> DevicePublicKeyRegistrationAsync(
if (metadataService?.ConformanceTesting() is true && metadataEntry is null && attType != AttestationType.None && devicePublicKeyAuthenticatorOutput.Fmt is not "fido-u2f")
throw new Fido2VerificationException(Fido2ErrorCode.AaGuidNotFound, "AAGUID not found in MDS test metadata");

TrustAnchor.Verify(metadataEntry, trustPath);
TrustAnchor.Verify(metadataEntry, trustPath, metadataService?.ConformanceTesting() is true);

// Check status reports for authenticator with undesirable status
var latestStatusReport = metadataEntry?.GetLatestStatusReport();
Expand Down
13 changes: 12 additions & 1 deletion Src/Fido2/AuthenticatorResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ protected AuthenticatorResponse(ReadOnlySpan<byte> utf8EncodedJson)
Type = response.Type;
Challenge = response.Challenge;
Origin = response.Origin;
TokenBinding = response.TokenBinding;
}

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

protected void BaseVerify(IReadOnlySet<string> fullyQualifiedExpectedOrigins, ReadOnlySpan<byte> originalChallenge)
[JsonPropertyName("tokenBinding")]
public TokenBindingDto? TokenBinding { get; set; }

protected void BaseVerify(IReadOnlySet<string> fullyQualifiedExpectedOrigins, ReadOnlySpan<byte> originalChallenge, byte[]? requestTokenBindingId)
{
if (Type is not "webauthn.create" && Type is not "webauthn.get")
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAuthenticatorResponse, $"Type must be 'webauthn.create' or 'webauthn.get'. Was '{Type}'");
Expand All @@ -79,6 +83,13 @@ protected void BaseVerify(IReadOnlySet<string> fullyQualifiedExpectedOrigins, Re
// 12. Verify that the value of C.origin matches the Relying Party's origin.
if (!fullyQualifiedExpectedOrigins.Contains(fullyQualifiedOrigin))
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})");

// 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.
// 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.
if (TokenBinding != null)
{
TokenBinding.Verify(requestTokenBindingId);
}
}

/*
Expand Down
15 changes: 8 additions & 7 deletions Src/Fido2/Extensions/CryptoUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public static HashAlgorithmName HashAlgFromCOSEAlg(COSE.Algorithm alg)
};
}

public static bool ValidateTrustChain(X509Certificate2[] trustPath, X509Certificate2[] attestationRootCertificates)
public static bool ValidateTrustChain(X509Certificate2[] trustPath, X509Certificate2[] attestationRootCertificates, bool conformance = false)
{
// https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-metadata-statement-v2.0-id-20180227.html#widl-MetadataStatement-attestationRootCertificates

Expand All @@ -59,7 +59,9 @@ public static bool ValidateTrustChain(X509Certificate2[] trustPath, X509Certific
// A trust anchor can be a root certificate, an intermediate CA certificate or even the attestation certificate itself.

// Let's check the simplest case first. If subject and issuer are the same, and the attestation cert is in the list, that's all the validation we need
if (trustPath.Length == 1 && trustPath[0].Subject.Equals(trustPath[0].Issuer, StringComparison.Ordinal))

// We have the same singular root cert in trustpath and it is in attestationRootCertificates
if (trustPath.Length == 1)
{
foreach (X509Certificate2 cert in attestationRootCertificates)
{
Expand All @@ -68,7 +70,6 @@ public static bool ValidateTrustChain(X509Certificate2[] trustPath, X509Certific
return true;
}
}
return false;
}

// If the attestation cert is not self signed, we will need to build a chain
Expand Down Expand Up @@ -101,10 +102,10 @@ public static bool ValidateTrustChain(X509Certificate2[] trustPath, X509Certific
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;

// if the attestation cert has a CDP extension, go ahead and turn on online revocation checking
if (!string.IsNullOrEmpty(CDPFromCertificateExts(trustPath[0].Extensions)))
chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;

// don't allow unknown root now that we have a custom root
if (!string.IsNullOrEmpty(CDPFromCertificateExts(trustPath[0].Extensions)) && !conformance)
chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
// don't allow unknown root now that we have a custom root
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;

// now, verify chain again with all checks turned on
Expand Down
5 changes: 4 additions & 1 deletion Src/Fido2/Fido2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,11 @@ public async Task<MakeNewCredentialResult> MakeNewCredentialAsync(
AuthenticatorAttestationRawResponse attestationResponse,
CredentialCreateOptions originalOptions,
IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser,
byte[]? requestTokenBindingId = null,
CancellationToken cancellationToken = default)
{
var parsedResponse = AuthenticatorAttestationResponse.Parse(attestationResponse);
var success = await parsedResponse.VerifyAsync(originalOptions, _config, isCredentialIdUniqueToUser, _metadataService, cancellationToken);
var success = await parsedResponse.VerifyAsync(originalOptions, _config, isCredentialIdUniqueToUser, _metadataService, requestTokenBindingId, cancellationToken);

// todo: Set Errormessage etc.
return new MakeNewCredentialResult(
Expand Down Expand Up @@ -111,6 +112,7 @@ public async Task<VerifyAssertionResult> MakeAssertionAsync(
IReadOnlyList<byte[]> storedDevicePublicKeys,
uint storedSignatureCounter,
IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredentialIdCallback,
byte[]? requestTokenBindingId = null,
CancellationToken cancellationToken = default)
{
var parsedResponse = AuthenticatorAssertionResponse.Parse(assertionResponse);
Expand All @@ -122,6 +124,7 @@ public async Task<VerifyAssertionResult> MakeAssertionAsync(
storedSignatureCounter,
isUserHandleOwnerOfCredentialIdCallback,
_metadataService,
requestTokenBindingId,
cancellationToken);

return result;
Expand Down
2 changes: 2 additions & 0 deletions Src/Fido2/IFido2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ Task<VerifyAssertionResult> MakeAssertionAsync(
IReadOnlyList<byte[]> storedDevicePublicKeys,
uint storedSignatureCounter,
IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredentialIdCallback,
byte[]? requestTokenBindingId = null,
CancellationToken cancellationToken = default);

Task<MakeNewCredentialResult> MakeNewCredentialAsync(
AuthenticatorAttestationRawResponse attestationResponse,
CredentialCreateOptions originalOptions,
IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser,
byte[]? requestTokenBindingId = null,
CancellationToken cancellationToken = default);

CredentialCreateOptions RequestNewCredential(
Expand Down
41 changes: 41 additions & 0 deletions Src/Fido2/TokenBindingDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Text.Json.Serialization;

namespace Fido2NetLib
{
public class TokenBindingDto
{
/// <summary>
/// Either "present" or "supported". https://www.w3.org/TR/webauthn/#enumdef-tokenbindingstatus
/// supported: Indicates the client supports token binding, but it was not negotiated when communicating with the Relying Party.
/// present: Indicates token binding was used when communicating with the Relying Party. In this case, the id member MUST be present
/// </summary>
[JsonPropertyName("status")]
public string? Status { get; set; }

/// <summary>
/// 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.
/// </summary>
[JsonPropertyName("id")]
public string? Id { get; set; }

public void Verify(byte[]? requestTokenbinding)
{
// validation by the FIDO conformance tool (more than spec says)
switch (Status)
{
case "present":
if (string.IsNullOrEmpty(Id))
throw new Fido2VerificationException("TokenBinding status was present but Id is missing");
var b64 = Base64Url.Encode(requestTokenbinding);
if (Id != b64)
throw new Fido2VerificationException("Tokenbinding Id does not match");
break;
case "supported":
case "not-supported":
break;
default:
throw new Fido2VerificationException("Malformed tokenbinding status field");
}
}
}
}
Loading