Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions APIMatic.Core.Test/MockTypes/Http/Request/HttpRequestData.cs
Original file line number Diff line number Diff line change
@@ -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<string, string[]> Headers { get; }
public Stream Body { get; set; }
public IReadOnlyDictionary<string, string[]> Query { get; }
public IReadOnlyDictionary<string, string> Cookies { get; }
public string Protocol { get; }
public string ContentType { get; }
public long? ContentLength { get; }

public HttpRequestData(
IDictionary<string, string[]> headers,
Stream body)
{
Headers = new ReadOnlyDictionary<string, string[]>(headers);
Body = body;
}
}
}
41 changes: 41 additions & 0 deletions APIMatic.Core.Test/Security/Cryptography/DigestCodecTests.cs
Original file line number Diff line number Diff line change
@@ -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<ArgumentException>(() => codec.Decode(input));
}

[TestCase(-1)]
public void DigestCodec_Create_Exception(int invalidValue)
{
var encodingType = (EncodingType)invalidValue;
Assert.Throws<ArgumentOutOfRangeException>(() => DigestCodecFactory.Create(encodingType));
}
}
}
35 changes: 35 additions & 0 deletions APIMatic.Core.Test/Security/HmacFactoryTests.cs
Original file line number Diff line number Diff line change
@@ -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<HMACSHA256>(hmac);
}

[Test]
public void HmacAlgorithmSha512_HmacFactoryCreate_ReturnsHMACSHA512()
{
var key = new byte[] { 4, 5, 6 };
var hmac = HmacFactory.Create(HmacAlgorithm.Sha512, key);
Assert.IsInstanceOf<HMACSHA512>(hmac);
}

[Test]
public void UnsupportedAlgorithm_HmacFactoryCreate_ThrowsNotSupportedException()
{
var key = new byte[] { 7, 8, 9 };
const HmacAlgorithm invalidAlgorithm = (HmacAlgorithm)999;
Assert.Throws<NotSupportedException>(() => HmacFactory.Create(invalidAlgorithm, key));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
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<string, string[]>()
: new Dictionary<string, string[]> { { 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<ArgumentNullException>(() => CreateVerifier(EncodingType.Hex, secretKey: null));
Assert.Throws<ArgumentNullException>(() => CreateVerifier(EncodingType.Hex, secretKey: ""));
}

[Test]
public void Constructor_ThrowsOnNullOrEmptyHeader_OnCreate_ThrowsException()
{
Assert.Throws<ArgumentNullException>(() => CreateVerifier(EncodingType.Hex, headerName: null));
Assert.Throws<ArgumentNullException>(() => 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);
}

[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)
{
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()
};
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
3 changes: 3 additions & 0 deletions APIMatic.Core/APIMatic.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,8 @@
<ItemGroup>
<InternalsVisibleTo Include="APIMatic.Core.Test" />
</ItemGroup>
<ItemGroup>
<Folder Include="Http\Abstractions\" />
</ItemGroup>

</Project>
60 changes: 60 additions & 0 deletions APIMatic.Core/Http/Abstractions/IHttpRequestData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.IO;

namespace APIMatic.Core.Http.Abstractions
{
/// <summary>
/// Represents the contract for HTTP request data, including method, URL, headers, body, query parameters, cookies, protocol, content type, and content length.
/// </summary>
public interface IHttpRequestData
{
/// <summary>
/// Gets the HTTP method (e.g., GET, POST, PUT, DELETE).
/// </summary>
string Method { get; }

/// <summary>
/// Gets the request URL.
/// </summary>
Uri Url { get; }

/// <summary>
/// Gets the collection of HTTP headers.
/// </summary>
IReadOnlyDictionary<string, string[]> Headers { get; }

/// <summary>
/// Gets the request body as a stream.
/// </summary>
Stream Body { get; }

/// <summary>
/// Gets the collection of query parameters.
/// </summary>
/// <remarks>
/// Caller owns disposal.
/// </remarks>
IReadOnlyDictionary<string, string[]> Query { get; }

/// <summary>
/// Gets the collection of cookies.
/// </summary>
IReadOnlyDictionary<string, string> Cookies { get; }

/// <summary>
/// Gets the HTTP protocol version (e.g., "HTTP/1.1").
/// </summary>
string Protocol { get; }

/// <summary>
/// Gets the content type of the request (e.g., "application/json").
/// </summary>
string ContentType { get; }

/// <summary>
/// Gets the content length of the request body, if known.
/// </summary>
long? ContentLength { get; }
}
}
Loading
Loading