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

Support MDS3 #238

Merged
merged 4 commits into from Sep 13, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 0 additions & 15 deletions Demo/Controller.cs
Expand Up @@ -76,14 +76,7 @@ private string FormatException(Exception e)
var exts = new AuthenticationExtensionsClientInputs()
{
Extensions = true,
UserVerificationIndex = true,
Location = true,
UserVerificationMethod = true,
BiometricAuthenticatorPerformanceBounds = new AuthenticatorBiometricPerfBounds
{
FAR = float.MaxValue,
FRR = float.MaxValue
}
};

var options = _fido2.RequestNewCredential(user, existingKeys, authenticatorSelection, attType.ToEnum<AttestationConveyancePreference>(), exts);
Expand Down Expand Up @@ -165,14 +158,6 @@ public ActionResult AssertionOptionsPost([FromForm] string username, [FromForm]

var exts = new AuthenticationExtensionsClientInputs()
{
SimpleTransactionAuthorization = "FIDO",
GenericTransactionAuthorization = new TxAuthGenericArg
{
ContentType = "text/plain",
Content = new byte[] { 0x46, 0x49, 0x44, 0x4F }
},
UserVerificationIndex = true,
Location = true,
UserVerificationMethod = true
};

Expand Down
10 changes: 1 addition & 9 deletions Demo/Startup.cs
Expand Up @@ -47,21 +47,13 @@ public void ConfigureServices(IServiceCollection services)
options.ServerName = "FIDO2 Test";
options.Origin = Configuration["fido2:origin"];
options.TimestampDriftTolerance = Configuration.GetValue<int>("fido2:timestampDriftTolerance");
options.MDSAccessKey = Configuration["fido2:MDSAccessKey"];
options.MDSCacheDirPath = Configuration["fido2:MDSCacheDirPath"];
})
.AddCachedMetadataService(config =>
{
//They'll be used in a "first match wins" way in the order registered

if (!string.IsNullOrWhiteSpace(Configuration["fido2:MDSAccessKey"]))
{
config.AddFidoMetadataRepository(Configuration["fido2:MDSAccessKey"]);
}
config.AddStaticMetadataRepository();
config.AddFidoMetadataRepository();
});


}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
Expand Down
8 changes: 0 additions & 8 deletions Demo/TestController.cs
Expand Up @@ -138,14 +138,6 @@ public IActionResult AssertionOptionsTest([FromBody] TEST_AssertionClientParams
var exts = new AuthenticationExtensionsClientInputs
{
AppID = _origin,
SimpleTransactionAuthorization = "FIDO",
GenericTransactionAuthorization = new TxAuthGenericArg
{
ContentType = "text/plain",
Content = new byte[] { 0x46, 0x49, 0x44, 0x4F }
},
UserVerificationIndex = true,
Location = true,
UserVerificationMethod = true
};
if (null != assertionClientParams.Extensions && null != assertionClientParams.Extensions.Example)
Expand Down
1 change: 0 additions & 1 deletion Demo/appsettings.json
Expand Up @@ -3,7 +3,6 @@
"serverDomain": "localhost",
"origin": "https://localhost:44329",
"timestampDriftTolerance": 300000,
"MDSAccessKey": null,
},
"Logging": {
"IncludeScopes": false,
Expand Down
7 changes: 3 additions & 4 deletions README.md
Expand Up @@ -14,7 +14,7 @@ The quickest way to get started with FIDO2 and WebAuthn is with the [Passwordles
### Purpose

<img align="right" width="100px" src="https://dotnetfoundation.org/img/logo_big.svg" />
Our purpose is to enable passwordless sign in for all .net apps (asp, core, native).
Our purpose is to enable passwordless sign in for all .NET apps (asp, core, native).

To provide a developer friendly and well tested [.NET](https://dotnet.microsoft.com/) [FIDO2 Server](https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-server-v2.0-rd-20180702.html) / [WebAuthn relying party](https://www.w3.org/TR/webauthn/#relying-party) library for the easy validation of [registration](https://www.w3.org/TR/webauthn/#usecase-registration) ([attestation](https://www.w3.org/TR/webauthn/#attestation)) and [authentication](https://www.w3.org/TR/webauthn/#usecase-authentication) ([assertion](https://www.w3.org/TR/webauthn/#authentication-assertion)) of [FIDO2](https://fidoalliance.org/fido2/) / [WebAuthn](https://www.w3.org/TR/webauthn/) credentials, in order to increase the adoption of the technology, ultimately defeating phishing attacks.

Expand Down Expand Up @@ -56,7 +56,7 @@ Read more:
- ✅ [Face ID and Touch ID for the Web](https://webkit.org/blog/11312/meet-face-id-and-touch-id-for-the-web/) (aka "Apple Hello")
- ✅ All currently referenced cryptographic algorithms for FIDO2 Server ([spec](https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-server-v2.0-rd-20180702.html#other))
- ✅ All current attestation formats: "packed", "tpm", "android-key", "android-safetynet", "fido-u2f", "apple", and "none" ([spec](https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-server-v2.0-rd-20180702.html))
- ✅ FIDO2 Server attestation validation via FIDO Metadata Service ([spec](https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-server-v2.0-rd-20180702.html))
- ✅ FIDO2 Server attestation validation via FIDO Metadata Service V3 ([spec](https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html))
- ✅ WebAuthn extensions ([spec](https://www.w3.org/TR/webauthn/#extensions))
- ✅ Examples & demos
- ✅ Intellisense documentation
Expand All @@ -67,7 +67,6 @@ Read more:

*Only some options are mentioned here, see the [Configuration](https://github.com/abergs/fido2-net-lib/blob/master/fido2-net-lib/Fido2NetLib.cs) class for all options*

* `fido2:MDSAccessKey` - App Secret / environment variable that holds the FIDO2 MDS AccessKey. *Required when using the default [MetadataService provider](https://fidoalliance.org/mds/).*
* `fido2:MDSCacheDirPath` - App Secret / environment variable that sets the cache path for the MDS. Defaults to "current user's temporary folder"/fido2mdscache. *Optional when using the default [MetadataService provider](https://fidoalliance.org/mds/).*

## Examples
Expand Down Expand Up @@ -243,4 +242,4 @@ Support this project with your organization. Your logo will show up here with a

### .NET Foundation

This project is supported by the [.NET Foundation](https://dotnetfoundation.org).
This project is supported by the [.NET Foundation](https://dotnetfoundation.org).
56 changes: 27 additions & 29 deletions Src/Fido2.AspNet/DistributedCacheMetadataService.cs
Expand Up @@ -18,7 +18,7 @@ public class DistributedCacheMetadataService : IMetadataService
protected readonly TimeSpan _defaultCacheInterval = TimeSpan.FromHours(25);

protected readonly ConcurrentDictionary<Guid, MetadataStatement> _metadataStatements;
protected readonly ConcurrentDictionary<Guid, MetadataTOCPayloadEntry> _entries;
protected readonly ConcurrentDictionary<Guid, MetadataBLOBPayloadEntry> _entries;

protected const string CACHE_PREFIX = "DistributedCacheMetadataService";

Expand All @@ -30,7 +30,7 @@ public class DistributedCacheMetadataService : IMetadataService
_repositories = repositories.ToList();
_cache = cache;
_metadataStatements = new ConcurrentDictionary<Guid, MetadataStatement>();
_entries = new ConcurrentDictionary<Guid, MetadataTOCPayloadEntry>();
_entries = new ConcurrentDictionary<Guid, MetadataBLOBPayloadEntry>();
_log = log;
}

Expand All @@ -39,7 +39,7 @@ public virtual bool ConformanceTesting()
return _repositories.First().GetType() == typeof(ConformanceMetadataRepository);
}

public virtual MetadataTOCPayloadEntry GetEntry(Guid aaguid)
public virtual MetadataBLOBPayloadEntry GetEntry(Guid aaguid)
{
if (!IsInitialized())
throw new InvalidOperationException("MetadataService must be initialized");
Expand Down Expand Up @@ -73,8 +73,8 @@ protected virtual string GetEntryCacheKey(IMetadataRepository repository, Guid a

protected virtual async Task LoadTocEntryStatement(
IMetadataRepository repository,
MetadataTOCPayload toc,
MetadataTOCPayloadEntry entry,
MetadataBLOBPayload blob,
MetadataBLOBPayloadEntry entry,
DateTime? cacheUntil = null)
{
if (entry.AaGuid != null && !_entries.ContainsKey(Guid.Parse(entry.AaGuid)))
Expand All @@ -100,17 +100,15 @@ protected virtual string GetEntryCacheKey(IMetadataRepository repository, Guid a

try
{
var statement = await repository.GetMetadataStatement(toc, entry);

if (!string.IsNullOrWhiteSpace(statement.AaGuid))
if (!string.IsNullOrWhiteSpace(entry.AaGuid))
{
var statementJson = JsonConvert.SerializeObject(statement, Formatting.Indented);
var statementJson = JsonConvert.SerializeObject(entry.MetadataStatement, Formatting.Indented);

_log?.LogDebug("{0}:{1}\n{2}", statement.AaGuid, statement.Description, statementJson);
_log?.LogDebug("{0}:{1}\n{2}", entry.AaGuid, entry.MetadataStatement.Description, statementJson);

var aaGuid = Guid.Parse(statement.AaGuid);
var aaGuid = Guid.Parse(entry.AaGuid);

_metadataStatements.TryAdd(aaGuid, statement);
_metadataStatements.TryAdd(aaGuid, entry.MetadataStatement);
_entries.TryAdd(aaGuid, entry);

if (cacheUntil.HasValue)
Expand All @@ -131,11 +129,11 @@ protected virtual string GetEntryCacheKey(IMetadataRepository repository, Guid a
}
}

private DateTime? GetCacheUntilTime(MetadataTOCPayload toc)
private DateTime? GetCacheUntilTime(MetadataBLOBPayload blob)
{
if (!string.IsNullOrWhiteSpace(toc?.NextUpdate)
if (!string.IsNullOrWhiteSpace(blob?.NextUpdate)
&& DateTime.TryParseExact(
toc.NextUpdate,
blob.NextUpdate,
new[] { "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "o" }, //Sould be ISO8601 date but allow for other ISO formats too
System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.AssumeUniversal | System.Globalization.DateTimeStyles.AdjustToUniversal,
Expand All @@ -153,56 +151,56 @@ protected virtual string GetEntryCacheKey(IMetadataRepository repository, Guid a

protected virtual async Task InitializeRepository(IMetadataRepository repository)
{
var tocCacheKey = GetTocCacheKey(repository);
var blobCacheKey = GetTocCacheKey(repository);

var cachedToc = await _cache.GetStringAsync(tocCacheKey);
var cachedToc = await _cache.GetStringAsync(blobCacheKey);

MetadataTOCPayload toc;
MetadataBLOBPayload blob;

DateTime? cacheUntil = null;

if (cachedToc != null)
{
toc = JsonConvert.DeserializeObject<MetadataTOCPayload>(cachedToc);
cacheUntil = GetCacheUntilTime(toc);
blob = JsonConvert.DeserializeObject<MetadataBLOBPayload>(cachedToc);
cacheUntil = GetCacheUntilTime(blob);
}
else
{
_log?.LogInformation($"TOC for {repository.GetType().Name} not cached so loading from MDS...");
_log?.LogInformation($"BLOB for {repository.GetType().Name} not cached so loading from MDS...");

try
{
toc = await repository.GetToc();
blob = await repository.GetBLOB();
}
catch (Exception ex)
{
_log?.LogError(ex, "Error getting TOC from {0}", repository.GetType().Name);
_log?.LogError(ex, "Error getting BLOB from {0}", repository.GetType().Name);
throw;
}

_log?.LogInformation($"TOC for {repository.GetType().Name} not cached so loading from MDS... Done.");
_log?.LogInformation($"BLOB for {repository.GetType().Name} not cached so loading from MDS... Done.");

cacheUntil = GetCacheUntilTime(toc);
cacheUntil = GetCacheUntilTime(blob);

if (cacheUntil.HasValue)
{
await _cache.SetStringAsync(
tocCacheKey,
JsonConvert.SerializeObject(toc),
blobCacheKey,
JsonConvert.SerializeObject(blob),
new DistributedCacheEntryOptions()
{
AbsoluteExpiration = cacheUntil
});
}
}

foreach (var entry in toc.Entries)
foreach (var entry in blob.Entries)
{
if (!string.IsNullOrEmpty(entry.AaGuid)) //Only load FIDO2 entries
{
try
{
await LoadTocEntryStatement(repository, toc, entry, cacheUntil);
await LoadTocEntryStatement(repository, blob, entry, cacheUntil);
}
catch (Exception ex)
{
Expand Down
9 changes: 1 addition & 8 deletions Src/Fido2.AspNet/Fido2NetLibBuilderExtensions.cs
Expand Up @@ -55,12 +55,6 @@ public static IFido2MetadataServiceBuilder AddFileSystemMetadataRepository(this
return builder;
}

public static IFido2MetadataServiceBuilder AddStaticMetadataRepository(this IFido2MetadataServiceBuilder builder)
{
builder.Services.AddTransient<IMetadataRepository, StaticMetadataRepository>();

return builder;
}
public static IFido2MetadataServiceBuilder AddConformanceMetadataRepository(
this IFido2MetadataServiceBuilder builder,
HttpClient client = null,
Expand All @@ -75,12 +69,11 @@ public static IFido2MetadataServiceBuilder AddStaticMetadataRepository(this IFid
}
public static IFido2MetadataServiceBuilder AddFidoMetadataRepository(
this IFido2MetadataServiceBuilder builder,
string accessToken,
HttpClient client = null)
{
builder.Services.AddTransient<IMetadataRepository>(provider =>
{
return new Fido2MetadataServiceRepository(accessToken, client);
return new Fido2MetadataServiceRepository(client);
});

return builder;
Expand Down
2 changes: 1 addition & 1 deletion Src/Fido2.AspNet/NullMetadataService.cs
Expand Up @@ -10,7 +10,7 @@ bool IMetadataService.ConformanceTesting()
return false;
}

MetadataTOCPayloadEntry IMetadataService.GetEntry(Guid aaguid)
MetadataBLOBPayloadEntry IMetadataService.GetEntry(Guid aaguid)
{
return null;
}
Expand Down
5 changes: 0 additions & 5 deletions Src/Fido2.Models/Fido2Configuration.cs
Expand Up @@ -40,11 +40,6 @@ public class Fido2Configuration
/// </summary>
public string Origin { get; set; }

/// <summary>
/// MDSAccessKey
/// </summary>
public string MDSAccessKey { get; set; }

/// <summary>
/// MDSCacheDirPath
/// </summary>
Expand Down
@@ -1,46 +1,46 @@
using Newtonsoft.Json;
namespace Fido2NetLib
{
/// <summary>
/// Represents the MetadataTOCPayload
/// </summary>
/// <remarks>
/// <see xref="https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-metadata-service-v2.0-rd-20180702.html#metadata-toc-payload-dictionary"/>
/// </remarks>
public class MetadataTOCPayload
using Newtonsoft.Json;

namespace Fido2NetLib
{
/// <summary>
/// Represents the MetadataBLOBPayload
/// </summary>
/// <remarks>
/// <see xref="https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#metadata-blob-payload-dictionary"/>
/// </remarks>
public class MetadataBLOBPayload
{
/// <summary>
/// Gets or sets the legalHeader, if present, contains a legal guide for accessing and using metadata.
/// <summary>
/// Gets or sets the legalHeader, if present, contains a legal guide for accessing and using metadata.
/// </summary>
/// <remarks>
/// This value MAY contain URL(s) pointing to further information, such as a full Terms and Conditions statement.
/// </remarks>
[JsonProperty("legalHeader")]
public string LegalHeader { get; set; }
/// <summary>
/// Gets or sets the serial number of this UAF Metadata TOC Payload.
/// <summary>
/// Gets or sets the serial number of this UAF Metadata BLOB Payload.
/// </summary>
/// <remarks>
/// Serial numbers MUST be consecutive and strictly monotonic, i.e. the successor TOC will have a no value exactly incremented by one.
/// Serial numbers MUST be consecutive and strictly monotonic, i.e. the successor BLOB will have a no value exactly incremented by one.
/// </remarks>
[JsonProperty("no", Required = Required.Always)]
public int Number { get; set; }
/// <summary>
/// Gets or sets a formatted date (ISO-8601) when the next update will be provided at latest.
/// <summary>
/// Gets or sets a formatted date (ISO-8601) when the next update will be provided at latest.
/// </summary>
[JsonProperty("nextUpdate", Required = Required.Always)]
public string NextUpdate { get; set; }
/// <summary>
/// Gets or sets a list of zero or more entries of <see cref="MetadataTOCPayloadEntry"/>.
/// <summary>
/// Gets or sets a list of zero or more entries of <see cref="MetadataBLOBPayloadEntry"/>.
/// </summary>
[JsonProperty("entries", Required = Required.Always)]
public MetadataTOCPayloadEntry[] Entries { get; set; }
/// <summary>
/// The "alg" property from the original JWT header. Used to validate MetadataStatements.
/// </summary>
public MetadataBLOBPayloadEntry[] Entries { get; set; }

/// <summary>
/// The "alg" property from the original JWT header. Used to validate MetadataStatements.
/// </summary>
[JsonProperty("jwtAlg", Required = Required.Default)]
public string JwtAlg { get; set; }
}
}
}
}