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
62 changes: 53 additions & 9 deletions csharp/benchmarks/VerifierBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Order;

namespace TrueLayer.Signing.Benchmarks;

/// <summary>
/// Benchmarks for signature verification operations.
/// Compares the builder-based Verifier API with the optimized VerifierSpan API (NET8+).
/// </summary>
[MemoryDiagnoser]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[RankColumn]
public class VerifierBenchmarks
{
private string? _signature;
private Verifier? _verifier;
private string _signature = null!;
private byte[] _publicKeyPemBytes = null!;
private string _method = null!;
private string _path = null!;
private KeyValuePair<string, byte[]>[] _headersBytes = null!;
private byte[] _bodyBytes = null!;

[Params("SmallPayment", "MediumMandate", "LargeWebhook")]
public string Scenario { get; set; } = "SmallPayment";

[GlobalSetup]
public void Setup()
{
var privateKey = TestData.GetPrivateKey();
var publicKey = TestData.GetPublicKey();
var scenario = TestData.Scenarios.SmallPayment;
var scenario = GetScenario(Scenario);

_signature = Signer.SignWith(TestData.Kid, privateKey)
.Method(scenario.Method)
Expand All @@ -28,16 +39,49 @@ public void Setup()
.Body(scenario.Body)
.Sign();

_verifier = Verifier.VerifyWith(publicKey)
// Setup for span-based VerifierSpan
_publicKeyPemBytes = Encoding.UTF8.GetBytes(TestData.PublicKeyPem);
_method = scenario.Method;
_path = scenario.Path;
_headersBytes = scenario.Headers
.Select(h => new KeyValuePair<string, byte[]>(h.Key, Encoding.UTF8.GetBytes(h.Value)))
.ToArray();
_bodyBytes = Encoding.UTF8.GetBytes(scenario.Body);
}

private static TestData.RequestScenario GetScenario(string name) => name switch
{
"SmallPayment" => TestData.Scenarios.SmallPayment,
"MediumMandate" => TestData.Scenarios.MediumMandate,
"LargeWebhook" => TestData.Scenarios.LargeWebhook,
_ => TestData.Scenarios.SmallPayment
};

[Benchmark(Baseline = true, Description = "Verifier (with PEM parsing)")]
public void VerifierBuilderWithPemParsing()
{
var scenario = GetScenario(Scenario);
using var key = ECDsa.Create();
key.ImportFromPem(TestData.PublicKeyPem);

Verifier.VerifyWith(key)
.Method(scenario.Method)
.Path(scenario.Path)
.Headers(scenario.Headers)
.Body(scenario.Body);
.Body(scenario.Body)
.Verify(_signature);
}

[Benchmark(Description = "Verify Request")]
public void VerifyRequest()
[Benchmark(Description = "VerifierSpan (with PEM parsing)")]
public void VerifierSpanWithPemParsing()
{
_verifier!.Verify(_signature!);
VerifierSpan.VerifyWithPem(
_publicKeyPemBytes,
_method,
_path,
_headersBytes,
_bodyBytes,
_signature
);
}
}
102 changes: 102 additions & 0 deletions csharp/src/Util.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,108 @@ internal static byte[] BuildV2SigningPayload(
return payload.ToArray();
}

#if NET8_0_OR_GREATER
/// <summary>
/// Calculate the exact size needed for a V2 signing payload.
/// </summary>
internal static int CalculateV2SigningPayloadSize(
string method,
string path,
ReadOnlySpan<(string, byte[])> headers,
ReadOnlySpan<byte> body)
{
int totalSize = 0;

// Method (uppercase) + space
string methodUpper = method.ToUpperInvariant();
totalSize += Encoding.UTF8.GetByteCount(methodUpper) + SpaceBytes.Length;

// Path + newline
totalSize += Encoding.UTF8.GetByteCount(path) + NewlineBytes.Length;

// Headers: "name: value\n" for each
for (int i = 0; i < headers.Length; i++)
{
var (name, value) = headers[i];
totalSize += Encoding.UTF8.GetByteCount(name);
totalSize += ColonSpaceBytes.Length;
totalSize += value.Length;
totalSize += NewlineBytes.Length;
}

// Body
totalSize += body.Length;

return totalSize;
}

/// <summary>
/// Build signing payload directly into a destination span.
/// Returns the number of bytes written.
/// The destination span must be large enough (use CalculateV2SigningPayloadSize).
/// </summary>
internal static int BuildV2SigningPayloadInto(
Span<byte> destination,
string method,
string path,
ReadOnlySpan<(string, byte[])> headers,
ReadOnlySpan<byte> body)
{
int position = 0;

// Write method (uppercase) + space
string methodUpper = method.ToUpperInvariant();
position += Encoding.UTF8.GetBytes(methodUpper, destination.Slice(position));
SpaceBytes.AsSpan().CopyTo(destination.Slice(position));
position += SpaceBytes.Length;

// Write path + newline
position += Encoding.UTF8.GetBytes(path, destination.Slice(position));
NewlineBytes.AsSpan().CopyTo(destination.Slice(position));
position += NewlineBytes.Length;

// Write headers
for (int i = 0; i < headers.Length; i++)
{
var (name, value) = headers[i];
position += Encoding.UTF8.GetBytes(name, destination.Slice(position));
ColonSpaceBytes.AsSpan().CopyTo(destination.Slice(position));
position += ColonSpaceBytes.Length;
value.AsSpan().CopyTo(destination.Slice(position));
position += value.Length;
NewlineBytes.AsSpan().CopyTo(destination.Slice(position));
position += NewlineBytes.Length;
}

// Write body
body.CopyTo(destination.Slice(position));
position += body.Length;

return position;
}

/// <summary>
/// Build signing payload from method, path, some/none/all headers and body.
/// Optimized for .NET 8+ using span-based operations with pre-calculated size.
/// Eliminates List allocations and intermediate copies.
/// </summary>
internal static byte[] BuildV2SigningPayload(
string method,
string path,
ReadOnlySpan<(string, byte[])> headers,
ReadOnlySpan<byte> body)
{
// Calculate exact size
int totalSize = CalculateV2SigningPayloadSize(method, path, headers, body);

// Allocate and write
byte[] payload = new byte[totalSize];
BuildV2SigningPayloadInto(payload, method, path, headers, body);

return payload;
}
#endif

/// <summary>Convert to utf-8 bytes</summary>
internal static byte[] ToUtf8(this string text) => Encoding.UTF8.GetBytes(text);

Expand Down
2 changes: 1 addition & 1 deletion csharp/src/Verifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public static Verifier VerifyWithJwks(ReadOnlySpan<byte> jwksJson)
}
}

/// <summary>Start building a `Tl-Signature` header verifier usinga a public key.</summary>
/// <summary>Start building a `Tl-Signature` header verifier using a public key.</summary>
public static Verifier VerifyWith(ECDsa publicKey) => new Verifier(publicKey);

/// <summary>Extract a header value from unverified jws Tl-Signature.</summary>
Expand Down
Loading
Loading