From 50f3a7ed90d2df5bd52e9aa68a90f932b420944e Mon Sep 17 00:00:00 2001 From: david-beinder Date: Sat, 14 Oct 2023 23:41:03 +0200 Subject: [PATCH] Add credProtect extension to Fido2.Models --- .../Converters/FidoEnumConverter.cs | 27 +++++++++++----- .../AuthenticationExtensionsClientInputs.cs | 22 +++++++++++++ .../AuthenticationExtensionsClientOutputs.cs | 9 ++++++ .../Objects/CredentialProtectionPolicy.cs | 31 +++++++++++++++++++ Test/Converters/FidoEnumConverterTests.cs | 8 +++++ 5 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 Src/Fido2.Models/Objects/CredentialProtectionPolicy.cs diff --git a/Src/Fido2.Models/Converters/FidoEnumConverter.cs b/Src/Fido2.Models/Converters/FidoEnumConverter.cs index 7dc42480..957fb2d5 100644 --- a/Src/Fido2.Models/Converters/FidoEnumConverter.cs +++ b/Src/Fido2.Models/Converters/FidoEnumConverter.cs @@ -10,15 +10,26 @@ public sealed class FidoEnumConverter<[DynamicallyAccessedMembers(DynamicallyAcc { public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - string text = reader.GetString(); - - if (EnumNameMapper.TryGetValue(reader.GetString(), out T value)) - { - return value; - } - else + switch (reader.TokenType) { - throw new JsonException($"Invalid enum value = {text}"); + case JsonTokenType.String: + string text = reader.GetString(); + if (EnumNameMapper.TryGetValue(text, out T value)) + return value; + else + throw new JsonException($"Invalid enum value = \"{text}\""); + + case JsonTokenType.Number: + if (!reader.TryGetInt32(out var number)) + throw new JsonException($"Invalid enum value = {reader.GetString()}"); + var casted = (T)(object)number; // ints can always be casted to enum, even when the value is not defined + if (Enum.IsDefined(casted)) + return casted; + else + throw new JsonException($"Invalid enum value = {number}"); + + default: + throw new JsonException($"Invalid enum value ({reader.TokenType})"); } } diff --git a/Src/Fido2.Models/Objects/AuthenticationExtensionsClientInputs.cs b/Src/Fido2.Models/Objects/AuthenticationExtensionsClientInputs.cs index 9d89fef6..426a466b 100644 --- a/Src/Fido2.Models/Objects/AuthenticationExtensionsClientInputs.cs +++ b/Src/Fido2.Models/Objects/AuthenticationExtensionsClientInputs.cs @@ -69,5 +69,27 @@ public sealed class AuthenticationExtensionsClientInputs [JsonPropertyName("prf")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public AuthenticationExtensionsPRFInputs? PRF { get; set; } + + /// + /// This registration extension allows relying parties to specify a credential protection policy when creating a credential. + /// Additionally, authenticators MAY choose to establish a default credential protection policy greater than UserVerificationOptional (the lowest level) + /// and unilaterally enforce such policy. Authenticators not supporting some form of user verification MUST NOT support this extension. + /// Authenticators supporting some form of user verification MUST process this extension and persist the credProtect value with the credential, + /// even if the authenticator is not protected by some form of user verification at the time. + /// https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-credProtect-extension + /// + [JsonPropertyName("credentialProtectionPolicy")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public CredentialProtectionPolicy? CredentialProtectionPolicy { get; set; } + + /// + /// This controls whether it is better to fail to create a credential rather than ignore the protection policy. + /// When true, and CredentialProtectionPolicy's value is + /// either UserVerificationOptionalWithCredentialIdList or UserVerificationRequired, the platform + /// SHOULD NOT create the credential in a way that does not implement the requested protection policy. + /// + [JsonPropertyName("enforceCredentialProtectionPolicy")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? EnforceCredentialProtectionPolicy { get; set; } } diff --git a/Src/Fido2.Models/Objects/AuthenticationExtensionsClientOutputs.cs b/Src/Fido2.Models/Objects/AuthenticationExtensionsClientOutputs.cs index 00548c5e..a5fb94b7 100644 --- a/Src/Fido2.Models/Objects/AuthenticationExtensionsClientOutputs.cs +++ b/Src/Fido2.Models/Objects/AuthenticationExtensionsClientOutputs.cs @@ -65,4 +65,13 @@ public class AuthenticationExtensionsClientOutputs [JsonPropertyName("prf")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public AuthenticationExtensionsPRFOutputs? PRF { get; set; } + + + /// + /// The CredentialProtectionPolicy stored alongside the created credential + /// https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-credProtect-extension + /// + [JsonPropertyName("credProtect")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public CredentialProtectionPolicy? CredProtect { get; set; } } diff --git a/Src/Fido2.Models/Objects/CredentialProtectionPolicy.cs b/Src/Fido2.Models/Objects/CredentialProtectionPolicy.cs new file mode 100644 index 00000000..298f3232 --- /dev/null +++ b/Src/Fido2.Models/Objects/CredentialProtectionPolicy.cs @@ -0,0 +1,31 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Fido2NetLib.Objects; + +/// +/// CredentialProtectionPolicy +/// https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-credProtect-extension +/// +[JsonConverter(typeof(FidoEnumConverter))] +public enum CredentialProtectionPolicy +{ + /// + /// This reflects "FIDO_2_0" semantics. In this configuration, performing some form of user verification is OPTIONAL with or without credentialID list. + /// This is the default state of the credential if the extension is not specified + /// + [EnumMember(Value = "userVerificationOptional")] + UserVerificationOptional = 0x01, + + /// + /// In this configuration, credential is discovered only when its credentialID is provided by the platform or when some form of user verification is performed. + /// + [EnumMember(Value = "userVerificationOptionalWithCredentialIDList")] + UserVerificationOptionalWithCredentialIdList = 0x02, + + /// + /// TThis reflects that discovery and usage of the credential MUST be preceded by some form of user verification. + /// + [EnumMember(Value = "userVerificationRequired")] + UserVerificationRequired = 0x03 +} diff --git a/Test/Converters/FidoEnumConverterTests.cs b/Test/Converters/FidoEnumConverterTests.cs index 6b823ae0..e07e691e 100644 --- a/Test/Converters/FidoEnumConverterTests.cs +++ b/Test/Converters/FidoEnumConverterTests.cs @@ -29,6 +29,14 @@ public void CorrectlyFallsBackToMemberName() Assert.Equal(ABC.A, JsonSerializer.Deserialize("\"a\"")); } + [Fact] + public void CorrectlyDeserializesNumericEnumValue() + { + Assert.Equal(CredentialProtectionPolicy.UserVerificationRequired, JsonSerializer.Deserialize($"{CredentialProtectionPolicy.UserVerificationRequired:d}")); + Assert.Throws(() => JsonSerializer.Deserialize($"99")); + Assert.Throws(() => JsonSerializer.Deserialize($"99.7")); + } + [Fact] public void DeserializationIsCaseInsensitive() {