ContextR.Propagation.Signing¶
HMAC-based context signing for tamper detection across service boundaries.
When to use this package¶
Use ContextR.Propagation.Signing when propagated context must not be modified in transit. The package signs all mapped property headers on inject and verifies them on extract using HMAC-SHA256.
This package does not provide encryption (confidentiality). For encryption scenarios, see Encryption with DataProtection.
Install¶
Dependencies: ContextR.Propagation. No additional NuGet packages required — HMACSHA256 is part of the .NET runtime.
Quick start¶
builder.Services.AddContextR(ctx =>
{
ctx.Add<TenantContext>(reg => reg
.MapProperty(c => c.TenantId, "X-Tenant-Id")
.MapProperty(c => c.Region, "X-Region")
.UseContextSigning<TenantContext>(o =>
o.Key = Convert.FromBase64String("your-base64-key-here"))
.UseAspNetCore()
.UseGlobalHttpPropagation());
});
That's it. All outgoing requests include an X-Context-Signature header. All incoming requests are verified before context is extracted.
No custom ISigningKeyProvider implementation is needed — the key is configured inline.
How it works¶
Inject (outgoing)¶
- The inner propagator injects all mapped property headers normally
- All injected key/value pairs are collected
- Keys are sorted using
StringComparison.Ordinal(byte-order, case-sensitive) - Signing input is built as
key1=value1\nkey2=value2\n(trailing newline) - HMAC-SHA256 is computed over the UTF-8 bytes
- The signature is encoded as
<base64url-hmac>.<keyVersion>and added as a header
Extract (incoming)¶
- The signature header is read and decoded
- All other context headers are collected during inner propagator extraction
- The same canonical signing input is built
- HMAC-SHA256 is recomputed using the key version from the signature header
- Constant-time comparison verifies the signature
- If valid, context is returned. If invalid, the failure handler is triggered
Tamper detection coverage¶
The signing detects:
- Modified values — changing any header value invalidates the signature
- Removed headers — removing a header changes the signing input
- Added headers — adding a header that was not part of the original signed set changes the signing input
Configuration¶
.UseContextSigning<TenantContext>(o =>
{
o.Key = hmacKeyBytes; // inline key (simplest)
o.SignatureHeader = "X-TenantContext-Sig"; // default: X-Context-Signature
})
Key rotation¶
Inline key rotation¶
For simple deployments, configure multiple key versions directly:
.UseContextSigning<TenantContext>(o =>
{
o.AddKey(1, Convert.FromBase64String("old-key-base64"));
o.AddKey(2, Convert.FromBase64String("new-key-base64"));
o.CurrentKeyVersion = 2;
})
The signature header includes the key version, enabling zero-downtime key rotation:
- Deploy v2 key alongside v1 via
AddKey() - Set
CurrentKeyVersion = 2— new signatures use v2 - After all in-flight requests drain, remove v1
Custom key provider (advanced)¶
For dynamic key management (e.g., keys from Azure Key Vault, AWS Secrets Manager), implement ISigningKeyProvider and register it in DI:
public sealed class VaultSigningKeyProvider : ISigningKeyProvider
{
public byte[] GetKey(string keyId, int version) => /* fetch from vault */;
public int GetCurrentVersion(string keyId) => /* current version from vault */;
}
builder.Services.AddSingleton<ISigningKeyProvider, VaultSigningKeyProvider>();
builder.Services.AddContextR(ctx =>
{
ctx.Add<TenantContext>(reg => reg
.MapProperty(c => c.TenantId, "X-Tenant-Id")
.UseContextSigning<TenantContext>(o => o.KeyId = "context-hmac-key")
.UseAspNetCore()
.UseGlobalHttpPropagation());
});
When KeyId is set (without inline keys), the registered ISigningKeyProvider is resolved from DI.
Failure handling¶
Signing failures integrate with the existing propagation failure handler:
ctx.Add<TenantContext>(reg => reg
.MapProperty(c => c.TenantId, "X-Tenant-Id")
.UseContextSigning<TenantContext>(o =>
o.Key = hmacKeyBytes)
.OnPropagationFailure<TenantContext>(failure =>
{
logger.LogWarning("Signing failure: {Reason} for {Key}",
failure.RawValue, failure.Key);
return PropagationFailureAction.SkipContext;
}));
Failure reasons (available as constants on SigningFailureReasons):
| Reason | When |
|---|---|
SignatureInvalid |
HMAC verification failed (tampered headers) |
SignatureMissing |
No signature header on incoming request |
SignatureMalformed |
Signature header could not be parsed |
KeyNotFound |
Key provider could not resolve the key ID or version |
Canonical signing input format¶
The signing input is deterministic and documented for cross-platform compatibility:
- Collect all context header key/value pairs (excluding the signature header)
- Sort by key using
StringComparison.Ordinal - Format as
key=value\n(newline-separated, trailing newline) - UTF-8 encode
Example for headers X-Region: us-east-1 and X-Tenant-Id: acme:
Signature header format¶
Example: dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk.3
The base64url encoding uses - instead of +, _ instead of /, and no padding — safe for HTTP headers.
File map¶
| File | Role |
|---|---|
ISigningKeyProvider.cs |
Key provider contract (advanced) |
SigningOptions.cs |
Configuration options with inline key support |
SigningFailureReasons.cs |
Failure reason constants |
ContextRSigningExtensions.cs |
UseContextSigning<T>() registration |
Internal/SigningContextPropagator.cs |
IContextPropagator<T> decorator |
Internal/InMemorySigningKeyProvider.cs |
Built-in key provider for inline keys |
Internal/CanonicalSigningInput.cs |
Deterministic signing input builder |
Internal/SignatureCodec.cs |
Base64url signature encode/decode |
Testing¶
tests/ContextR.Propagation.Signing.UnitTests— propagator round-trip, tamper detection, key rotation, canonical ordering, codectests/ContextR.Propagation.Signing.IntegrationTests— end-to-end with ASP.NET Core TestHost and HttpClient propagation