Skip to content

Commit

Permalink
Switch BE/BS config options from bool to enum
Browse files Browse the repository at this point in the history
Update BE/BS policy logic
Add more assertion tests, move more error strings to error messages
Get metadata into assertion flow for DPK
  • Loading branch information
aseigler committed Mar 7, 2023
1 parent 0131ee7 commit 457412d
Show file tree
Hide file tree
Showing 10 changed files with 1,552 additions and 62 deletions.
6 changes: 3 additions & 3 deletions Demo/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;

using Fido2NetLib;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -52,8 +52,8 @@ public void ConfigureServices(IServiceCollection services)
options.Origins = Configuration.GetSection("fido2:origins").Get<HashSet<string>>();
options.TimestampDriftTolerance = Configuration.GetValue<int>("fido2:timestampDriftTolerance");
options.MDSCacheDirPath = Configuration["fido2:MDSCacheDirPath"];
options.AllowBackupEligibleCredential = Configuration.GetValue<bool>("fido2:allowBackupEligibleCredential");
options.AllowBackedUpCredential = Configuration.GetValue<bool>("fido2:allowBackedUpCredential");
options.BackupEligibleCredentialPolicy = Configuration.GetValue<Fido2Configuration.CredentialBackupPolicy>("fido2:backupEligibleCredentialPolicy");
options.BackedUpCredentialPolicy = Configuration.GetValue<Fido2Configuration.CredentialBackupPolicy>("fido2:backedUpCredentialPolicy");
})
.AddCachedMetadataService(config =>
{
Expand Down
4 changes: 2 additions & 2 deletions Demo/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"serverDomain": "localhost",
"origins": [ "https://localhost:44329" ],
"timestampDriftTolerance": 300000,
"allowBackupEligibleCredential": true,
"allowBackedUpCredential": true
"backupEligibleCredentialPolicy": "allowed",
"backedUpCredentialPolicy": "allowed"
},
"Logging": {
"IncludeScopes": false,
Expand Down
26 changes: 24 additions & 2 deletions Src/Fido2.Models/Fido2Configuration.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;

namespace Fido2NetLib;

Expand Down Expand Up @@ -117,10 +118,31 @@ public ISet<string> FullyQualifiedOrigins
/// <summary>
/// Whether or not to accept a backup eligible credential
/// </summary>
public bool AllowBackupEligibleCredential { get; set; } = true;
public CredentialBackupPolicy BackupEligibleCredentialPolicy { get; set; } = CredentialBackupPolicy.Allowed;

/// <summary>
/// Whether or not to accept a backed up credential
/// </summary>
public bool AllowBackedUpCredential { get; set; } = true;
public CredentialBackupPolicy BackedUpCredentialPolicy { get; set; } = CredentialBackupPolicy.Allowed;

public enum CredentialBackupPolicy
{
/// <summary>
/// This value indicates that the Relying Party requires backup eligible or backed up credentials.
/// </summary>
[EnumMember(Value = "required")]
Required,

/// <summary>
/// This value indicates that the Relying Party allows backup eligible or backed up credentials.
/// </summary>
[EnumMember(Value = "allowed")]
Allowed,

/// <summary>
/// This value indicates that the Relying Party does not allow backup eligible or backed up credentials.
/// </summary>
[EnumMember(Value = "disallowed")]
Disallowed
}
}
102 changes: 54 additions & 48 deletions Src/Fido2/AuthenticatorAssertionResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,111 +67,107 @@ public static AuthenticatorAssertionResponse Parse(AuthenticatorAssertionRawResp
List<byte[]> storedDevicePublicKeys,
uint storedSignatureCounter,
IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredId,
IMetadataService metadataService,
CancellationToken cancellationToken = default)
{
BaseVerify(config.FullyQualifiedOrigins, options.Challenge);

if (Raw.Type != PublicKeyCredentialType.PublicKey)
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, "AssertionResponse type must be public-key");
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, Fido2ErrorMessages.AssertionResponseNotPublicKey);

if (Raw.Id is null)
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, "Id is missing");
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, Fido2ErrorMessages.AssertionResponseIdMissing);

if (Raw.RawId is null)
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, "RawId is missing");
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, Fido2ErrorMessages.AssertionResponseRawIdMissing);

