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

WIP: Support device public key and passkeys #356

Merged
merged 13 commits into from
Jun 21, 2023
3 changes: 2 additions & 1 deletion Src/Fido2.Models/Exceptions/Fido2ErrorCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@ public enum Fido2ErrorCode
UnimplementedAlgorithm,
BackupEligibilityRequirementNotMet,
BackupStateRequirementNotMet,
CredentialAlgorithmRequirementNotMet
CredentialAlgorithmRequirementNotMet,
DevicePublicKeyAuthentication
}
4 changes: 3 additions & 1 deletion Src/Fido2.Models/Objects/AssertionVerificationResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ public class AssertionVerificationResult : Fido2ResponseBase
/// The latest value of the signature counter in the authenticator data from any ceremony using the public key credential source.
/// </summary>
public uint SignCount { get; set; }

/// <summary>
aseigler marked this conversation as resolved.
Show resolved Hide resolved
/// The latest value of the BS flag in the authenticator data from any ceremony using the public key credential source.
/// </summary>
public bool BS { get; set; }

/// <summary>
///
/// The public key portion of a hardware-bound device key pair
/// </summary>
public byte[] DevicePublicKey { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
using System;
namespace Fido2NetLib.Objects;

using System;
using System.Text.Json.Serialization;

namespace Fido2NetLib.Objects
public sealed class AuthenticationExtensionsDevicePublicKeyInputs
{
public sealed class AuthenticationExtensionsDevicePublicKeyInputs
{
[JsonPropertyName("attestation")]
public string Attestation { get; set; } = "none";
[JsonPropertyName("attestation")]
public string Attestation { get; set; } = "none";

[JsonPropertyName("attestationFormats")]
public string[] AttestationFormats { get; set; } = Array.Empty<string>();
}
[JsonPropertyName("attestationFormats")]
public string[] AttestationFormats { get; set; } = Array.Empty<string>();
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
using System.Text.Json.Serialization;
namespace Fido2NetLib.Objects;

namespace Fido2NetLib.Objects
using System.Text.Json.Serialization;

public sealed class AuthenticationExtensionsDevicePublicKeyOutputs
{
public sealed class AuthenticationExtensionsDevicePublicKeyOutputs
{
[JsonConverter(typeof(Base64UrlConverter))]
[JsonPropertyName("authenticatorOutput")]
public byte[] AuthenticatorOutput { get; set; }
[JsonConverter(typeof(Base64UrlConverter))]
[JsonPropertyName("authenticatorOutput")]
public byte[] AuthenticatorOutput { get; set; }

[JsonConverter(typeof(Base64UrlConverter))]
[JsonPropertyName("signature")]
public byte[] Signature { get; set; }
}
[JsonConverter(typeof(Base64UrlConverter))]
[JsonPropertyName("signature")]
public byte[] Signature { get; set; }
}
1 change: 1 addition & 0 deletions Src/Fido2.Models/Objects/PublicKeyCredentialType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public enum PublicKeyCredentialType
{
[EnumMember(Value = "public-key")]
PublicKey,

[EnumMember(Value = "invalid")]
aseigler marked this conversation as resolved.
Show resolved Hide resolved
Invalid
}
14 changes: 7 additions & 7 deletions Src/Fido2/AuthenticatorAssertionResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ byte[] hash
if (matches.Count > 1)
{
// Some form of error has occurred. It is indeterminate whether this is a known device. Terminate these verification steps.
throw new Fido2VerificationException(Fido2ErrorCode.MissingStoredPublicKey, Fido2ErrorMessages.MissingStoredPublicKey);
throw new Fido2VerificationException(Fido2ErrorCode.DevicePublicKeyAuthentication, Fido2ErrorMessages.NonUniqueDevicePublicKey);
}
// exactly one match
else if (matches.Count is 1)
Expand Down Expand Up @@ -307,10 +307,10 @@ byte[] hash
// This is a known device public key with a valid signature and valid attestation and thus a known device. Terminate these verification steps.
_ = verifier.Verify(devicePublicKeyAuthenticatorOutput.AttStmt, devicePublicKeyAuthenticatorOutput.AuthData, devicePublicKeyAuthenticatorOutput.Hash);
}
catch
catch (Exception ex)
{
// Some form of error has occurred. It is indeterminate whether this is a known device. Terminate these verification steps.
throw;
throw new Fido2VerificationException(Fido2ErrorCode.DevicePublicKeyAuthentication, Fido2ErrorMessages.InvalidDevicePublicKeyAttestation, ex);
}
}
}
Expand Down Expand Up @@ -341,10 +341,10 @@ byte[] hash
_ = verifier.Verify(devicePublicKeyAuthenticatorOutput.AttStmt, devicePublicKeyAuthenticatorOutput.AuthData, devicePublicKeyAuthenticatorOutput.Hash);
return devicePublicKeyAuthenticatorOutput.GetBytes();
}
catch
catch (Exception ex)
{
// Some form of error has occurred. It is indeterminate whether this is a known device. Terminate these verification steps.
throw;
throw new Fido2VerificationException(Fido2ErrorCode.DevicePublicKeyAuthentication, Fido2ErrorMessages.InvalidDevicePublicKeyAttestation, ex);
}
}
}
Expand All @@ -353,7 +353,7 @@ byte[] hash
// Otherwise there is some form of error: we recieved a known dpk value, but one or more of the
// accompanying aaguid, scope, or fmt values did not match what the Relying Party has stored
// along with that dpk value. Terminate these verification steps.
throw new Fido2VerificationException(Fido2ErrorCode.MissingStoredPublicKey, Fido2ErrorMessages.MissingStoredPublicKey);
throw new Fido2VerificationException(Fido2ErrorCode.DevicePublicKeyAuthentication, Fido2ErrorMessages.MissingStoredPublicKey);
}
}
}
Expand Down Expand Up @@ -381,7 +381,7 @@ byte[] hash
catch
{
// Some form of error has occurred. It is indeterminate whether this is a known device. Terminate these verification steps.
aseigler marked this conversation as resolved.
Show resolved Hide resolved
throw;
throw new Fido2VerificationException(Fido2ErrorCode.MissingStoredPublicKey, Fido2ErrorMessages.MissingStoredPublicKey);
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Src/Fido2/AuthenticatorAttestationResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ 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.BackupEligibility)
if (authData.IsBackupEligibile)
throw new Fido2VerificationException(Fido2ErrorCode.BackupEligibilityRequirementNotMet, Fido2ErrorMessages.BackupEligibilityRequirementNotMet);

