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

conformance 1.7.20 4 #531

Merged
merged 20 commits into from
Oct 18, 2024
Merged
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: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -334,3 +334,5 @@ ASALocalRun/
/Test/coverage.netcoreapp3.1.cobertura.xml
.DS_Store
/testEnvironments.json

Demo/Conformance/
1 change: 0 additions & 1 deletion Demo/TestController.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Fido2NetLib;
using Fido2NetLib.Development;
using Fido2NetLib.Objects;
Expand Down
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
2 changes: 2 additions & 0 deletions Src/Fido2.Models/CredentialCreateOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ public static CredentialCreateOptions Create(
PubKeyCredParam.ES512,
PubKeyCredParam.RS512,
PubKeyCredParam.PS512,
PubKeyCredParam.RS1
abergs marked this conversation as resolved.
Show resolved Hide resolved
],
AuthenticatorSelection = authenticatorSelection,
Attestation = attestationConveyancePreference,
Expand Down Expand Up @@ -185,6 +186,7 @@ public sealed class PubKeyCredParam(
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
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; }
aseigler marked this conversation as resolved.
Show resolved Hide resolved
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; }
aseigler marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
Expand All @@ -33,10 +33,11 @@ public sealed class AuthenticationExtensionsClientInputs
/// <summary>
/// This extension enables use of a user verification method.
/// https://www.w3.org/TR/webauthn/#sctn-uvm-extension
/// TODO: Remove this completely as it's removed in L3
/// </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
12 changes: 10 additions & 2 deletions Src/Fido2/AuthenticatorAssertionResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public static AuthenticatorAssertionResponse Parse(AuthenticatorAssertionRawResp
/// <param name="storedSignatureCounter">The stored counter value for this CredentialId</param>
/// <param name="isUserHandleOwnerOfCredId">A function that returns <see langword="true"/> if user handle is owned by the credential ID.</param>
/// <param name="metadataService"></param>
/// <param name="requestTokenBindingId">DO NOT USE - Deprecated, but kept in code due to conformance testing tool</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
public async Task<VerifyAssertionResult> VerifyAsync(
AssertionOptions options,
Expand All @@ -61,9 +62,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);
aseigler marked this conversation as resolved.
Show resolved Hide resolved

if (Raw.Type != PublicKeyCredentialType.PublicKey)
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, Fido2ErrorMessages.AssertionResponseNotPublicKey);
Expand Down Expand Up @@ -115,15 +117,20 @@ public async Task<VerifyAssertionResult> VerifyAsync(
// https://www.w3.org/TR/webauthn/#sctn-appid-extension
// FIDO AppID Extension:
// 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.

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);

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

var conformanceTesting = metadataService != null && metadataService.ConformanceTesting();