// 1. If the allowCredentials option was given when this authentication ceremony was initiated, verify that credential.id identifies one of the public key credentials that were listed in allowCredentials.
// 5. If the allowCredentials option was given when this authentication ceremony was initiated, verify that credential.id identifies one of the public key credentials that were listed in allowCredentials.
if (options.AllowCredentials != null && options.AllowCredentials.Any())
{
// might need to transform x.Id and raw.id as described in https://www.w3.org/TR/webauthn/#publickeycredential
if (!options.AllowCredentials.Any(x => x.Id.SequenceEqual(Raw.Id)))
throw new Fido2VerificationException("Invalid");
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, Fido2ErrorMessages.CredentialIdNotInAllowedCredentials);
}

// 2. Identify the user being authenticated and verify that this user is the owner of the public key credential source credentialSource identified by credential.id
// 6. Identify the user being authenticated and verify that this user is the owner of the public key credential source credentialSource identified by credential.id
if (UserHandle != null)
{
if (UserHandle.Length is 0)
throw new Fido2VerificationException(Fido2ErrorMessages.UserHandleIsEmpty);

if (await isUserHandleOwnerOfCredId(new IsUserHandleOwnerOfCredentialIdParams(Raw.Id, UserHandle), cancellationToken) is false)
{
throw new Fido2VerificationException("User is not owner of the public key identified by the credential id");
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, Fido2ErrorMessages.UserHandleNotOwnerOfPublicKey);
}
}

// 3. Using credential’s id attribute(or the corresponding rawId, if base64url encoding is inappropriate for your use case), look up the corresponding credential public key.
// Credential public key passed in via storePublicKey parameter

// 4. Let cData, authData and sig denote the value of credential’s response's clientDataJSON, authenticatorData, and signature respectively.
// 7. Let cData, authData and sig denote the value of credential’s response's clientDataJSON, authenticatorData, and signature respectively.
//var cData = Raw.Response.ClientDataJson;
var authData = new AuthenticatorData(AuthenticatorData);
//var sig = Raw.Response.Signature;

// 5. Let JSONtext be the result of running UTF-8 decode on the value of cData.
// 8. Let JSONtext be the result of running UTF-8 decode on the value of cData.
// var JSONtext = Encoding.UTF8.GetBytes(cData.ToString());

// 7. Verify that the value of C.type is the string webauthn.get.
// 10. Verify that the value of C.type is the string webauthn.get.
if (Type is not "webauthn.get")
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, "AssertionResponse must be webauthn.get");
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, Fido2ErrorMessages.AssertionTypeNotWebAuthnGet);

// 8. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the PublicKeyCredentialRequestOptions passed to the get() call.
// 9. Verify that the value of C.origin matches the Relying Party's origin.
// done in base class
// 11. Verify that the value of C.challenge equals the base64url encoding of options.challenge.
// 12. Verify that the value of C.origin matches the Relying Party's origin.
// Both handled in BaseVerify

// 11. Verify that the rpIdHash in aData is the SHA - 256 hash of the RP ID expected by the Relying Party.
// 13. Verify that the rpIdHash in aData is the SHA - 256 hash of the RP ID expected by the Relying Party.

// 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.Extensions?.AppID ?? false ? options.Extensions?.AppID : options.RpId;
byte[] hashedRpId = SHA256.HashData(Encoding.UTF8.GetBytes(rpid ?? string.Empty));
byte[] hashedClientDataJson = SHA256.HashData(Raw.Response.ClientDataJson);
byte[] hash = SHA256.HashData(Raw.Response.ClientDataJson);

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

byte[] devicePublicKeyResult = null;
if (Raw.Extensions?.DevicePubKey is not null)
{
devicePublicKeyResult = DevicePublicKeyAuthentication(storedDevicePublicKeys, Raw.Extensions, AuthenticatorData, hashedClientDataJson);
}
// 14. Verify that the UP bit of the flags in authData is set.
if (!authData.UserPresent)
throw new Fido2VerificationException(Fido2ErrorCode.UserPresentFlagNotSet, Fido2ErrorMessages.UserPresentFlagNotSet);

