From 7cde5a130aa7d844d9f2e31bfb40ff7201afcd23 Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Mon, 8 Sep 2025 14:15:44 +0500 Subject: [PATCH 1/7] feat(webhook-callbacks): add support for verifying http request signatures - Adds a ISignatureVerifier contract for custom signature verification - Adds configurable HMAC signature verification - Adds test for signature verification - Adds DigestCodec hex, Base64 and Base64Url support --- .../MockTypes/Http/Request/HttpRequestData.cs | 29 +++ .../Security/Cryptography/DigestCodecTests.cs | 41 +++++ .../HmacSignatureVerifierTests.cs | 162 +++++++++++++++++ .../SignatureVerificationExtensionsTests.cs | 21 +++ APIMatic.Core/APIMatic.Core.csproj | 3 + .../Http/Abstractions/IHttpRequestData.cs | 60 +++++++ .../Abstractions/ISignatureVerifier.cs | 23 +++ .../Cryptography/Base64DigestCodec.cs | 24 +++ .../Cryptography/Base64UrlDigestCodec.cs | 38 ++++ .../Cryptography/DigestCodecFactory.cs | 29 +++ .../Security/Cryptography/HexDigestCodec.cs | 48 +++++ .../Security/Cryptography/IDigestCodec.cs | 15 ++ .../HmacSignatureVerifier.cs | 168 ++++++++++++++++++ .../SignatureVerifierExtensions.cs | 31 ++++ APIMatic.Core/Types/EncodingType.cs | 9 + .../SignatureVerificationException.cs | 19 ++ APIMatic.Core/Types/Sdk/VerificationResult.cs | 32 ++++ .../Utilities/IHttpRequestDataExtensions.cs | 36 ++++ .../Utilities/Json/JsonPointerResolver.cs | 13 +- README.md | 2 + 20 files changed, 801 insertions(+), 2 deletions(-) create mode 100644 APIMatic.Core.Test/MockTypes/Http/Request/HttpRequestData.cs create mode 100644 APIMatic.Core.Test/Security/Cryptography/DigestCodecTests.cs create mode 100644 APIMatic.Core.Test/Security/SignatureVerifier/HmacSignatureVerifierTests.cs create mode 100644 APIMatic.Core.Test/Security/SignatureVerifier/SignatureVerificationExtensionsTests.cs create mode 100644 APIMatic.Core/Http/Abstractions/IHttpRequestData.cs create mode 100644 APIMatic.Core/Security/Abstractions/ISignatureVerifier.cs create mode 100644 APIMatic.Core/Security/Cryptography/Base64DigestCodec.cs create mode 100644 APIMatic.Core/Security/Cryptography/Base64UrlDigestCodec.cs create mode 100644 APIMatic.Core/Security/Cryptography/DigestCodecFactory.cs create mode 100644 APIMatic.Core/Security/Cryptography/HexDigestCodec.cs create mode 100644 APIMatic.Core/Security/Cryptography/IDigestCodec.cs create mode 100644 APIMatic.Core/Security/SignatureVerifier/HmacSignatureVerifier.cs create mode 100644 APIMatic.Core/Security/SignatureVerifier/SignatureVerifierExtensions.cs create mode 100644 APIMatic.Core/Types/EncodingType.cs create mode 100644 APIMatic.Core/Types/Sdk/Exceptions/SignatureVerificationException.cs create mode 100644 APIMatic.Core/Types/Sdk/VerificationResult.cs create mode 100644 APIMatic.Core/Utilities/IHttpRequestDataExtensions.cs diff --git a/APIMatic.Core.Test/MockTypes/Http/Request/HttpRequestData.cs b/APIMatic.Core.Test/MockTypes/Http/Request/HttpRequestData.cs new file mode 100644 index 00000000..8b71066e --- /dev/null +++ b/APIMatic.Core.Test/MockTypes/Http/Request/HttpRequestData.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using APIMatic.Core.Http.Abstractions; + +namespace APIMatic.Core.Test.MockTypes.Http.Request +{ + public class HttpRequestData : IHttpRequestData + { + public string Method { get; } + public Uri Url { get; } + public IReadOnlyDictionary Headers { get; } + public Stream Body { get; set; } + public IReadOnlyDictionary Query { get; } + public IReadOnlyDictionary Cookies { get; } + public string Protocol { get; } + public string ContentType { get; } + public long? ContentLength { get; } + + public HttpRequestData( + IDictionary headers, + Stream body) + { + Headers = new ReadOnlyDictionary(headers); + Body = body; + } + } +} diff --git a/APIMatic.Core.Test/Security/Cryptography/DigestCodecTests.cs b/APIMatic.Core.Test/Security/Cryptography/DigestCodecTests.cs new file mode 100644 index 00000000..f400c9eb --- /dev/null +++ b/APIMatic.Core.Test/Security/Cryptography/DigestCodecTests.cs @@ -0,0 +1,41 @@ +using System; +using APIMatic.Core.Security.Cryptography; +using APIMatic.Core.Types; +using NUnit.Framework; + +namespace APIMatic.Core.Test.Security.Cryptography +{ + public class DigestCodecTests + { + [TestCase(EncodingType.Hex, "4A6F686E", new byte[] { 0x4A, 0x6F, 0x68, 0x6E })] + [TestCase(EncodingType.Base64, "SGVsbG8=", new byte[] { 0x48, 0x65, 0x6C, 0x6C, 0x6F })] + [TestCase(EncodingType.Base64Url, "SGVsbG8", new byte[] { 0x48, 0x65, 0x6C, 0x6C, 0x6F })] + [TestCase(EncodingType.Base64Url, "SG", new byte[] { 0x48 })] + public void DigestCodec_Decode_Success(EncodingType encodingType, string input, byte[] expected) + { + var codec = DigestCodecFactory.Create(encodingType); + var result = codec.Decode(input); + Assert.AreEqual(expected, result); + } + + [TestCase(EncodingType.Hex, "")] + [TestCase(EncodingType.Hex, null)] + [TestCase(EncodingType.Hex, "ABC")] + [TestCase(EncodingType.Base64, "")] + [TestCase(EncodingType.Base64, null)] + [TestCase(EncodingType.Base64Url, "")] + [TestCase(EncodingType.Base64Url, null)] + public void DigestCodecIncorrectInput_Decode_DigestCodec_Create_Exception(EncodingType encodingType, string input) + { + var codec = DigestCodecFactory.Create(encodingType); + Assert.Throws(() => codec.Decode(input)); + } + + [TestCase(-1)] + public void DigestCodec_Create_Exception(int invalidValue) + { + var encodingType = (EncodingType)invalidValue; + Assert.Throws(() => DigestCodecFactory.Create(encodingType)); + } + } +} diff --git a/APIMatic.Core.Test/Security/SignatureVerifier/HmacSignatureVerifierTests.cs b/APIMatic.Core.Test/Security/SignatureVerifier/HmacSignatureVerifierTests.cs new file mode 100644 index 00000000..700653c0 --- /dev/null +++ b/APIMatic.Core.Test/Security/SignatureVerifier/HmacSignatureVerifierTests.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using APIMatic.Core.Security.SignatureVerifier; +using APIMatic.Core.Test.MockTypes.Http.Request; +using APIMatic.Core.Types; +using NUnit.Framework; + +namespace APIMatic.Core.Test.Security.SignatureVerifier; + +[TestFixture] +public class HmacSignatureVerifierTests +{ + private const string SecretKey = "test_secret"; + private const string HeaderName = "X-Signature"; + private const string Payload = "hello world"; + + private static HttpRequestData CreateRequest(string headerValue, string headerName = HeaderName, string payload = Payload) + { + var headers = headerValue == null + ? new Dictionary() + : new Dictionary { { headerName, new[] { headerValue } } }; + return new HttpRequestData(headers, new MemoryStream(System.Text.Encoding.UTF8.GetBytes(payload))); + } + + private static HmacSignatureVerifier CreateVerifier( + EncodingType encodingType, + string headerName = HeaderName, + string secretKey = SecretKey, + string signatureValueTemplate = "{digest}") + { + return new HmacSignatureVerifier(secretKey, headerName, encodingType, signatureValueTemplate: signatureValueTemplate); + } + + [Test] + public void Constructor_ThrowsOnNullOrEmptySecretKey_OnCreate_ThrowsException() + { + Assert.Throws(() => CreateVerifier(EncodingType.Hex, secretKey: null)); + Assert.Throws(() => CreateVerifier(EncodingType.Hex, secretKey: "")); + } + + [Test] + public void Constructor_ThrowsOnNullOrEmptyHeader_OnCreate_ThrowsException() + { + Assert.Throws(() => CreateVerifier(EncodingType.Hex, headerName: null)); + Assert.Throws(() => CreateVerifier(EncodingType.Hex, headerName: "")); + } + + [Test] + public async Task NullHeader_OnVerifyAsync_ReturnsFailure() + { + var request = CreateRequest(null); + var verifier = CreateVerifier(EncodingType.Hex); + var result = await verifier.VerifyAsync(request); + Assert.IsFalse(result.IsSuccess); + Assert.AreEqual($"Signature header '{HeaderName}' is missing.", result.Errors.First()); + } + + [Test] + public async Task MissingHeader_OnVerifyAsync_ReturnsFailure() + { + var request = CreateRequest(string.Empty); + var verifier = CreateVerifier(EncodingType.Hex); + var result = await verifier.VerifyAsync(request); + Assert.IsFalse(result.IsSuccess); + Assert.AreEqual($"Malformed signature header '{HeaderName}' value.", result.Errors.First()); + } + + [Test] + public async Task MalformedHeader_OnVerifyAsync_ReturnsFailure() + { + var request = CreateRequest(""); + var verifier = CreateVerifier(EncodingType.Hex); + var result = await verifier.VerifyAsync(request); + Assert.IsFalse(result.IsSuccess); + StringAssert.Contains("Malformed", result.Errors.First()); + } + + [Test] + public async Task SignatureDecodingFails_OnVerifyAsync_ReturnsFailure() + { + var request = CreateRequest("not-a-valid-hex"); + var verifier = CreateVerifier(EncodingType.Hex); + var result = await verifier.VerifyAsync(request); + Assert.IsFalse(result.IsSuccess); + } + + [TestCase(EncodingType.Hex)] + [TestCase(EncodingType.Base64)] + [TestCase(EncodingType.Base64Url)] + public async Task CorrectSignature_OnVerifyAsync_ReturnsSuccess(EncodingType encodingType) + { + string encodedDigest = GetDigest(encodingType, SecretKey, Payload); + var request = CreateRequest(encodedDigest); + var verifier = CreateVerifier(encodingType); + var result = await verifier.VerifyAsync(request); + Assert.IsTrue(result.IsSuccess); + } + + [TestCase(EncodingType.Hex, "deadbeef")] + [TestCase(EncodingType.Base64, "Zm9vYmFyYmF6")] + [TestCase(EncodingType.Base64Url, "Zm9vYmFyYmF6")] + public async Task IncorrectSignature_OnVerifyAsync_ReturnsFailure(EncodingType encodingType, string badDigest) + { + var request = CreateRequest(badDigest); + var verifier = CreateVerifier(encodingType); + var result = await verifier.VerifyAsync(request); + Assert.IsFalse(result.IsSuccess); + StringAssert.Contains("failed", result.Errors.First()); + } + + [Test] + public async Task TemplateExtractsDigest_OnVerifyAsync_ReturnsSuccess() + { + var hmac = new System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(SecretKey)); + var hash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(Payload)); + var digest = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + const string template = "prefix-{digest}-suffix"; + var signatureValue = $"prefix-{digest}-suffix"; + var request = CreateRequest(signatureValue); + var verifier = CreateVerifier(EncodingType.Hex, signatureValueTemplate: template); + var result = await verifier.VerifyAsync(request); + Assert.IsTrue(result.IsSuccess); + } + + [Test] + public async Task TemplateDoesNotMatch_OnVerifyAsync_ReturnsFailure() + { + const string template = "prefix-{digest}-suffix"; + const string signatureValue = $"wrongprefix-deadbeef-wrongsuffix"; + var request = CreateRequest(signatureValue); + var verifier = CreateVerifier(EncodingType.Hex, signatureValueTemplate: template); + var result = await verifier.VerifyAsync(request); + Assert.IsFalse(result.IsSuccess); + } + + [Test] + public async Task TemplateDoesNotContainDigest_OnVerifyAsync_ReturnsFailure() + { + const string template = "prefix-{wrong}-suffix"; + const string signatureValue = $"wrongprefix-deadbeef-wrongsuffix"; + var request = CreateRequest(signatureValue); + var verifier = CreateVerifier(EncodingType.Hex, signatureValueTemplate: template); + var result = await verifier.VerifyAsync(request); + Assert.IsFalse(result.IsSuccess); + } + + private static string GetDigest(EncodingType encodingType, string secretKey, string payload) + { + var hmac = new System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(secretKey)); + var hash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(payload)); + return encodingType switch + { + EncodingType.Hex => BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(), + EncodingType.Base64 => Convert.ToBase64String(hash), + EncodingType.Base64Url => Convert.ToBase64String(hash).Replace('+', '-').Replace('/', '_').TrimEnd('='), + _ => BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant() + }; + } +} diff --git a/APIMatic.Core.Test/Security/SignatureVerifier/SignatureVerificationExtensionsTests.cs b/APIMatic.Core.Test/Security/SignatureVerifier/SignatureVerificationExtensionsTests.cs new file mode 100644 index 00000000..94033169 --- /dev/null +++ b/APIMatic.Core.Test/Security/SignatureVerifier/SignatureVerificationExtensionsTests.cs @@ -0,0 +1,21 @@ +using APIMatic.Core.Security.SignatureVerifier; +using NUnit.Framework; + +namespace APIMatic.Core.Test.Security.SignatureVerifier +{ + [TestFixture] + public class SignatureVerificationExtensionsTests + { + [TestCase(null, null, true)] + [TestCase(null, new byte[] { 1, 2, 3 }, false)] + [TestCase(new byte[] { 1, 2, 3 }, null, false)] + [TestCase(new byte[] { 1, 2, 3 }, new byte[] { 1, 2 }, false)] + [TestCase(new byte[] { }, new byte[] { }, true)] + [TestCase(new byte[] { 1, 2, 3, 4 }, new byte[] { 1, 2, 3, 4 }, true)] + [TestCase(new byte[] { 1, 2, 3, 4 }, new byte[] { 1, 2, 3, 5 }, false)] + public void ConstantTimeEquals_VariousInputs_ReturnsExpected(byte[] a, byte[] b, bool expected) + { + Assert.AreEqual(expected, a.ConstantTimeEquals(b)); + } + } +} diff --git a/APIMatic.Core/APIMatic.Core.csproj b/APIMatic.Core/APIMatic.Core.csproj index a7549d72..bb813c55 100644 --- a/APIMatic.Core/APIMatic.Core.csproj +++ b/APIMatic.Core/APIMatic.Core.csproj @@ -49,5 +49,8 @@ + + + diff --git a/APIMatic.Core/Http/Abstractions/IHttpRequestData.cs b/APIMatic.Core/Http/Abstractions/IHttpRequestData.cs new file mode 100644 index 00000000..aaeac0a4 --- /dev/null +++ b/APIMatic.Core/Http/Abstractions/IHttpRequestData.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace APIMatic.Core.Http.Abstractions +{ + /// + /// Represents the contract for HTTP request data, including method, URL, headers, body, query parameters, cookies, protocol, content type, and content length. + /// + public interface IHttpRequestData + { + /// + /// Gets the HTTP method (e.g., GET, POST, PUT, DELETE). + /// + string Method { get; } + + /// + /// Gets the request URL. + /// + Uri Url { get; } + + /// + /// Gets the collection of HTTP headers. + /// + IReadOnlyDictionary Headers { get; } + + /// + /// Gets the request body as a stream. + /// + Stream Body { get; } + + /// + /// Gets the collection of query parameters. + /// + /// + /// Caller owns disposal. + /// + IReadOnlyDictionary Query { get; } + + /// + /// Gets the collection of cookies. + /// + IReadOnlyDictionary Cookies { get; } + + /// + /// Gets the HTTP protocol version (e.g., "HTTP/1.1"). + /// + string Protocol { get; } + + /// + /// Gets the content type of the request (e.g., "application/json"). + /// + string ContentType { get; } + + /// + /// Gets the content length of the request body, if known. + /// + long? ContentLength { get; } + } +} \ No newline at end of file diff --git a/APIMatic.Core/Security/Abstractions/ISignatureVerifier.cs b/APIMatic.Core/Security/Abstractions/ISignatureVerifier.cs new file mode 100644 index 00000000..f12e4d98 --- /dev/null +++ b/APIMatic.Core/Security/Abstractions/ISignatureVerifier.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using APIMatic.Core.Http.Abstractions; +using APIMatic.Core.Types.Sdk; + +namespace APIMatic.Core.Security.Abstractions +{ + /// + /// Defines a contract for verifying the signature of an HTTP request. + /// + public interface ISignatureVerifier + { + /// + /// Verifies the signature of the specified HTTP request. + /// + /// The HTTP request data to verify. + /// A token to monitor for cancellation requests. + /// + /// VerificationResult containing the outcome of the verification process. + /// + Task VerifyAsync(IHttpRequestData request, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/APIMatic.Core/Security/Cryptography/Base64DigestCodec.cs b/APIMatic.Core/Security/Cryptography/Base64DigestCodec.cs new file mode 100644 index 00000000..a52a0cd2 --- /dev/null +++ b/APIMatic.Core/Security/Cryptography/Base64DigestCodec.cs @@ -0,0 +1,24 @@ +using System; + +namespace APIMatic.Core.Security.Cryptography +{ + /// + /// Base64 digest codec implementation. + /// + public class Base64DigestCodec : IDigestCodec + { + /// + /// Decodes a Base64 string back into a byte array. + /// + /// The Base64 string to decode. + /// The decoded byte array. + /// Thrown when the input is not a valid Base64 string. + public byte[] Decode(string encoded) + { + if (string.IsNullOrEmpty(encoded)) + throw new ArgumentException("Input cannot be null or empty", nameof(encoded)); + + return Convert.FromBase64String(encoded); + } + } +} diff --git a/APIMatic.Core/Security/Cryptography/Base64UrlDigestCodec.cs b/APIMatic.Core/Security/Cryptography/Base64UrlDigestCodec.cs new file mode 100644 index 00000000..3e3916eb --- /dev/null +++ b/APIMatic.Core/Security/Cryptography/Base64UrlDigestCodec.cs @@ -0,0 +1,38 @@ +using System; + +namespace APIMatic.Core.Security.Cryptography +{ + /// + /// Base64Url digest codec implementation. + /// + public class Base64UrlDigestCodec : IDigestCodec + { + /// + /// Decodes a Base64Url string back into a byte array. + /// + /// The Base64Url string to decode. + /// The decoded byte array. + /// Thrown when the input is not a valid Base64Url string. + public byte[] Decode(string encoded) + { + if (string.IsNullOrEmpty(encoded)) + throw new ArgumentException("Input cannot be null or empty", nameof(encoded)); + + // Restore padding and standard Base64 characters + var base64 = encoded.Replace('-', '+').Replace('_', '/'); + + // Add padding if necessary + switch (base64.Length % 4) + { + case 2: + base64 += "=="; + break; + case 3: + base64 += "="; + break; + } + + return Convert.FromBase64String(base64); + } + } +} diff --git a/APIMatic.Core/Security/Cryptography/DigestCodecFactory.cs b/APIMatic.Core/Security/Cryptography/DigestCodecFactory.cs new file mode 100644 index 00000000..49b79488 --- /dev/null +++ b/APIMatic.Core/Security/Cryptography/DigestCodecFactory.cs @@ -0,0 +1,29 @@ +using System; +using APIMatic.Core.Types; + +namespace APIMatic.Core.Security.Cryptography +{ + /// + /// Factory class for creating digest codecs based on encoding type. + /// + public static class DigestCodecFactory + { + /// + /// Creates a digest codec for the specified encoding type. + /// + /// The encoding type to use. + /// A digest codec instance. + /// Thrown when an unsupported encoding type is specified. + public static IDigestCodec Create(EncodingType digestEncoding) + { + return digestEncoding switch + { + EncodingType.Hex => new HexDigestCodec(), + EncodingType.Base64 => new Base64DigestCodec(), + EncodingType.Base64Url => new Base64UrlDigestCodec(), + _ => throw new ArgumentOutOfRangeException(nameof(digestEncoding), + $"Unsupported encoding type: {digestEncoding}") + }; + } + } +} diff --git a/APIMatic.Core/Security/Cryptography/HexDigestCodec.cs b/APIMatic.Core/Security/Cryptography/HexDigestCodec.cs new file mode 100644 index 00000000..6f86cb79 --- /dev/null +++ b/APIMatic.Core/Security/Cryptography/HexDigestCodec.cs @@ -0,0 +1,48 @@ +using System; +using System.Text; + +namespace APIMatic.Core.Security.Cryptography +{ + /// + /// encodedadecimal digest codec implementation. + /// + public class HexDigestCodec : IDigestCodec + { + /// + /// Decodes a hexadecimal string back into a byte array. + /// + /// The hexadecimal string to decode. + /// The decoded byte array. + /// Thrown when the input is null, empty, or has invalid length. + /// Thrown when the input is not a valid hexadecimal string. + public byte[] Decode(string encoded) + { + if (string.IsNullOrEmpty(encoded)) + throw new ArgumentException("Input cannot be null or empty", nameof(encoded)); + + // Remove any whitespace and convert to uppercase for consistency + encoded = encoded.Replace(" ", "").Replace("-", "").ToUpperInvariant(); + + // Hex string must have even length + if (encoded.Length % 2 != 0) + throw new ArgumentException("Hexadecimal string must have even length", nameof(encoded)); + + byte[] bytes = new byte[encoded.Length / 2]; + + for (int i = 0; i < bytes.Length; i++) + { + string hexPair = encoded.Substring(i * 2, 2); + try + { + bytes[i] = Convert.ToByte(hexPair, 16); + } + catch (FormatException) + { + throw new FormatException($"Invalid hexadecimal character in string at position {i * 2}: '{hexPair}'"); + } + } + + return bytes; + } + } +} diff --git a/APIMatic.Core/Security/Cryptography/IDigestCodec.cs b/APIMatic.Core/Security/Cryptography/IDigestCodec.cs new file mode 100644 index 00000000..20fcd43e --- /dev/null +++ b/APIMatic.Core/Security/Cryptography/IDigestCodec.cs @@ -0,0 +1,15 @@ +namespace APIMatic.Core.Security.Cryptography +{ + /// + /// Interface for encoding and decoding digest values. + /// + public interface IDigestCodec + { + /// + /// Decodes a string representation back into a byte array. + /// + /// The encoded string to decode. + /// The decoded byte array. + byte[] Decode(string encoded); + } +} diff --git a/APIMatic.Core/Security/SignatureVerifier/HmacSignatureVerifier.cs b/APIMatic.Core/Security/SignatureVerifier/HmacSignatureVerifier.cs new file mode 100644 index 00000000..06490856 --- /dev/null +++ b/APIMatic.Core/Security/SignatureVerifier/HmacSignatureVerifier.cs @@ -0,0 +1,168 @@ +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using APIMatic.Core.Http.Abstractions; +using APIMatic.Core.Security.Abstractions; +using APIMatic.Core.Security.Cryptography; +using APIMatic.Core.Types; +using APIMatic.Core.Types.Sdk; +using APIMatic.Core.Utilities; + +namespace APIMatic.Core.Security.SignatureVerifier +{ + /// + /// HMAC-based signature verifier for HTTP requests. + /// + public class HmacSignatureVerifier : ISignatureVerifier + { + /// + /// Name of the header carrying the provided signature (case-insensitive lookup). + /// + private readonly string _signatureHeader; + + /// + /// HMAC algorithm used for signature generation (default: HMACSHA256). + /// + private readonly HMAC _signatureAlgorithm; + + /// + /// Optional template for the expected signature value, where `{digest}` is replaced + /// by the encoded digest. If omitted, the expected signature is the encoded digest itself. + /// + private readonly string _signatureValueTemplate; + + /// + /// Resolves the request data into a byte array for signature computation. + /// + private readonly Func> _resolver; + + private readonly IDigestCodec _digestCodec; + + /// + /// Initializes a new instance of the HmacSignatureVerifier class. + /// + /// The secret key for HMAC computation. + /// The name of the header containing the signature. + /// Optional custom resolver for extracting data to sign. + /// Optional HMAC algorithm factory. + /// The encoding type for the signature. + /// Template for signature format. + public HmacSignatureVerifier( + string secretKey, + string signatureHeader, + EncodingType digestEncoding, + Func> resolver = null, + HMAC hashAlgFactory = null, + string signatureValueTemplate = "{digest}") + { + if (string.IsNullOrWhiteSpace(secretKey)) + throw new ArgumentNullException(nameof(secretKey), "Secret key cannot be null or Empty."); + + if (string.IsNullOrWhiteSpace(signatureHeader)) + throw new ArgumentNullException(nameof(signatureHeader), "Signature header cannot be null or Empty."); + + var secretKeyBytes = Encoding.UTF8.GetBytes(secretKey); + + _signatureHeader = signatureHeader; + _signatureAlgorithm = hashAlgFactory ?? new HMACSHA256(secretKeyBytes); + + // Create encoder and decoder using factory + _digestCodec = DigestCodecFactory.Create(digestEncoding); + + _signatureValueTemplate = signatureValueTemplate; + _resolver = resolver ?? (async (request, cancellationToken) => + await request.ReadBodyStreamToByteArrayAsync(cancellationToken).ConfigureAwait(false)); + } + + /// + /// Verifies the HMAC signature of the specified HTTP request. + /// + /// The HTTP request data to verify. + /// A token to cancel the asynchronous operation. + /// + /// A indicating whether the signature is valid or the reason for failure. + /// + public async Task VerifyAsync(IHttpRequestData request, + CancellationToken cancellationToken = default) + { + // Case-insensitive header lookup + var headerEntry = request.Headers.FirstOrDefault(h => + string.Equals(h.Key, _signatureHeader, StringComparison.OrdinalIgnoreCase)); + + if (headerEntry.Key == null) + { + return VerificationResult.Failure(new[] { $"Signature header '{_signatureHeader}' is missing." }); + } + + if (!TryExtractSignature(headerEntry.Value.FirstOrDefault(), out var providedSignature)) + { + return VerificationResult.Failure(new[] { $"Malformed signature header '{_signatureHeader}' value." }); + } + + var resolvedTemplateBytes = await _resolver.Invoke(request, cancellationToken).ConfigureAwait(false); + + var computedHash = _signatureAlgorithm.ComputeHash(resolvedTemplateBytes); + + return providedSignature.ConstantTimeEquals(computedHash) + ? VerificationResult.Success() + : VerificationResult.Failure(new[] { "Signature verification failed." }); + } + + /// + /// Extracts the digest value from the signature header according to the template. + /// + /// The signature header value. + /// The Signature value template. + /// The extracted digest string. + private static string ExtractDigestFromTemplate(string signatureValue, string signatureValueTemplate) + { + if (string.IsNullOrEmpty(signatureValue)) + return string.Empty; + + // If template is just "{digest}", return the signature as-is + if (signatureValueTemplate == "{digest}") + return signatureValue; + + // Extract digest from template + var digestIndex = signatureValueTemplate.IndexOf("{digest}", StringComparison.Ordinal); + if (digestIndex == -1) + return string.Empty; + + var prefix = signatureValueTemplate[..digestIndex]; + var suffix = signatureValueTemplate[(digestIndex + 8)..]; + + if (!signatureValue.StartsWith(prefix) || !signatureValue.EndsWith(suffix)) + return string.Empty; + + return signatureValue.Substring(prefix.Length, signatureValue.Length - prefix.Length - suffix.Length); + } + + /// + /// Attempts to extract and decode the signature from the header values. + /// + /// The signature header value. + /// The decoded signature bytes. + /// True if extraction and decoding succeeded, false otherwise. + private bool TryExtractSignature(string signatureValue, out byte[] signature) + { + signature = null; + var digest = ExtractDigestFromTemplate(signatureValue, _signatureValueTemplate); + + if (string.IsNullOrEmpty(digest)) + return false; + + try + { + signature = _digestCodec.Decode(digest); + return true; + } + catch + { + return false; + } + } + } +} diff --git a/APIMatic.Core/Security/SignatureVerifier/SignatureVerifierExtensions.cs b/APIMatic.Core/Security/SignatureVerifier/SignatureVerifierExtensions.cs new file mode 100644 index 00000000..c24e8003 --- /dev/null +++ b/APIMatic.Core/Security/SignatureVerifier/SignatureVerifierExtensions.cs @@ -0,0 +1,31 @@ +namespace APIMatic.Core.Security.SignatureVerifier +{ + internal static class SignatureVerifierExtensions + { + /// + /// Performs a secure comparison of two byte arrays to prevent timing attacks. + /// + /// First byte array. + /// Second byte array. + /// True if arrays are equal, false otherwise. + public static bool ConstantTimeEquals(this byte[] a, byte[] b) + { + if (a == null && b == null) + return true; + + if (a == null || b == null) + return false; + + if (a.Length != b.Length) + return false; + + var result = 0; + for (int i = 0; i < a.Length; i++) + { + result |= a[i] ^ b[i]; + } + + return result == 0; + } + } +} diff --git a/APIMatic.Core/Types/EncodingType.cs b/APIMatic.Core/Types/EncodingType.cs new file mode 100644 index 00000000..34ad0807 --- /dev/null +++ b/APIMatic.Core/Types/EncodingType.cs @@ -0,0 +1,9 @@ +namespace APIMatic.Core.Types +{ + public enum EncodingType + { + Hex, + Base64, + Base64Url + } +} \ No newline at end of file diff --git a/APIMatic.Core/Types/Sdk/Exceptions/SignatureVerificationException.cs b/APIMatic.Core/Types/Sdk/Exceptions/SignatureVerificationException.cs new file mode 100644 index 00000000..0ad7046f --- /dev/null +++ b/APIMatic.Core/Types/Sdk/Exceptions/SignatureVerificationException.cs @@ -0,0 +1,19 @@ +using System; +using System.Runtime.Serialization; + +namespace APIMatic.Core.Types.Sdk.Exceptions +{ + [Serializable] + public class SignatureVerificationException : Exception + { + public SignatureVerificationException() {} + + public SignatureVerificationException(string message) : base(message) {} + + public SignatureVerificationException(string message, Exception innerException) + : base(message, innerException) {} + + protected SignatureVerificationException(SerializationInfo info, StreamingContext context) + : base(info, context) {} + } +} \ No newline at end of file diff --git a/APIMatic.Core/Types/Sdk/VerificationResult.cs b/APIMatic.Core/Types/Sdk/VerificationResult.cs new file mode 100644 index 00000000..fc764a25 --- /dev/null +++ b/APIMatic.Core/Types/Sdk/VerificationResult.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace APIMatic.Core.Types.Sdk +{ + /// + /// Represents the result of an operation that can either succeed + /// or fail with an error message. + /// + public class VerificationResult + { + public bool IsSuccess => Errors == null; + + public IReadOnlyCollection Errors { get; } + + protected VerificationResult(string[] error) + { + Errors = error; + } + + /// + /// Creates a successful result. + /// + public static VerificationResult Success() => + new VerificationResult(null); + + /// + /// Creates a failed result with the given error message. + /// + public static VerificationResult Failure(string[] error) => + new VerificationResult(error); + } +} \ No newline at end of file diff --git a/APIMatic.Core/Utilities/IHttpRequestDataExtensions.cs b/APIMatic.Core/Utilities/IHttpRequestDataExtensions.cs new file mode 100644 index 00000000..a98e59a5 --- /dev/null +++ b/APIMatic.Core/Utilities/IHttpRequestDataExtensions.cs @@ -0,0 +1,36 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using APIMatic.Core.Http.Abstractions; + +namespace APIMatic.Core.Utilities +{ + /// + /// Extension methods for IHttpRequestData. + /// + internal static class IHttpRequestDataExtensions + { + /// + /// Reads the request body stream and converts it to a byte array. + /// + /// The HTTP request data. + /// Cancellation token for the operation. + /// A byte array containing the request body data. + internal static async Task ReadBodyStreamToByteArrayAsync(this IHttpRequestData requestData, + CancellationToken cancellationToken = default) + { + if (requestData.Body == null) + return Array.Empty(); + + if (requestData.Body.CanSeek) + requestData.Body.Position = 0; + + using (var memoryStream = new MemoryStream()) + { + await requestData.Body.CopyToAsync(memoryStream, 81920, cancellationToken).ConfigureAwait(false); + return memoryStream.ToArray(); + } + } + } +} \ No newline at end of file diff --git a/APIMatic.Core/Utilities/Json/JsonPointerResolver.cs b/APIMatic.Core/Utilities/Json/JsonPointerResolver.cs index c9ab7d08..ff932c9f 100644 --- a/APIMatic.Core/Utilities/Json/JsonPointerResolver.cs +++ b/APIMatic.Core/Utilities/Json/JsonPointerResolver.cs @@ -4,9 +4,9 @@ namespace APIMatic.Core.Utilities.Json { - internal static class JsonPointerResolver + public static class JsonPointerResolver { - public static string ResolveScopedJsonValue(string pointerString, string jsonBody, string jsonHeaders) + internal static string ResolveScopedJsonValue(string pointerString, string jsonBody, string jsonHeaders) { if (string.IsNullOrEmpty(pointerString) || !pointerString.Contains('#')) return null; @@ -27,6 +27,15 @@ public static string ResolveScopedJsonValue(string pointerString, string jsonBod return null; } } + + public static string ResolveJsonValue(string pointerString, string json) + { + if (string.IsNullOrEmpty(pointerString) || !pointerString.Contains('#')) + return null; + var path = pointerString.Split('#')[1]; + var jsonPointer = new JsonPointer(path); + return ExtractValueByPointer(jsonPointer, json); + } private static string ExtractValueByPointer(JsonPointer jsonPointer, string json) { diff --git a/README.md b/README.md index 01be839e..b1a95c84 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ This project contains core logic and the utilities for the APIMatic's C# SDK | [`HttpLoggingConfiguration`](APIMatic.Core/Utilities/Logger/Configuration/HttpLoggingConfiguration.cs) | Abstract class representing configuration settings for HTTP request/response logging | | [`RequestLoggingConfiguration`](APIMatic.Core/Utilities/Logger/Configuration/RequestLoggingConfiguration.cs) | Represents the configuration settings for logging HTTP responses | | [`ResponseLoggingConfiguration`](APIMatic.Core/Utilities/Logger/Configuration/ResponseLoggingConfiguration.cs) | Carries the common configuration that will be applicable to all the ApiCalls | +| [`ISignatureVerifier`](APIMatic.Core/Security/Abstractions/ISignatureVerifier.cs) | Defines a contract for verifying the signature of an HTTP request. | +| [`HmacSignatureVerifier`](APIMatic.Core/Security/SignatureVerifier/HmacSignatureVerifier.cs) | HMAC-based signature verifier for HTTP requests. | [nuget-url]: https://www.nuget.org/packages/APIMatic.Core From 91f0ffc24200865440f602088ee4717ce0826d03 Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Mon, 8 Sep 2025 14:21:46 +0500 Subject: [PATCH 2/7] update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b1a95c84..3f202e74 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ This project contains core logic and the utilities for the APIMatic's C# SDK | [`HttpLoggingConfiguration`](APIMatic.Core/Utilities/Logger/Configuration/HttpLoggingConfiguration.cs) | Abstract class representing configuration settings for HTTP request/response logging | | [`RequestLoggingConfiguration`](APIMatic.Core/Utilities/Logger/Configuration/RequestLoggingConfiguration.cs) | Represents the configuration settings for logging HTTP responses | | [`ResponseLoggingConfiguration`](APIMatic.Core/Utilities/Logger/Configuration/ResponseLoggingConfiguration.cs) | Carries the common configuration that will be applicable to all the ApiCalls | +| [`IHttpRequestData`](APIMatic.Core/Http/Abstractions/IHttpRequestData.cs) | Represents the contract for HTTP request data | | [`ISignatureVerifier`](APIMatic.Core/Security/Abstractions/ISignatureVerifier.cs) | Defines a contract for verifying the signature of an HTTP request. | | [`HmacSignatureVerifier`](APIMatic.Core/Security/SignatureVerifier/HmacSignatureVerifier.cs) | HMAC-based signature verifier for HTTP requests. | From 6703276bc5121a0ef80ce119261f457927399abd Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Tue, 9 Sep 2025 10:24:04 +0500 Subject: [PATCH 3/7] rename resolver to requestSignatureTemplateResolverAsync and add doc block for _digestCodec --- .../SignatureVerifier/HmacSignatureVerifier.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/APIMatic.Core/Security/SignatureVerifier/HmacSignatureVerifier.cs b/APIMatic.Core/Security/SignatureVerifier/HmacSignatureVerifier.cs index 06490856..14481f04 100644 --- a/APIMatic.Core/Security/SignatureVerifier/HmacSignatureVerifier.cs +++ b/APIMatic.Core/Security/SignatureVerifier/HmacSignatureVerifier.cs @@ -37,8 +37,11 @@ public class HmacSignatureVerifier : ISignatureVerifier /// /// Resolves the request data into a byte array for signature computation. /// - private readonly Func> _resolver; + private readonly Func> _requestSignatureTemplateResolverAsync; + /// + /// Codec used for encoding and decoding digests based on the specified encoding type. + /// private readonly IDigestCodec _digestCodec; /// @@ -46,7 +49,7 @@ public class HmacSignatureVerifier : ISignatureVerifier /// /// The secret key for HMAC computation. /// The name of the header containing the signature. - /// Optional custom resolver for extracting data to sign. + /// Optional custom resolver for extracting data to sign. /// Optional HMAC algorithm factory. /// The encoding type for the signature. /// Template for signature format. @@ -54,7 +57,7 @@ public HmacSignatureVerifier( string secretKey, string signatureHeader, EncodingType digestEncoding, - Func> resolver = null, + Func> requestSignatureTemplateResolverAsync = null, HMAC hashAlgFactory = null, string signatureValueTemplate = "{digest}") { @@ -73,7 +76,7 @@ public HmacSignatureVerifier( _digestCodec = DigestCodecFactory.Create(digestEncoding); _signatureValueTemplate = signatureValueTemplate; - _resolver = resolver ?? (async (request, cancellationToken) => + _requestSignatureTemplateResolverAsync = requestSignatureTemplateResolverAsync ?? (async (request, cancellationToken) => await request.ReadBodyStreamToByteArrayAsync(cancellationToken).ConfigureAwait(false)); } @@ -102,7 +105,7 @@ public async Task VerifyAsync(IHttpRequestData request, return VerificationResult.Failure(new[] { $"Malformed signature header '{_signatureHeader}' value." }); } - var resolvedTemplateBytes = await _resolver.Invoke(request, cancellationToken).ConfigureAwait(false); + var resolvedTemplateBytes = await _requestSignatureTemplateResolverAsync.Invoke(request, cancellationToken).ConfigureAwait(false); var computedHash = _signatureAlgorithm.ComputeHash(resolvedTemplateBytes); From f01a82aa762a43c84a5ebf768fc92c953c69a6f0 Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Wed, 17 Sep 2025 11:43:15 +0500 Subject: [PATCH 4/7] - add configurable Hmac has algorithm - add tests for HmacFactory - add unit test for ExpectedTemplate and SignatureValue --- .../Security/HmacFactoryTests.cs | 35 +++++++++++++ .../HmacSignatureVerifierTests.cs | 32 +++++++++++- .../HmacSignatureVerifier.cs | 50 +++++++++++-------- APIMatic.Core/Types/Sdk/HashAlgorithm.cs | 29 +++++++++++ 4 files changed, 123 insertions(+), 23 deletions(-) create mode 100644 APIMatic.Core.Test/Security/HmacFactoryTests.cs create mode 100644 APIMatic.Core/Types/Sdk/HashAlgorithm.cs diff --git a/APIMatic.Core.Test/Security/HmacFactoryTests.cs b/APIMatic.Core.Test/Security/HmacFactoryTests.cs new file mode 100644 index 00000000..4802a5c6 --- /dev/null +++ b/APIMatic.Core.Test/Security/HmacFactoryTests.cs @@ -0,0 +1,35 @@ +using System; +using System.Security.Cryptography; +using APIMatic.Core.Types.Sdk; +using NUnit.Framework; + +namespace APIMatic.Core.Test.Security +{ + [TestFixture] + public class HmacFactoryTests + { + [Test] + public void HmacAlgorithmSha256_HmacFactoryCreate_ReturnsHMACSHA256() + { + var key = new byte[] { 1, 2, 3 }; + var hmac = HmacFactory.Create(HmacAlgorithm.Sha256, key); + Assert.IsInstanceOf(hmac); + } + + [Test] + public void HmacAlgorithmSha512_HmacFactoryCreate_ReturnsHMACSHA512() + { + var key = new byte[] { 4, 5, 6 }; + var hmac = HmacFactory.Create(HmacAlgorithm.Sha512, key); + Assert.IsInstanceOf(hmac); + } + + [Test] + public void UnsupportedAlgorithm_HmacFactoryCreate_ThrowsNotSupportedException() + { + var key = new byte[] { 7, 8, 9 }; + const HmacAlgorithm invalidAlgorithm = (HmacAlgorithm)999; + Assert.Throws(() => HmacFactory.Create(invalidAlgorithm, key)); + } + } +} diff --git a/APIMatic.Core.Test/Security/SignatureVerifier/HmacSignatureVerifierTests.cs b/APIMatic.Core.Test/Security/SignatureVerifier/HmacSignatureVerifierTests.cs index 700653c0..21e91e2b 100644 --- a/APIMatic.Core.Test/Security/SignatureVerifier/HmacSignatureVerifierTests.cs +++ b/APIMatic.Core.Test/Security/SignatureVerifier/HmacSignatureVerifierTests.cs @@ -134,7 +134,7 @@ public async Task TemplateDoesNotMatch_OnVerifyAsync_ReturnsFailure() var verifier = CreateVerifier(EncodingType.Hex, signatureValueTemplate: template); var result = await verifier.VerifyAsync(request); Assert.IsFalse(result.IsSuccess); - } + } [Test] public async Task TemplateDoesNotContainDigest_OnVerifyAsync_ReturnsFailure() @@ -146,6 +146,36 @@ public async Task TemplateDoesNotContainDigest_OnVerifyAsync_ReturnsFailure() var result = await verifier.VerifyAsync(request); Assert.IsFalse(result.IsSuccess); } + + [Test] + public async Task CorrectDigestButIncorrectExpectedTemplate_OnVerifyAsync_ReturnsFailure() + { + var hmac = new System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(SecretKey)); + var hash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(Payload)); + var digest = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + // The expected template doesn't match the signature value template, though digest is correct + const string expectedTemplate = "sha26={digest}"; + var signatureValue = $"sha256={digest}"; + var request = CreateRequest(signatureValue); + var verifier = CreateVerifier(EncodingType.Hex, signatureValueTemplate: expectedTemplate); + var result = await verifier.VerifyAsync(request); + Assert.IsFalse(result.IsSuccess); + } + + [Test] + public async Task CorrectDigestButIncorrectSignatureValue_OnVerifyAsync_ReturnsFailure() + { + var hmac = new System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(SecretKey)); + var hash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(Payload)); + var digest = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + const string expectedTemplate = "sha256={digest}"; + // The signature value does not match the template, though digest is correct + var signatureValue = $"sha25={digest}"; + var request = CreateRequest(signatureValue); + var verifier = CreateVerifier(EncodingType.Hex, signatureValueTemplate: expectedTemplate); + var result = await verifier.VerifyAsync(request); + Assert.IsFalse(result.IsSuccess); + } private static string GetDigest(EncodingType encodingType, string secretKey, string payload) { diff --git a/APIMatic.Core/Security/SignatureVerifier/HmacSignatureVerifier.cs b/APIMatic.Core/Security/SignatureVerifier/HmacSignatureVerifier.cs index 14481f04..40c637b7 100644 --- a/APIMatic.Core/Security/SignatureVerifier/HmacSignatureVerifier.cs +++ b/APIMatic.Core/Security/SignatureVerifier/HmacSignatureVerifier.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -23,11 +22,6 @@ public class HmacSignatureVerifier : ISignatureVerifier /// private readonly string _signatureHeader; - /// - /// HMAC algorithm used for signature generation (default: HMACSHA256). - /// - private readonly HMAC _signatureAlgorithm; - /// /// Optional template for the expected signature value, where `{digest}` is replaced /// by the encoded digest. If omitted, the expected signature is the encoded digest itself. @@ -39,6 +33,16 @@ public class HmacSignatureVerifier : ISignatureVerifier /// private readonly Func> _requestSignatureTemplateResolverAsync; + /// + /// The HMAC algorithm used for signature computation. + /// + private readonly HmacAlgorithm _signatureAlgorithm; + + /// + /// The secret key, encoded as a byte array, used for HMAC operations. + /// + private readonly byte[] _encodedSecretKey; + /// /// Codec used for encoding and decoding digests based on the specified encoding type. /// @@ -50,7 +54,7 @@ public class HmacSignatureVerifier : ISignatureVerifier /// The secret key for HMAC computation. /// The name of the header containing the signature. /// Optional custom resolver for extracting data to sign. - /// Optional HMAC algorithm factory. + /// Optional HMAC algorithm. /// The encoding type for the signature. /// Template for signature format. public HmacSignatureVerifier( @@ -58,7 +62,7 @@ public HmacSignatureVerifier( string signatureHeader, EncodingType digestEncoding, Func> requestSignatureTemplateResolverAsync = null, - HMAC hashAlgFactory = null, + HmacAlgorithm hashAlgorithm = HmacAlgorithm.Sha256, string signatureValueTemplate = "{digest}") { if (string.IsNullOrWhiteSpace(secretKey)) @@ -67,17 +71,15 @@ public HmacSignatureVerifier( if (string.IsNullOrWhiteSpace(signatureHeader)) throw new ArgumentNullException(nameof(signatureHeader), "Signature header cannot be null or Empty."); - var secretKeyBytes = Encoding.UTF8.GetBytes(secretKey); - + _encodedSecretKey = Encoding.UTF8.GetBytes(secretKey); _signatureHeader = signatureHeader; - _signatureAlgorithm = hashAlgFactory ?? new HMACSHA256(secretKeyBytes); - - // Create encoder and decoder using factory + _signatureAlgorithm = hashAlgorithm; _digestCodec = DigestCodecFactory.Create(digestEncoding); - _signatureValueTemplate = signatureValueTemplate; - _requestSignatureTemplateResolverAsync = requestSignatureTemplateResolverAsync ?? (async (request, cancellationToken) => - await request.ReadBodyStreamToByteArrayAsync(cancellationToken).ConfigureAwait(false)); + _requestSignatureTemplateResolverAsync = requestSignatureTemplateResolverAsync ?? + (async (request, cancellationToken) => + await request.ReadBodyStreamToByteArrayAsync(cancellationToken) + .ConfigureAwait(false)); } /// @@ -105,13 +107,17 @@ public async Task VerifyAsync(IHttpRequestData request, return VerificationResult.Failure(new[] { $"Malformed signature header '{_signatureHeader}' value." }); } - var resolvedTemplateBytes = await _requestSignatureTemplateResolverAsync.Invoke(request, cancellationToken).ConfigureAwait(false); - - var computedHash = _signatureAlgorithm.ComputeHash(resolvedTemplateBytes); + var resolvedTemplateBytes = await _requestSignatureTemplateResolverAsync.Invoke(request, cancellationToken) + .ConfigureAwait(false); - return providedSignature.ConstantTimeEquals(computedHash) - ? VerificationResult.Success() - : VerificationResult.Failure(new[] { "Signature verification failed." }); + using (var hmac = HmacFactory.Create(_signatureAlgorithm, _encodedSecretKey)) + { + var computedHash = hmac.ComputeHash(resolvedTemplateBytes); + + return providedSignature.ConstantTimeEquals(computedHash) + ? VerificationResult.Success() + : VerificationResult.Failure(new[] { "Signature verification failed." }); + } } /// diff --git a/APIMatic.Core/Types/Sdk/HashAlgorithm.cs b/APIMatic.Core/Types/Sdk/HashAlgorithm.cs new file mode 100644 index 00000000..3bb42f4b --- /dev/null +++ b/APIMatic.Core/Types/Sdk/HashAlgorithm.cs @@ -0,0 +1,29 @@ +using System; +using System.Security.Cryptography; + +namespace APIMatic.Core.Types.Sdk +{ + public enum HmacAlgorithm + { + Sha256, + Sha512 + } + + internal static class HmacFactory + { + public static HMAC Create(HmacAlgorithm algorithm, byte[] keyBytes) + { + switch (algorithm) + { + case HmacAlgorithm.Sha256: + return new HMACSHA256(keyBytes); + + case HmacAlgorithm.Sha512: + return new HMACSHA512(keyBytes); + + default: + throw new NotSupportedException($"Unsupported HMAC algorithm: {algorithm}"); + } + } + } +} From 4e9622a0fa7809fbd2fdf4c903da1cced11d51f8 Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Tue, 30 Sep 2025 13:29:19 +0500 Subject: [PATCH 5/7] code cleanup --- .../Security/Cryptography/DigestCodecTests.cs | 10 ++++- .../SignatureVerificationExtensionsTests.cs | 2 +- .../IHttpRequestDataExtensionsTests.cs | 28 +++++++++++++ .../Utilities/Json/JsonPointerResolverTest.cs | 13 ++++++ .../Cryptography/Base64UrlDigestCodec.cs | 1 + .../Cryptography/DigestCodecFactory.cs | 9 ++-- .../Security/Cryptography/HexDigestCodec.cs | 4 +- .../HmacSignatureVerifier.cs | 12 +++--- .../SignatureVerifierExtensions.cs | 42 ++++++++++++------- .../SignatureVerificationException.cs | 19 --------- 10 files changed, 90 insertions(+), 50 deletions(-) create mode 100644 APIMatic.Core.Test/Utilities/IHttpRequestDataExtensionsTests.cs delete mode 100644 APIMatic.Core/Types/Sdk/Exceptions/SignatureVerificationException.cs diff --git a/APIMatic.Core.Test/Security/Cryptography/DigestCodecTests.cs b/APIMatic.Core.Test/Security/Cryptography/DigestCodecTests.cs index f400c9eb..78d88c25 100644 --- a/APIMatic.Core.Test/Security/Cryptography/DigestCodecTests.cs +++ b/APIMatic.Core.Test/Security/Cryptography/DigestCodecTests.cs @@ -20,7 +20,6 @@ public void DigestCodec_Decode_Success(EncodingType encodingType, string input, [TestCase(EncodingType.Hex, "")] [TestCase(EncodingType.Hex, null)] - [TestCase(EncodingType.Hex, "ABC")] [TestCase(EncodingType.Base64, "")] [TestCase(EncodingType.Base64, null)] [TestCase(EncodingType.Base64Url, "")] @@ -31,11 +30,18 @@ public void DigestCodecIncorrectInput_Decode_DigestCodec_Create_Exception(Encodi Assert.Throws(() => codec.Decode(input)); } + [TestCase(EncodingType.Hex, "ABC")] + public void DigestCodecIncorrectFormat_Decode_DigestCodec_Create_Exception(EncodingType encodingType, string input) + { + var codec = DigestCodecFactory.Create(encodingType); + Assert.Throws(() => codec.Decode(input)); + } + [TestCase(-1)] public void DigestCodec_Create_Exception(int invalidValue) { var encodingType = (EncodingType)invalidValue; - Assert.Throws(() => DigestCodecFactory.Create(encodingType)); + Assert.Throws(() => DigestCodecFactory.Create(encodingType)); } } } diff --git a/APIMatic.Core.Test/Security/SignatureVerifier/SignatureVerificationExtensionsTests.cs b/APIMatic.Core.Test/Security/SignatureVerifier/SignatureVerificationExtensionsTests.cs index 94033169..095ae2fd 100644 --- a/APIMatic.Core.Test/Security/SignatureVerifier/SignatureVerificationExtensionsTests.cs +++ b/APIMatic.Core.Test/Security/SignatureVerifier/SignatureVerificationExtensionsTests.cs @@ -15,7 +15,7 @@ public class SignatureVerificationExtensionsTests [TestCase(new byte[] { 1, 2, 3, 4 }, new byte[] { 1, 2, 3, 5 }, false)] public void ConstantTimeEquals_VariousInputs_ReturnsExpected(byte[] a, byte[] b, bool expected) { - Assert.AreEqual(expected, a.ConstantTimeEquals(b)); + Assert.AreEqual(expected, SignatureVerifierExtensions.FixedTimeEquals(a, b)); } } } diff --git a/APIMatic.Core.Test/Utilities/IHttpRequestDataExtensionsTests.cs b/APIMatic.Core.Test/Utilities/IHttpRequestDataExtensionsTests.cs new file mode 100644 index 00000000..bffd5ce9 --- /dev/null +++ b/APIMatic.Core.Test/Utilities/IHttpRequestDataExtensionsTests.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using APIMatic.Core.Test.MockTypes.Http.Request; +using APIMatic.Core.Utilities; +using NUnit.Framework; + +namespace APIMatic.Core.Test.Utilities +{ + [TestFixture] + public class IHttpRequestDataExtensionsTests + { + + [TestCase(null, new byte[0])] + [TestCase("", new byte[0])] + [TestCase("hello", new byte[] { 104, 101, 108, 108, 111 })] + public async Task ReadBodyStreamToByteArrayAsync_VariousBodies_ReturnsExpected(string body, byte[] expected) + { + var stream = body == null ? null : new MemoryStream(Encoding.UTF8.GetBytes(body)); + var request = new HttpRequestData(new Dictionary(), stream); + + var result = await request.ReadBodyStreamToByteArrayAsync(); + + Assert.AreEqual(expected, result); + } + } +} diff --git a/APIMatic.Core.Test/Utilities/Json/JsonPointerResolverTest.cs b/APIMatic.Core.Test/Utilities/Json/JsonPointerResolverTest.cs index 9cdfbed0..1a0bd5b3 100644 --- a/APIMatic.Core.Test/Utilities/Json/JsonPointerResolverTest.cs +++ b/APIMatic.Core.Test/Utilities/Json/JsonPointerResolverTest.cs @@ -81,5 +81,18 @@ public void ResolveScopedJsonValue_BooleanToken_ReturnsTokenToString() Assert.AreEqual("True", result); } + + [TestCase("#/name", "{\"name\":\"John\",\"age\":30}", "John")] + [TestCase("#/invalid", "{\"name\":\"John\"}", null)] + [TestCase(null, "{\"name\":\"John\"}", null)] + [TestCase("", "{\"name\":\"John\"}", null)] + [TestCase("/name", "{\"name\":\"John\"}", null)] + [TestCase("#/age", "{\"age\":30}", "30")] + [TestCase("#/name", null, null)] + public void ResolveJsonValue_VariousCases_ReturnsExpected(string jsonPointer, string json, string expected) + { + var result = JsonPointerResolver.ResolveJsonValue(jsonPointer, json); + Assert.AreEqual(expected, result); + } } } diff --git a/APIMatic.Core/Security/Cryptography/Base64UrlDigestCodec.cs b/APIMatic.Core/Security/Cryptography/Base64UrlDigestCodec.cs index 3e3916eb..d82b34de 100644 --- a/APIMatic.Core/Security/Cryptography/Base64UrlDigestCodec.cs +++ b/APIMatic.Core/Security/Cryptography/Base64UrlDigestCodec.cs @@ -12,6 +12,7 @@ public class Base64UrlDigestCodec : IDigestCodec /// /// The Base64Url string to decode. /// The decoded byte array. + /// Thrown when the input is null. /// Thrown when the input is not a valid Base64Url string. public byte[] Decode(string encoded) { diff --git a/APIMatic.Core/Security/Cryptography/DigestCodecFactory.cs b/APIMatic.Core/Security/Cryptography/DigestCodecFactory.cs index 49b79488..78f26f4a 100644 --- a/APIMatic.Core/Security/Cryptography/DigestCodecFactory.cs +++ b/APIMatic.Core/Security/Cryptography/DigestCodecFactory.cs @@ -11,18 +11,17 @@ public static class DigestCodecFactory /// /// Creates a digest codec for the specified encoding type. /// - /// The encoding type to use. + /// The encoding type to use. /// A digest codec instance. /// Thrown when an unsupported encoding type is specified. - public static IDigestCodec Create(EncodingType digestEncoding) + public static IDigestCodec Create(EncodingType encodingType) { - return digestEncoding switch + return encodingType switch { EncodingType.Hex => new HexDigestCodec(), EncodingType.Base64 => new Base64DigestCodec(), EncodingType.Base64Url => new Base64UrlDigestCodec(), - _ => throw new ArgumentOutOfRangeException(nameof(digestEncoding), - $"Unsupported encoding type: {digestEncoding}") + _ => throw new NotSupportedException($"Unsupported encoding type: {encodingType}") }; } } diff --git a/APIMatic.Core/Security/Cryptography/HexDigestCodec.cs b/APIMatic.Core/Security/Cryptography/HexDigestCodec.cs index 6f86cb79..56eecaa3 100644 --- a/APIMatic.Core/Security/Cryptography/HexDigestCodec.cs +++ b/APIMatic.Core/Security/Cryptography/HexDigestCodec.cs @@ -4,7 +4,7 @@ namespace APIMatic.Core.Security.Cryptography { /// - /// encodedadecimal digest codec implementation. + /// HexDigestCodec digest codec implementation. /// public class HexDigestCodec : IDigestCodec { @@ -25,7 +25,7 @@ public byte[] Decode(string encoded) // Hex string must have even length if (encoded.Length % 2 != 0) - throw new ArgumentException("Hexadecimal string must have even length", nameof(encoded)); + throw new FormatException("Hexadecimal string must have even length"); byte[] bytes = new byte[encoded.Length / 2]; diff --git a/APIMatic.Core/Security/SignatureVerifier/HmacSignatureVerifier.cs b/APIMatic.Core/Security/SignatureVerifier/HmacSignatureVerifier.cs index 40c637b7..4fb83466 100644 --- a/APIMatic.Core/Security/SignatureVerifier/HmacSignatureVerifier.cs +++ b/APIMatic.Core/Security/SignatureVerifier/HmacSignatureVerifier.cs @@ -47,6 +47,8 @@ public class HmacSignatureVerifier : ISignatureVerifier /// Codec used for encoding and decoding digests based on the specified encoding type. /// private readonly IDigestCodec _digestCodec; + + private const string DigestPlaceHolder = "{digest}"; /// /// Initializes a new instance of the HmacSignatureVerifier class. @@ -81,7 +83,7 @@ public HmacSignatureVerifier( await request.ReadBodyStreamToByteArrayAsync(cancellationToken) .ConfigureAwait(false)); } - + /// /// Verifies the HMAC signature of the specified HTTP request. /// @@ -114,7 +116,7 @@ public async Task VerifyAsync(IHttpRequestData request, { var computedHash = hmac.ComputeHash(resolvedTemplateBytes); - return providedSignature.ConstantTimeEquals(computedHash) + return SignatureVerifierExtensions.FixedTimeEquals(computedHash, providedSignature) ? VerificationResult.Success() : VerificationResult.Failure(new[] { "Signature verification failed." }); } @@ -132,16 +134,16 @@ private static string ExtractDigestFromTemplate(string signatureValue, string si return string.Empty; // If template is just "{digest}", return the signature as-is - if (signatureValueTemplate == "{digest}") + if (signatureValueTemplate == DigestPlaceHolder) return signatureValue; // Extract digest from template - var digestIndex = signatureValueTemplate.IndexOf("{digest}", StringComparison.Ordinal); + var digestIndex = signatureValueTemplate.IndexOf(DigestPlaceHolder, StringComparison.Ordinal); if (digestIndex == -1) return string.Empty; var prefix = signatureValueTemplate[..digestIndex]; - var suffix = signatureValueTemplate[(digestIndex + 8)..]; + var suffix = signatureValueTemplate[(digestIndex + DigestPlaceHolder.Length)..]; if (!signatureValue.StartsWith(prefix) || !signatureValue.EndsWith(suffix)) return string.Empty; diff --git a/APIMatic.Core/Security/SignatureVerifier/SignatureVerifierExtensions.cs b/APIMatic.Core/Security/SignatureVerifier/SignatureVerifierExtensions.cs index c24e8003..4a122a13 100644 --- a/APIMatic.Core/Security/SignatureVerifier/SignatureVerifierExtensions.cs +++ b/APIMatic.Core/Security/SignatureVerifier/SignatureVerifierExtensions.cs @@ -1,3 +1,6 @@ +using System; +using System.Runtime.CompilerServices; + namespace APIMatic.Core.Security.SignatureVerifier { internal static class SignatureVerifierExtensions @@ -5,27 +8,34 @@ internal static class SignatureVerifierExtensions /// /// Performs a secure comparison of two byte arrays to prevent timing attacks. /// - /// First byte array. - /// Second byte array. + /// First byte array. + /// Second byte array. /// True if arrays are equal, false otherwise. - public static bool ConstantTimeEquals(this byte[] a, byte[] b) + /// + /// This implementation is copied from CryptographicOperations in System.Security.Cryptography + /// + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public static bool FixedTimeEquals(ReadOnlySpan left, ReadOnlySpan right) { - if (a == null && b == null) - return true; - - if (a == null || b == null) - return false; - - if (a.Length != b.Length) + // NoOptimization because we want this method to be exactly as non-short-circuiting + // as written. + // + // NoInlining because the NoOptimization would get lost if the method got inlined. + + if (left.Length != right.Length) + { return false; - - var result = 0; - for (int i = 0; i < a.Length; i++) + } + + int length = left.Length; + int accum = 0; + + for (int i = 0; i < length; i++) { - result |= a[i] ^ b[i]; + accum |= left[i] - right[i]; } - - return result == 0; + + return accum == 0; } } } diff --git a/APIMatic.Core/Types/Sdk/Exceptions/SignatureVerificationException.cs b/APIMatic.Core/Types/Sdk/Exceptions/SignatureVerificationException.cs deleted file mode 100644 index 0ad7046f..00000000 --- a/APIMatic.Core/Types/Sdk/Exceptions/SignatureVerificationException.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Runtime.Serialization; - -namespace APIMatic.Core.Types.Sdk.Exceptions -{ - [Serializable] - public class SignatureVerificationException : Exception - { - public SignatureVerificationException() {} - - public SignatureVerificationException(string message) : base(message) {} - - public SignatureVerificationException(string message, Exception innerException) - : base(message, innerException) {} - - protected SignatureVerificationException(SerializationInfo info, StreamingContext context) - : base(info, context) {} - } -} \ No newline at end of file From bba870ff75719b88bfa8902ebd706263ae5a074e Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Tue, 30 Sep 2025 15:18:07 +0500 Subject: [PATCH 6/7] move IDigestCodec to DigestCodec abstract class implementation --- .../Security/Cryptography/DigestCodecTests.cs | 9 +++++---- .../Security/Cryptography/Base64DigestCodec.cs | 4 ++-- .../Security/Cryptography/Base64UrlDigestCodec.cs | 4 ++-- .../{DigestCodecFactory.cs => DigestCodec.cs} | 13 ++++++++++--- .../Security/Cryptography/HexDigestCodec.cs | 4 ++-- .../Security/Cryptography/IDigestCodec.cs | 15 --------------- .../SignatureVerifier/HmacSignatureVerifier.cs | 4 ++-- 7 files changed, 23 insertions(+), 30 deletions(-) rename APIMatic.Core/Security/Cryptography/{DigestCodecFactory.cs => DigestCodec.cs} (65%) delete mode 100644 APIMatic.Core/Security/Cryptography/IDigestCodec.cs diff --git a/APIMatic.Core.Test/Security/Cryptography/DigestCodecTests.cs b/APIMatic.Core.Test/Security/Cryptography/DigestCodecTests.cs index 78d88c25..0f5b4928 100644 --- a/APIMatic.Core.Test/Security/Cryptography/DigestCodecTests.cs +++ b/APIMatic.Core.Test/Security/Cryptography/DigestCodecTests.cs @@ -9,11 +9,12 @@ public class DigestCodecTests { [TestCase(EncodingType.Hex, "4A6F686E", new byte[] { 0x4A, 0x6F, 0x68, 0x6E })] [TestCase(EncodingType.Base64, "SGVsbG8=", new byte[] { 0x48, 0x65, 0x6C, 0x6C, 0x6F })] + [TestCase(EncodingType.Base64, " ", new byte[]{})] [TestCase(EncodingType.Base64Url, "SGVsbG8", new byte[] { 0x48, 0x65, 0x6C, 0x6C, 0x6F })] [TestCase(EncodingType.Base64Url, "SG", new byte[] { 0x48 })] public void DigestCodec_Decode_Success(EncodingType encodingType, string input, byte[] expected) { - var codec = DigestCodecFactory.Create(encodingType); + var codec = DigestCodec.Create(encodingType); var result = codec.Decode(input); Assert.AreEqual(expected, result); } @@ -26,14 +27,14 @@ public void DigestCodec_Decode_Success(EncodingType encodingType, string input, [TestCase(EncodingType.Base64Url, null)] public void DigestCodecIncorrectInput_Decode_DigestCodec_Create_Exception(EncodingType encodingType, string input) { - var codec = DigestCodecFactory.Create(encodingType); + var codec = DigestCodec.Create(encodingType); Assert.Throws(() => codec.Decode(input)); } [TestCase(EncodingType.Hex, "ABC")] public void DigestCodecIncorrectFormat_Decode_DigestCodec_Create_Exception(EncodingType encodingType, string input) { - var codec = DigestCodecFactory.Create(encodingType); + var codec = DigestCodec.Create(encodingType); Assert.Throws(() => codec.Decode(input)); } @@ -41,7 +42,7 @@ public void DigestCodecIncorrectFormat_Decode_DigestCodec_Create_Exception(Encod public void DigestCodec_Create_Exception(int invalidValue) { var encodingType = (EncodingType)invalidValue; - Assert.Throws(() => DigestCodecFactory.Create(encodingType)); + Assert.Throws(() => DigestCodec.Create(encodingType)); } } } diff --git a/APIMatic.Core/Security/Cryptography/Base64DigestCodec.cs b/APIMatic.Core/Security/Cryptography/Base64DigestCodec.cs index a52a0cd2..2890f941 100644 --- a/APIMatic.Core/Security/Cryptography/Base64DigestCodec.cs +++ b/APIMatic.Core/Security/Cryptography/Base64DigestCodec.cs @@ -5,7 +5,7 @@ namespace APIMatic.Core.Security.Cryptography /// /// Base64 digest codec implementation. /// - public class Base64DigestCodec : IDigestCodec + internal class Base64DigestCodec : DigestCodec { /// /// Decodes a Base64 string back into a byte array. @@ -13,7 +13,7 @@ public class Base64DigestCodec : IDigestCodec /// The Base64 string to decode. /// The decoded byte array. /// Thrown when the input is not a valid Base64 string. - public byte[] Decode(string encoded) + public override byte[] Decode(string encoded) { if (string.IsNullOrEmpty(encoded)) throw new ArgumentException("Input cannot be null or empty", nameof(encoded)); diff --git a/APIMatic.Core/Security/Cryptography/Base64UrlDigestCodec.cs b/APIMatic.Core/Security/Cryptography/Base64UrlDigestCodec.cs index d82b34de..d1d5a49c 100644 --- a/APIMatic.Core/Security/Cryptography/Base64UrlDigestCodec.cs +++ b/APIMatic.Core/Security/Cryptography/Base64UrlDigestCodec.cs @@ -5,7 +5,7 @@ namespace APIMatic.Core.Security.Cryptography /// /// Base64Url digest codec implementation. /// - public class Base64UrlDigestCodec : IDigestCodec + internal class Base64UrlDigestCodec : DigestCodec { /// /// Decodes a Base64Url string back into a byte array. @@ -14,7 +14,7 @@ public class Base64UrlDigestCodec : IDigestCodec /// The decoded byte array. /// Thrown when the input is null. /// Thrown when the input is not a valid Base64Url string. - public byte[] Decode(string encoded) + public override byte[] Decode(string encoded) { if (string.IsNullOrEmpty(encoded)) throw new ArgumentException("Input cannot be null or empty", nameof(encoded)); diff --git a/APIMatic.Core/Security/Cryptography/DigestCodecFactory.cs b/APIMatic.Core/Security/Cryptography/DigestCodec.cs similarity index 65% rename from APIMatic.Core/Security/Cryptography/DigestCodecFactory.cs rename to APIMatic.Core/Security/Cryptography/DigestCodec.cs index 78f26f4a..dcd91b83 100644 --- a/APIMatic.Core/Security/Cryptography/DigestCodecFactory.cs +++ b/APIMatic.Core/Security/Cryptography/DigestCodec.cs @@ -4,17 +4,24 @@ namespace APIMatic.Core.Security.Cryptography { /// - /// Factory class for creating digest codecs based on encoding type. + /// Abstract class for encoding and decoding digest values. /// - public static class DigestCodecFactory + internal abstract class DigestCodec { + /// + /// Decodes a string representation back into a byte array. + /// + /// The encoded string to decode. + /// The decoded byte array. + public abstract byte[] Decode(string encoded); + /// /// Creates a digest codec for the specified encoding type. /// /// The encoding type to use. /// A digest codec instance. /// Thrown when an unsupported encoding type is specified. - public static IDigestCodec Create(EncodingType encodingType) + public static DigestCodec Create(EncodingType encodingType) { return encodingType switch { diff --git a/APIMatic.Core/Security/Cryptography/HexDigestCodec.cs b/APIMatic.Core/Security/Cryptography/HexDigestCodec.cs index 56eecaa3..2c76425d 100644 --- a/APIMatic.Core/Security/Cryptography/HexDigestCodec.cs +++ b/APIMatic.Core/Security/Cryptography/HexDigestCodec.cs @@ -6,7 +6,7 @@ namespace APIMatic.Core.Security.Cryptography /// /// HexDigestCodec digest codec implementation. /// - public class HexDigestCodec : IDigestCodec + internal class HexDigestCodec : DigestCodec { /// /// Decodes a hexadecimal string back into a byte array. @@ -15,7 +15,7 @@ public class HexDigestCodec : IDigestCodec /// The decoded byte array. /// Thrown when the input is null, empty, or has invalid length. /// Thrown when the input is not a valid hexadecimal string. - public byte[] Decode(string encoded) + public override byte[] Decode(string encoded) { if (string.IsNullOrEmpty(encoded)) throw new ArgumentException("Input cannot be null or empty", nameof(encoded)); diff --git a/APIMatic.Core/Security/Cryptography/IDigestCodec.cs b/APIMatic.Core/Security/Cryptography/IDigestCodec.cs deleted file mode 100644 index 20fcd43e..00000000 --- a/APIMatic.Core/Security/Cryptography/IDigestCodec.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace APIMatic.Core.Security.Cryptography -{ - /// - /// Interface for encoding and decoding digest values. - /// - public interface IDigestCodec - { - /// - /// Decodes a string representation back into a byte array. - /// - /// The encoded string to decode. - /// The decoded byte array. - byte[] Decode(string encoded); - } -} diff --git a/APIMatic.Core/Security/SignatureVerifier/HmacSignatureVerifier.cs b/APIMatic.Core/Security/SignatureVerifier/HmacSignatureVerifier.cs index 4fb83466..a3cfedb5 100644 --- a/APIMatic.Core/Security/SignatureVerifier/HmacSignatureVerifier.cs +++ b/APIMatic.Core/Security/SignatureVerifier/HmacSignatureVerifier.cs @@ -46,7 +46,7 @@ public class HmacSignatureVerifier : ISignatureVerifier /// /// Codec used for encoding and decoding digests based on the specified encoding type. /// - private readonly IDigestCodec _digestCodec; + private readonly DigestCodec _digestCodec; private const string DigestPlaceHolder = "{digest}"; @@ -76,7 +76,7 @@ public HmacSignatureVerifier( _encodedSecretKey = Encoding.UTF8.GetBytes(secretKey); _signatureHeader = signatureHeader; _signatureAlgorithm = hashAlgorithm; - _digestCodec = DigestCodecFactory.Create(digestEncoding); + _digestCodec = DigestCodec.Create(digestEncoding); _signatureValueTemplate = signatureValueTemplate; _requestSignatureTemplateResolverAsync = requestSignatureTemplateResolverAsync ?? (async (request, cancellationToken) => From ef543a17ad7aed30c2c65c406c62c7800cdc837d Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Tue, 30 Sep 2025 15:18:59 +0500 Subject: [PATCH 7/7] remvoe unused imports --- APIMatic.Core/Security/Cryptography/HexDigestCodec.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/APIMatic.Core/Security/Cryptography/HexDigestCodec.cs b/APIMatic.Core/Security/Cryptography/HexDigestCodec.cs index 2c76425d..b7f3f55a 100644 --- a/APIMatic.Core/Security/Cryptography/HexDigestCodec.cs +++ b/APIMatic.Core/Security/Cryptography/HexDigestCodec.cs @@ -1,5 +1,4 @@ using System; -using System.Text; namespace APIMatic.Core.Security.Cryptography {