From 8e8baa9679da7348bf2fde06103519a4f003c37c Mon Sep 17 00:00:00 2001 From: Jason Nelson Date: Thu, 7 Nov 2024 18:10:44 -0800 Subject: [PATCH 1/8] Use BCL Base64Url implementation --- Demo/TestController.cs | 5 +- Src/Fido2.Models/Base64Url.cs | 158 ------------------ .../Converters/Base64UrlConverter.cs | 25 ++- Src/Fido2.Models/Fido2.Models.csproj | 6 +- .../AttestationFormat/AndroidSafetyNet.cs | 3 +- .../Metadata/ConformanceMetadataRepository.cs | 5 +- .../Fido2MetadataServiceRepository.cs | 5 +- Src/Fido2/TokenBindingDto.cs | 5 +- 8 files changed, 38 insertions(+), 174 deletions(-) delete mode 100644 Src/Fido2.Models/Base64Url.cs diff --git a/Demo/TestController.cs b/Demo/TestController.cs index eca9ea6b..9dd18034 100644 --- a/Demo/TestController.cs +++ b/Demo/TestController.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Buffers.Text; +using System.Text; using System.Text.Json; using Fido2NetLib; using Fido2NetLib.Development; @@ -42,7 +43,7 @@ public OkObjectResult MakeCredentialOptionsTest([FromBody] TEST_MakeCredentialPa try { - username = Base64Url.Decode(opts.Username); + username = Base64Url.DecodeFromChars(opts.Username); } catch (FormatException) { diff --git a/Src/Fido2.Models/Base64Url.cs b/Src/Fido2.Models/Base64Url.cs deleted file mode 100644 index 2431d0eb..00000000 --- a/Src/Fido2.Models/Base64Url.cs +++ /dev/null @@ -1,158 +0,0 @@ -using System.Buffers; -using System.Buffers.Text; - -namespace Fido2NetLib; - -/// -/// Helper class to handle Base64Url. Based on Carbon.Jose source code. -/// -public static class Base64Url -{ - /// - /// Converts arg data to a Base64Url encoded string. - /// - public static string Encode(ReadOnlySpan arg) - { - int base64Length = (int)(((long)arg.Length + 2) / 3 * 4); - - char[] pooledBuffer = ArrayPool.Shared.Rent(base64Length); - - Convert.TryToBase64Chars(arg, pooledBuffer, out int encodedLength); - - Span base64Url = pooledBuffer.AsSpan(0, encodedLength); - - for (int i = 0; i < base64Url.Length; i++) - { - ref char c = ref base64Url[i]; - - switch (c) - { - case '+': - c = '-'; - break; - case '/': - c = '_'; - break; - } - } - - int equalIndex = base64Url.IndexOf('='); - - if (equalIndex > -1) // remove trailing equal characters - { - base64Url = base64Url.Slice(0, equalIndex); - } - - var result = new string(base64Url); - - ArrayPool.Shared.Return(pooledBuffer, clearArray: true); - - return result; - } - - /// - /// Decodes a Base64Url encoded string to its raw bytes. - /// - public static byte[] Decode(ReadOnlySpan text) - { - int padCharCount = (text.Length % 4) switch - { - 2 => 2, - 3 => 1, - _ => 0 - }; - - int encodedLength = text.Length + padCharCount; - - char[] buffer = ArrayPool.Shared.Rent(encodedLength); - - text.CopyTo(buffer); - - for (int i = 0; i < text.Length; i++) - { - ref char c = ref buffer[i]; - - switch (c) - { - case '-': - c = '+'; - break; - case '_': - c = '/'; - break; - } - } - - if (padCharCount == 1) - { - buffer[encodedLength - 1] = '='; - } - else if (padCharCount == 2) - { - buffer[encodedLength - 1] = '='; - buffer[encodedLength - 2] = '='; - } - - var result = Convert.FromBase64CharArray(buffer, 0, encodedLength); - - ArrayPool.Shared.Return(buffer, true); - - return result; - } - - - /// - /// Decodes a Base64Url encoded string to its raw bytes. - /// - public static byte[] DecodeUtf8(ReadOnlySpan text) - { - int padCharCount = (text.Length % 4) switch - { - 2 => 2, - 3 => 1, - _ => 0 - }; - - int encodedLength = text.Length + padCharCount; - - byte[] buffer = ArrayPool.Shared.Rent(encodedLength); - - text.CopyTo(buffer); - - for (int i = 0; i < text.Length; i++) - { - ref byte c = ref buffer[i]; - - switch ((char)c) - { - case '-': - c = (byte)'+'; - break; - case '_': - c = (byte)'/'; - break; - } - } - - if (padCharCount == 1) - { - buffer[encodedLength - 1] = (byte)'='; - } - else if (padCharCount == 2) - { - buffer[encodedLength - 1] = (byte)'='; - buffer[encodedLength - 2] = (byte)'='; - } - - if (OperationStatus.Done != Base64.DecodeFromUtf8InPlace(buffer.AsSpan(0, encodedLength), out int decodedLength)) - { - throw new FormatException("The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters."); - } - - var result = buffer.AsSpan(0, decodedLength).ToArray(); - - ArrayPool.Shared.Return(buffer, true); - - return result; - } -} diff --git a/Src/Fido2.Models/Converters/Base64UrlConverter.cs b/Src/Fido2.Models/Converters/Base64UrlConverter.cs index 6ae5f52f..46c65500 100644 --- a/Src/Fido2.Models/Converters/Base64UrlConverter.cs +++ b/Src/Fido2.Models/Converters/Base64UrlConverter.cs @@ -1,4 +1,6 @@ -using System.Text.Json; +using System.Buffers; +using System.Buffers.Text; +using System.Text.Json; using System.Text.Json.Serialization; namespace Fido2NetLib; @@ -9,19 +11,30 @@ namespace Fido2NetLib; public sealed class Base64UrlConverter : JsonConverter { public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { + { if (!reader.HasValueSequence) { - return Base64Url.DecodeUtf8(reader.ValueSpan); + return Base64Url.DecodeFromUtf8(reader.ValueSpan); } else { - return Base64Url.Decode(reader.GetString()); - } + return Base64Url.DecodeFromChars(reader.GetString()); + } } public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options) { - writer.WriteStringValue(Base64Url.Encode(value)); + var rentedBuffer = ArrayPool.Shared.Rent(Base64Url.GetEncodedLength(value.Length)); + + try + { + Base64Url.EncodeToUtf8(value, rentedBuffer, out _, out int written); + + writer.WriteStringValue(rentedBuffer.AsSpan(0..written)); + } + finally + { + ArrayPool.Shared.Return(rentedBuffer); + } } } diff --git a/Src/Fido2.Models/Fido2.Models.csproj b/Src/Fido2.Models/Fido2.Models.csproj index aff00326..be4559ee 100644 --- a/Src/Fido2.Models/Fido2.Models.csproj +++ b/Src/Fido2.Models/Fido2.Models.csproj @@ -1,4 +1,4 @@ - + $(SupportedTargetFrameworks) @@ -9,4 +9,8 @@ true + + + + diff --git a/Src/Fido2/AttestationFormat/AndroidSafetyNet.cs b/Src/Fido2/AttestationFormat/AndroidSafetyNet.cs index c4d261e7..ccb45728 100644 --- a/Src/Fido2/AttestationFormat/AndroidSafetyNet.cs +++ b/Src/Fido2/AttestationFormat/AndroidSafetyNet.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers.Text; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; @@ -46,7 +47,7 @@ public override async ValueTask VerifyAsync(VerifyAttes try { - jwtHeaderBytes = Base64Url.Decode(jwtComponents[0]); + jwtHeaderBytes = Base64Url.DecodeFromChars(jwtComponents[0]); } catch (FormatException) { diff --git a/Src/Fido2/Metadata/ConformanceMetadataRepository.cs b/Src/Fido2/Metadata/ConformanceMetadataRepository.cs index ba7ef711..5c477fa2 100644 --- a/Src/Fido2/Metadata/ConformanceMetadataRepository.cs +++ b/Src/Fido2/Metadata/ConformanceMetadataRepository.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers.Text; using System.Collections.Generic; using System.Linq; using System.Net.Http; @@ -132,7 +133,7 @@ public async Task DeserializeAndValidateBlobAsync(string ra throw new Fido2MetadataException("The JWT does not have the 3 expected components"); var blobHeader = jwtParts[0]; - using var jsonDoc = JsonDocument.Parse(Base64Url.Decode(blobHeader)); + using var jsonDoc = JsonDocument.Parse(Base64Url.DecodeFromChars(blobHeader)); var tokenHeader = jsonDoc.RootElement; var blobAlg = tokenHeader.TryGetProperty("alg", out var algEl) @@ -235,7 +236,7 @@ public async Task DeserializeAndValidateBlobAsync(string ra var blobPayload = ((JsonWebToken)validateTokenResult.SecurityToken).EncodedPayload; - MetadataBLOBPayload blob = JsonSerializer.Deserialize(Base64Url.Decode(blobPayload), FidoModelSerializerContext.Default.MetadataBLOBPayload)!; + MetadataBLOBPayload blob = JsonSerializer.Deserialize(Base64Url.DecodeFromChars(blobPayload), FidoModelSerializerContext.Default.MetadataBLOBPayload)!; blob.JwtAlg = blobAlg; return blob; } diff --git a/Src/Fido2/Metadata/Fido2MetadataServiceRepository.cs b/Src/Fido2/Metadata/Fido2MetadataServiceRepository.cs index 0e874117..d4dda950 100644 --- a/Src/Fido2/Metadata/Fido2MetadataServiceRepository.cs +++ b/Src/Fido2/Metadata/Fido2MetadataServiceRepository.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers.Text; using System.Linq; using System.Net.Http; using System.Security.Cryptography; @@ -67,7 +68,7 @@ private async Task DeserializeAndValidateBlobAsync(string r throw new ArgumentException("The JWT does not have the 3 expected components"); var blobHeaderString = jwtParts[0]; - using var blobHeaderDoc = JsonDocument.Parse(Base64Url.Decode(blobHeaderString)); + using var blobHeaderDoc = JsonDocument.Parse(Base64Url.DecodeFromChars(blobHeaderString)); var blobHeader = blobHeaderDoc.RootElement; string blobAlg = blobHeader.TryGetProperty("alg", out var algEl) @@ -186,7 +187,7 @@ private async Task DeserializeAndValidateBlobAsync(string r var blobPayload = ((JsonWebToken)validateTokenResult.SecurityToken).EncodedPayload; - MetadataBLOBPayload blob = JsonSerializer.Deserialize(Base64Url.Decode(blobPayload), FidoModelSerializerContext.Default.MetadataBLOBPayload)!; + MetadataBLOBPayload blob = JsonSerializer.Deserialize(Base64Url.DecodeFromChars(blobPayload), FidoModelSerializerContext.Default.MetadataBLOBPayload)!; blob.JwtAlg = blobAlg; return blob; } diff --git a/Src/Fido2/TokenBindingDto.cs b/Src/Fido2/TokenBindingDto.cs index 3651b301..6238940c 100644 --- a/Src/Fido2/TokenBindingDto.cs +++ b/Src/Fido2/TokenBindingDto.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System.Buffers.Text; +using System.Text.Json.Serialization; namespace Fido2NetLib; public class TokenBindingDto @@ -25,7 +26,7 @@ public void Verify(byte[]? requestTokenbinding) case "present": if (string.IsNullOrEmpty(Id)) throw new Fido2VerificationException("TokenBinding status was present but Id is missing"); - var b64 = Base64Url.Encode(requestTokenbinding); + var b64 = Base64Url.EncodeToString(requestTokenbinding); if (Id != b64) throw new Fido2VerificationException("Tokenbinding Id does not match"); break; From d75c2932ca4dea4cd061ab816db311a21dc6bee7 Mon Sep 17 00:00:00 2001 From: Jason Nelson Date: Thu, 7 Nov 2024 18:11:39 -0800 Subject: [PATCH 2/8] [Testing] React to Base64Url changes (encoder is now strict, and no longer supports Base64) --- .../Attestation/AndroidSafetyNet.cs | 19 ++-- Tests/Fido2.Tests/Attestation/Apple.cs | 8 +- Tests/Fido2.Tests/AuthenticatorResponse.cs | 106 +++++++++--------- Tests/Fido2.Tests/Base64UrlTest.cs | 49 -------- .../ExistingU2fRegistrationDataTests.cs | 15 +-- Tests/Fido2.Tests/Fido2Tests.cs | 17 +-- Tests/Fido2.Tests/MockClientData.cs | 16 +++ .../TestFiles/attestationOptionsNone.json | 8 +- .../TestFiles/attestationOptionsPacked.json | 31 ++--- .../attestationOptionsTrustKeyT110.json | 8 +- .../TestFiles/attestationOptionsU2F.json | 8 +- Tests/Fido2.Tests/TestFiles/options1.json | 2 +- Tests/Fido2.Tests/TestFiles/options2.json | 2 +- 13 files changed, 130 insertions(+), 159 deletions(-) delete mode 100644 Tests/Fido2.Tests/Base64UrlTest.cs create mode 100644 Tests/Fido2.Tests/MockClientData.cs diff --git a/Tests/Fido2.Tests/Attestation/AndroidSafetyNet.cs b/Tests/Fido2.Tests/Attestation/AndroidSafetyNet.cs index 0d290e90..1f950aaa 100644 --- a/Tests/Fido2.Tests/Attestation/AndroidSafetyNet.cs +++ b/Tests/Fido2.Tests/Attestation/AndroidSafetyNet.cs @@ -1,4 +1,5 @@ -using System.Security.Claims; +using System.Buffers.Text; +using System.Security.Claims; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; @@ -278,9 +279,9 @@ public async Task TestAndroidSafetyNetResponseJWTMissingX5c() { var response = (byte[])_attestationObject["attStmt"]["response"]; var jwtParts = Encoding.UTF8.GetString(response).Split('.'); - var jwtHeaderJSON = JObject.Parse(Encoding.UTF8.GetString(Base64Url.Decode(jwtParts.First()))); + var jwtHeaderJSON = JObject.Parse(Encoding.UTF8.GetString(Base64Url.DecodeFromChars(jwtParts.First()))); jwtHeaderJSON.Remove("x5c"); - jwtParts[0] = Base64Url.Encode(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(jwtHeaderJSON))); + jwtParts[0] = Base64Url.EncodeToString(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(jwtHeaderJSON))); response = Encoding.UTF8.GetBytes(string.Join(".", jwtParts)); var attStmt = (CborMap)_attestationObject["attStmt"]; attStmt.Set("response", new CborByteString(response)); @@ -293,10 +294,10 @@ public async Task TestAndroidSafetyNetResponseJWTX5cNoKeys() { var response = (byte[])_attestationObject["attStmt"]["response"]; var jwtParts = Encoding.UTF8.GetString(response).Split('.'); - var jwtHeaderJSON = JObject.Parse(Encoding.UTF8.GetString(Base64Url.Decode(jwtParts.First()))); + var jwtHeaderJSON = JObject.Parse(Encoding.UTF8.GetString(Base64Url.DecodeFromChars(jwtParts.First()))); jwtHeaderJSON.Remove("x5c"); jwtHeaderJSON.Add("x5c", JToken.FromObject(new List { })); - jwtParts[0] = Base64Url.Encode(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(jwtHeaderJSON))); + jwtParts[0] = Base64Url.EncodeToString(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(jwtHeaderJSON))); response = Encoding.UTF8.GetBytes(string.Join(".", jwtParts)); var attStmt = (CborMap)_attestationObject["attStmt"]; attStmt.Set("response", new CborByteString(response)); @@ -309,10 +310,10 @@ public async Task TestAndroidSafetyNetResponseJWTX5cInvalidString() { var response = (byte[])_attestationObject["attStmt"]["response"]; var jwtParts = Encoding.UTF8.GetString(response).Split('.'); - var jwtHeaderJSON = JObject.Parse(Encoding.UTF8.GetString(Base64Url.Decode(jwtParts.First()))); + var jwtHeaderJSON = JObject.Parse(Encoding.UTF8.GetString(Base64Url.DecodeFromChars(jwtParts.First()))); jwtHeaderJSON.Remove("x5c"); jwtHeaderJSON.Add("x5c", JToken.FromObject(new List { "RjFEMA==" })); - jwtParts[0] = Base64Url.Encode(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(jwtHeaderJSON))); + jwtParts[0] = Base64Url.EncodeToString(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(jwtHeaderJSON))); response = Encoding.UTF8.GetBytes(string.Join(".", jwtParts)); var attStmt = (CborMap)_attestationObject["attStmt"]; attStmt.Set("response", new CborByteString(response)); @@ -325,7 +326,7 @@ public async Task TestAndroidSafetyNetJwtInvalid() { var response = (byte[])_attestationObject["attStmt"]["response"]; var jwtParts = Encoding.UTF8.GetString(response).Split('.'); - var jwtHeaderJSON = JObject.Parse(Encoding.UTF8.GetString(Base64Url.Decode(jwtParts.First()))); + var jwtHeaderJSON = JObject.Parse(Encoding.UTF8.GetString(Base64Url.DecodeFromChars(jwtParts.First()))); jwtHeaderJSON.Remove("x5c"); byte[] x5c = null; using (var ecdsaAtt = ECDsa.Create()) @@ -341,7 +342,7 @@ public async Task TestAndroidSafetyNetJwtInvalid() } jwtHeaderJSON.Add("x5c", JToken.FromObject(new List { Convert.ToBase64String(x5c) })); - jwtParts[0] = Base64Url.Encode(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(jwtHeaderJSON))); + jwtParts[0] = Base64Url.EncodeToString(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(jwtHeaderJSON))); response = Encoding.UTF8.GetBytes(string.Join(".", jwtParts)); var attStmt = (CborMap)_attestationObject["attStmt"]; attStmt.Set("response", new CborByteString(response)); diff --git a/Tests/Fido2.Tests/Attestation/Apple.cs b/Tests/Fido2.Tests/Attestation/Apple.cs index 6bcc7267..06a8d704 100644 --- a/Tests/Fido2.Tests/Attestation/Apple.cs +++ b/Tests/Fido2.Tests/Attestation/Apple.cs @@ -200,11 +200,11 @@ public async Task TestApplePublicKeyMismatch() var authData = new AuthenticatorData(_rpIdHash, _flags, _signCount, _acd, GetExtensions()).ToByteArray(); _attestationObject.Set("authData", new CborByteString(authData)); - var clientData = new + var clientData = new MockClientData { - type = "webauthn.create", - challenge = _challenge, - origin = "https://www.passwordless.dev", + Type = "webauthn.create", + Challenge = _challenge, + Origin = "https://www.passwordless.dev", }; var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(clientData); diff --git a/Tests/Fido2.Tests/AuthenticatorResponse.cs b/Tests/Fido2.Tests/AuthenticatorResponse.cs index de9c0af5..cfb4e5ea 100644 --- a/Tests/Fido2.Tests/AuthenticatorResponse.cs +++ b/Tests/Fido2.Tests/AuthenticatorResponse.cs @@ -1,4 +1,5 @@ -using System.Formats.Cbor; +using System.Buffers.Text; +using System.Formats.Cbor; using System.Security.Cryptography; using System.Text; using System.Text.Json; @@ -21,7 +22,7 @@ public void CanDeserialize() var response = JsonSerializer.Deserialize("""{"type":"webauthn.get","challenge":"J4fjxBV-BNywGRJRm8JZ7znvdiZo9NINObNBpnKnJQEOtplTMF0ERuIrzrkeoO-dNMoeMZjhzqfar7eWRANvPeNFPrB5Q6zlS1ZFPf37F3suIwpXi9NCpFA_RlBSiygLmvcIOa57_QHubZQD3cv0UWtRTLslJjmgumphMc7EFN8","origin":"https://www.passwordless.dev"}"""); Assert.Equal("webauthn.get", response.Type); - Assert.Equal(Base64Url.Decode("J4fjxBV-BNywGRJRm8JZ7znvdiZo9NINObNBpnKnJQEOtplTMF0ERuIrzrkeoO-dNMoeMZjhzqfar7eWRANvPeNFPrB5Q6zlS1ZFPf37F3suIwpXi9NCpFA_RlBSiygLmvcIOa57_QHubZQD3cv0UWtRTLslJjmgumphMc7EFN8"), response.Challenge); + Assert.Equal(Base64Url.DecodeFromChars("J4fjxBV-BNywGRJRm8JZ7znvdiZo9NINObNBpnKnJQEOtplTMF0ERuIrzrkeoO-dNMoeMZjhzqfar7eWRANvPeNFPrB5Q6zlS1ZFPf37F3suIwpXi9NCpFA_RlBSiygLmvcIOa57_QHubZQD3cv0UWtRTLslJjmgumphMc7EFN8"), response.Challenge); Assert.Equal("https://www.passwordless.dev", response.Origin); } @@ -65,11 +66,11 @@ public async Task TestAuthenticatorOriginsAsync(string origin, string expectedOr acd ).ToByteArray(); - byte[] clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new + byte[] clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new MockClientData { - type = "webauthn.create", - challenge = challenge, - origin = rp + Type = "webauthn.create", + Challenge = challenge, + Origin = rp }); var rawResponse = new AuthenticatorAttestationRawResponse { @@ -170,11 +171,11 @@ public async Task TestAuthenticatorOriginsFail(string origin, string expectedOri 0, acd ).ToByteArray(); - var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new + var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new MockClientData { - type = "webauthn.create", - challenge = challenge, - origin = rp + Type = "webauthn.create", + Challenge = challenge, + Origin = rp }); var rawResponse = new AuthenticatorAttestationRawResponse @@ -242,7 +243,7 @@ public async Task TestAuthenticatorOriginsFail(string origin, string expectedOri public void TestAuthenticatorAttestationRawResponse() { var challenge = RandomNumberGenerator.GetBytes(128); - var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new + var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new MockClientData { Type = "webauthn.create", Challenge = challenge, @@ -384,7 +385,7 @@ public async Task TestAuthenticatorAttestationResponseInvalidType() { var challenge = RandomNumberGenerator.GetBytes(128); var rp = "https://www.passwordless.dev"; - var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new + var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new MockClientData { Type = "webauthn.get", Challenge = challenge, @@ -459,11 +460,11 @@ public async Task TestAuthenticatorAttestationResponseInvalidRawId(byte[] value) { var challenge = RandomNumberGenerator.GetBytes(128); var rp = "https://www.passwordless.dev"; - byte[] clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new + byte[] clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new MockClientData { - type = "webauthn.create", - challenge = challenge, - origin = rp, + Type = "webauthn.create", + Challenge = challenge, + Origin = rp, }); var rawResponse = new AuthenticatorAttestationRawResponse @@ -532,11 +533,11 @@ public async Task TestAuthenticatorAttestationResponseInvalidRawType() { var challenge = RandomNumberGenerator.GetBytes(128); var rp = "https://www.passwordless.dev"; - var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new + var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new MockClientData { - type = "webauthn.create", - challenge = challenge, - origin = rp, + Type = "webauthn.create", + Challenge = challenge, + Origin = rp, }); var rawResponse = new AuthenticatorAttestationRawResponse @@ -612,11 +613,11 @@ public async Task TestAuthenticatorAttestationResponseRpidMismatch() null ).ToByteArray(); - var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new + var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new MockClientData { - type = "webauthn.create", - challenge = challenge, - origin = rp, + Type = "webauthn.create", + Challenge = challenge, + Origin = rp }); var rawResponse = new AuthenticatorAttestationRawResponse @@ -693,11 +694,11 @@ public async Task TestAuthenticatorAttestationResponseNotUserPresentAsync() null ).ToByteArray(); - var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new + var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new MockClientData { - type = "webauthn.create", - challenge = challenge, - origin = rp + Type = "webauthn.create", + Challenge = challenge, + Origin = rp }); var rawResponse = new AuthenticatorAttestationRawResponse @@ -776,11 +777,11 @@ public async Task TestAuthenticatorAttestationResponseBackupEligiblePolicyRequir null ).ToByteArray(); - var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new + var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new MockClientData { - type = "webauthn.create", - challenge = challenge, - origin = rp, + Type = "webauthn.create", + Challenge = challenge, + Origin = rp, }); var rawResponse = new AuthenticatorAttestationRawResponse @@ -857,11 +858,10 @@ public async Task TestAuthenticatorAttestationResponseBackupEligiblePolicyDisall null ).ToByteArray(); - var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new - { - type = "webauthn.create", - challenge = challenge, - origin = rp, + var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new MockClientData { + Type = "webauthn.create", + Challenge = challenge, + Origin = rp, }); var rawResponse = new AuthenticatorAttestationRawResponse @@ -938,11 +938,11 @@ public async Task TestAuthenticatorAttestationResponseNoAttestedCredentialData() null ).ToByteArray(); - var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new + var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new MockClientData { - type = "webauthn.create", - challenge = challenge, - origin = rp, + Type = "webauthn.create", + Challenge = challenge, + Origin = rp, }); var rawResponse = new AuthenticatorAttestationRawResponse @@ -1019,11 +1019,11 @@ public async Task TestAuthenticatorAttestationResponseUnknownAttestationType() acd ).ToByteArray(); - var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new + var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new MockClientData { - type = "webauthn.create", - challenge = challenge, - origin = rp, + Type = "webauthn.create", + Challenge = challenge, + Origin = rp }); var rawResponse = new AuthenticatorAttestationRawResponse @@ -1100,11 +1100,11 @@ public async Task TestAuthenticatorAttestationResponseNotUniqueCredId() 0, acd ).ToByteArray(); - var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new + var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new MockClientData { - type = "webauthn.create", - challenge = challenge, - origin = rp, + Type = "webauthn.create", + Challenge = challenge, + Origin = rp }); var rawResponse = new AuthenticatorAttestationRawResponse @@ -1180,11 +1180,11 @@ public async Task TestAuthenticatorAttestationResponseUVRequired() 0, acd ).ToByteArray(); - var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new + var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new MockClientData { - type = "webauthn.create", - challenge = challenge, - origin = rp, + Type = "webauthn.create", + Challenge = challenge, + Origin = rp }); var rawResponse = new AuthenticatorAttestationRawResponse diff --git a/Tests/Fido2.Tests/Base64UrlTest.cs b/Tests/Fido2.Tests/Base64UrlTest.cs deleted file mode 100644 index 30518149..00000000 --- a/Tests/Fido2.Tests/Base64UrlTest.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Text; - -using Fido2NetLib; - -namespace fido2_net_lib.Test; - -public class Base64UrlTest -{ - [Theory] - [MemberData(nameof(GetData))] - public void EncodeAndDecodeResultsAreEqual(byte[] data) - { - // Act - var encodedString = Base64Url.Encode(data); - var decodedBytes = Base64Url.Decode(encodedString); - - // Assert - Assert.Equal(data, decodedBytes); - - // Ensure this also works with the Utf8 decoder - Assert.Equal(data, Base64Url.DecodeUtf8(Encoding.UTF8.GetBytes(encodedString))); - } - - public static IEnumerable GetData() - { - return new TestDataGenerator(); - } - - private class TestDataGenerator : TheoryData - { - public TestDataGenerator() - { - Add("A"u8.ToArray()); - Add("This is a string fragment to test Base64Url encoding & decoding."u8.ToArray()); - Add(Array.Empty()); - } - } - - [Fact] - public static void Format_BadBase64Char() - { - const string Format_BadBase64Char = "The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters."; - var ex = Assert.Throws(() => Base64Url.Decode("rCQqQMqKVO/geUyc9aENh85Mt2g1JHAUKUG27WZVE68===")); - Assert.Equal(Format_BadBase64Char, ex.Message); - - ex = Assert.Throws(() => Base64Url.DecodeUtf8(Encoding.UTF8.GetBytes("rCQqQMqKVO/geUyc9aENh85Mt2g1JHAUKUG27WZVE68==="))); - Assert.Equal(Format_BadBase64Char, ex.Message); - } -} diff --git a/Tests/Fido2.Tests/ExistingU2fRegistrationDataTests.cs b/Tests/Fido2.Tests/ExistingU2fRegistrationDataTests.cs index d031fbc1..6097498e 100644 --- a/Tests/Fido2.Tests/ExistingU2fRegistrationDataTests.cs +++ b/Tests/Fido2.Tests/ExistingU2fRegistrationDataTests.cs @@ -1,4 +1,5 @@ -using System.Security.Cryptography; +using System.Buffers.Text; +using System.Security.Cryptography; using Fido2NetLib; using Fido2NetLib.Cbor; @@ -13,15 +14,15 @@ public async Task TestFido2AssertionWithExistingU2fRegistrationWithAppId() { // u2f registration with appId var appId = "https://localhost:44336"; - var keyHandleData = Base64Url.Decode("2uzGTqu9XGoDQpRBhkv3qDYWzEEZrDjOHT94fHe3J9VXl6KpaY6jL1C4gCAVSBCWZejOn-EYSyXfiG7RDQqgKw"); - var publicKeyData = Base64Url.Decode("BEKJkJiDzo8wlrYbAHmyz5a5vShbkStO58ZO7F-hy4fvBp6TowCZoV2dNGcxIN1yT18799bb_WuP0Yq_DSv5a-U"); + var keyHandleData = Base64Url.DecodeFromChars("2uzGTqu9XGoDQpRBhkv3qDYWzEEZrDjOHT94fHe3J9VXl6KpaY6jL1C4gCAVSBCWZejOn-EYSyXfiG7RDQqgKw"); + var publicKeyData = Base64Url.DecodeFromChars("BEKJkJiDzo8wlrYbAHmyz5a5vShbkStO58ZO7F-hy4fvBp6TowCZoV2dNGcxIN1yT18799bb_WuP0Yq_DSv5a-U"); //key as cbor var publicKey = CreatePublicKeyFromU2fRegistrationData(keyHandleData, publicKeyData); var options = new AssertionOptions { - Challenge = Base64Url.Decode("mNxQVDWI8+ahBXeQJ8iS4jk5pDUd5KetZGVOwSkw2X0"), + Challenge = Convert.FromBase64String("mNxQVDWI8+ahBXeQJ8iS4jk5pDUd5KetZGVOwSkw2X0="), RpId = "localhost", AllowCredentials = new[] { @@ -44,9 +45,9 @@ public async Task TestFido2AssertionWithExistingU2fRegistrationWithAppId() }, Response = new AuthenticatorAssertionRawResponse.AssertionResponse { - AuthenticatorData = Base64Url.Decode("B6_fPoU4uitIYRHXuNfpbqt5mrDWz8cp7d1noAUrAucBAAABTQ"), - ClientDataJson = Base64Url.Decode("eyJjaGFsbGVuZ2UiOiJtTnhRVkRXSTgtYWhCWGVRSjhpUzRqazVwRFVkNUtldFpHVk93U2t3MlgwIiwib3JpZ2luIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzMzYiLCJ0eXBlIjoid2ViYXV0aG4uZ2V0In0"), - Signature = Base64Url.Decode("MEQCICHV36RVY9DdFmKZgxF5Z_yScpPPsKcj__8KcPmngtGHAiAq_SzmTY8rZz4-5uNNiz3h6xO9osNTh7O7Mjqtoxul8w") + AuthenticatorData = Base64Url.DecodeFromChars("B6_fPoU4uitIYRHXuNfpbqt5mrDWz8cp7d1noAUrAucBAAABTQ"), + ClientDataJson = Base64Url.DecodeFromChars("eyJjaGFsbGVuZ2UiOiJtTnhRVkRXSTgtYWhCWGVRSjhpUzRqazVwRFVkNUtldFpHVk93U2t3MlgwIiwib3JpZ2luIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzMzYiLCJ0eXBlIjoid2ViYXV0aG4uZ2V0In0"), + Signature = Base64Url.DecodeFromChars("MEQCICHV36RVY9DdFmKZgxF5Z_yScpPPsKcj__8KcPmngtGHAiAq_SzmTY8rZz4-5uNNiz3h6xO9osNTh7O7Mjqtoxul8w") } }; diff --git a/Tests/Fido2.Tests/Fido2Tests.cs b/Tests/Fido2.Tests/Fido2Tests.cs index cfe2ed45..6368b790 100644 --- a/Tests/Fido2.Tests/Fido2Tests.cs +++ b/Tests/Fido2.Tests/Fido2Tests.cs @@ -113,11 +113,11 @@ public byte[] _clientDataJson { get { - return JsonSerializer.SerializeToUtf8Bytes(new + return JsonSerializer.SerializeToUtf8Bytes(new MockClientData { - type = "webauthn.create", - challenge = _challenge, - origin = rp + Type = "webauthn.create", + Challenge = _challenge, + Origin = rp }); } } @@ -947,12 +947,13 @@ internal static async Task MakeAssertionResponseAsync( var challenge = new byte[128]; RandomNumberGenerator.Fill(challenge); - var clientData = new + var clientData = new MockClientData { - type = "webauthn.get", - challenge = challenge, - origin = rp, + Type = "webauthn.get", + Challenge = challenge, + Origin = rp }; + var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(clientData); var hashedClientDataJson = SHA256.HashData(clientDataJson); diff --git a/Tests/Fido2.Tests/MockClientData.cs b/Tests/Fido2.Tests/MockClientData.cs new file mode 100644 index 00000000..98dad377 --- /dev/null +++ b/Tests/Fido2.Tests/MockClientData.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace Fido2NetLib; + +public sealed class MockClientData +{ + [JsonPropertyName("type")] + public required string Type { get; set; } + + [JsonConverter(typeof(Base64UrlConverter))] + [JsonPropertyName("challenge")] + public required byte[] Challenge { get; set; } + + [JsonPropertyName("origin")] + public required string Origin { get; set; } +} diff --git a/Tests/Fido2.Tests/TestFiles/attestationOptionsNone.json b/Tests/Fido2.Tests/TestFiles/attestationOptionsNone.json index 5a2e9f31..e9aa861b 100644 --- a/Tests/Fido2.Tests/TestFiles/attestationOptionsNone.json +++ b/Tests/Fido2.Tests/TestFiles/attestationOptionsNone.json @@ -4,10 +4,10 @@ "rp":{ "name":"localhost", "id":"localhost"}, - "user":{ - "name":"aseigler", - "id":"ABC", - "displayName":"aseigler" + "user": { + "name": "aseigler", + "id": "QUJD", + "displayName": "aseigler" }, "challenge":"-8OfJQc6LzKXCIdLZeD27v-ZLUhjTANxzqMjUNMlCmTKGYYK54hQ56AqAjlHotHll688d0ZRM6_L8lGOq3CZNw", "pubKeyCredParams":[ diff --git a/Tests/Fido2.Tests/TestFiles/attestationOptionsPacked.json b/Tests/Fido2.Tests/TestFiles/attestationOptionsPacked.json index d50b6b39..0954584d 100644 --- a/Tests/Fido2.Tests/TestFiles/attestationOptionsPacked.json +++ b/Tests/Fido2.Tests/TestFiles/attestationOptionsPacked.json @@ -1,20 +1,21 @@ { - "status":"ok", - "errorMessage":"", - "rp":{ - "name":"localhost", - "id":"localhost"}, - "user":{ - "name":"aseigler", - "id":"ABC", - "displayName":"aseigler" + "status": "ok", + "errorMessage": "", + "rp": { + "name": "localhost", + "id": "localhost" }, - "challenge":"P_JKQid1tvs4BltiZ1CsEfXl3GZ0IpmLPUQFlY-o0x9sgvCKyW5zPRJcO773ei8OwXCyF9uZN6_pyzXNOAJR7A", - "pubKeyCredParams":[ + "user": { + "name": "aseigler", + "id": "QUJD", + "displayName": "aseigler" + }, + "challenge": "P_JKQid1tvs4BltiZ1CsEfXl3GZ0IpmLPUQFlY-o0x9sgvCKyW5zPRJcO773ei8OwXCyF9uZN6_pyzXNOAJR7A", + "pubKeyCredParams": [ { - "type":"public-key", - "alg":-7 + "type": "public-key", + "alg": -7 } ], - "timeout":0 -} + "timeout": 0 +} \ No newline at end of file diff --git a/Tests/Fido2.Tests/TestFiles/attestationOptionsTrustKeyT110.json b/Tests/Fido2.Tests/TestFiles/attestationOptionsTrustKeyT110.json index f4457d9b..324a7ca0 100644 --- a/Tests/Fido2.Tests/TestFiles/attestationOptionsTrustKeyT110.json +++ b/Tests/Fido2.Tests/TestFiles/attestationOptionsTrustKeyT110.json @@ -4,10 +4,10 @@ "rp":{ "name":"MyID", "id":"localhost"}, - "user":{ - "name":"Ela Park", - "id":"ABC", - "displayName":"Ela Park" + "user": { + "name": "Ela Park", + "id": "QUJD", + "displayName": "Ela Park" }, "challenge":"o8oCrLEUQ9f8R1SgrTkIPO_RgxpE__Vy9OI2f1g2gpM", "pubKeyCredParams":[ diff --git a/Tests/Fido2.Tests/TestFiles/attestationOptionsU2F.json b/Tests/Fido2.Tests/TestFiles/attestationOptionsU2F.json index a55aaeda..7d486e05 100644 --- a/Tests/Fido2.Tests/TestFiles/attestationOptionsU2F.json +++ b/Tests/Fido2.Tests/TestFiles/attestationOptionsU2F.json @@ -4,10 +4,10 @@ "rp":{ "name":"localhost", "id":"localhost"}, - "user":{ - "name":"aseigler", - "id":"ABC", - "displayName":"aseigler" + "user": { + "name": "aseigler", + "id": "QUJD", + "displayName": "aseigler" }, "challenge":"aL2uwApgwumBzTYCcoL0_4DRv_mfYykzgqJBFoJj_WCKNZOpvUQnyjdwMWIWKcY844tyDNLO5pQPBMJrHPz_3g", "pubKeyCredParams":[ diff --git a/Tests/Fido2.Tests/TestFiles/options1.json b/Tests/Fido2.Tests/TestFiles/options1.json index 2bbb642c..69d73cf4 100644 --- a/Tests/Fido2.Tests/TestFiles/options1.json +++ b/Tests/Fido2.Tests/TestFiles/options1.json @@ -7,7 +7,7 @@ }, "user": { "name": "anders", - "id": "ASD", + "id": "QUJD", "displayName": "anders" }, "challenge": "CBsfWRMr39ViLU9p8JLnvcXBIpP1TRWxWhI_or7UIIzxR8chyobvgfW-Vwz8eOnUOUHr6adHE6C_PHDgPNIpVw", diff --git a/Tests/Fido2.Tests/TestFiles/options2.json b/Tests/Fido2.Tests/TestFiles/options2.json index 299cb94e..ecc138b1 100644 --- a/Tests/Fido2.Tests/TestFiles/options2.json +++ b/Tests/Fido2.Tests/TestFiles/options2.json @@ -7,7 +7,7 @@ }, "user": { "name": "anders", - "id": "ASD", + "id": "QUJD", "displayName": "anders" }, "challenge": "sP4MiwodjreC8-80IMjcyWNlo_Y1SJXmFgQNBilnjdf30WRsjFDhDYmfY4-4uhq2HFjYREbXdr6Vjuvz2XvTjA==", From b49f1a2b61b35922377430b961c0ecf3dc4a9c70 Mon Sep 17 00:00:00 2001 From: Jason Nelson Date: Thu, 7 Nov 2024 18:23:48 -0800 Subject: [PATCH 3/8] Apply formatting --- Demo/TestController.cs | 1 + Src/Fido2.Models/Converters/Base64UrlConverter.cs | 4 ++-- Src/Fido2.Models/Metadata/BiometricStatusReport.cs | 3 +-- Src/Fido2/AuthenticatorAssertionResponse.cs | 1 - Src/Fido2/MakeAssertionParams.cs | 4 +--- Src/Fido2/Objects/CredentialPublicKey.cs | 2 ++ Tests/Fido2.Tests/AuthenticatorResponse.cs | 3 ++- Tests/Fido2.Tests/CredentialPublicKeyTests.cs | 1 + Tests/Fido2.Tests/Fido2Tests.cs | 2 ++ 9 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Demo/TestController.cs b/Demo/TestController.cs index 9dd18034..045962bb 100644 --- a/Demo/TestController.cs +++ b/Demo/TestController.cs @@ -1,6 +1,7 @@ using System.Buffers.Text; using System.Text; using System.Text.Json; + using Fido2NetLib; using Fido2NetLib.Development; using Fido2NetLib.Objects; diff --git a/Src/Fido2.Models/Converters/Base64UrlConverter.cs b/Src/Fido2.Models/Converters/Base64UrlConverter.cs index 46c65500..62c72480 100644 --- a/Src/Fido2.Models/Converters/Base64UrlConverter.cs +++ b/Src/Fido2.Models/Converters/Base64UrlConverter.cs @@ -11,7 +11,7 @@ namespace Fido2NetLib; public sealed class Base64UrlConverter : JsonConverter { public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { + { if (!reader.HasValueSequence) { return Base64Url.DecodeFromUtf8(reader.ValueSpan); @@ -19,7 +19,7 @@ public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS else { return Base64Url.DecodeFromChars(reader.GetString()); - } + } } public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options) diff --git a/Src/Fido2.Models/Metadata/BiometricStatusReport.cs b/Src/Fido2.Models/Metadata/BiometricStatusReport.cs index 5fa85be9..7c12043a 100644 --- a/Src/Fido2.Models/Metadata/BiometricStatusReport.cs +++ b/Src/Fido2.Models/Metadata/BiometricStatusReport.cs @@ -1,5 +1,4 @@ -using System.ComponentModel.DataAnnotations; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace Fido2NetLib; diff --git a/Src/Fido2/AuthenticatorAssertionResponse.cs b/Src/Fido2/AuthenticatorAssertionResponse.cs index 30dc5f6f..bf94b4a5 100644 --- a/Src/Fido2/AuthenticatorAssertionResponse.cs +++ b/Src/Fido2/AuthenticatorAssertionResponse.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; diff --git a/Src/Fido2/MakeAssertionParams.cs b/Src/Fido2/MakeAssertionParams.cs index 312f9b2b..fb01ea52 100644 --- a/Src/Fido2/MakeAssertionParams.cs +++ b/Src/Fido2/MakeAssertionParams.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; +using System.ComponentModel; namespace Fido2NetLib; diff --git a/Src/Fido2/Objects/CredentialPublicKey.cs b/Src/Fido2/Objects/CredentialPublicKey.cs index 7913d258..74efcad6 100644 --- a/Src/Fido2/Objects/CredentialPublicKey.cs +++ b/Src/Fido2/Objects/CredentialPublicKey.cs @@ -1,7 +1,9 @@ using System; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; + using Fido2NetLib.Cbor; + using NSec.Cryptography; namespace Fido2NetLib.Objects; diff --git a/Tests/Fido2.Tests/AuthenticatorResponse.cs b/Tests/Fido2.Tests/AuthenticatorResponse.cs index cfb4e5ea..eca8c4ea 100644 --- a/Tests/Fido2.Tests/AuthenticatorResponse.cs +++ b/Tests/Fido2.Tests/AuthenticatorResponse.cs @@ -858,7 +858,8 @@ public async Task TestAuthenticatorAttestationResponseBackupEligiblePolicyDisall null ).ToByteArray(); - var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new MockClientData { + var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new MockClientData + { Type = "webauthn.create", Challenge = challenge, Origin = rp, diff --git a/Tests/Fido2.Tests/CredentialPublicKeyTests.cs b/Tests/Fido2.Tests/CredentialPublicKeyTests.cs index eb515691..ecb3ae3e 100644 --- a/Tests/Fido2.Tests/CredentialPublicKeyTests.cs +++ b/Tests/Fido2.Tests/CredentialPublicKeyTests.cs @@ -1,5 +1,6 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; + using Fido2NetLib; using Fido2NetLib.Objects; diff --git a/Tests/Fido2.Tests/Fido2Tests.cs b/Tests/Fido2.Tests/Fido2Tests.cs index 6368b790..9c70358b 100644 --- a/Tests/Fido2.Tests/Fido2Tests.cs +++ b/Tests/Fido2.Tests/Fido2Tests.cs @@ -16,7 +16,9 @@ using Moq; using NSec.Cryptography; + using Test; + using static Fido2NetLib.AuthenticatorAttestationResponse; namespace fido2_net_lib.Test; From f97f2cea2d489b6c6c49dc184e612754c6424cd3 Mon Sep 17 00:00:00 2001 From: Jason Nelson Date: Tue, 12 Nov 2024 11:25:15 -0800 Subject: [PATCH 4/8] Update deps --- Src/Fido2.Models/Fido2.Models.csproj | 2 +- Src/Fido2/Fido2.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Src/Fido2.Models/Fido2.Models.csproj b/Src/Fido2.Models/Fido2.Models.csproj index be4559ee..34d32d3c 100644 --- a/Src/Fido2.Models/Fido2.Models.csproj +++ b/Src/Fido2.Models/Fido2.Models.csproj @@ -10,7 +10,7 @@ - + diff --git a/Src/Fido2/Fido2.csproj b/Src/Fido2/Fido2.csproj index 04ca19a7..0726c3ea 100644 --- a/Src/Fido2/Fido2.csproj +++ b/Src/Fido2/Fido2.csproj @@ -12,9 +12,9 @@ - + - + From 0f0dc6442106569afcb012320d425eddde2905f2 Mon Sep 17 00:00:00 2001 From: Jason Nelson Date: Tue, 12 Nov 2024 12:04:19 -0800 Subject: [PATCH 5/8] Eliminate allocation in Base64UrlConverter and improve error message when encountering invalid data --- .../Converters/Base64UrlConverter.cs | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/Src/Fido2.Models/Converters/Base64UrlConverter.cs b/Src/Fido2.Models/Converters/Base64UrlConverter.cs index 62c72480..473313a0 100644 --- a/Src/Fido2.Models/Converters/Base64UrlConverter.cs +++ b/Src/Fido2.Models/Converters/Base64UrlConverter.cs @@ -1,4 +1,6 @@ -using System.Buffers; +#nullable enable + +using System.Buffers; using System.Buffers.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -12,13 +14,44 @@ public sealed class Base64UrlConverter : JsonConverter { public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (!reader.HasValueSequence) + byte[]? rentedBuffer = null; + + scoped ReadOnlySpan source; + + if (!reader.HasValueSequence && !reader.ValueIsEscaped) { - return Base64Url.DecodeFromUtf8(reader.ValueSpan); + source = reader.ValueSpan; } else { - return Base64Url.DecodeFromChars(reader.GetString()); + int valueLength = reader.HasValueSequence ? checked((int)reader.ValueSequence.Length) : reader.ValueSpan.Length; + + Span buffer = valueLength <= 32 ? stackalloc byte[32] : (rentedBuffer = ArrayPool.Shared.Rent(valueLength)); + int bytesRead = reader.CopyString(buffer); + source = buffer[..bytesRead]; + } + + try + { + return Base64Url.DecodeFromUtf8(source); + } + catch + { + if (Base64.IsValid(source)) + { + throw new JsonException("Expected data to be in Base64Url format, but received Base64 encoding instead"); + } + else + { + throw new JsonException("Invalid Base64Url data"); + } + } + finally + { + if (rentedBuffer != null) + { + ArrayPool.Shared.Return(rentedBuffer); + } } } From 6b3bf89c197d2832bf1067db400f634e3386a626 Mon Sep 17 00:00:00 2001 From: Jason Nelson Date: Tue, 12 Nov 2024 12:09:53 -0800 Subject: [PATCH 6/8] Add Base64UrlConverter.EnableRelaxedDecoding feature --- Src/Fido2.Models/Converters/Base64UrlConverter.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Src/Fido2.Models/Converters/Base64UrlConverter.cs b/Src/Fido2.Models/Converters/Base64UrlConverter.cs index 473313a0..3210c6db 100644 --- a/Src/Fido2.Models/Converters/Base64UrlConverter.cs +++ b/Src/Fido2.Models/Converters/Base64UrlConverter.cs @@ -12,6 +12,8 @@ namespace Fido2NetLib; /// public sealed class Base64UrlConverter : JsonConverter { + public static bool EnableRelaxedDecoding { get; set; } + public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { byte[]? rentedBuffer = null; @@ -39,7 +41,14 @@ public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS { if (Base64.IsValid(source)) { - throw new JsonException("Expected data to be in Base64Url format, but received Base64 encoding instead"); + if (EnableRelaxedDecoding) + { + return Base64Url.DecodeFromUtf8(source); + } + else + { + throw new JsonException("Expected data to be in Base64Url format, but received Base64 encoding instead."); + } } else { From e11a2fdf01cdfa84dd992eb0acfd68abf2bc7dc8 Mon Sep 17 00:00:00 2001 From: Jason Nelson Date: Tue, 12 Nov 2024 12:17:36 -0800 Subject: [PATCH 7/8] Format Base64UrlConverter --- Src/Fido2.Models/Converters/Base64UrlConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/Fido2.Models/Converters/Base64UrlConverter.cs b/Src/Fido2.Models/Converters/Base64UrlConverter.cs index 3210c6db..20748f4f 100644 --- a/Src/Fido2.Models/Converters/Base64UrlConverter.cs +++ b/Src/Fido2.Models/Converters/Base64UrlConverter.cs @@ -43,7 +43,7 @@ public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS { if (EnableRelaxedDecoding) { - return Base64Url.DecodeFromUtf8(source); + return Base64Url.DecodeFromUtf8(source); } else { From 7aebefdf3179bfc8d6f0baeca7a7ce9d9652a468 Mon Sep 17 00:00:00 2001 From: Jason Nelson Date: Tue, 12 Nov 2024 12:33:40 -0800 Subject: [PATCH 8/8] Fix EnableRelaxedDecoding implementation and add test coverage --- .../Converters/Base64UrlConverter.cs | 22 ++++++++++-- Tests/Fido2.Tests/Fido2UserTests.cs | 36 +++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 Tests/Fido2.Tests/Fido2UserTests.cs diff --git a/Src/Fido2.Models/Converters/Base64UrlConverter.cs b/Src/Fido2.Models/Converters/Base64UrlConverter.cs index 20748f4f..0c2d40ff 100644 --- a/Src/Fido2.Models/Converters/Base64UrlConverter.cs +++ b/Src/Fido2.Models/Converters/Base64UrlConverter.cs @@ -37,13 +37,29 @@ public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS { return Base64Url.DecodeFromUtf8(source); } - catch + catch (Exception ex) { if (Base64.IsValid(source)) { + static byte[] DecodeBase64FromUtf8(scoped ReadOnlySpan source) + { + var rentedBuffer = ArrayPool.Shared.Rent(Base64.GetMaxDecodedFromUtf8Length(source.Length)); + + try + { + _ = Base64.DecodeFromUtf8(source, rentedBuffer, out _, out int written); + + return rentedBuffer[0..written]; + } + finally + { + ArrayPool.Shared.Return(rentedBuffer); + } + } + if (EnableRelaxedDecoding) { - return Base64Url.DecodeFromUtf8(source); + return DecodeBase64FromUtf8(source); } else { @@ -52,7 +68,7 @@ public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS } else { - throw new JsonException("Invalid Base64Url data"); + throw new JsonException(ex.Message, ex); } } finally diff --git a/Tests/Fido2.Tests/Fido2UserTests.cs b/Tests/Fido2.Tests/Fido2UserTests.cs new file mode 100644 index 00000000..11fda02c --- /dev/null +++ b/Tests/Fido2.Tests/Fido2UserTests.cs @@ -0,0 +1,36 @@ +using System.Text.Json; + +namespace Fido2NetLib.Tests; + +public class Base64UrlConverterTests +{ + [Fact] + public void RelaxedDecodingWorks() + { + string jsonText = + """ + { + "name": "anders", + "id": "7w80qsdaWWm5R+KK2O64MO/WSitunFbxH1H8mE9IVORxzbXdxXDY1VRQlayoK/Lmh3MI/p0M59Rh98D8r4EoJw==", + "displayName": "anders" + } + """; + + Base64UrlConverter.EnableRelaxedDecoding = false; + + try + { + _ = JsonSerializer.Deserialize(jsonText); + } + catch (JsonException ex) + { + Assert.Equal("Expected data to be in Base64Url format, but received Base64 encoding instead.", ex.Message); + } + + Base64UrlConverter.EnableRelaxedDecoding = true; + + var user = JsonSerializer.Deserialize(jsonText); + + Assert.Equal("7w80qsdaWWm5R+KK2O64MO/WSitunFbxH1H8mE9IVORxzbXdxXDY1VRQlayoK/Lmh3MI/p0M59Rh98D8r4EoJw==", Convert.ToBase64String(user.Id)); + } +}