// 12. Verify that the User Present bit of the flags in authData is set.
// UNLESS...userVerification is set to preferred or discouraged?
// See Server-ServerAuthenticatorAssertionResponse-Resp3 Test server processing authenticatorData
// P-5 Send a valid ServerAuthenticatorAssertionResponse both authenticatorData.flags.UV and authenticatorData.flags.UP are not set, for userVerification set to "preferred", and check that server succeeds
// P-8 Send a valid ServerAuthenticatorAssertionResponse both authenticatorData.flags.UV and authenticatorData.flags.UP are not set, for userVerification set to "discouraged", and check that server succeeds
// if ((!authData.UserPresent) && (options.UserVerification != UserVerificationRequirement.Discouraged && options.UserVerification != UserVerificationRequirement.Preferred)) throw new Fido2VerificationException("User Present flag not set in authenticator data");

// 13 If user verification is required for this assertion, verify that the User Verified bit of the flags in aData is set.
// UNLESS...userPresent is true?
// see ee Server-ServerAuthenticatorAssertionResponse-Resp3 Test server processing authenticatorData
// P-8 Send a valid ServerAuthenticatorAssertionResponse both authenticatorData.flags.UV and authenticatorData.flags.UP are not set, for userVerification set to "discouraged", and check that server succeeds
if (options.UserVerification is UserVerificationRequirement.Required && !authData.UserVerified)
// 15. If the Relying Party requires user verification for this assertion, verify that the UV bit of the flags in authData is set.
if (options.UserVerification is UserVerificationRequirement.Required && !authData.UserVerified)
throw new Fido2VerificationException(Fido2ErrorCode.UserVerificationRequirementNotMet, Fido2ErrorMessages.UserVerificationRequirementNotMet);

// 16. If the credential backup state is used as part of Relying Party business logic or policy, let currentBe and currentBs be the values of the BE and BS bits, respectively, of the flags in authData.
// Compare currentBe and currentBs with credentialRecord.BE and credentialRecord.BS and apply Relying Party policy, if any.
if (authData.IsBackupEligible && !config.AllowBackupEligibleCredential)
if (authData.IsBackupEligible && config.BackupEligibleCredentialPolicy is Fido2Configuration.CredentialBackupPolicy.Disallowed ||
!authData.IsBackupEligible && config.BackupEligibleCredentialPolicy is Fido2Configuration.CredentialBackupPolicy.Required)
throw new Fido2VerificationException(Fido2ErrorCode.BackupEligibilityRequirementNotMet, Fido2ErrorMessages.BackupEligibilityRequirementNotMet);

if (authData.BackupState && !config.AllowBackedUpCredential)
if (authData.BackupState && config.BackedUpCredentialPolicy is Fido2Configuration.CredentialBackupPolicy.Disallowed ||
!authData.BackupState && config.BackedUpCredentialPolicy is Fido2Configuration.CredentialBackupPolicy.Required)
throw new Fido2VerificationException(Fido2ErrorCode.BackupStateRequirementNotMet, Fido2ErrorMessages.BackupStateRequirementNotMet);

// 14. Verify that the values of the client extension outputs in clientExtensionResults and the authenticator extension outputs in the extensions in authData are as expected, considering the client extension input values that were given as the extensions option in the get() call.In particular, any extension identifier values in the clientExtensionResults and the extensions in authData MUST be also be present as extension identifier values in the extensions member of options, i.e., no extensions are present that were not requested. In the general case, the meaning of "are as expected" is specific to the Relying Party and which extensions are in use.
// todo: Verify this (and implement extensions on options)
// 17. Verify that the values of the client extension outputs in clientExtensionResults and the authenticator extension outputs in the extensions in authData are as expected,
// considering the client extension input values that were given in options.extensions and any specific policy of the Relying Party regarding unsolicited extensions,
// i.e., those that were not specified as part of options.extensions. In the general case, the meaning of "are as expected" is specific to the Relying Party and which extensions are in use.
byte[] devicePublicKeyResult = null;
if (Raw.Extensions?.DevicePubKey is not null)
{
devicePublicKeyResult = DevicePublicKeyAuthentication(storedDevicePublicKeys, Raw.Extensions, AuthenticatorData, hash);
}

// Pretty sure these conditions are not able to be met due to the AuthenticatorData constructor implementation
if (authData.HasExtensionsData && (authData.Extensions is null || authData.Extensions.Length is 0))
throw new Fido2VerificationException(Fido2ErrorCode.MalformedExtensionsDetected, Fido2ErrorMessages.MalformedExtensionsDetected);

if (!authData.HasExtensionsData && authData.Extensions != null)
throw new Fido2VerificationException(Fido2ErrorCode.UnexpectedExtensionsDetected, Fido2ErrorMessages.UnexpectedExtensionsDetected);