if (!authData.HasAttestedCredentialData)
Expand Down Expand Up @@ -211,7 +211,7 @@ public static AuthenticatorAttestationResponse Parse(AuthenticatorAttestationRaw
PublicKey = authData.AttestedCredentialData.CredentialPublicKey.GetBytes(),
SignCount = authData.SignCount,
//Transports = result of response.getTransports();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we not return Transports anymore?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We didn't return transports before, in fact we didn't even collect it in the JavaScript at all, I added in this but it looks like it needs to be added to all of the other registration JavaScript files, and collect transports in AuthenticatorAttestationRawResponse.ResponseData, and then populate transports in the controller from the raw response. There are a few reasons to collect transports, to help with user facing dialogs, but also to compare what transports are being used against a policy, or comparing what transports should be available according to the authenticator metadata. Basically this is incomplete at the moment.

BE = authData.BackupEligibility,
BE = authData.IsBackupEligibile,
BS = authData.BackupState,
AttestationObject = Raw.Response.AttestationObject,
AttestationClientDataJSON = Raw.Response.ClientDataJson,
Expand Down
17 changes: 17 additions & 0 deletions Src/Fido2/DevelopmentInMemoryStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,49 +72,66 @@ public class StoredCredential
/// The type of the public key credential source.
/// </summary>
public PublicKeyCredentialType Type { get; set; } = PublicKeyCredentialType.PublicKey;

/// <summary>
/// The Credential ID of the public key credential source.
/// </summary>
public byte[] Id { get; set; }
aseigler marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// The credential public key of the public key credential source.
/// </summary>
public byte[] PublicKey { get; set; }

/// <summary>
/// The latest value of the signature counter in the authenticator data from any ceremony using the public key credential source.
/// </summary>
public uint SignCount { get; set; }

/// <summary>
/// The value returned from getTransports() when the public key credential source was registered.
/// </summary>
public AuthenticatorTransport[] Transports { get; set; }

/// <summary>
/// The value of the BE flag when the public key credential source was created.
/// </summary>
public bool BE { get; set; }

/// <summary>
/// The latest value of the BS flag in the authenticator data from any ceremony using the public key credential source.
/// </summary>
public bool BS { get; set; }

/// <summary>
/// The value of the attestationObject attribute when the public key credential source was registered.
/// Storing this enables the Relying Party to reference the credential's attestation statement at a later time.
/// </summary>
public byte[] AttestationObject { get; set; }

/// <summary>
/// The value of the clientDataJSON attribute when the public key credential source was registered.
/// Storing this in combination with the above attestationObject item enables the Relying Party to re-verify the attestation signature at a later time.
/// </summary>
public byte[] AttestationClientDataJSON { get; set; }

/// <summary>
///
/// </summary>
public List<byte[]> DevicePublicKeys { get; set; }

public byte[] UserId { get; set; }

public PublicKeyCredentialDescriptor Descriptor { get; set; }
Copy link
Collaborator

@abergs abergs Jun 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we deprecate the descriptor or do we still have a use for it? (Now that we've added Id, PublicKey, Type to the class itself)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I don't see a reason to store both Id and Descriptor.


public byte[] UserHandle { get; set; }

public uint SignatureCounter => SignCount;

public string CredType { get; set; }

public DateTime RegDate { get; set; }

public Guid AaGuid { get; set; }

}
2 changes: 2 additions & 0 deletions Src/Fido2/Fido2ErrorMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ internal static class Fido2ErrorMessages
public static readonly string NonUniqueCredentialId = "CredentialId is not unique to this user";
public static readonly string MissingAttestationType = "Missing attestation type";
public static readonly string InvalidAttestationCertSubject = "Invalid attestation cert subject";
public static readonly string InvalidDevicePublicKeyAttestation = "Invalid devicePublicKey attestation";
public static readonly string NonUniqueDevicePublicKey = "More than one devicePublicKey match";

public static readonly string UnimplementedAlgorithm_Ecdaa_Packed = "ECDAA support for packed attestation is not yet implemented";
public static readonly string UnimplementedAlgorithm_Ecdaa_Tpm = "ECDAA support for TPM attestation is not yet implemented";
Expand Down
2 changes: 1 addition & 1 deletion Src/Fido2/Objects/AttestedCredentialData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public sealed class AttestedCredentialData
/// <see cref="https://www.w3.org/TR/webauthn/#attested-credential-data"/>
/// </summary>
private readonly int _minLength = Marshal.SizeOf(typeof(Guid)) + sizeof(ushort) + sizeof(byte) + sizeof(byte);
aseigler marked this conversation as resolved.
Show resolved Hide resolved
private readonly int _maxCredentialIdLength = 1023;
private const int _maxCredentialIdLength = 1023;

/// <summary>
/// Instantiates an AttestedCredentialData object from an aaguid, credentialID, and CredentialPublicKey
Expand Down
2 changes: 1 addition & 1 deletion Src/Fido2/Objects/AuthenticatorData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public sealed class AuthenticatorData
/// A backup eligible public key credential source is referred to as a multi-device credential whereas one that is not backup eligible is referred to as a single-device credential.
/// <see cref="https://w3c.github.io/webauthn/#backup-eligibility"/>
/// </summary>
public bool BackupEligibility => _flags.HasFlag(AuthenticatorFlags.BE);
public bool IsBackupEligibile => _flags.HasFlag(AuthenticatorFlags.BE);

/// <summary>
/// The current backup state of a multi-device credential as determined by the current managing authenticator.
Expand Down