mirror of
https://github.com/bitwarden/mobile
synced 2026-01-18 16:33:15 +00:00
[PM-5731] Create C# WebAuthn authenticator to support maui apps (#2951)
* [PM-5731] feat: implement get assertion params object * [PM-5731] feat: add first test * [PM-5731] feat: add rp mismatch test * [PM-5731] feat: ask for credentials when found * [PM-5731] feat: find discoverable credentials * [PM-5731] feat: add tests for successful UV requests * [PM-5731] feat: add user does not consent test * [PM-5731] feat: check for UV when reprompt is active * [PM-5731] fix: tests a bit, needed some additional "arrange" steps * [PM-5731] feat: add support for counter * [PM-5731] feat: implement assertion without signature * [PM-5732] feat: finish authenticator assertion implementation note: CryptoFunctionService still needs Sign implemenation * [PM-5731] chore: minor clean up * [PM-5731] feat: scaffold make credential * [PM-5731] feat: start implementing attestation * [PM-5731] feat: implement credential exclusion * [PM-5731] feat: add new credential confirmaiton * [PM-5731] feat: implement credential creation * [PM-5731] feat: add user verification checks * [PM-5731] feat: add unknown error handling * [PM-5731] chore: clean up unusued params * [PM-5731] feat: partial attestation implementation * [PM-5731] feat: implement key generation * [PM-5731] feat: return public key in DER format * [PM-5731] feat: implement signing * [PM-5731] feat: remove logging * [PM-5731] chore: use primary constructor * [PM-5731] chore: add Async to method names * [PM-5731] feat: add support for silent discoverability * [PM-5731] feat: add support for specifying user presence requirement * [PM-5731] feat: ensure unlocked vault * [PM-5731] chore: clean up and refactor assertion tests * [PM-5731] chore: clean up and refactor attestation tests * [PM-5731] chore: add user presence todo comment * [PM-5731] feat: scaffold fido2 client * PM-5731 Fix build updating discoverable flag * [PM-5731] fix: failing test * [PM-5731] feat: add sameOriginWithAncestor and user id length checks * [PM-5731] feat: add incomplete rpId verification * [PM-5731] chore: document uri helpers * [PM-5731] feat: implement fido2 client createCredential * [PM-5731] feat: implement credential assertion in client * fix wrong signature format (cherry picked from commita1c9ebf01f) * [PM-5731] fix: issues after cherry-pick * Fix incompatible GUID conversions (cherry picked from commitc801b2fc3a) * [PM-5731] chore: remove default constructor * [PM-5731] feat: refactor user interface to increase flexibility * [PM-5731] feat: implement generic assertion user interface class * [PM-5731] feat: remove ability to make user presence optional * [PM-5731] chore: remove logging comments * [PM-5731] feat: add native reprompt support to the authenticator * [PM-5731] feat: allow pre and post UV * [PM-5731] chore: add `Async` to method name. Remove `I` from struct * [PM-5731] fix: discoverable string repr lowercase * [PM-5731] chore: don't use C# 12 features * [PM-5731] fix: replace magic strings and numbers with contants and enums * [PM-5731] fix: use UTC creation date * [PM-5731] fix: formatting * [PM-5731] chore: use properties for public fields * [PM-5731] chore: remove TODO * [PM-5731] fix: IsValidRpId --------- Co-authored-by: Federico Maccaroni <fedemkr@gmail.com> Co-authored-by: mpbw2 <59324545+mpbw2@users.noreply.github.com>
This commit is contained in:
@@ -38,12 +38,38 @@ namespace Bit.Core.Utilities
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the host (and not port) of the given uri.
|
||||
/// Does not support plain hostnames without a protocol.
|
||||
///
|
||||
/// Input => Output examples:
|
||||
/// <para>https://bitwarden.com => bitwarden.com</para>
|
||||
/// <para>https://login.bitwarden.com:1337 => login.bitwarden.com</para>
|
||||
/// <para>https://sub.login.bitwarden.com:1337 => sub.login.bitwarden.com</para>
|
||||
/// <para>https://localhost:8080 => localhost</para>
|
||||
/// <para>localhost => null</para>
|
||||
/// <para>bitwarden => null</para>
|
||||
/// <para>127.0.0.1 => 127.0.0.1</para>
|
||||
/// </summary>
|
||||
public static string GetHostname(string uriString)
|
||||
{
|
||||
var uri = GetUri(uriString);
|
||||
return string.IsNullOrEmpty(uri?.Host) ? null : uri.Host;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the host and port of the given uri.
|
||||
/// Does not support plain hostnames without
|
||||
///
|
||||
/// Input => Output examples:
|
||||
/// <para>https://bitwarden.com => bitwarden.com</para>
|
||||
/// <para>https://login.bitwarden.com:1337 => login.bitwarden.com:1337</para>
|
||||
/// <para>https://sub.login.bitwarden.com:1337 => sub.login.bitwarden.com:1337</para>
|
||||
/// <para>https://localhost:8080 => localhost:8080</para>
|
||||
/// <para>localhost => null</para>
|
||||
/// <para>bitwarden => null</para>
|
||||
/// <para>127.0.0.1 => 127.0.0.1</para>
|
||||
/// </summary>
|
||||
public static string GetHost(string uriString)
|
||||
{
|
||||
var uri = GetUri(uriString);
|
||||
@@ -61,6 +87,19 @@ namespace Bit.Core.Utilities
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the second and top level domain of the given uri.
|
||||
/// Does not support plain hostnames without
|
||||
///
|
||||
/// Input => Output examples:
|
||||
/// <para>https://bitwarden.com => bitwarden.com</para>
|
||||
/// <para>https://login.bitwarden.com:1337 => bitwarden.com</para>
|
||||
/// <para>https://sub.login.bitwarden.com:1337 => bitwarden.com</para>
|
||||
/// <para>https://localhost:8080 => localhost</para>
|
||||
/// <para>localhost => null</para>
|
||||
/// <para>bitwarden => null</para>
|
||||
/// <para>127.0.0.1 => 127.0.0.1</para>
|
||||
/// </summary>
|
||||
public static string GetDomain(string uriString)
|
||||
{
|
||||
var uri = GetUri(uriString);
|
||||
|
||||
18
src/Core/Utilities/Fido2/AuthenticatorSelectionCriteria.cs
Normal file
18
src/Core/Utilities/Fido2/AuthenticatorSelectionCriteria.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
#nullable enable
|
||||
/// <summary>
|
||||
/// The Relying Party's requirements of the authenticator used in the creation of the credential.
|
||||
/// </summary>
|
||||
public class AuthenticatorSelectionCriteria
|
||||
{
|
||||
public bool? RequireResidentKey { get; set; }
|
||||
public string? ResidentKey { get; set; }
|
||||
public string UserVerification { get; set; } = "preferred";
|
||||
|
||||
/// <summary>
|
||||
/// This member is intended for use by Relying Parties that wish to select the appropriate authenticators to participate in the create() operation.
|
||||
/// </summary>
|
||||
// public AuthenticatorAttachment? AuthenticatorAttachment { get; set; } // not used
|
||||
}
|
||||
}
|
||||
8
src/Core/Utilities/Fido2/Fido2AlgorithmIdentifier.cs
Normal file
8
src/Core/Utilities/Fido2/Fido2AlgorithmIdentifier.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public enum Fido2AlgorithmIdentifier : int
|
||||
{
|
||||
ES256 = -7,
|
||||
RS256 = -257,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/// <summary>
|
||||
/// Represents the metadata of a discoverable credential for a FIDO2 authenticator.
|
||||
/// See: https://www.w3.org/TR/webauthn-3/#sctn-op-silent-discovery
|
||||
/// </summary>
|
||||
public class Fido2AuthenticatorDiscoverableCredentialMetadata
|
||||
{
|
||||
public string Type { get; set; }
|
||||
|
||||
public byte[] Id { get; set; }
|
||||
|
||||
public string RpId { get; set; }
|
||||
|
||||
public byte[] UserHandle { get; set; }
|
||||
|
||||
public string UserName { get; set; }
|
||||
}
|
||||
37
src/Core/Utilities/Fido2/Fido2AuthenticatorException.cs
Normal file
37
src/Core/Utilities/Fido2/Fido2AuthenticatorException.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class Fido2AuthenticatorException : Exception
|
||||
{
|
||||
public Fido2AuthenticatorException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class NotAllowedError : Fido2AuthenticatorException
|
||||
{
|
||||
public NotAllowedError() : base("NotAllowedError")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class NotSupportedError : Fido2AuthenticatorException
|
||||
{
|
||||
public NotSupportedError() : base("NotSupportedError")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class InvalidStateError : Fido2AuthenticatorException
|
||||
{
|
||||
public InvalidStateError() : base("InvalidStateError")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class UnknownError : Fido2AuthenticatorException
|
||||
{
|
||||
public UnknownError() : base("UnknownError")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,25 @@
|
||||
{
|
||||
public class Fido2AuthenticatorGetAssertionParams
|
||||
{
|
||||
/** The caller’s RP ID, as determined by the user agent and the client. */
|
||||
public string RpId { get; set; }
|
||||
|
||||
public string CredentialId { get; set; }
|
||||
/** The hash of the serialized client data, provided by the client. */
|
||||
public byte[] Hash { get; set; }
|
||||
|
||||
public string Counter { get; set; }
|
||||
public PublicKeyCredentialDescriptor[] AllowCredentialDescriptorList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Instructs the authenticator to require a user-verifying gesture in order to complete the request. Examples of such gestures are fingerprint scan or a PIN.
|
||||
/// </summary>
|
||||
public bool RequireUserVerification { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The challenge to be signed by the authenticator.
|
||||
/// </summary>
|
||||
public byte[] Challenge { get; set; }
|
||||
|
||||
public object Extensions { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
using System;
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class Fido2AuthenticatorGetAssertionResult
|
||||
{
|
||||
public byte[] AuthenticatorData { get; set; }
|
||||
|
||||
public byte[] Signature { get; set; }
|
||||
|
||||
public Fido2AuthenticatorGetAssertionSelectedCredential SelectedCredential { get; set; }
|
||||
}
|
||||
|
||||
public class Fido2AuthenticatorGetAssertionSelectedCredential {
|
||||
public byte[] Id { get; set; }
|
||||
|
||||
#nullable enable
|
||||
public byte[]? UserHandle { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class Fido2AuthenticatorMakeCredentialParams
|
||||
{
|
||||
/// <summary>
|
||||
/// The Relying Party's PublicKeyCredentialRpEntity.
|
||||
/// </summary>
|
||||
public PublicKeyCredentialRpEntity RpEntity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Relying Party's PublicKeyCredentialRpEntity.
|
||||
/// </summary>
|
||||
public PublicKeyCredentialUserEntity UserEntity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The hash of the serialized client data, provided by the client.
|
||||
/// </summary>
|
||||
public byte[] Hash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A sequence of pairs of PublicKeyCredentialType and public key algorithms (COSEAlgorithmIdentifier) requested by the Relying Party. This sequence is ordered from most preferred to least preferred. The authenticator makes a best-effort to create the most preferred credential that it can.
|
||||
/// </summary>
|
||||
public PublicKeyCredentialParameters[] CredTypesAndPubKeyAlgs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///An OPTIONAL list of PublicKeyCredentialDescriptor objects provided by the Relying Party with the intention that, if any of these are known to the authenticator, it SHOULD NOT create a new credential. excludeCredentialDescriptorList contains a list of known credentials.
|
||||
/// </summary>
|
||||
public PublicKeyCredentialDescriptor[] ExcludeCredentialDescriptorList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The effective resident key requirement for credential creation, a Boolean value determined by the client. Resident is synonymous with discoverable. */
|
||||
/// </summary>
|
||||
public bool RequireResidentKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The effective user verification requirement for assertion, a Boolean value provided by the client.
|
||||
/// </summary>
|
||||
public bool RequireUserVerification { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// CTAP2 authenticators support setting this to false, but we only support the WebAuthn authenticator model which does not have that option.
|
||||
/// </summary>
|
||||
// public bool RequireUserPresence { get; set; } // Always required
|
||||
|
||||
/// <summary>
|
||||
/// The authenticator's attestation preference, a string provided by the client. This is a hint that the client gives to the authenticator about what kind of attestation statement it would like. The authenticator makes a best-effort to satisfy the preference.
|
||||
/// Note: Attestation statements are not supported at this time.
|
||||
/// </summary>
|
||||
// public string AttestationPreference { get; set; }
|
||||
|
||||
public object Extensions { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class Fido2AuthenticatorMakeCredentialResult
|
||||
{
|
||||
public byte[] CredentialId { get; set; }
|
||||
|
||||
public byte[] AttestationObject { get; set; }
|
||||
|
||||
public byte[] AuthData { get; set; }
|
||||
|
||||
public byte[] PublicKey { get; set; }
|
||||
|
||||
public int PublicKeyAlgorithm { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Parameters for asserting a credential.
|
||||
///
|
||||
/// This class is an extended version of the WebAuthn struct:
|
||||
/// https://www.w3.org/TR/webauthn-2/#dictdef-publickeycredentialrequestoptions
|
||||
/// </summary>
|
||||
public class Fido2ClientAssertCredentialParams
|
||||
{
|
||||
/// <summary>
|
||||
/// A value which is true if and only if the caller’s environment settings object is same-origin with its ancestors.
|
||||
/// It is false if caller is cross-origin.
|
||||
/// </summary>
|
||||
public bool SameOriginWithAncestors { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The challenge that the selected authenticator signs, along with other data, when producing an authentication
|
||||
/// assertion.
|
||||
/// </summary>
|
||||
public required byte[] Challenge { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The relying party identifier claimed by the caller. If omitted, its value will be the CredentialsContainer
|
||||
/// object's relevant settings object's origin's effective domain.
|
||||
/// </summary>
|
||||
public string RpId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Relying Party's origin (e.g., "https://example.com").
|
||||
/// </summary>
|
||||
public string Origin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of PublicKeyCredentialDescriptor objects representing public key credentials acceptable to the caller,
|
||||
/// in descending order of the caller’s preference (the first item in the list is the most preferred credential,
|
||||
/// and so on down the list).
|
||||
/// </summary>
|
||||
public PublicKeyCredentialDescriptor[] AllowCredentials { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The Relying Party's requirements regarding user verification for the get() operation.
|
||||
/// </summary>
|
||||
public string UserVerification { get; set; } = "preferred";
|
||||
|
||||
/// <summary>
|
||||
/// This time, in milliseconds, that the caller is willing to wait for the call to complete.
|
||||
/// This is treated as a hint, and MAY be overridden by the client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is not currently supported.
|
||||
/// </remarks>
|
||||
public int? Timeout { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
/// <summary>
|
||||
/// The result of asserting a credential.
|
||||
///
|
||||
/// See: https://www.w3.org/TR/webauthn-2/#publickeycredential
|
||||
/// </summary>
|
||||
public class Fido2ClientAssertCredentialResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Base64url encoding of the credential identifer.
|
||||
/// </summary>
|
||||
public required string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The credential identifier.
|
||||
/// </summary>
|
||||
public required byte[] RawId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The JSON-compatible serialization of client datapassed to the authenticator by the client in
|
||||
/// order to generate this assertion.
|
||||
/// </summary>
|
||||
public required byte[] ClientDataJSON { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The authenticator data returned by the authenticator.
|
||||
/// </summary>
|
||||
public required byte[] AuthenticatorData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The raw signature returned from the authenticator.
|
||||
/// </summary>
|
||||
public required byte[] Signature { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user handle returned from the authenticator, or null if the authenticator did not
|
||||
/// return a user handle.
|
||||
/// </summary>
|
||||
public byte[]? UserHandle { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
/// <summary>
|
||||
/// This class represents an authenticator's response to a client's request for generation of a
|
||||
/// new authentication assertion given the WebAuthn Relying Party's challenge.
|
||||
/// This response contains a cryptographic signature proving possession of the credential private key,
|
||||
/// and optionally evidence of user consent to a specific transaction.
|
||||
///
|
||||
/// See: https://www.w3.org/TR/webauthn-2/#iface-authenticatorassertionresponse
|
||||
/// </summary>
|
||||
public class Fido2ClientAuthenticatorAssertionResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The JSON-compatible serialization of client data passed to the authenticator by the client
|
||||
/// in order to generate this assertion. The exact JSON serialization MUST be preserved, as the
|
||||
/// hash of the serialized client data has been computed over it.
|
||||
/// </summary>
|
||||
public required byte[] ClientDataJSON { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The authenticator data returned by the authenticator.
|
||||
/// </summary>
|
||||
public required byte[] AuthenticatorData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raw signature returned from the authenticator.
|
||||
/// </summary>
|
||||
public required byte[] Signature { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user handle returned from the authenticator, or null if the authenticator did not return a user handle.
|
||||
/// </summary>
|
||||
public byte[] UserHandle { get; set; } = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Parameters for creating a new credential.
|
||||
/// </summary>
|
||||
public class Fido2ClientCreateCredentialParams
|
||||
{
|
||||
/// <summary>
|
||||
/// The Relaying Parties origin, see: https://html.spec.whatwg.org/multipage/browsers.html#concept-origin
|
||||
/// </summary>
|
||||
public required string Origin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A value which is true if and only if the caller’s environment settings object is same-origin with its ancestors.
|
||||
/// It is false if caller is cross-origin.
|
||||
/// </summary>
|
||||
public bool SameOriginWithAncestors { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Relying Party's preference for attestation conveyance
|
||||
/// </summary>
|
||||
public string? Attestation { get; set; } = "none";
|
||||
|
||||
/// <summary>
|
||||
/// The Relying Party's requirements of the authenticator used in the creation of the credential.
|
||||
/// </summary>
|
||||
public AuthenticatorSelectionCriteria? AuthenticatorSelection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Challenge intended to be used for generating the newly created credential's attestation object.
|
||||
/// </summary>
|
||||
public required byte[] Challenge { get; set; } // base64url encoded
|
||||
|
||||
/// <summary>
|
||||
/// This member is intended for use by Relying Parties that wish to limit the creation of multiple credentials for
|
||||
/// the same account on a single authenticator. The client is requested to return an error if the new credential would
|
||||
/// be created on an authenticator that also contains one of the credentials enumerated in this parameter.
|
||||
/// </summary>
|
||||
public PublicKeyCredentialDescriptor[]? ExcludeCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This member contains additional parameters requesting additional processing by the client and authenticator.
|
||||
/// Not currently supported.
|
||||
/// </summary>
|
||||
public object? Extensions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This member contains information about the desired properties of the credential to be created.
|
||||
/// The sequence is ordered from most preferred to least preferred.
|
||||
/// The client makes a best-effort to create the most preferred credential that it can.
|
||||
/// </summary>
|
||||
public required PublicKeyCredentialParameters[] PubKeyCredParams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Data about the Relying Party responsible for the request.
|
||||
/// </summary>
|
||||
public required PublicKeyCredentialRpEntity Rp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Data about the user account for which the Relying Party is requesting attestation.
|
||||
/// </summary>
|
||||
public required PublicKeyCredentialUserEntity User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This member specifies a time, in milliseconds, that the caller is willing to wait for the call to complete.
|
||||
/// This is treated as a hint, and MAY be overridden by the client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is not currently supported.
|
||||
/// </remarks>
|
||||
public int? Timeout { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
/// <summary>
|
||||
/// The result of creating a new credential.
|
||||
///
|
||||
/// This class is an extended version of the WebAuthn struct:
|
||||
/// https://www.w3.org/TR/webauthn-3/#credentialcreationdata-attestationobjectresult
|
||||
/// </summary>
|
||||
public class Fido2ClientCreateCredentialResult
|
||||
{
|
||||
public byte[] CredentialId { get; set; }
|
||||
public byte[] ClientDataJSON { get; set; }
|
||||
public byte[] AttestationObject { get; set; }
|
||||
public byte[] AuthData { get; set; }
|
||||
public byte[] PublicKey { get; set; }
|
||||
public int PublicKeyAlgorithm { get; set; }
|
||||
public string[] Transports { get; set; }
|
||||
}
|
||||
}
|
||||
25
src/Core/Utilities/Fido2/Fido2ClientException.cs
Normal file
25
src/Core/Utilities/Fido2/Fido2ClientException.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class Fido2ClientException : Exception
|
||||
{
|
||||
public enum ErrorCode
|
||||
{
|
||||
NotAllowedError,
|
||||
TypeError,
|
||||
SecurityError,
|
||||
UriBlockedError,
|
||||
NotSupportedError,
|
||||
InvalidStateError,
|
||||
UnknownError
|
||||
}
|
||||
|
||||
public ErrorCode Code { get; }
|
||||
public string Reason { get; }
|
||||
|
||||
public Fido2ClientException(ErrorCode code, string reason) : base($"{code} ({reason})")
|
||||
{
|
||||
Code = code;
|
||||
Reason = reason;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/Core/Utilities/Fido2/Fido2DomainUtils.cs
Normal file
40
src/Core/Utilities/Fido2/Fido2DomainUtils.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class Fido2DomainUtils
|
||||
{
|
||||
// Loosely based on:
|
||||
// https://html.spec.whatwg.org/multipage/browsers.html#is-a-registrable-domain-suffix-of-or-is-equal-to
|
||||
public static bool IsValidRpId(string rpId, string origin)
|
||||
{
|
||||
if (rpId == null || rpId == "" || origin == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// We only care about the domain part of the origin, not the protocol or port so we remove them here,
|
||||
// while still keeping ipv6 intact.
|
||||
// https is enforced in the client, so we don't need to worry about that here
|
||||
var originWithoutProtocolOrPort = Regex.Replace(origin, @"(https?://)?([^:/]+)(:\d+)?(/.*)?", "$2$4");
|
||||
if (Uri.CheckHostName(rpId) != UriHostNameType.Dns || Uri.CheckHostName(originWithoutProtocolOrPort) != UriHostNameType.Dns)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rpId == originWithoutProtocolOrPort)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!DomainName.TryParse(rpId, out var parsedRpId) || !DomainName.TryParse(originWithoutProtocolOrPort, out var parsedOrgin))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return parsedOrgin.Tld == parsedRpId.Tld &&
|
||||
parsedOrgin.Domain == parsedRpId.Domain &&
|
||||
(parsedOrgin.SubDomain == parsedRpId.SubDomain || parsedOrgin.SubDomain.EndsWith(parsedRpId.SubDomain));
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/Core/Utilities/Fido2/Fido2GetAssertionUserInterface.cs
Normal file
52
src/Core/Utilities/Fido2/Fido2GetAssertionUserInterface.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Bit.Core.Abstractions;
|
||||
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
/// <summary>
|
||||
/// This implementation is used when all interactions are handled by the operating system.
|
||||
/// Most often the user has already picked a credential by the time the Authenticator is called,
|
||||
/// so this class just returns those values.
|
||||
///
|
||||
/// This class has no corresponding attestation variant, because that operation requires that the
|
||||
/// user interacts with the app directly.
|
||||
/// </summary>
|
||||
public class Fido2GetAssertionUserInterface : IFido2GetAssertionUserInterface
|
||||
{
|
||||
private readonly string _cipherId;
|
||||
private readonly bool _userVerified = false;
|
||||
private readonly Func<Task> _ensureUnlockedVaultCallback;
|
||||
private readonly Func<Task<bool>> _verifyUserCallback;
|
||||
|
||||
/// <param name="cipherId">The cipherId for the credential that the user has already picker</param>
|
||||
/// <param name="userVerified">True if the user has already been verified by the operating system</param>
|
||||
public Fido2GetAssertionUserInterface(string cipherId, bool userVerified, Func<Task> ensureUnlockedVaultCallback, Func<Task<bool>> verifyUserCallback)
|
||||
{
|
||||
_cipherId = cipherId;
|
||||
_userVerified = userVerified;
|
||||
_ensureUnlockedVaultCallback = ensureUnlockedVaultCallback;
|
||||
_verifyUserCallback = verifyUserCallback;
|
||||
}
|
||||
|
||||
public async Task<(string CipherId, bool UserVerified)> PickCredentialAsync(Fido2GetAssertionUserInterfaceCredential[] credentials)
|
||||
{
|
||||
if (credentials.Length == 0 || !credentials.Any(c => c.CipherId == _cipherId))
|
||||
{
|
||||
throw new NotAllowedError();
|
||||
}
|
||||
|
||||
var credential = credentials.First(c => c.CipherId == _cipherId);
|
||||
var verified = _userVerified;
|
||||
if (credential.RequireUserVerification && !verified)
|
||||
{
|
||||
verified = await _verifyUserCallback();
|
||||
}
|
||||
|
||||
return (CipherId: _cipherId, UserVerified: verified);
|
||||
}
|
||||
|
||||
public Task EnsureUnlockedVaultAsync()
|
||||
{
|
||||
return _ensureUnlockedVaultCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class PublicKeyCredentialAlgorithmDescriptor {
|
||||
public byte[] Id {get; set;}
|
||||
public string[] Transports;
|
||||
public string Type;
|
||||
public int Algorithm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class PublicKeyCredentialDescriptor {
|
||||
public byte[] Id { get; set; }
|
||||
public string[] Transports { get; set; }
|
||||
public string Type { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
15
src/Core/Utilities/Fido2/PublicKeyCredentialParameters.cs
Normal file
15
src/Core/Utilities/Fido2/PublicKeyCredentialParameters.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
/// <summary>
|
||||
/// A description of a key type and algorithm.
|
||||
///</example>
|
||||
public class PublicKeyCredentialParameters
|
||||
{
|
||||
public string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cose algorithm identifier, e.g. -7 for ES256.
|
||||
/// </summary>
|
||||
public int Alg { get; set; }
|
||||
}
|
||||
}
|
||||
9
src/Core/Utilities/Fido2/PublicKeyCredentialRpEntity.cs
Normal file
9
src/Core/Utilities/Fido2/PublicKeyCredentialRpEntity.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class PublicKeyCredentialRpEntity
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class PublicKeyCredentialUserEntity {
|
||||
public byte[] Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string DisplayName { get; set; }
|
||||
public string Icon { get; set; }
|
||||
}
|
||||
}
|
||||
70
src/Core/Utilities/GuidExtensions.cs
Normal file
70
src/Core/Utilities/GuidExtensions.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Bit.Core.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for converting between standard and raw GUID formats.
|
||||
///
|
||||
/// Note: Not optimized for performance. Don't use in performance-critical code.
|
||||
/// </summary>
|
||||
public static class GuidExtensions
|
||||
{
|
||||
public static byte[] GuidToRawFormat(this string guidString)
|
||||
{
|
||||
if (guidString == null)
|
||||
{
|
||||
throw new ArgumentException("GUID parameter is null", nameof(guidString));
|
||||
}
|
||||
|
||||
if (!IsValidGuid(guidString)) {
|
||||
throw new FormatException("GUID parameter is invalid");
|
||||
}
|
||||
|
||||
var arr = new byte[16];
|
||||
|
||||
arr[0] = byte.Parse(guidString.Substring(0, 2), NumberStyles.HexNumber); // Parse ##......-....-....-....-............
|
||||
arr[1] = byte.Parse(guidString.Substring(2, 2), NumberStyles.HexNumber); // Parse ..##....-....-....-....-............
|
||||
arr[2] = byte.Parse(guidString.Substring(4, 2), NumberStyles.HexNumber); // Parse ....##..-....-....-....-............
|
||||
arr[3] = byte.Parse(guidString.Substring(6, 2), NumberStyles.HexNumber); // Parse ......##-....-....-....-............
|
||||
|
||||
arr[4] = byte.Parse(guidString.Substring(9, 2), NumberStyles.HexNumber); // Parse ........-##..-....-....-............
|
||||
arr[5] = byte.Parse(guidString.Substring(11, 2), NumberStyles.HexNumber); // Parse ........-..##-....-....-............
|
||||
|
||||
arr[6] = byte.Parse(guidString.Substring(14, 2), NumberStyles.HexNumber); // Parse ........-....-##..-....-............
|
||||
arr[7] = byte.Parse(guidString.Substring(16, 2), NumberStyles.HexNumber); // Parse ........-....-..##-....-............
|
||||
|
||||
arr[8] = byte.Parse(guidString.Substring(19, 2), NumberStyles.HexNumber); // Parse ........-....-....-##..-............
|
||||
arr[9] = byte.Parse(guidString.Substring(21, 2), NumberStyles.HexNumber); // Parse ........-....-....-..##-............
|
||||
|
||||
arr[10] = byte.Parse(guidString.Substring(24, 2), NumberStyles.HexNumber); // Parse ........-....-....-....-##..........
|
||||
arr[11] = byte.Parse(guidString.Substring(26, 2), NumberStyles.HexNumber); // Parse ........-....-....-....-..##........
|
||||
arr[12] = byte.Parse(guidString.Substring(28, 2), NumberStyles.HexNumber); // Parse ........-....-....-....-....##......
|
||||
arr[13] = byte.Parse(guidString.Substring(30, 2), NumberStyles.HexNumber); // Parse ........-....-....-....-......##....
|
||||
arr[14] = byte.Parse(guidString.Substring(32, 2), NumberStyles.HexNumber); // Parse ........-....-....-....-........##..
|
||||
arr[15] = byte.Parse(guidString.Substring(34, 2), NumberStyles.HexNumber); // Parse ........-....-....-....-..........##
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
public static string GuidToStandardFormat(this byte[] guidBytes)
|
||||
{
|
||||
if (guidBytes == null)
|
||||
{
|
||||
throw new ArgumentException("GUID parameter is null", nameof(guidBytes));
|
||||
}
|
||||
|
||||
if (guidBytes.Length != 16)
|
||||
{
|
||||
throw new ArgumentException("Invalid raw GUID format", nameof(guidBytes));
|
||||
}
|
||||
|
||||
return Convert.ToHexString(guidBytes).ToLower().Insert(8, "-").Insert(13, "-").Insert(18, "-").Insert(23, "-" );
|
||||
}
|
||||
|
||||
public static bool IsValidGuid(string guid)
|
||||
{
|
||||
return Regex.IsMatch(guid, @"^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$", RegexOptions.ECMAScript);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user