// 15.
// Done earlier, hashedClientDataJson
// 18. Let hash be the result of computing a hash over the cData using SHA-256.
// done earlier in step 13

// 16. Using the credential public key looked up in step 3, verify that sig is a valid signature over the binary concatenation of aData and hash.
byte[] data = DataHelper.Concat(Raw.Response.AuthenticatorData, hashedClientDataJson);
// 19. Using credentialRecord.publicKey, verify that sig is a valid signature over the binary concatenation of authData and hash.
byte[] data = DataHelper.Concat(Raw.Response.AuthenticatorData, hash);

if (storedPublicKey is null || storedPublicKey.Length is 0)
throw new Fido2VerificationException(Fido2ErrorCode.MissingStoredPublicKey, Fido2ErrorMessages.MissingStoredPublicKey);
Expand Down Expand Up @@ -208,11 +204,21 @@ public static AuthenticatorAssertionResponse Parse(AuthenticatorAssertionRawResp
var verifier = AttestationVerifier.Create(fmt);

// 4. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature, by using the attestation statement format fmt’s verification procedure given attStmt, authData and hash.
(var attType, var trustPath) = verifier.Verify(attStmt, AuthenticatorData, hashedClientDataJson);
(var attType, var trustPath) = verifier.Verify(attStmt, AuthenticatorData, hash);

// 5. If validation is successful, obtain a list of acceptable trust anchors (attestation root certificates or ECDAA-Issuer public keys)
// for that attestation type and attestation statement format fmt, from a trusted source or from policy.
// For example, the FIDO Metadata Service [FIDOMetadataService] provides one way to obtain such information, using the aaguid in the attestedCredentialData in authData.

MetadataBLOBPayloadEntry metadataEntry = null;
if (metadataService != null)
metadataEntry = await metadataService.GetEntryAsync(authData.AttestedCredentialData.AaGuid, cancellationToken);

// while conformance testing, we must reject any authenticator that we cannot get metadata for
if (metadataService?.ConformanceTesting() is true && metadataEntry is null && attType != AttestationType.None && fmt is not "fido-u2f")
throw new Fido2VerificationException(Fido2ErrorCode.AaGuidNotFound, "AAGUID not found in MDS test metadata");

// 5. If validation is successful, obtain a list of acceptable trust anchors (i.e. attestation root certificates) for that attestation type and attestation statement format fmt, from a trusted source or from policy.
// The aaguid in the attested credential data can be used to guide this lookup.
// TODO: Why? What do we do with this info?
AuthenticatorAttestationResponse.VerifyTrustAnchor(metadataEntry, trustPath);
}

return new AssertionVerificationResult
Expand Down
5 changes: 3 additions & 2 deletions Src/Fido2/AuthenticatorAttestationResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ public static AuthenticatorAttestationResponse Parse(AuthenticatorAttestationRaw
throw new Fido2VerificationException(Fido2ErrorCode.UserVerificationRequirementNotMet, Fido2ErrorMessages.UserVerificationRequirementNotMet);

// 15. If the Relying Party uses the credential's backup eligibility to inform its user experience flows and/or policies, evaluate the BE bit of the flags in authData.
if (authData.IsBackupEligible && !config.AllowBackupEligibleCredential)
if (authData.IsBackupEligible && config.BackupEligibleCredentialPolicy is Fido2Configuration.CredentialBackupPolicy.Disallowed ||
!authData.IsBackupEligible && config.BackupEligibleCredentialPolicy is Fido2Configuration.CredentialBackupPolicy.Required)
throw new Fido2VerificationException(Fido2ErrorCode.BackupEligibilityRequirementNotMet, Fido2ErrorMessages.BackupEligibilityRequirementNotMet);

if (!authData.HasAttestedCredentialData)
Expand Down Expand Up @@ -279,7 +280,7 @@ byte[] hash
return devicePublicKeyAuthenticatorOutput.GetBytes();
}

private static void VerifyTrustAnchor(MetadataBLOBPayloadEntry metadataEntry, X509Certificate2[] trustPath)
public static void VerifyTrustAnchor(MetadataBLOBPayloadEntry metadataEntry, X509Certificate2[] trustPath)
{
if (trustPath != null && metadataEntry?.MetadataStatement?.AttestationTypes is not null)
{
Expand Down

0 comments on commit 457412d

Please sign in to comment.