Skip to content

Commit f539c2f

Browse files
author
syh-jeffk
authored
Corrected attestation validation based on authenticator MDS status reports. (#290)
* Corrected attestation validation based on authenticator MDS status reports. * Corrected merge of master branch into mds-verification.
1 parent 6a8fb0b commit f539c2f

File tree

7 files changed

+132
-21
lines changed

7 files changed

+132
-21
lines changed

Src/Fido2.Models/Fido2Configuration.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,18 @@ public HashSet<string> FullyQualifiedOrigins
9292
/// </summary>
9393
public string MDSCacheDirPath { get; set; }
9494

95+
/// <summary>
96+
/// List of metadata statuses for an authenticator that should cause attestations to be rejected.
97+
/// </summary>
98+
public AuthenticatorStatus[] UndesiredAuthenticatorMetadataStatuses { get; set; } = new AuthenticatorStatus[]
99+
{
100+
AuthenticatorStatus.ATTESTATION_KEY_COMPROMISE,
101+
AuthenticatorStatus.USER_VERIFICATION_BYPASS,
102+
AuthenticatorStatus.USER_KEY_REMOTE_COMPROMISE,
103+
AuthenticatorStatus.USER_KEY_PHYSICAL_COMPROMISE,
104+
AuthenticatorStatus.REVOKED
105+
};
106+
95107
/// <summary>
96108
/// Create the configuration for Fido2
97109
/// </summary>

Src/Fido2.Models/Metadata/MetadataBLOBPayloadEntry.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.ComponentModel.DataAnnotations;
2+
using System.Linq;
23
using System.Text.Json.Serialization;
34

45
namespace Fido2NetLib
@@ -78,5 +79,14 @@ public sealed class MetadataBLOBPayloadEntry
7879
/// </remarks>
7980
[JsonPropertyName("rogueListHash")]
8081
public string RogueListHash { get; set; }
82+
83+
/// <summary>
84+
/// Gets the latest, most current status report for the authenticator.
85+
/// </summary>
86+
/// <returns>Latest status report, or null if there are no reports.</returns>
87+
public StatusReport GetLatestStatusReport()
88+
{
89+
return StatusReports.LastOrDefault();
90+
}
8191
}
8292
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System;
2+
using System.Runtime.Serialization;
3+
4+
namespace Fido2NetLib
5+
{
6+
/// <summary>
7+
/// Exception thrown when a new attestation comes from an authenticator with a current reported security issue.
8+
/// </summary>
9+
[Serializable]
10+
public class UndesiredMetdatataStatusFido2VerificationException : Fido2VerificationException
11+
{
12+
public UndesiredMetdatataStatusFido2VerificationException(StatusReport statusReport) : base($"Authenticator found with undesirable status. Was {statusReport.Status}")
13+
{
14+
StatusReport = statusReport;
15+
}
16+
17+
protected UndesiredMetdatataStatusFido2VerificationException(SerializationInfo info, StreamingContext context) : base(info, context) { }
18+
19+
/// <summary>
20+
/// Status report from the authenticator that caused the attestation to be rejected.
21+
/// </summary>
22+
public StatusReport StatusReport { get; }
23+
}
24+
}

Src/Fido2/AuthenticatorAttestationResponse.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -211,12 +211,10 @@ static bool ContainsAttestationType(MetadataBLOBPayloadEntry entry, MetadataAtte
211211
}
212212

213213
// Check status resports for authenticator with undesirable status
214-
foreach (var report in metadataEntry?.StatusReports ?? Array.Empty<StatusReport>())
214+
var latestStatusReport = metadataEntry?.GetLatestStatusReport();
215+
if (latestStatusReport != null && config.UndesiredAuthenticatorMetadataStatuses.Contains(latestStatusReport.Status))
215216
{
216-
if (report.Status.IsUndesired())
217-
{
218-
throw new Fido2VerificationException($"Authenticator found with undesirable status. Was {report.Status}");
219-
}
217+
throw new UndesiredMetdatataStatusFido2VerificationException(latestStatusReport);
220218
}
221219

222220
// 16. Assess the attestation trustworthiness using the outputs of the verification procedure in step 14, as follows:

Src/Fido2/Extensions/AuthenticatorStatusExtensions.cs

Lines changed: 0 additions & 15 deletions
This file was deleted.

Test/Fido2Tests.cs

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
using Microsoft.Extensions.DependencyInjection;
2222
using Microsoft.Extensions.Internal;
2323
using Microsoft.Extensions.Logging;
24-
24+
using Moq;
2525
using NSec.Cryptography;
2626

2727
using Xunit;
@@ -630,6 +630,87 @@ public async Task TestInvalidU2FAttestationASync()
630630
Assert.True(acd.ToByteArray().SequenceEqual(acdBytes));
631631
}
632632

