diff --git a/Src/Fido2/AttestationFormat/AndroidSafetyNet.cs b/Src/Fido2/AttestationFormat/AndroidSafetyNet.cs index b770cd51..bec4d89a 100644 --- a/Src/Fido2/AttestationFormat/AndroidSafetyNet.cs +++ b/Src/Fido2/AttestationFormat/AndroidSafetyNet.cs @@ -20,19 +20,6 @@ internal sealed class AndroidSafetyNet : AttestationVerifier { private const int _driftTolerance = 0; - private static X509Certificate2 GetX509Certificate(string certString) - { - try - { - var certBytes = Convert.FromBase64String(certString); - return new X509Certificate2(certBytes); - } - catch (Exception ex) - { - throw new ArgumentException("Could not parse X509 certificate", ex); - } - } - public override (AttestationType, X509Certificate2[]) Verify() { // 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform @@ -45,7 +32,7 @@ public override (AttestationType, X509Certificate2[]) Verify() throw new Fido2VerificationException("Invalid version in SafetyNet data"); } - if (!(_attStmt["response"] is CborByteString { Length: > 0})) + if (!(_attStmt["response"] is CborByteString { Length: > 0 })) throw new Fido2VerificationException("Invalid response in SafetyNet data"); var response = (byte[])_attStmt["response"]!; @@ -64,19 +51,27 @@ public override (AttestationType, X509Certificate2[]) Verify() using var jwtHeaderJsonDoc = JsonDocument.Parse(Base64Url.Decode(jwtHeaderString)); var jwtHeaderJson = jwtHeaderJsonDoc.RootElement; - string[] x5cStrings = jwtHeaderJson.TryGetProperty("x5c", out var x5cEl) && x5cEl.ValueKind is JsonValueKind.Array - ? x5cEl.ToStringArray() - : throw new Fido2VerificationException("SafetyNet response JWT header missing x5c"); + if (!jwtHeaderJson.TryGetProperty("x5c", out var x5cEl)) + { + throw new Fido2VerificationException("SafetyNet response JWT header missing x5c"); + } - if (x5cStrings.Length is 0) + if (!x5cEl.TryDecodeArrayOfBase64EncodedBytes(out var x5cRawKeys)) + { + throw new Fido2VerificationException("SafetyNet response JWT header has a malformed x5c value"); + } + + if (x5cRawKeys.Length is 0) + { throw new Fido2VerificationException("No keys were present in the TOC header in SafetyNet response JWT"); + } - var certs = new X509Certificate2[x5cStrings.Length]; + var certs = new X509Certificate2[x5cRawKeys.Length]; var keys = new List(certs.Length); for (int i = 0; i < certs.Length; i++) { - var cert = GetX509Certificate(x5cStrings[i]); + var cert = X509CertificateHelper.CreateFromRawData(x5cRawKeys[i]); certs[i] = cert; if (cert.GetECDsaPublicKey() is ECDsa ecdsaPublicKey) diff --git a/Src/Fido2/AttestationFormat/FidoU2f.cs b/Src/Fido2/AttestationFormat/FidoU2f.cs index c0c8f771..8645646c 100644 --- a/Src/Fido2/AttestationFormat/FidoU2f.cs +++ b/Src/Fido2/AttestationFormat/FidoU2f.cs @@ -27,7 +27,7 @@ public override (AttestationType, X509Certificate2[]) Verify() throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, Fido2ErrorMessages.MalformedX5c_FidoU2fAttestation); } - var attCert = new X509Certificate2((byte[])X5c[0]); + var attCert = new X509Certificate2((byte[])x5cArray[0]); // TODO : Check why this variable isn't used. Remove it or use it. var u2ftransports = U2FTransportsFromAttnCert(attCert.Extensions); diff --git a/Src/Fido2/AttestationFormat/Packed.cs b/Src/Fido2/AttestationFormat/Packed.cs index 33abcc5e..49c54664 100644 --- a/Src/Fido2/AttestationFormat/Packed.cs +++ b/Src/Fido2/AttestationFormat/Packed.cs @@ -59,7 +59,7 @@ public override (AttestationType, X509Certificate2[]?) Verify() for (int i = 0; i < trustPath.Length; i++) { - if (X5c[i] is CborByteString { Length: > 0 } x5cObject) + if (x5cArray[i] is CborByteString { Length: > 0 } x5cObject) { var x5cCert = new X509Certificate2(x5cObject.Value); diff --git a/Src/Fido2/Extensions/BinaryWriterExtensions.cs b/Src/Fido2/Extensions/IBufferWriterExtensions.cs similarity index 60% rename from Src/Fido2/Extensions/BinaryWriterExtensions.cs rename to Src/Fido2/Extensions/IBufferWriterExtensions.cs index 5c77d952..725bcd45 100644 --- a/Src/Fido2/Extensions/BinaryWriterExtensions.cs +++ b/Src/Fido2/Extensions/IBufferWriterExtensions.cs @@ -1,12 +1,12 @@ using System; +using System.Buffers; using System.Buffers.Binary; -using System.IO; namespace Fido2NetLib; -internal static class BinaryWriterExtensions +internal static class IBufferWriterExtensions { - public static void WriteUInt16BigEndian(this BinaryWriter writer, ushort value) + public static void WriteUInt16BigEndian(this IBufferWriter writer, ushort value) { Span buffer = stackalloc byte[2]; @@ -15,7 +15,7 @@ public static void WriteUInt16BigEndian(this BinaryWriter writer, ushort value) writer.Write(buffer); } - public static void WriteUInt32BigEndian(this BinaryWriter writer, uint value) + public static void WriteUInt32BigEndian(this IBufferWriter writer, uint value) { Span buffer = stackalloc byte[4]; diff --git a/Src/Fido2/Extensions/JsonElementExtensions.cs b/Src/Fido2/Extensions/JsonElementExtensions.cs index 21a25f1e..90d40758 100644 --- a/Src/Fido2/Extensions/JsonElementExtensions.cs +++ b/Src/Fido2/Extensions/JsonElementExtensions.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; namespace Fido2NetLib; @@ -19,4 +20,29 @@ public static string[] ToStringArray(this in JsonElement el) return result; } + + public static bool TryDecodeArrayOfBase64EncodedBytes(this in JsonElement el, [NotNullWhen(true)] out byte[][]? result) + { + if (el.ValueKind is JsonValueKind.Array) + { + result = new byte[el.GetArrayLength()][]; + + int i = 0; + + try + { + foreach (var item in el.EnumerateArray()) + { + result[i++] = item.GetBytesFromBase64()!; + } + + return true; + } + catch { } + } + + result = null; + + return false; + } } diff --git a/Src/Fido2/Extensions/X509CertificateHelper.cs b/Src/Fido2/Extensions/X509CertificateHelper.cs new file mode 100644 index 00000000..58bc83b5 --- /dev/null +++ b/Src/Fido2/Extensions/X509CertificateHelper.cs @@ -0,0 +1,35 @@ +using System; +using System.Security.Cryptography.X509Certificates; + +namespace Fido2NetLib; + +internal static class X509CertificateHelper +{ + public static X509Certificate2 CreateFromBase64String(string base64String) + { + byte[] rawData; + + try + { + rawData = Convert.FromBase64String(base64String); + } + catch + { + throw new Exception("Invalid base64 data found parsing X509 certificate"); + } + + return CreateFromRawData(rawData); + } + + public static X509Certificate2 CreateFromRawData(byte[] rawData) + { + try + { + return new X509Certificate2(rawData); + } + catch (Exception ex) + { + throw new Exception("Could not parse X509 certificate", ex); + } + } +} diff --git a/Src/Fido2/Metadata/ConformanceMetadataRepository.cs b/Src/Fido2/Metadata/ConformanceMetadataRepository.cs index 99f032a4..2d26cfdc 100644 --- a/Src/Fido2/Metadata/ConformanceMetadataRepository.cs +++ b/Src/Fido2/Metadata/ConformanceMetadataRepository.cs @@ -112,19 +112,6 @@ private Task DownloadDataAsync(string url, CancellationToken cancellatio return _httpClient.GetByteArrayAsync(url, cancellationToken); } - private X509Certificate2 GetX509Certificate(string key) - { - try - { - var certBytes = Convert.FromBase64String(key); - return new X509Certificate2(certBytes); - } - catch (Exception ex) - { - throw new ArgumentException("Could not parse X509 certificate.", ex); - } - } - public async Task DeserializeAndValidateBlob(string rawBLOBJwt, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(rawBLOBJwt)) @@ -133,7 +120,7 @@ public async Task DeserializeAndValidateBlob(string rawBLOB var jwtParts = rawBLOBJwt.Split('.'); if (jwtParts.Length != 3) - throw new ArgumentException("The JWT does not have the 3 expected components"); + throw new Fido2MetadataException("The JWT does not have the 3 expected components"); var blobHeader = jwtParts[0]; using var jsonDoc = JsonDocument.Parse(Base64Url.Decode(blobHeader)); @@ -141,19 +128,25 @@ public async Task DeserializeAndValidateBlob(string rawBLOB var blobAlg = tokenHeader.TryGetProperty("alg", out var algEl) ? algEl.GetString()! - : throw new ArgumentNullException("No alg value was present in the BLOB header."); + : throw new Fido2MetadataException("No alg value was present in the BLOB header."); + + if (!tokenHeader.TryGetProperty("x5c", out var x5cEl)) + { + throw new Fido2MetadataException("No x5c array was present in the BLOB header."); + } - var blobCertStrings = tokenHeader.TryGetProperty("x5c", out var x5cEl) && x5cEl.ValueKind is JsonValueKind.Array - ? x5cEl.ToStringArray() - : throw new ArgumentException("No x5c array was present in the BLOB header."); + if (!x5cEl.TryDecodeArrayOfBase64EncodedBytes(out var x5cRawKeys)) + { + throw new Fido2MetadataException("Malformed x5c array in the BLOB header."); + } - var rootCert = GetX509Certificate(ROOT_CERT); - var blobCertificates = new X509Certificate2[blobCertStrings.Length]; - var blobPublicKeys = new List(blobCertStrings.Length); + var rootCert = X509CertificateHelper.CreateFromBase64String(ROOT_CERT); + var blobCertificates = new X509Certificate2[x5cRawKeys.Length]; + var blobPublicKeys = new List(x5cRawKeys.Length); - for (int i = 0; i < blobCertStrings.Length; i++) + for (int i = 0; i < x5cRawKeys.Length; i++) { - var cert = GetX509Certificate(blobCertStrings[i]); + var cert = X509CertificateHelper.CreateFromRawData(x5cRawKeys[i]); blobCertificates[i] = cert; if (cert.GetECDsaPublicKey() is ECDsa ecdsaPublicKey) @@ -213,7 +206,7 @@ public async Task DeserializeAndValidateBlob(string rawBLOB // otherwise we have to manually validate that the root in the chain we are testing is the root we downloaded if (rootCert.Thumbprint.Equals(certChain.ChainElements[^1].Certificate.Thumbprint, StringComparison.Ordinal) && // and that the number of elements in the chain accounts for what was in x5c plus the root we added - certChain.ChainElements.Count == (blobCertStrings.Length + 1) && + certChain.ChainElements.Count == (x5cRawKeys.Length + 1) && // and that the root cert has exactly one status listed against it certChain.ChainElements[^1].ChainElementStatus.Length == 1 && // and that that status is a status of exactly UntrustedRoot diff --git a/Src/Fido2/Metadata/Fido2MetadataServiceRepository.cs b/Src/Fido2/Metadata/Fido2MetadataServiceRepository.cs index a2c8e0b1..4e33c1bb 100644 --- a/Src/Fido2/Metadata/Fido2MetadataServiceRepository.cs +++ b/Src/Fido2/Metadata/Fido2MetadataServiceRepository.cs @@ -76,22 +76,8 @@ private async Task DownloadDataAsync(string url, CancellationToken cance .GetByteArrayAsync(url, cancellationToken); } - private X509Certificate2 GetX509Certificate(string certString) - { - try - { - var certBytes = Convert.FromBase64String(certString); - return new X509Certificate2(certBytes); - } - catch (Exception ex) - { - throw new ArgumentException("Could not parse X509 certificate.", ex); - } - } - private async Task DeserializeAndValidateBlobAsync(string rawBLOBJwt, CancellationToken cancellationToken) { - if (string.IsNullOrWhiteSpace(rawBLOBJwt)) throw new ArgumentNullException(nameof(rawBLOBJwt)); @@ -106,22 +92,31 @@ private async Task DeserializeAndValidateBlobAsync(string r string blobAlg = blobHeader.TryGetProperty("alg", out var algEl) ? algEl.GetString()! - : throw new ArgumentNullException("No alg value was present in the BLOB header."); + : throw new Fido2MetadataException("No alg value was present in the BLOB header"); - string[] keyStrings = blobHeader.TryGetProperty("x5c", out var x5cEl) && x5cEl.ValueKind is JsonValueKind.Array - ? x5cEl.ToStringArray() - : throw new ArgumentNullException("No x5c array was present in the BLOB header."); - if (keyStrings.Length is 0) - throw new ArgumentException("No keys were present in the BLOB header."); + if (!blobHeader.TryGetProperty("x5c", out var x5cEl)) + { + throw new Fido2MetadataException("No x5c value was present in the BLOB header"); + } + + if (!x5cEl.TryDecodeArrayOfBase64EncodedBytes(out var x5cRawKeys)) + { + throw new Fido2MetadataException("The x5c value in the BLOB header is malformed"); + } + + if (x5cRawKeys.Length is 0) + { + throw new Fido2MetadataException("No x5c keys were present in the BLOB header"); + } - var rootCert = GetX509Certificate(ROOT_CERT); - var blobCerts = new X509Certificate2[keyStrings.Length]; - var keys = new SecurityKey[keyStrings.Length]; + var rootCert = X509CertificateHelper.CreateFromBase64String(ROOT_CERT); + var blobCerts = new X509Certificate2[x5cRawKeys.Length]; + var keys = new SecurityKey[x5cRawKeys.Length]; for (int i = 0; i < blobCerts.Length; i++) { - var cert = GetX509Certificate(keyStrings[i]); + var cert = X509CertificateHelper.CreateFromRawData(x5cRawKeys[i]); blobCerts[i] = cert; @@ -188,7 +183,7 @@ private async Task DeserializeAndValidateBlobAsync(string r // otherwise we have to manually validate that the root in the chain we are testing is the root we downloaded if (rootCert.Thumbprint == certChain.ChainElements[^1].Certificate.Thumbprint && // and that the number of elements in the chain accounts for what was in x5c plus the root we added - certChain.ChainElements.Count == (keyStrings.Length + 1) && + certChain.ChainElements.Count == (x5cRawKeys.Length + 1) && // and that the root cert has exactly one status listed against it certChain.ChainElements[^1].ChainElementStatus.Length == 1 && // and that that status is a status of exactly UntrustedRoot diff --git a/Src/Fido2/Objects/AttestedCredentialData.cs b/Src/Fido2/Objects/AttestedCredentialData.cs index 3b44ea3e..a20c5f92 100644 --- a/Src/Fido2/Objects/AttestedCredentialData.cs +++ b/Src/Fido2/Objects/AttestedCredentialData.cs @@ -1,8 +1,6 @@ -#nullable disable - -using System; +using System; +using System.Buffers; using System.Buffers.Binary; -using System.IO; using System.Runtime.InteropServices; using Fido2NetLib.Exceptions; @@ -15,7 +13,7 @@ public sealed class AttestedCredentialData /// Minimum length of the attested credential data structure. AAGUID + credentialID length + credential ID + credential public key. /// /// - private readonly int _minLength = Marshal.SizeOf(typeof(Guid)) + sizeof(ushort) + sizeof(byte) + sizeof(byte); + private static readonly int _minLength = Marshal.SizeOf() + sizeof(ushort) + sizeof(byte) + sizeof(byte); /// /// Instantiates an AttestedCredentialData object from an aaguid, credentialID, and CredentialPublicKey @@ -102,7 +100,7 @@ internal AttestedCredentialData(ReadOnlyMemory data, out int bytesRead) /// public CredentialPublicKey CredentialPublicKey { get; private set; } - internal static void SwapBytes(byte[] bytes, int index1, int index2) + private static void SwapBytes(byte[] bytes, int index1, int index2) { var temp = bytes[index1]; bytes[index1] = bytes[index2]; @@ -112,22 +110,22 @@ internal static void SwapBytes(byte[] bytes, int index1, int index2) /// /// AAGUID is sent as big endian byte array, this converter is for little endian systems. /// - public static Guid FromBigEndian(byte[] Aaguid) + public static Guid FromBigEndian(byte[] aaGuid) { - SwapBytes(Aaguid, 0, 3); - SwapBytes(Aaguid, 1, 2); - SwapBytes(Aaguid, 4, 5); - SwapBytes(Aaguid, 6, 7); + SwapBytes(aaGuid, 0, 3); + SwapBytes(aaGuid, 1, 2); + SwapBytes(aaGuid, 4, 5); + SwapBytes(aaGuid, 6, 7); - return new Guid(Aaguid); + return new Guid(aaGuid); } /// /// AAGUID is sent as big endian byte array, this converter is for little endian systems. /// - public static byte[] AaGuidToBigEndian(Guid AaGuid) + public static byte[] AaGuidToBigEndian(Guid aaGuid) { - var aaguid = AaGuid.ToByteArray(); + var aaguid = aaGuid.ToByteArray(); SwapBytes(aaguid, 0, 3); SwapBytes(aaguid, 1, 2); @@ -144,28 +142,33 @@ public override string ToString() public byte[] ToByteArray() { - using var ms = new MemoryStream(); - using (var writer = new BinaryWriter(ms)) + var writer = new ArrayBufferWriter(16 + 2 + CredentialID.Length + 512); + + WriteTo(writer); + + return writer.WrittenSpan.ToArray(); + } + + public void WriteTo(IBufferWriter writer) + { + // Write the aaguid as big endian bytes + if (BitConverter.IsLittleEndian) { - // Write the aaguid bytes out, reverse if we're on a little endian system - if (BitConverter.IsLittleEndian) - { - writer.Write(AaGuidToBigEndian(AaGuid)); - } - else - { - writer.Write(AaGuid.ToByteArray()); - } - - // Write the length of credential ID, as big endian bytes of a 16-bit unsigned integer - writer.WriteUInt16BigEndian((ushort)CredentialID.Length); - - // Write CredentialID bytes - writer.Write(CredentialID); - - // Write credential public key bytes - writer.Write(CredentialPublicKey.GetBytes()); + writer.Write(AaGuidToBigEndian(AaGuid)); } - return ms.ToArray(); + else + { + _ = AaGuid.TryWriteBytes(writer.GetSpan(16)); + writer.Advance(16); + } + + // Write the length of credential ID, as big endian bytes of a 16-bit unsigned integer + writer.WriteUInt16BigEndian((ushort)CredentialID.Length); + + // Write CredentialID bytes + writer.Write(CredentialID); + + // Write credential public key bytes + writer.Write(CredentialPublicKey.GetBytes()); } } diff --git a/Src/Fido2/Objects/AuthenticatorData.cs b/Src/Fido2/Objects/AuthenticatorData.cs index b0486126..9427aafd 100644 --- a/Src/Fido2/Objects/AuthenticatorData.cs +++ b/Src/Fido2/Objects/AuthenticatorData.cs @@ -1,7 +1,7 @@ #nullable disable using System; -using System.IO; +using System.Buffers; using Fido2NetLib.Cbor; using Fido2NetLib.Exceptions; @@ -14,7 +14,7 @@ public sealed class AuthenticatorData /// Minimum length of the authenticator data structure. /// /// - internal const int MinLength = SHA256HashLenBytes + sizeof(AuthenticatorFlags) + sizeof(UInt32); + internal const int MinLength = SHA256HashLenBytes + sizeof(AuthenticatorFlags) + sizeof(uint); private const int SHA256HashLenBytes = 32; // 256 bits, 8 bits per byte @@ -124,27 +124,24 @@ public AuthenticatorData(byte[] authData) public byte[] ToByteArray() { - using var ms = new MemoryStream(); + var writer = new ArrayBufferWriter(512); - using (var writer = new BinaryWriter(ms)) - { - writer.Write(RpIdHash); + writer.Write(RpIdHash); - writer.Write((byte)_flags); + writer.Write(stackalloc byte[1] { (byte)_flags }); - writer.WriteUInt32BigEndian(SignCount); + writer.WriteUInt32BigEndian(SignCount); - if (HasAttestedCredentialData) - { - writer.Write(AttestedCredentialData.ToByteArray()); - } + if (HasAttestedCredentialData) + { + AttestedCredentialData.WriteTo(writer); + } - if (HasExtensionsData) - { - writer.Write(Extensions.GetBytes()); - } + if (HasExtensionsData) + { + writer.Write(Extensions.GetBytes()); } - return ms.ToArray(); + return writer.WrittenSpan.ToArray(); } } diff --git a/Src/Fido2/Objects/Extensions.cs b/Src/Fido2/Objects/Extensions.cs index 4e7d4f3c..87abaf24 100644 --- a/Src/Fido2/Objects/Extensions.cs +++ b/Src/Fido2/Objects/Extensions.cs @@ -1,4 +1,6 @@ -namespace Fido2NetLib.Objects; +using System; + +namespace Fido2NetLib.Objects; /// /// @@ -6,8 +8,11 @@ public sealed class Extensions { private readonly byte[] _extensionBytes; + public Extensions(byte[] extensions) { + ArgumentNullException.ThrowIfNull(extensions); + _extensionBytes = extensions; } @@ -18,4 +23,3 @@ public byte[] GetBytes() return _extensionBytes; } } - diff --git a/Test/Attestation/AndroidSafetyNet.cs b/Test/Attestation/AndroidSafetyNet.cs index 3efe9cbc..ae3c210a 100644 --- a/Test/Attestation/AndroidSafetyNet.cs +++ b/Test/Attestation/AndroidSafetyNet.cs @@ -323,7 +323,7 @@ public void TestAndroidSafetyNetResponseJWTX5cInvalidString() response = Encoding.UTF8.GetBytes(string.Join(".", jwtParts)); var attStmt = (CborMap)_attestationObject["attStmt"]; attStmt.Set("response", new CborByteString(response)); - var ex = Assert.ThrowsAsync(() => MakeAttestationResponseAsync()); + var ex = Assert.ThrowsAnyAsync(async () => await MakeAttestationResponseAsync()); Assert.Equal("Could not parse X509 certificate", ex.Result.Message); }