// 14. Verify that the UP bit of the flags in authData is set.
if (!authData.UserPresent)
// Todo: Conformance testing verifies the UVP flags differently than W3C spec, simplify this by removing the mention of conformanceTesting when conformance tools are updated)
if (!authData.UserPresent && !conformanceTesting)
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.
Expand Down Expand Up @@ -174,6 +181,7 @@ public async Task<VerifyAssertionResult> VerifyAsync(
if (authData.SignCount > 0 && authData.SignCount <= storedSignatureCounter)
throw new Fido2VerificationException(Fido2ErrorCode.InvalidSignCount, Fido2ErrorMessages.SignCountIsLessThanSignatureCounter);


return new VerifyAssertionResult
{
CredentialId = Raw.Id,
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 ? FidoValidationMode.FidoConformance2024 : FidoValidationMode.Default);

// 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 @@ -253,7 +257,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 ? FidoValidationMode.FidoConformance2024 : FidoValidationMode.Default);

// Check status reports for authenticator with undesirable status
var latestStatusReport = metadataEntry?.GetLatestStatusReport();
Expand Down
11 changes: 10 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,11 @@ protected AuthenticatorResponse(ReadOnlySpan<byte> utf8EncodedJson)
[JsonPropertyName("origin")]
public string Origin { get; }

protected void BaseVerify(IReadOnlySet<string> fullyQualifiedExpectedOrigins, ReadOnlySpan<byte> originalChallenge)
// [Obsolete("This property is not used and will be removed in a future version once the conformance tool stops testing for it.")]
[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 +84,10 @@ 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.
TokenBinding?.Verify(requestTokenBindingId);
}

/*
Expand Down
7 changes: 4 additions & 3 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, FidoValidationMode validationMode = FidoValidationMode.Default)
{
// https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-metadata-statement-v2.0-id-20180227.html#widl-MetadataStatement-attestationRootCertificates

Expand All @@ -59,6 +59,8 @@ 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

// We have the same singular root cert in trustpath and it is in attestationRootCertificates
if (trustPath.Length == 1 && trustPath[0].Subject.Equals(trustPath[0].Issuer, StringComparison.Ordinal))
{
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,7 +102,7 @@ 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)))
if (!string.IsNullOrEmpty(CDPFromCertificateExts(trustPath[0].Extensions)) && validationMode != FidoValidationMode.FidoConformance2024)
chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;

// don't allow unknown root now that we have a custom root
Expand Down
7 changes: 6 additions & 1 deletion Src/Fido2/Fido2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,18 @@ public CredentialCreateOptions RequestNewCredential(
/// <param name="attestationResponse">The attestation response from the authenticator.</param>
/// <param name="originalOptions">The original options that was sent to the client.</param>
/// <param name="isCredentialIdUniqueToUser">The delegate used to validate that the CredentialID is unique to this user.</param>
/// <param name="requestTokenBindingId">DO NOT USE - Deprecated, but kept in code due to conformance testing tool</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns></returns>
public async Task<RegisteredPublicKeyCredential> MakeNewCredentialAsync(
AuthenticatorAttestationRawResponse attestationResponse,
CredentialCreateOptions originalOptions,
IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser,
byte[]? requestTokenBindingId = null,
abergs marked this conversation as resolved.
Show resolved Hide resolved
CancellationToken cancellationToken = default)
{
var parsedResponse = AuthenticatorAttestationResponse.Parse(attestationResponse);
var credential = await parsedResponse.VerifyAsync(originalOptions, _config, isCredentialIdUniqueToUser, _metadataService, cancellationToken);
var credential = await parsedResponse.VerifyAsync(originalOptions, _config, isCredentialIdUniqueToUser, _metadataService, requestTokenBindingId, cancellationToken);

return credential;
}
Expand Down Expand Up @@ -105,6 +107,7 @@ public AssertionOptions GetAssertionOptions(
/// <param name="storedDevicePublicKeys">The stored device public keys.</param>
/// <param name="storedSignatureCounter">The stored value of the signature counter.</param>
/// <param name="isUserHandleOwnerOfCredentialIdCallback">The delegate used to validate that the user handle is indeed owned of the CredentialId.</param>
/// <param name="requestTokenBindingId">DO NOT USE - Deprecated, but kept in code due to conformance testing tool</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns></returns>
public async Task<VerifyAssertionResult> MakeAssertionAsync(
Expand All @@ -114,6 +117,7 @@ public async Task<VerifyAssertionResult> MakeAssertionAsync(
IReadOnlyList<byte[]> storedDevicePublicKeys,
uint storedSignatureCounter,
IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredentialIdCallback,
byte[]? requestTokenBindingId = null,
abergs marked this conversation as resolved.
Show resolved Hide resolved
CancellationToken cancellationToken = default)
{
var parsedResponse = AuthenticatorAssertionResponse.Parse(assertionResponse);
Expand All @@ -125,6 +129,7 @@ public async Task<VerifyAssertionResult> MakeAssertionAsync(
storedSignatureCounter,
isUserHandleOwnerOfCredentialIdCallback,
_metadataService,
requestTokenBindingId,
cancellationToken);

return result;
Expand Down
6 changes: 6 additions & 0 deletions Src/Fido2/FidoValidationMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
public enum FidoValidationMode
{
WebAuthNLevel3,
FidoConformance2024,
Default = WebAuthNLevel3
}
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<RegisteredPublicKeyCredential> MakeNewCredentialAsync(
AuthenticatorAttestationRawResponse attestationResponse,
CredentialCreateOptions originalOptions,
IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser,
byte[]? requestTokenBindingId = null,
CancellationToken cancellationToken = default);
abergs marked this conversation as resolved.
Show resolved Hide resolved

CredentialCreateOptions RequestNewCredential(
Expand Down
39 changes: 39 additions & 0 deletions Src/Fido2/TokenBindingDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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
Loading