633+
[Fact]
634+
public async Task TestMdsStatusReportsSuccessAsync()
635+
{
636+
var options = JsonSerializer.Deserialize<CredentialCreateOptions>(await File.ReadAllTextAsync("./attestationNoneOptions.json"));
637+
var response = JsonSerializer.Deserialize<AuthenticatorAttestationRawResponse>(await File.ReadAllTextAsync("./attestationNoneResponse.json"));
638+
639+
var mockMetadataService = new Mock<IMetadataService>(MockBehavior.Strict);
640+
mockMetadataService.Setup(m => m.GetEntryAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
641+
.ReturnsAsync(new MetadataBLOBPayloadEntry()
642+
{
643+
StatusReports = new StatusReport[]
644+
{
645+
new StatusReport() { Status = AuthenticatorStatus.FIDO_CERTIFIED }
646+
}
647+
});
648+
mockMetadataService.Setup(m => m.ConformanceTesting()).Returns(false);
649+
650+
var o = AuthenticatorAttestationResponse.Parse(response);
651+
await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), mockMetadataService.Object, null, CancellationToken.None);
652+
}
653+
654+
[Fact]
655+
public async Task TestMdsStatusReportsUndesiredAsync()
656+
{
657+
var options = JsonSerializer.Deserialize<CredentialCreateOptions>(await File.ReadAllTextAsync("./attestationNoneOptions.json"));
658+
var response = JsonSerializer.Deserialize<AuthenticatorAttestationRawResponse>(await File.ReadAllTextAsync("./attestationNoneResponse.json"));
659+
660+
var mockMetadataService = new Mock<IMetadataService>(MockBehavior.Strict);
661+
mockMetadataService.Setup(m => m.GetEntryAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
662+
.ReturnsAsync(new MetadataBLOBPayloadEntry()
663+
{
664+
StatusReports = new StatusReport[]
665+
{
666+
new StatusReport() { Status = AuthenticatorStatus.FIDO_CERTIFIED },
667+
new StatusReport() { Status = AuthenticatorStatus.REVOKED }
668+
}
669+
});
670+
mockMetadataService.Setup(m => m.ConformanceTesting()).Returns(false);
671+
672+
var o = AuthenticatorAttestationResponse.Parse(response);
673+
await Assert.ThrowsAsync<UndesiredMetdatataStatusFido2VerificationException>(() =>
674+
o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), mockMetadataService.Object, null, CancellationToken.None));
675+
}
676+
677+
[Fact]
678+
public async Task TestMdsStatusReportsUndesiredFixedAsync()
679+
{
680+
var options = JsonSerializer.Deserialize<CredentialCreateOptions>(await File.ReadAllTextAsync("./attestationNoneOptions.json"));
681+
var response = JsonSerializer.Deserialize<AuthenticatorAttestationRawResponse>(await File.ReadAllTextAsync("./attestationNoneResponse.json"));
682+
683+
var mockMetadataService = new Mock<IMetadataService>(MockBehavior.Strict);
684+
mockMetadataService.Setup(m => m.GetEntryAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
685+
.ReturnsAsync(new MetadataBLOBPayloadEntry()
686+
{
687+
StatusReports = new StatusReport[]
688+
{
689+
new StatusReport() { Status = AuthenticatorStatus.FIDO_CERTIFIED },
690+
new StatusReport() { Status = AuthenticatorStatus.REVOKED },
691+
new StatusReport() { Status = AuthenticatorStatus.UPDATE_AVAILABLE }
692+
}
693+
});
694+
mockMetadataService.Setup(m => m.ConformanceTesting()).Returns(false);
695+
696+
var o = AuthenticatorAttestationResponse.Parse(response);
697+
await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), mockMetadataService.Object, null, CancellationToken.None);
698+
}
699+
700+
[Fact]
701+
public async Task TestMdsStatusReportsNullAsync()
702+
{
703+
var options = JsonSerializer.Deserialize<CredentialCreateOptions>(await File.ReadAllTextAsync("./attestationNoneOptions.json"));
704+
var response = JsonSerializer.Deserialize<AuthenticatorAttestationRawResponse>(await File.ReadAllTextAsync("./attestationNoneResponse.json"));
705+
706+
var mockMetadataService = new Mock<IMetadataService>(MockBehavior.Strict);
707+
mockMetadataService.Setup(m => m.GetEntryAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>())).ReturnsAsync((MetadataBLOBPayloadEntry)null);
708+
mockMetadataService.Setup(m => m.ConformanceTesting()).Returns(false);
709+
710+
var o = AuthenticatorAttestationResponse.Parse(response);
711+
await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), mockMetadataService.Object, null, CancellationToken.None);
712+
}
713+
633714
//public void TestHasCorrentAAguid()
634715
//{
635716
// var expectedAaguid = new Uint8Array([

Test/Test.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2727
</PackageReference>
2828
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
29+
<PackageReference Include="Moq" Version="4.17.2" />
2930
<PackageReference Include="ReportGenerator" Version="5.0.0" />
3031
<PackageReference Include="xunit" Version="2.4.1" />
3132
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">

0 commit comments

Comments
 (0)