Skip to content

Conversation

@tl-Roberto-Mancinelli
Copy link
Contributor

@tl-Roberto-Mancinelli tl-Roberto-Mancinelli commented Nov 14, 2025

Summary

Introduces a high-performance span-based verification API (VerifierSpan) for .NET 8+ that significantly reduces memory allocations while maintaining full compatibility with the existing Verifier API.

Motivation

The existing builder-based Verifier API creates multiple intermediate allocations during signature verification, particularly when processing request bodies and constructing the JWS signing string. For high-throughput scenarios, these allocations create GC pressure and can impact performance.

Changes

New Files

  • src/VerifierSpan.cs: High-performance span-based verification API

    • VerifierSpan.VerifyWithPem() - Verify with PEM-encoded public key
    • VerifierSpan.VerifyWith() - Verify with pre-parsed ECDsa key (most optimized)
    • Zero-allocation design using stackalloc for typical payloads
    • Direct ECDsa.VerifyData() calls bypassing Jose.JWT overhead
  • test/VerifierSpanTest.cs: Comprehensive test suite (38 tests)

    • All scenarios from UsageTest.cs mirrored for VerifierSpan
    • Additional invalid signature rejection tests
    • Large payload handling tests
    • Cross-compatibility with multiple key pairs

Modified Files

  • src/Util.cs: Added span-based utility methods for .NET 8+

    • CalculateV2SigningPayloadSize() - Pre-calculate exact buffer sizes
    • BuildV2SigningPayloadInto() - Zero-copy payload building into spans
  • src/Verifier.cs: Fixed file corruption (typos on lines 62, 64)

  • benchmarks/VerifierBenchmarks.cs: Updated to compare both APIs

    • Removed pre-parsed key benchmarks (focusing on PEM parsing scenario)
    • Added VerifierSpan benchmarks across 3 payload sizes

Performance Improvements

Benchmarks show significant memory reduction across all payload sizes:

Scenario Method Allocated Alloc Ratio Gen0 Gen1
SmallPayment Verifier 14.84 KB 1.00x 0.98 -
VerifierSpan 4.92 KB 0.33x 0 -
MediumMandate Verifier 31.32 KB 1.00x 2.93 -
VerifierSpan 5.23 KB 0.17x 0 -
LargeWebhook Verifier 142.52 KB 1.00x 16.60 0.98
VerifierSpan 29.24 KB 0.21x 2.93 0

Key Improvements:

  • 67-83% memory reduction depending on payload size
  • Zero Gen0 collections for small/medium payloads
  • Eliminated Gen1 collections entirely
  • Similar or better execution time (~680-780μs dominated by ECDSA crypto)

Implementation Details

Span-Based Optimizations

  1. Stack Allocation: Uses stackalloc for buffers <4KB (typical requests)
  2. Direct Buffer Building: Constructs JWS signing string without intermediate allocations
  3. Pre-calculated Sizes: Determines exact buffer requirements upfront
  4. Zero-Copy Operations: Writes directly into pre-allocated spans
  5. Platform-Specific: Uses .NET 9+ System.Buffers.Text.Base64Url when available

Architecture

// Typical usage
VerifierSpan.VerifyWithPem(
    publicKeyPemBytes,
    method: "POST",
    path: "/payments",
    headers: headersBytes,
    body: bodyBytes,
    tlSignature: signature
);

// Most optimized: reuse parsed key
using var key = publicKeyPem.ParsePem();
VerifierSpan.VerifyWith(key, method, path, headers, body, signature);

Key Design Decisions

  • Separate API: Avoids breaking changes to existing Verifier builder pattern
  • Conditional Compilation: Only available for .NET 8+ (#if NET8_0_OR_GREATER)
  • Functional Style: Static methods with all parameters upfront (no builder)
  • Span Parameters: Accepts ReadOnlySpan<byte> for body to avoid allocations

Testing

  • 96 total tests across net8.0, net9.0, net10.0
  • 38 VerifierSpan-specific tests covering:
    • Valid signature scenarios (22 tests)
    • Invalid signature rejection (7 tests)
    • Data mismatch detection (6 tests)
    • Error conditions (3 tests)

All tests pass across all target frameworks.

Breaking Changes

None - This is a purely additive change. The existing Verifier API remains unchanged and fully supported.

Migration Guide

For high-throughput scenarios where performance matters:

Before:

Verifier.VerifyWithPem(publicKeyPem)
    .Method("POST")
    .Path(path)
    .Headers(headers)
    .Body(body)
    .Verify(tlSignature);

After (NET8+):

VerifierSpan.VerifyWithPem(
    Encoding.UTF8.GetBytes(publicKeyPem),
    "POST",
    path,
    headers.Select(h => new KeyValuePair<string, byte[]>(h.Key, Encoding.UTF8.GetBytes(h.Value))),
    Encoding.UTF8.GetBytes(body),
    tlSignature
);

Future Work

Potential areas for further optimization:

  • SignerSpan API for signing operations
  • Support for pre-encoded headers to eliminate UTF8 encoding

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants