diff --git a/.gitignore b/.gitignore
index 8aa11f5f..c66d1cc4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -334,3 +334,5 @@ ASALocalRun/
/Test/coverage.netcoreapp3.1.cobertura.xml
.DS_Store
/testEnvironments.json
+
+Demo/Conformance/
\ No newline at end of file
diff --git a/Demo/TestController.cs b/Demo/TestController.cs
index d0d67163..5967a8af 100644
--- a/Demo/TestController.cs
+++ b/Demo/TestController.cs
@@ -1,6 +1,5 @@
using System.Text;
using System.Text.Json;
-using System.Text.Json.Serialization;
using Fido2NetLib;
using Fido2NetLib.Development;
using Fido2NetLib.Objects;
diff --git a/Src/Fido2.Models/AuthenticatorAttestationRawResponse.cs b/Src/Fido2.Models/AuthenticatorAttestationRawResponse.cs
index 278e8eb2..80337144 100644
--- a/Src/Fido2.Models/AuthenticatorAttestationRawResponse.cs
+++ b/Src/Fido2.Models/AuthenticatorAttestationRawResponse.cs
@@ -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; }
diff --git a/Src/Fido2.Models/CredentialCreateOptions.cs b/Src/Fido2.Models/CredentialCreateOptions.cs
index 1e5b2239..43de046b 100644
--- a/Src/Fido2.Models/CredentialCreateOptions.cs
+++ b/Src/Fido2.Models/CredentialCreateOptions.cs
@@ -134,6 +134,7 @@ public static CredentialCreateOptions Create(
PubKeyCredParam.ES512,
PubKeyCredParam.RS512,
PubKeyCredParam.PS512,
+ PubKeyCredParam.RS1
],
AuthenticatorSelection = authenticatorSelection,
Attestation = attestationConveyancePreference,
@@ -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);
}
///
diff --git a/Src/Fido2.Models/Objects/AuthenticationExtensionsClientInputs.cs b/Src/Fido2.Models/Objects/AuthenticationExtensionsClientInputs.cs
index db10e328..f6f8e4d1 100644
--- a/Src/Fido2.Models/Objects/AuthenticationExtensionsClientInputs.cs
+++ b/Src/Fido2.Models/Objects/AuthenticationExtensionsClientInputs.cs
@@ -12,14 +12,14 @@ public sealed class AuthenticationExtensionsClientInputs
///
[JsonPropertyName("example.extension.bool")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public object Example { get; set; }
+ public bool? Example { get; set; }
///
/// 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
///
[JsonPropertyName("appid")]
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonIgnore(Condition = JsonIgnoreCondition.Always)]
public string AppID { get; set; }
///
@@ -33,10 +33,11 @@ public sealed class AuthenticationExtensionsClientInputs
///
/// 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
///
[JsonPropertyName("uvm")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public bool? UserVerificationMethod { get; set; }
+ public bool? UserVerificationMethod { private get; set; }
#nullable enable
///
diff --git a/Src/Fido2.Models/Objects/AuthenticationExtensionsClientOutputs.cs b/Src/Fido2.Models/Objects/AuthenticationExtensionsClientOutputs.cs
index edcf8059..b5f15b59 100644
--- a/Src/Fido2.Models/Objects/AuthenticationExtensionsClientOutputs.cs
+++ b/Src/Fido2.Models/Objects/AuthenticationExtensionsClientOutputs.cs
@@ -9,7 +9,7 @@ public class AuthenticationExtensionsClientOutputs
///
[JsonPropertyName("example.extension.bool")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public object Example { get; set; }
+ public bool? Example { get; set; }
#nullable enable
diff --git a/Src/Fido2/AuthenticatorAssertionResponse.cs b/Src/Fido2/AuthenticatorAssertionResponse.cs
index e2b30b13..166901a2 100644
--- a/Src/Fido2/AuthenticatorAssertionResponse.cs
+++ b/Src/Fido2/AuthenticatorAssertionResponse.cs
@@ -52,6 +52,7 @@ public static AuthenticatorAssertionResponse Parse(AuthenticatorAssertionRawResp
/// The stored counter value for this CredentialId
/// A function that returns if user handle is owned by the credential ID.
///
+ /// DO NOT USE - Deprecated, but kept in code due to conformance testing tool
/// The used to propagate notifications that the operation should be canceled.
public async Task VerifyAsync(
AssertionOptions options,
@@ -61,9 +62,10 @@ public async Task 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);
@@ -115,15 +117,20 @@ public async Task 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.
@@ -174,6 +181,7 @@ public async Task VerifyAsync(
if (authData.SignCount > 0 && authData.SignCount <= storedSignatureCounter)
throw new Fido2VerificationException(Fido2ErrorCode.InvalidSignCount, Fido2ErrorMessages.SignCountIsLessThanSignatureCounter);
+
return new VerifyAssertionResult
{
CredentialId = Raw.Id,
diff --git a/Src/Fido2/AuthenticatorAttestationResponse.cs b/Src/Fido2/AuthenticatorAttestationResponse.cs
index d5665ae8..1522e54b 100644
--- a/Src/Fido2/AuthenticatorAttestationResponse.cs
+++ b/Src/Fido2/AuthenticatorAttestationResponse.cs
@@ -60,6 +60,7 @@ public async Task VerifyAsync(
Fido2Configuration config,
IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser,
IMetadataService? metadataService,
+ byte[]? requestTokenBindingId,
CancellationToken cancellationToken = default)
{
// https://www.w3.org/TR/webauthn/#registering-a-new-credential
@@ -74,7 +75,10 @@ public async Task 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);
@@ -149,7 +153,7 @@ public async Task 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.
@@ -186,7 +190,7 @@ public async Task VerifyAsync(
return new RegisteredPublicKeyCredential
{
- Type = Raw.Type,
+ Type = Raw.Type.Value,
Id = authData.AttestedCredentialData.CredentialId,
PublicKey = authData.AttestedCredentialData.CredentialPublicKey.GetBytes(),
SignCount = authData.SignCount,
@@ -253,7 +257,7 @@ private async Task 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();
diff --git a/Src/Fido2/AuthenticatorResponse.cs b/Src/Fido2/AuthenticatorResponse.cs
index 22c31635..eddee047 100644
--- a/Src/Fido2/AuthenticatorResponse.cs
+++ b/Src/Fido2/AuthenticatorResponse.cs
@@ -48,6 +48,7 @@ protected AuthenticatorResponse(ReadOnlySpan utf8EncodedJson)
Type = response.Type;
Challenge = response.Challenge;
Origin = response.Origin;
+ TokenBinding = response.TokenBinding;
}
public const int MAX_ORIGINS_TO_PRINT = 5;
@@ -62,7 +63,11 @@ protected AuthenticatorResponse(ReadOnlySpan utf8EncodedJson)
[JsonPropertyName("origin")]
public string Origin { get; }
- protected void BaseVerify(IReadOnlySet fullyQualifiedExpectedOrigins, ReadOnlySpan 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 fullyQualifiedExpectedOrigins, ReadOnlySpan 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}'");
@@ -79,6 +84,10 @@ protected void BaseVerify(IReadOnlySet 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);
}
/*
diff --git a/Src/Fido2/Extensions/CryptoUtils.cs b/Src/Fido2/Extensions/CryptoUtils.cs
index 719d5eaf..8714becf 100644
--- a/Src/Fido2/Extensions/CryptoUtils.cs
+++ b/Src/Fido2/Extensions/CryptoUtils.cs
@@ -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
@@ -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)
@@ -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
@@ -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
diff --git a/Src/Fido2/Fido2.cs b/Src/Fido2/Fido2.cs
index 11dc1b93..8ebf338b 100644
--- a/Src/Fido2/Fido2.cs
+++ b/Src/Fido2/Fido2.cs
@@ -65,16 +65,18 @@ public CredentialCreateOptions RequestNewCredential(
/// The attestation response from the authenticator.
/// The original options that was sent to the client.
/// The delegate used to validate that the CredentialID is unique to this user.
+ /// DO NOT USE - Deprecated, but kept in code due to conformance testing tool
/// The used to propagate notifications that the operation should be canceled.
///
public async Task MakeNewCredentialAsync(
AuthenticatorAttestationRawResponse attestationResponse,
CredentialCreateOptions originalOptions,
IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser,
+ byte[]? requestTokenBindingId = null,
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;
}
@@ -105,6 +107,7 @@ public AssertionOptions GetAssertionOptions(
/// The stored device public keys.
/// The stored value of the signature counter.
/// The delegate used to validate that the user handle is indeed owned of the CredentialId.
+ /// DO NOT USE - Deprecated, but kept in code due to conformance testing tool
/// The used to propagate notifications that the operation should be canceled.
///
public async Task MakeAssertionAsync(
@@ -114,6 +117,7 @@ public async Task MakeAssertionAsync(
IReadOnlyList storedDevicePublicKeys,
uint storedSignatureCounter,
IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredentialIdCallback,
+ byte[]? requestTokenBindingId = null,
CancellationToken cancellationToken = default)
{
var parsedResponse = AuthenticatorAssertionResponse.Parse(assertionResponse);
@@ -125,6 +129,7 @@ public async Task MakeAssertionAsync(
storedSignatureCounter,
isUserHandleOwnerOfCredentialIdCallback,
_metadataService,
+ requestTokenBindingId,
cancellationToken);
return result;
diff --git a/Src/Fido2/FidoValidationMode.cs b/Src/Fido2/FidoValidationMode.cs
new file mode 100644
index 00000000..fbaf736f
--- /dev/null
+++ b/Src/Fido2/FidoValidationMode.cs
@@ -0,0 +1,6 @@
+public enum FidoValidationMode
+{
+ WebAuthNLevel3,
+ FidoConformance2024,
+ Default = WebAuthNLevel3
+}
diff --git a/Src/Fido2/IFido2.cs b/Src/Fido2/IFido2.cs
index a536bbef..8b58de02 100644
--- a/Src/Fido2/IFido2.cs
+++ b/Src/Fido2/IFido2.cs
@@ -20,12 +20,14 @@ Task MakeAssertionAsync(
IReadOnlyList storedDevicePublicKeys,
uint storedSignatureCounter,
IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredentialIdCallback,
+ byte[]? requestTokenBindingId = null,
CancellationToken cancellationToken = default);
Task MakeNewCredentialAsync(
AuthenticatorAttestationRawResponse attestationResponse,
CredentialCreateOptions originalOptions,
IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser,
+ byte[]? requestTokenBindingId = null,
CancellationToken cancellationToken = default);
CredentialCreateOptions RequestNewCredential(
diff --git a/Src/Fido2/TokenBindingDto.cs b/Src/Fido2/TokenBindingDto.cs
new file mode 100644
index 00000000..3651b301
--- /dev/null
+++ b/Src/Fido2/TokenBindingDto.cs
@@ -0,0 +1,39 @@
+using System.Text.Json.Serialization;
+namespace Fido2NetLib;
+
+public class TokenBindingDto
+{
+ ///
+ /// 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
+ ///
+ [JsonPropertyName("status")]
+ public string? Status { get; set; }
+
+ ///
+ /// 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.
+ ///
+ [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");
+ }
+ }
+}
diff --git a/Src/Fido2/TrustAnchor.cs b/Src/Fido2/TrustAnchor.cs
index 6a02daed..cbdbcdb3 100644
--- a/Src/Fido2/TrustAnchor.cs
+++ b/Src/Fido2/TrustAnchor.cs
@@ -1,60 +1,65 @@
-using System;
-using System.Linq;
-using System.Security.Cryptography.X509Certificates;
-
-using Fido2NetLib.Exceptions;
-
-namespace Fido2NetLib;
-
-public static class TrustAnchor
-{
- public static void Verify(MetadataBLOBPayloadEntry? metadataEntry, X509Certificate2[] trustPath)
- {
- if (trustPath != null && metadataEntry?.MetadataStatement?.AttestationTypes is not null)
- {
- static bool ContainsAttestationType(MetadataBLOBPayloadEntry entry, MetadataAttestationType type)
- {
- return entry.MetadataStatement.AttestationTypes.Contains(type.ToEnumMemberValue());
- }
-
- // If the authenticator's metadata requires basic full attestation, build and verify the chain
- if (ContainsAttestationType(metadataEntry, MetadataAttestationType.ATTESTATION_BASIC_FULL) ||
- ContainsAttestationType(metadataEntry, MetadataAttestationType.ATTESTATION_PRIVACY_CA))
- {
- string[] certStrings = metadataEntry.MetadataStatement.AttestationRootCertificates;
- var attestationRootCertificates = new X509Certificate2[certStrings.Length];
-
- for (int i = 0; i < attestationRootCertificates.Length; i++)
- {
- attestationRootCertificates[i] = new X509Certificate2(Convert.FromBase64String(certStrings[i]));
- }
-
- if (!CryptoUtils.ValidateTrustChain(trustPath, attestationRootCertificates))
- {
- throw new Fido2VerificationException(Fido2ErrorMessages.InvalidCertificateChain);
- }
- }
-
- else if (ContainsAttestationType(metadataEntry, MetadataAttestationType.ATTESTATION_ANONCA))
- {
- // skip verification for Anonymization CA (AnonCA)
- }
- else // otherwise, ensure the certificate is self signed
- {
- var trustPath0 = trustPath[0];
-
- if (!string.Equals(trustPath0.Subject, trustPath0.Issuer, StringComparison.Ordinal))
- {
- // TODO: Improve this error message
- throw new Fido2VerificationException("Attestation with full attestation from authenticator that does not support full attestation");
- }
- }
-
- // TODO: Verify all MetadataAttestationTypes are correctly handled
-
- // [ ] ATTESTATION_ECDAA "ecdaa" | currently handled as self signed w/ no test coverage
- // [ ] ATTESTATION_ANONCA "anonca" | currently not verified w/ no test coverage
- // [ ] ATTESTATION_NONE "none" | currently handled as self signed w/ no test coverage
- }
- }
-}
+using System;
+using System.Linq;
+using System.Security.Cryptography.X509Certificates;
+
+using Fido2NetLib.Exceptions;
+
+namespace Fido2NetLib;
+
+public static class TrustAnchor
+{
+ public static void Verify(MetadataBLOBPayloadEntry? metadataEntry, X509Certificate2[] trustPath, FidoValidationMode validationMode = FidoValidationMode.Default)
+ {
+ if (trustPath != null && metadataEntry?.MetadataStatement?.AttestationTypes is not null)
+ {
+ static bool ContainsAttestationType(MetadataBLOBPayloadEntry entry, MetadataAttestationType type)
+ {
+ return entry.MetadataStatement.AttestationTypes.Contains(type.ToEnumMemberValue());
+ }
+
+ // If the authenticator's metadata requires basic full attestation, build and verify the chain
+ if (ContainsAttestationType(metadataEntry, MetadataAttestationType.ATTESTATION_BASIC_FULL) ||
+ ContainsAttestationType(metadataEntry, MetadataAttestationType.ATTESTATION_PRIVACY_CA))
+ {
+ string[] certStrings = metadataEntry.MetadataStatement.AttestationRootCertificates;
+ var attestationRootCertificates = new X509Certificate2[certStrings.Length];
+
+ for (int i = 0; i < attestationRootCertificates.Length; i++)
+ {
+ attestationRootCertificates[i] = new X509Certificate2(Convert.FromBase64String(certStrings[i]));
+ }
+
+ if (trustPath.Length > 1 && attestationRootCertificates.Any(c => string.Equals(c.Thumbprint, trustPath[^1].Thumbprint, StringComparison.Ordinal)))
+ {
+ throw new Fido2VerificationException(Fido2ErrorMessages.InvalidCertificateChain);
+ }
+
+ if (!CryptoUtils.ValidateTrustChain(trustPath, attestationRootCertificates, validationMode))
+ {
+ throw new Fido2VerificationException(Fido2ErrorMessages.InvalidCertificateChain);
+ }
+ }
+
+ else if (ContainsAttestationType(metadataEntry, MetadataAttestationType.ATTESTATION_ANONCA))
+ {
+ // skip verification for Anonymization CA (AnonCA)
+ }
+ else // otherwise, ensure the certificate is self signed
+ {
+ var trustPath0 = trustPath[0];
+
+ if (!string.Equals(trustPath0.Subject, trustPath0.Issuer, StringComparison.Ordinal))
+ {
+ // TODO: Improve this error message
+ throw new Fido2VerificationException("Attestation with full attestation from authenticator that does not support full attestation");
+ }
+ }
+
+ // TODO: Verify all MetadataAttestationTypes are correctly handled
+
+ // [ ] ATTESTATION_ECDAA "ecdaa" | currently handled as self signed w/ no test coverage
+ // [ ] ATTESTATION_ANONCA "anonca" | currently not verified w/ no test coverage
+ // [ ] ATTESTATION_NONE "none" | currently handled as self signed w/ no test coverage
+ }
+ }
+}
diff --git a/Test/AuthenticatorResponse.cs b/Test/AuthenticatorResponse.cs
index 50ff27d0..93b6a064 100644
--- a/Test/AuthenticatorResponse.cs
+++ b/Test/AuthenticatorResponse.cs
@@ -253,7 +253,7 @@ public void TestAuthenticatorAttestationRawResponse()
{
AppID = true,
Extensions = ["foo", "bar"],
- Example = "test",
+ Example = true,
UserVerificationMethod = new ulong[][]
{
new ulong[]
@@ -279,7 +279,7 @@ public void TestAuthenticatorAttestationRawResponse()
Assert.Equal(clientDataJson, rawResponse.Response.ClientDataJson);
Assert.True(rawResponse.ClientExtensionResults.AppID);
Assert.Equal(new string[] { "foo", "bar" }, rawResponse.ClientExtensionResults.Extensions);
- Assert.Equal("test", rawResponse.ClientExtensionResults.Example);
+ Assert.True(rawResponse.ClientExtensionResults.Example);
Assert.Equal((ulong)4, rawResponse.ClientExtensionResults.UserVerificationMethod[0][0]);
Assert.True(rawResponse.ClientExtensionResults.PRF.Enabled);
Assert.Equal(rawResponse.ClientExtensionResults.PRF.Results.First, [0xf1, 0xd0]);
@@ -1212,7 +1212,7 @@ public void TestAuthenticatorAssertionRawResponse()
{
AppID = true,
Extensions = ["foo", "bar"],
- Example = "test",
+ Example = true,
UserVerificationMethod = new ulong[][]
{
new ulong[]
@@ -1240,11 +1240,12 @@ public void TestAuthenticatorAssertionRawResponse()
Assert.Equal([0xf1, 0xd0], assertionResponse.Response.UserHandle);
Assert.True(assertionResponse.ClientExtensionResults.AppID);
Assert.Equal(new string[] { "foo", "bar" }, assertionResponse.ClientExtensionResults.Extensions);
- Assert.Equal("test", assertionResponse.ClientExtensionResults.Example);
+ Assert.True(assertionResponse.ClientExtensionResults.Example);
Assert.Equal((ulong)4, assertionResponse.ClientExtensionResults.UserVerificationMethod[0][0]);
Assert.True(assertionResponse.ClientExtensionResults.PRF.Enabled);
Assert.Equal([0xf1, 0xd0], assertionResponse.ClientExtensionResults.PRF.Results.First);
Assert.Equal([0xf1, 0xd0], assertionResponse.ClientExtensionResults.PRF.Results.Second);
+
}
[Fact]
@@ -1288,7 +1289,7 @@ public async Task TestAuthenticatorAssertionTypeNotPublicKey()
{
AppID = false,
Extensions = ["foo", "bar"],
- Example = "test",
+ Example = true,
UserVerificationMethod = new ulong[][]
{
new ulong[]
@@ -1356,7 +1357,7 @@ public async Task TestAuthenticatorAssertionIdMissing()
{
AppID = false,
Extensions = ["foo", "bar"],
- Example = "test",
+ Example = true,
UserVerificationMethod = new ulong[][]
{
new ulong[]
@@ -1425,7 +1426,7 @@ public async Task TestAuthenticatorAssertionRawIdMissing()
{
AppID = false,
Extensions = ["foo", "bar"],
- Example = "test",
+ Example = true,
UserVerificationMethod = new ulong[][]
{
new ulong[]
@@ -1494,7 +1495,7 @@ public async Task TestAuthenticatorAssertionUserHandleEmpty()
{
AppID = false,
Extensions = ["foo", "bar"],
- Example = "test",
+ Example = true,
UserVerificationMethod = new ulong[][]
{
new ulong[]
@@ -1563,7 +1564,7 @@ public async Task TestAuthenticatorAssertionUserHandleNotOwnerOfPublicKey()
{
AppID = false,
Extensions = ["foo", "bar"],
- Example = "test",
+ Example = true,
UserVerificationMethod = new ulong[][]
{
new ulong[]
@@ -1632,7 +1633,7 @@ public async Task TestAuthenticatorAssertionTypeNotWebAuthnGet()
{
AppID = false,
Extensions = ["foo", "bar"],
- Example = "test",
+ Example = true,
UserVerificationMethod = new ulong[][]
{
new ulong[]
@@ -1703,7 +1704,7 @@ public async Task TestAuthenticatorAssertionAppId()
{
AppID = true,
Extensions = ["foo", "bar"],
- Example = "test",
+ Example = true,
UserVerificationMethod = new ulong[][]
{
new ulong[]
@@ -1773,7 +1774,7 @@ public async Task TestAuthenticatorAssertionInvalidRpIdHash()
{
AppID = false,
Extensions = ["foo", "bar"],
- Example = "test",
+ Example = true,
UserVerificationMethod = new ulong[][]
{
new ulong[]
@@ -1844,7 +1845,7 @@ public async Task TestAuthenticatorAssertionUPRequirementNotMet()
{
AppID = false,
Extensions = ["foo", "bar"],
- Example = "test",
+ Example = true,
UserVerificationMethod = new ulong[][]
{
new ulong[]
@@ -1914,7 +1915,7 @@ public async Task TestAuthenticatorAssertionUVPolicyNotMet()
{
AppID = false,
Extensions = ["foo", "bar"],
- Example = "test",
+ Example = true,
UserVerificationMethod = new ulong[][]
{
new ulong[]
@@ -1982,7 +1983,7 @@ public async Task TestAuthenticatorAssertionBEPolicyRequired()
{
AppID = false,
Extensions = ["foo", "bar"],
- Example = "test",
+ Example = true,
UserVerificationMethod = new ulong[][]
{
new ulong[]
@@ -2051,7 +2052,7 @@ public async Task TestAuthenticatorAssertionBEPolicyDisallow()
{
AppID = false,
Extensions = ["foo", "bar"],
- Example = "test",
+ Example = true,
UserVerificationMethod = new ulong[][]
{
new ulong[]
@@ -2120,7 +2121,7 @@ public async Task TestAuthenticatorAssertionBSPolicyRequired()
{
AppID = false,
Extensions = ["foo", "bar"],
- Example = "test",
+ Example = true,
UserVerificationMethod = new ulong[][]
{
new ulong[]
@@ -2189,7 +2190,7 @@ public async Task TestAuthenticatorAssertionBSPolicyDisallow()
{
AppID = false,
Extensions = ["foo", "bar"],
- Example = "test",
+ Example = true,
UserVerificationMethod = new ulong[][]
{
new ulong[]
@@ -2259,7 +2260,7 @@ public async Task TestAuthenticatorAssertionStoredPublicKeyMissing()
{
AppID = false,
Extensions = ["foo", "bar"],
- Example = "test",
+ Example = true,
UserVerificationMethod = new ulong[][]
{
new ulong[]
@@ -2328,7 +2329,7 @@ public async Task TestAuthenticatorAssertionInvalidSignature()
{
AppID = false,
Extensions = ["foo", "bar"],
- Example = "test",
+ Example = true,
UserVerificationMethod = new ulong[][]
{
new ulong[]
@@ -2404,7 +2405,7 @@ public async Task TestAuthenticatorAssertionSignCountSignature()
{
AppID = false,
Extensions = ["foo", "bar"],
- Example = "test",
+ Example = true,
UserVerificationMethod = new ulong[][]
{
new ulong[]
diff --git a/Test/Fido2Tests.cs b/Test/Fido2Tests.cs
index a6b578d7..db69354c 100644
--- a/Test/Fido2Tests.cs
+++ b/Test/Fido2Tests.cs
@@ -16,7 +16,7 @@
using Moq;
using NSec.Cryptography;
-
+using Test;
using static Fido2NetLib.AuthenticatorAttestationResponse;
namespace fido2_net_lib.Test;
@@ -76,6 +76,17 @@ static Fido2Tests()
];
}
+ private TestMetadataService CreateMetadataService(string cacheDir)
+ {
+ var repos = new List
+ {
+ new FileSystemMetadataRepository(cacheDir)
+ };
+ var simpleService = new TestMetadataService(repos);
+ simpleService.InitializeAsync().Wait();
+ return simpleService;
+ }
+
private async Task GetAsync(string filename)
{
return JsonSerializer.Deserialize(await File.ReadAllTextAsync(filename));
@@ -168,7 +179,7 @@ public async Task MakeAttestationResponseAsync()
{
AppID = true,
Extensions = ["foo", "bar"],
- Example = "test",
+ Example = true,
UserVerificationMethod = new ulong[][]
{
new ulong[]
@@ -411,7 +422,7 @@ public async Task TestFido2AssertionAsync()
var response = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationNoneResponse.json"));
var o = AuthenticatorAttestationResponse.Parse(response);
- await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, CancellationToken.None);
+ await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, null, CancellationToken.None);
var credId = "F1-3C-7F-08-3C-A2-29-E0-B4-03-E8-87-34-6E-FC-7F-98-53-10-3A-30-91-75-67-39-7A-D1-D8-AF-87-04-61-87-EF-95-31-85-60-F3-5A-1A-2A-CF-7D-B0-1D-06-B9-69-F9-AB-F4-EC-F3-07-3E-CF-0F-71-E8-84-E8-41-20";
var allowedCreds = new List() {
@@ -480,7 +491,7 @@ public async Task TestParsingAsync()
Assert.NotNull(jsonPost);
var o = AuthenticatorAttestationResponse.Parse(jsonPost);
- await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, CancellationToken.None);
+ await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, null, CancellationToken.None);
}
[Fact]
@@ -531,7 +542,60 @@ public async Task TestU2FAttestationAsync()
var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationResultsU2F.json"));
var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationOptionsU2F.json"));
var o = AuthenticatorAttestationResponse.Parse(jsonPost);
- await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, CancellationToken.None);
+ await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, null, CancellationToken.None);
+ }
+
+ [Fact]
+ public async Task TestPackedttestationAsyncFailTrustAnchorOnRootCertInTrustPath()
+ {
+ if (!OperatingSystem.IsWindows())
+ return;
+
+ var targetGuid = new Guid("42383245-4437-3343-3846-423445354132");
+ var metadataService = CreateMetadataService("./metadata");
+ metadataService.ChangeEntryGuid(new Guid("00000000-0000-0000-0000-000000000004"), targetGuid);
+ var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationResultsPacked.json"));
+ var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationOptionsPacked.json"));
+ var o = AuthenticatorAttestationResponse.Parse(jsonPost);
+ CborArray X5c = o.AttestationObject.AttStmt["x5c"] as CborArray;
+ var entry = await metadataService.GetEntryAsync(targetGuid);
+ foreach (var attRootCert in entry.MetadataStatement.AttestationRootCertificates)
+ X5c.Add(Encoding.UTF8.GetBytes(attRootCert));
+
+ await Assert.ThrowsAsync(() => o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), metadataService, null, CancellationToken.None));
+ }
+
+ [Fact]
+ public async Task TestU2FAttestationAsyncFailTrustAnchorBasicFull()
+ {
+ var metadataService = CreateMetadataService("./metadata");
+ metadataService.ChangeEntryGuid(new Guid("00000000-0000-0000-0000-000000000001"), new Guid("00000000-0000-0000-0000-000000000000"));
+ var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationResultsU2F.json"));
+ var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationOptionsU2F.json"));
+ var o = AuthenticatorAttestationResponse.Parse(jsonPost);
+ await Assert.ThrowsAsync(() => o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), metadataService, null, CancellationToken.None));
+ }
+
+ [Fact]
+ public async Task TestU2FAttestationAsyncCantFailTrustAnchorAnonca()
+ {
+ var metadataService = CreateMetadataService("./metadata");
+ metadataService.ChangeEntryGuid(new Guid("00000000-0000-0000-0000-000000000002"), new Guid("00000000-0000-0000-0000-000000000000"));
+ var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationResultsU2F.json"));
+ var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationOptionsU2F.json"));
+ var o = AuthenticatorAttestationResponse.Parse(jsonPost);
+ await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), metadataService, null, CancellationToken.None);
+ }
+
+ [Fact]
+ public async Task TestU2FAttestationAsyncFailTrustAnchorBasicSurrogate()
+ {
+ var metadataService = CreateMetadataService("./metadata");
+ metadataService.ChangeEntryGuid(new Guid("00000000-0000-0000-0000-000000000003"), new Guid("00000000-0000-0000-0000-000000000000"));
+ var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationResultsU2F.json"));
+ var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationOptionsU2F.json"));
+ var o = AuthenticatorAttestationResponse.Parse(jsonPost);
+ await Assert.ThrowsAsync(() => o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), metadataService, null, CancellationToken.None));
}
[Fact]
@@ -541,7 +605,7 @@ public async Task TestPackedAttestationAsync()
var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationOptionsPacked.json"));
var o = AuthenticatorAttestationResponse.Parse(jsonPost);
options.PubKeyCredParams.Add(new PubKeyCredParam(COSE.Algorithm.RS1, PublicKeyCredentialType.PublicKey));
- await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, CancellationToken.None);
+ await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, null, CancellationToken.None);
var authData = o.AttestationObject.AuthData;
var acdBytes = authData.AttestedCredentialData.ToByteArray();
var acd = AttestedCredentialData.Parse(acdBytes);
@@ -555,7 +619,7 @@ public async Task TestNoneAttestationAsync()
var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationOptionsNone.json"));
var o = AuthenticatorAttestationResponse.Parse(jsonPost);
- await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, CancellationToken.None);
+ await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, null, CancellationToken.None);
}
[Fact]
@@ -564,7 +628,7 @@ public async Task TestTPMSHA256AttestationAsync()
var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationTPMSHA256Response.json"));
var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationTPMSHA256Options.json"));
var o = AuthenticatorAttestationResponse.Parse(jsonPost);
- await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, CancellationToken.None);
+ await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, null, CancellationToken.None);
}
[Fact]
@@ -574,7 +638,7 @@ public async Task TestTPMSHA1AttestationAsync()
var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationTPMSHA1Options.json"));
var o = AuthenticatorAttestationResponse.Parse(jsonPost);
options.PubKeyCredParams.Add(new PubKeyCredParam(COSE.Algorithm.RS1, PublicKeyCredentialType.PublicKey));
- await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, CancellationToken.None);
+ await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, null, CancellationToken.None);
}
[Fact]
@@ -583,7 +647,7 @@ public async Task TestAndroidKeyAttestationAsync()
var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationAndroidKeyResponse.json"));
var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationAndroidKeyOptions.json"));
var o = AuthenticatorAttestationResponse.Parse(jsonPost);
- await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, CancellationToken.None);
+ await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, null, CancellationToken.None);
}
[Fact]
@@ -592,7 +656,7 @@ public async Task TaskPackedAttestation512()
var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationResultsPacked512.json"));
var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationOptionsPacked512.json"));
var o = AuthenticatorAttestationResponse.Parse(jsonPost);
- await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, CancellationToken.None);
+ await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, null, CancellationToken.None);
}
[Fact]
@@ -601,7 +665,7 @@ public async Task TestTrustKeyAttestationAsync()
var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationResultTrustKeyT110.json"));
var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationOptionsTrustKeyT110.json"));
var o = AuthenticatorAttestationResponse.Parse(jsonPost);
- await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, CancellationToken.None);
+ await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, null, CancellationToken.None);
var authData = o.AttestationObject.AuthData;
var acdBytes = authData.AttestedCredentialData.ToByteArray();
var acd = AttestedCredentialData.Parse(acdBytes);
@@ -618,7 +682,7 @@ public async Task TestInvalidU2FAttestationAsync()
var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationResultsATKey.json"));
var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationOptionsATKey.json"));
var o = AuthenticatorAttestationResponse.Parse(jsonPost);
- await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, CancellationToken.None);
+ await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, null, CancellationToken.None);
var authData = o.AttestationObject.AuthData;
var acdBytes = authData.AttestedCredentialData.ToByteArray();
var acd = AttestedCredentialData.Parse(acdBytes);
@@ -643,7 +707,7 @@ public async Task TestMdsStatusReportsSuccessAsync()
mockMetadataService.Setup(m => m.ConformanceTesting()).Returns(false);
var o = AuthenticatorAttestationResponse.Parse(response);
- await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), mockMetadataService.Object, CancellationToken.None);
+ await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), mockMetadataService.Object, null, CancellationToken.None);
}
[Fact]
@@ -666,7 +730,7 @@ public async Task TestMdsStatusReportsUndesiredAsync()
var o = AuthenticatorAttestationResponse.Parse(response);
await Assert.ThrowsAsync(() =>
- o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), mockMetadataService.Object, CancellationToken.None));
+ o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), mockMetadataService.Object, null, CancellationToken.None));
}
[Fact]
@@ -689,7 +753,7 @@ public async Task TestMdsStatusReportsUndesiredFixedAsync()
mockMetadataService.Setup(m => m.ConformanceTesting()).Returns(false);
var o = AuthenticatorAttestationResponse.Parse(response);
- await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), mockMetadataService.Object, CancellationToken.None);
+ await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), mockMetadataService.Object, null, CancellationToken.None);
}
[Fact]
@@ -703,7 +767,7 @@ public async Task TestMdsStatusReportsNullAsync()
mockMetadataService.Setup(m => m.ConformanceTesting()).Returns(false);
var o = AuthenticatorAttestationResponse.Parse(response);
- await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), mockMetadataService.Object, CancellationToken.None);
+ await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), mockMetadataService.Object, null, CancellationToken.None);
}
//public void TestHasCorrentAAguid()
diff --git a/Test/Test.csproj b/Test/Test.csproj
index 5fb30e72..12faf566 100644
--- a/Test/Test.csproj
+++ b/Test/Test.csproj
@@ -17,6 +17,7 @@
+
diff --git a/Test/TestFiles/metadata/256K1 U2F Authenticator anonca.json b/Test/TestFiles/metadata/256K1 U2F Authenticator anonca.json
new file mode 100644
index 00000000..e844de94
--- /dev/null
+++ b/Test/TestFiles/metadata/256K1 U2F Authenticator anonca.json
@@ -0,0 +1,68 @@
+{
+ "legalHeader": "https://fidoalliance.org/metadata/metadata-statement-legal-header/",
+ "description": "Virtual Secp256K1 U2F authenticator",
+ "aaguid": "00000000-0000-0000-0000-000000000002",
+ "alternativeDescriptions": {
+ "en-GB": "Virtual Secp256K1 U2F authenticator"
+ },
+ "attestationCertificateKeyIdentifiers": [
+ "564df7c0f8c655b6a11f6c4d19f3bf41e2fd0179"
+ ],
+ "protocolFamily": "u2f",
+ "schema": 3,
+ "authenticatorVersion": 2,
+ "upv": [
+ {
+ "major": 1,
+ "minor": 0
+ },
+ {
+ "major": 1,
+ "minor": 1
+ },
+ {
+ "major": 1,
+ "minor": 2
+ }
+ ],
+ "authenticationAlgorithms": [
+ "secp256r1_ecdsa_sha256_raw"
+ ],
+ "publicKeyAlgAndEncodings": [
+ "ecc_x962_raw"
+ ],
+ "attestationTypes": [
+ "anonca"
+ ],
+ "userVerificationDetails": [
+ [
+ {
+ "userVerificationMethod": "none"
+ }
+ ],
+ [
+ {
+ "userVerificationMethod": "presence_internal"
+ }
+ ]
+ ],
+ "keyProtection": [
+ "hardware",
+ "secure_element"
+ ],
+ "matcherProtection": [
+ "on_chip"
+ ],
+ "cryptoStrength": 128,
+ "attachmentHint": [
+ "external",
+ "wired",
+ "nfc",
+ "wireless"
+ ],
+ "tcDisplay": [],
+ "attestationRootCertificates": [
+ "MIIFwDCCA6gCCQCNm1u56oRwXTANBgkqhkiG9w0BAQsFADCBoTEYMBYGA1UEAwwPRklETzIgVEVTVCBST09UMTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMQwwCgYDVQQLDANDV0cxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNWTESMBAGA1UEBwwJV2FrZWZpZWxkMB4XDTE4MDMxNjE0MzUyN1oXDTQ1MDgwMTE0MzUyN1owgaExGDAWBgNVBAMMD0ZJRE8yIFRFU1QgUk9PVDExMC8GCSqGSIb3DQEJARYiY29uZm9ybWFuY2UtdG9vbHNAZmlkb2FsbGlhbmNlLm9yZzEWMBQGA1UECgwNRklETyBBbGxpYW5jZTEMMAoGA1UECwwDQ1dHMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTVkxEjAQBgNVBAcMCVdha2VmaWVsZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL11U5yAIVLMrL3xS8u8ysMSdOkDeoTO+RcAy+uXXp6k4SC+jOy37gICEtYI+MKQV1EMeMMf3rM1ueZAO3iPFa0NEdi/oQ7npnGjBNI8wMzD8FfNe6rWtzkDaHpsZW///MwWDpGyJR+Xyjcq6U4vS9bS6zZ7jslw0Oczx4UsYgOsIUXSSBaGOrRbxJ/JC5gnDYEYvtNM+PDPczLNKAyhdvBZWNWHr7MZ0P5TeJQcXsAoShRX2Y8U8fRNJm7SeiFKDP0Nn/QKxOSt7zGP4xt9nMasE1q2ZTdar2+W13CRz37RI0ZWpq/+YquoEbZ7Uj7NmBTcqhb260nmDER2FpwwYwPSark92IZbamozB8d7OEI1jJgsrjJhKan0EmRaWVBpHT4xYKdEu7r09S0JhKyU+52WDmmVQTMpYLrm4Xl7hRxyPyBYkalrozsGmPs8vlhNq3VsVbyBSMSpEmUaeAa7LLE9/Vh0agJLVFHh1ehYKJpzHnmmBXUqx0Fz3afmDm1NX0sr3O/6xIx1VSTViT3KNxBYpVH1qjHATLzuxcWmm+75fcJMiPYPSMXVmRb3Q1l91AM4BBeWhlP3Fbc7gDy0r+s7m0sGS6PT2J2rGog2rUxnJ+zCM11M7DeO0XM2nny4uRYPPk9w2EXzfvtdvieYU/5RB4RDm5TGxHhGXVZUgac5AgMBAAEwDQYJKoZIhvcNAQELBQADggIBAFt2XGd3k5GpbO1EUm3u60zT1fE6u6pOscp156k5VnsHgaHRHdIAPNLeLNmR7y5OnrXbh13CrGwU1q84jjJXpv+v14xUCc5i01yopFTQFLr4A7NHp2nNYfNhhIVSFAgW43EflJflbLEelCJzxLlWb5BoDsZeeNmEQsXIM1mJ26R3r0dzsHBb0uy+8LNR1gdVqdjhC8BLy3gh4+BWuidyZNt07LveDsSFW5rcj5wRrSx9hXPIyVpjQSljNvY7MVTouqJzNAAQMsTKkXPkTXldCop9Qo9UPkHRRm0l7LLtdaOoXrct0Ymocf8zxf9bFNiw9f4WRYQM6sMhzt8+s/oDilo4QhcUgeJEiEPESi6ynYTV62SHA4eMunUJ5dlCaRnFiR9DTImFa5IRzie326/nW/SPCaKc/yrFIihMMjJoSAPhpTb/K6yHOUG8r+KiQut7NzqGV301pQ9u62dGL5Oi1VXmCFlE2ramZs15BNOUyAo2CBbRJg3jKcdu/8QC6ojjDvQ863+7LPtn74wJC5RpUJsS0GhQWgq5pAXO3wA61Uobxi6MkOpCC0zBWx/d4CqpS4j4hFgxWBTXX48ihPu+hIxIF/AxbqtPvqLMExW/xZITn6ArpWyQ9e4SUVr3n3F33ap1XdDyZ0vwFcm18JQAtsvXT6qCLrWOXnHUgfn/+Viu"
+ ],
+ "icon": "data:image/png;base64,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"
+}
\ No newline at end of file
diff --git a/Test/TestFiles/metadata/256K1 U2F Authenticator basic_full.json b/Test/TestFiles/metadata/256K1 U2F Authenticator basic_full.json
new file mode 100644
index 00000000..c09f42b7
--- /dev/null
+++ b/Test/TestFiles/metadata/256K1 U2F Authenticator basic_full.json
@@ -0,0 +1,68 @@
+{
+ "legalHeader": "https://fidoalliance.org/metadata/metadata-statement-legal-header/",
+ "description": "Virtual Secp256K1 U2F authenticator",
+ "aaguid": "00000000-0000-0000-0000-000000000001",
+ "alternativeDescriptions": {
+ "en-GB": "Virtual Secp256K1 U2F authenticator"
+ },
+ "attestationCertificateKeyIdentifiers": [
+ "564df7c0f8c655b6a11f6c4d19f3bf41e2fd0179"
+ ],
+ "protocolFamily": "u2f",
+ "schema": 3,
+ "authenticatorVersion": 2,
+ "upv": [
+ {
+ "major": 1,
+ "minor": 0
+ },
+ {
+ "major": 1,
+ "minor": 1
+ },
+ {
+ "major": 1,
+ "minor": 2
+ }
+ ],
+ "authenticationAlgorithms": [
+ "secp256r1_ecdsa_sha256_raw"
+ ],
+ "publicKeyAlgAndEncodings": [
+ "ecc_x962_raw"
+ ],
+ "attestationTypes": [
+ "basic_full"
+ ],
+ "userVerificationDetails": [
+ [
+ {
+ "userVerificationMethod": "none"
+ }
+ ],
+ [
+ {
+ "userVerificationMethod": "presence_internal"
+ }
+ ]
+ ],
+ "keyProtection": [
+ "hardware",
+ "secure_element"
+ ],
+ "matcherProtection": [
+ "on_chip"
+ ],
+ "cryptoStrength": 128,
+ "attachmentHint": [
+ "external",
+ "wired",
+ "nfc",
+ "wireless"
+ ],
+ "tcDisplay": [],
+ "attestationRootCertificates": [
+ "MIIFwDCCA6gCCQCNm1u56oRwXTANBgkqhkiG9w0BAQsFADCBoTEYMBYGA1UEAwwPRklETzIgVEVTVCBST09UMTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMQwwCgYDVQQLDANDV0cxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNWTESMBAGA1UEBwwJV2FrZWZpZWxkMB4XDTE4MDMxNjE0MzUyN1oXDTQ1MDgwMTE0MzUyN1owgaExGDAWBgNVBAMMD0ZJRE8yIFRFU1QgUk9PVDExMC8GCSqGSIb3DQEJARYiY29uZm9ybWFuY2UtdG9vbHNAZmlkb2FsbGlhbmNlLm9yZzEWMBQGA1UECgwNRklETyBBbGxpYW5jZTEMMAoGA1UECwwDQ1dHMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTVkxEjAQBgNVBAcMCVdha2VmaWVsZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL11U5yAIVLMrL3xS8u8ysMSdOkDeoTO+RcAy+uXXp6k4SC+jOy37gICEtYI+MKQV1EMeMMf3rM1ueZAO3iPFa0NEdi/oQ7npnGjBNI8wMzD8FfNe6rWtzkDaHpsZW///MwWDpGyJR+Xyjcq6U4vS9bS6zZ7jslw0Oczx4UsYgOsIUXSSBaGOrRbxJ/JC5gnDYEYvtNM+PDPczLNKAyhdvBZWNWHr7MZ0P5TeJQcXsAoShRX2Y8U8fRNJm7SeiFKDP0Nn/QKxOSt7zGP4xt9nMasE1q2ZTdar2+W13CRz37RI0ZWpq/+YquoEbZ7Uj7NmBTcqhb260nmDER2FpwwYwPSark92IZbamozB8d7OEI1jJgsrjJhKan0EmRaWVBpHT4xYKdEu7r09S0JhKyU+52WDmmVQTMpYLrm4Xl7hRxyPyBYkalrozsGmPs8vlhNq3VsVbyBSMSpEmUaeAa7LLE9/Vh0agJLVFHh1ehYKJpzHnmmBXUqx0Fz3afmDm1NX0sr3O/6xIx1VSTViT3KNxBYpVH1qjHATLzuxcWmm+75fcJMiPYPSMXVmRb3Q1l91AM4BBeWhlP3Fbc7gDy0r+s7m0sGS6PT2J2rGog2rUxnJ+zCM11M7DeO0XM2nny4uRYPPk9w2EXzfvtdvieYU/5RB4RDm5TGxHhGXVZUgac5AgMBAAEwDQYJKoZIhvcNAQELBQADggIBAFt2XGd3k5GpbO1EUm3u60zT1fE6u6pOscp156k5VnsHgaHRHdIAPNLeLNmR7y5OnrXbh13CrGwU1q84jjJXpv+v14xUCc5i01yopFTQFLr4A7NHp2nNYfNhhIVSFAgW43EflJflbLEelCJzxLlWb5BoDsZeeNmEQsXIM1mJ26R3r0dzsHBb0uy+8LNR1gdVqdjhC8BLy3gh4+BWuidyZNt07LveDsSFW5rcj5wRrSx9hXPIyVpjQSljNvY7MVTouqJzNAAQMsTKkXPkTXldCop9Qo9UPkHRRm0l7LLtdaOoXrct0Ymocf8zxf9bFNiw9f4WRYQM6sMhzt8+s/oDilo4QhcUgeJEiEPESi6ynYTV62SHA4eMunUJ5dlCaRnFiR9DTImFa5IRzie326/nW/SPCaKc/yrFIihMMjJoSAPhpTb/K6yHOUG8r+KiQut7NzqGV301pQ9u62dGL5Oi1VXmCFlE2ramZs15BNOUyAo2CBbRJg3jKcdu/8QC6ojjDvQ863+7LPtn74wJC5RpUJsS0GhQWgq5pAXO3wA61Uobxi6MkOpCC0zBWx/d4CqpS4j4hFgxWBTXX48ihPu+hIxIF/AxbqtPvqLMExW/xZITn6ArpWyQ9e4SUVr3n3F33ap1XdDyZ0vwFcm18JQAtsvXT6qCLrWOXnHUgfn/+Viu"
+ ],
+ "icon": "data:image/png;base64,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"
+}
\ No newline at end of file
diff --git a/Test/TestFiles/metadata/256K1 U2F Authenticator basic_surrogate.json b/Test/TestFiles/metadata/256K1 U2F Authenticator basic_surrogate.json
new file mode 100644
index 00000000..c2c55c06
--- /dev/null
+++ b/Test/TestFiles/metadata/256K1 U2F Authenticator basic_surrogate.json
@@ -0,0 +1,68 @@
+{
+ "legalHeader": "https://fidoalliance.org/metadata/metadata-statement-legal-header/",
+ "description": "Virtual Secp256K1 U2F authenticator",
+ "aaguid": "00000000-0000-0000-0000-000000000003",
+ "alternativeDescriptions": {
+ "en-GB": "Virtual Secp256K1 U2F authenticator"
+ },
+ "attestationCertificateKeyIdentifiers": [
+ "564df7c0f8c655b6a11f6c4d19f3bf41e2fd0179"
+ ],
+ "protocolFamily": "u2f",
+ "schema": 3,
+ "authenticatorVersion": 2,
+ "upv": [
+ {
+ "major": 1,
+ "minor": 0
+ },
+ {
+ "major": 1,
+ "minor": 1
+ },
+ {
+ "major": 1,
+ "minor": 2
+ }
+ ],
+ "authenticationAlgorithms": [
+ "secp256r1_ecdsa_sha256_raw"
+ ],
+ "publicKeyAlgAndEncodings": [
+ "ecc_x962_raw"
+ ],
+ "attestationTypes": [
+ "basic_surrogate"
+ ],
+ "userVerificationDetails": [
+ [
+ {
+ "userVerificationMethod": "none"
+ }
+ ],
+ [
+ {
+ "userVerificationMethod": "presence_internal"
+ }
+ ]
+ ],
+ "keyProtection": [
+ "hardware",
+ "secure_element"
+ ],
+ "matcherProtection": [
+ "on_chip"
+ ],
+ "cryptoStrength": 128,
+ "attachmentHint": [
+ "external",
+ "wired",
+ "nfc",
+ "wireless"
+ ],
+ "tcDisplay": [],
+ "attestationRootCertificates": [
+ "MIIFwDCCA6gCCQCNm1u56oRwXTANBgkqhkiG9w0BAQsFADCBoTEYMBYGA1UEAwwPRklETzIgVEVTVCBST09UMTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMQwwCgYDVQQLDANDV0cxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNWTESMBAGA1UEBwwJV2FrZWZpZWxkMB4XDTE4MDMxNjE0MzUyN1oXDTQ1MDgwMTE0MzUyN1owgaExGDAWBgNVBAMMD0ZJRE8yIFRFU1QgUk9PVDExMC8GCSqGSIb3DQEJARYiY29uZm9ybWFuY2UtdG9vbHNAZmlkb2FsbGlhbmNlLm9yZzEWMBQGA1UECgwNRklETyBBbGxpYW5jZTEMMAoGA1UECwwDQ1dHMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTVkxEjAQBgNVBAcMCVdha2VmaWVsZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL11U5yAIVLMrL3xS8u8ysMSdOkDeoTO+RcAy+uXXp6k4SC+jOy37gICEtYI+MKQV1EMeMMf3rM1ueZAO3iPFa0NEdi/oQ7npnGjBNI8wMzD8FfNe6rWtzkDaHpsZW///MwWDpGyJR+Xyjcq6U4vS9bS6zZ7jslw0Oczx4UsYgOsIUXSSBaGOrRbxJ/JC5gnDYEYvtNM+PDPczLNKAyhdvBZWNWHr7MZ0P5TeJQcXsAoShRX2Y8U8fRNJm7SeiFKDP0Nn/QKxOSt7zGP4xt9nMasE1q2ZTdar2+W13CRz37RI0ZWpq/+YquoEbZ7Uj7NmBTcqhb260nmDER2FpwwYwPSark92IZbamozB8d7OEI1jJgsrjJhKan0EmRaWVBpHT4xYKdEu7r09S0JhKyU+52WDmmVQTMpYLrm4Xl7hRxyPyBYkalrozsGmPs8vlhNq3VsVbyBSMSpEmUaeAa7LLE9/Vh0agJLVFHh1ehYKJpzHnmmBXUqx0Fz3afmDm1NX0sr3O/6xIx1VSTViT3KNxBYpVH1qjHATLzuxcWmm+75fcJMiPYPSMXVmRb3Q1l91AM4BBeWhlP3Fbc7gDy0r+s7m0sGS6PT2J2rGog2rUxnJ+zCM11M7DeO0XM2nny4uRYPPk9w2EXzfvtdvieYU/5RB4RDm5TGxHhGXVZUgac5AgMBAAEwDQYJKoZIhvcNAQELBQADggIBAFt2XGd3k5GpbO1EUm3u60zT1fE6u6pOscp156k5VnsHgaHRHdIAPNLeLNmR7y5OnrXbh13CrGwU1q84jjJXpv+v14xUCc5i01yopFTQFLr4A7NHp2nNYfNhhIVSFAgW43EflJflbLEelCJzxLlWb5BoDsZeeNmEQsXIM1mJ26R3r0dzsHBb0uy+8LNR1gdVqdjhC8BLy3gh4+BWuidyZNt07LveDsSFW5rcj5wRrSx9hXPIyVpjQSljNvY7MVTouqJzNAAQMsTKkXPkTXldCop9Qo9UPkHRRm0l7LLtdaOoXrct0Ymocf8zxf9bFNiw9f4WRYQM6sMhzt8+s/oDilo4QhcUgeJEiEPESi6ynYTV62SHA4eMunUJ5dlCaRnFiR9DTImFa5IRzie326/nW/SPCaKc/yrFIihMMjJoSAPhpTb/K6yHOUG8r+KiQut7NzqGV301pQ9u62dGL5Oi1VXmCFlE2ramZs15BNOUyAo2CBbRJg3jKcdu/8QC6ojjDvQ863+7LPtn74wJC5RpUJsS0GhQWgq5pAXO3wA61Uobxi6MkOpCC0zBWx/d4CqpS4j4hFgxWBTXX48ihPu+hIxIF/AxbqtPvqLMExW/xZITn6ArpWyQ9e4SUVr3n3F33ap1XdDyZ0vwFcm18JQAtsvXT6qCLrWOXnHUgfn/+Viu"
+ ],
+ "icon": "data:image/png;base64,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"
+}
\ No newline at end of file
diff --git a/Test/TestFiles/metadata/Secp256R1 Packed Authenticator.json b/Test/TestFiles/metadata/Secp256R1 Packed Authenticator.json
new file mode 100644
index 00000000..db5b0b68
--- /dev/null
+++ b/Test/TestFiles/metadata/Secp256R1 Packed Authenticator.json
@@ -0,0 +1,128 @@
+{
+ "legalHeader": "https://fidoalliance.org/metadata/metadata-statement-legal-header/",
+ "description": "Virtual Packed authenticator",
+ "aaguid": "00000000-0000-0000-0000-000000000004",
+ "alternativeDescriptions": {
+ "en-GB": "Virtual Packed authenticator"
+ },
+ "protocolFamily": "fido2",
+ "authenticatorVersion": 2,
+ "upv": [
+ {
+ "major": 1,
+ "minor": 0
+ }
+ ],
+ "authenticationAlgorithms": [
+ "secp256r1_ecdsa_sha256_raw"
+ ],
+ "publicKeyAlgAndEncodings": [
+ "cose"
+ ],
+ "attestationTypes": [
+ "basic_full",
+ "basic_surrogate"
+ ],
+ "schema": 3,
+ "userVerificationDetails": [
+ [
+ {
+ "userVerificationMethod": "none"
+ }
+ ],
+ [
+ {
+ "userVerificationMethod": "presence_internal"
+ }
+ ],
+ [
+ {
+ "userVerificationMethod": "passcode_external",
+ "caDesc": {
+ "base": 10,
+ "minLength": 4
+ }
+ }
+ ],
+ [
+ {
+ "userVerificationMethod": "passcode_external",
+ "caDesc": {
+ "base": 10,
+ "minLength": 4
+ }
+ },
+ {
+ "userVerificationMethod": "presence_internal"
+ }
+ ]
+ ],
+ "keyProtection": [
+ "hardware",
+ "secure_element"
+ ],
+ "matcherProtection": [
+ "on_chip"
+ ],
+ "cryptoStrength": 128,
+ "attachmentHint": [
+ "external",
+ "wired",
+ "wireless",
+ "nfc"
+ ],
+ "tcDisplay": [],
+ "attestationRootCertificates": [
+ "MIIFwDCCA6gCCQCNm1u56oRwXTANBgkqhkiG9w0BAQsFADCBoTEYMBYGA1UEAwwPRklETzIgVEVTVCBST09UMTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMQwwCgYDVQQLDANDV0cxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNWTESMBAGA1UEBwwJV2FrZWZpZWxkMB4XDTE4MDMxNjE0MzUyN1oXDTQ1MDgwMTE0MzUyN1owgaExGDAWBgNVBAMMD0ZJRE8yIFRFU1QgUk9PVDExMC8GCSqGSIb3DQEJARYiY29uZm9ybWFuY2UtdG9vbHNAZmlkb2FsbGlhbmNlLm9yZzEWMBQGA1UECgwNRklETyBBbGxpYW5jZTEMMAoGA1UECwwDQ1dHMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTVkxEjAQBgNVBAcMCVdha2VmaWVsZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL11U5yAIVLMrL3xS8u8ysMSdOkDeoTO+RcAy+uXXp6k4SC+jOy37gICEtYI+MKQV1EMeMMf3rM1ueZAO3iPFa0NEdi/oQ7npnGjBNI8wMzD8FfNe6rWtzkDaHpsZW///MwWDpGyJR+Xyjcq6U4vS9bS6zZ7jslw0Oczx4UsYgOsIUXSSBaGOrRbxJ/JC5gnDYEYvtNM+PDPczLNKAyhdvBZWNWHr7MZ0P5TeJQcXsAoShRX2Y8U8fRNJm7SeiFKDP0Nn/QKxOSt7zGP4xt9nMasE1q2ZTdar2+W13CRz37RI0ZWpq/+YquoEbZ7Uj7NmBTcqhb260nmDER2FpwwYwPSark92IZbamozB8d7OEI1jJgsrjJhKan0EmRaWVBpHT4xYKdEu7r09S0JhKyU+52WDmmVQTMpYLrm4Xl7hRxyPyBYkalrozsGmPs8vlhNq3VsVbyBSMSpEmUaeAa7LLE9/Vh0agJLVFHh1ehYKJpzHnmmBXUqx0Fz3afmDm1NX0sr3O/6xIx1VSTViT3KNxBYpVH1qjHATLzuxcWmm+75fcJMiPYPSMXVmRb3Q1l91AM4BBeWhlP3Fbc7gDy0r+s7m0sGS6PT2J2rGog2rUxnJ+zCM11M7DeO0XM2nny4uRYPPk9w2EXzfvtdvieYU/5RB4RDm5TGxHhGXVZUgac5AgMBAAEwDQYJKoZIhvcNAQELBQADggIBAFt2XGd3k5GpbO1EUm3u60zT1fE6u6pOscp156k5VnsHgaHRHdIAPNLeLNmR7y5OnrXbh13CrGwU1q84jjJXpv+v14xUCc5i01yopFTQFLr4A7NHp2nNYfNhhIVSFAgW43EflJflbLEelCJzxLlWb5BoDsZeeNmEQsXIM1mJ26R3r0dzsHBb0uy+8LNR1gdVqdjhC8BLy3gh4+BWuidyZNt07LveDsSFW5rcj5wRrSx9hXPIyVpjQSljNvY7MVTouqJzNAAQMsTKkXPkTXldCop9Qo9UPkHRRm0l7LLtdaOoXrct0Ymocf8zxf9bFNiw9f4WRYQM6sMhzt8+s/oDilo4QhcUgeJEiEPESi6ynYTV62SHA4eMunUJ5dlCaRnFiR9DTImFa5IRzie326/nW/SPCaKc/yrFIihMMjJoSAPhpTb/K6yHOUG8r+KiQut7NzqGV301pQ9u62dGL5Oi1VXmCFlE2ramZs15BNOUyAo2CBbRJg3jKcdu/8QC6ojjDvQ863+7LPtn74wJC5RpUJsS0GhQWgq5pAXO3wA61Uobxi6MkOpCC0zBWx/d4CqpS4j4hFgxWBTXX48ihPu+hIxIF/AxbqtPvqLMExW/xZITn6ArpWyQ9e4SUVr3n3F33ap1XdDyZ0vwFcm18JQAtsvXT6qCLrWOXnHUgfn/+Viu"
+ ],
+ "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAACXBIWXMAAC4jAAAuIwF4pT92AAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAAthJREFUeNrslt9Lk1EYx7/vNte0vXOk7yS7qyWBYvnjIktGU0vDCwktV4KXpv3wB/4BBiIa/QC1wjkVUxNsUuuuzd1k6iBLCxIFzcDXOTZwY8r2sr1rp4uXZuoggryJfS8eeL6c53w45+E5HIoQgoOUCAesGCAGiAEAyX6LZdn19XWGYdRq9T8gkN1qa20VDlVZcZUQYpuZKS0tHTca9ywz6Hurq6s/zs6SP2kXwGI2AzjKqHQ63ft3k4SQpoYGAMWFRXvKLmoLAAwODPwdoLdHD2BkaOh3843J5HK59pTV1dwE8Gp8fP+OS4tL5rfmH6GQkO70oLuzc2jwuSop2dBrOCynk5KO9PX3Z2ZkMCkpqyvfGIYBcL+9w2qdKCoqCgQCAHieF2ofP3xkMr1W0IraulptQYHP7wNF7e2BNl8DIO34CQANd+u7u7oASEABqKupJYRU6a4DoGXxqaoUpZwWA9aJCUJI4QUtgFPqkwnSQwD69ProVxQMBtvb2iiKetDRwfN8KBTiOO7Zk6cA+noNLMsCyMo8zfn9HMflnMkCsLS4OD01DUB39RohxOl0yhMS4iiR3W6PbLszB3FxcbRCQQhRJCZKJBKxWCyTyeRyGoBUKv0y/xmATlcpi4+XyWQajQaAz+ebmpwEUF5RDkClUhVqC3gSnp+biz4HnN8PwO/3R5xAgMvNzk5mkkWUCMDq6nfBdzg2BDCtUABwOl2/fIdAig4IBoORKIjneQVNb3m3ii+XiEHp+wzpGelut/ul0QggEAiUXSm7def2vZaWtLS0hYWvH+Y+5Z/Ny8nNjf5USCSSSIw44XDY4dhQKpXDw8NiiqpvbBwdeVF1owoAu7aWmnrM0KPf3t6+VFLc1Nx8Pu/c6NiYSCSKPsket2d5ednj8UQcr9drX7e73ZtCyrJrVqs1HA4TQpZXVrxer+C7N90Wi8Vms+0fCyr2q4gBYoD/APBzAI6VNqGQPUqnAAAAAElFTkSuQmCC",
+ "supportedExtensions": [
+ {
+ "id": "hmac-secret",
+ "fail_if_unknown": false
+ },
+ {
+ "id": "credProtect",
+ "fail_if_unknown": false
+ }
+ ],
+ "authenticatorGetInfo": {
+ "versions": [
+ "U2F_V2",
+ "FIDO_2_0"
+ ],
+ "extensions": [
+ "credProtect",
+ "hmac-secret"
+ ],
+ "aaguid": "326adcf00cef46d0939298d6c4a84a72",
+ "options": {
+ "plat": false,
+ "rk": true,
+ "clientPin": true,
+ "up": true,
+ "uv": true,
+ "uvToken": false,
+ "config": false
+ },
+ "maxMsgSize": 1200,
+ "pinUvAuthProtocols": [
+ 1
+ ],
+ "maxCredentialCountInList": 16,
+ "maxCredentialIdLength": 128,
+ "transports": [
+ "usb",
+ "nfc"
+ ],
+ "algorithms": [
+ {
+ "type": "public-key",
+ "alg": -7
+ }
+ ],
+ "maxAuthenticatorConfigLength": 1024,
+ "defaultCredProtect": 2,
+ "firmwareVersion": 5
+ }
+}
\ No newline at end of file
diff --git a/Test/TestMetadataService.cs b/Test/TestMetadataService.cs
new file mode 100644
index 00000000..760656cc
--- /dev/null
+++ b/Test/TestMetadataService.cs
@@ -0,0 +1,21 @@
+using Fido2NetLib;
+
+namespace Test
+{
+ public class TestMetadataService : ConformanceMetadataService
+ {
+ public TestMetadataService(IEnumerable repositories) : base(repositories)
+ {
+ }
+
+ public void ChangeEntryGuid(Guid oldGuid, Guid newGuid)
+ {
+ if (!_entries.ContainsKey(oldGuid))
+ return;
+
+ _entries.Remove(oldGuid, out var entry);
+ entry.AaGuid = newGuid;
+ _entries.TryAdd(newGuid, entry);
+ }
+ }
+}