feat(rate-limiting): Implement core rate limiting functionality with configuration, decision-making, metrics, middleware, and service registration

- Add RateLimitConfig for configuration management with YAML binding support.
- Introduce RateLimitDecision to encapsulate the result of rate limit checks.
- Implement RateLimitMetrics for OpenTelemetry metrics tracking.
- Create RateLimitMiddleware for enforcing rate limits on incoming requests.
- Develop RateLimitService to orchestrate instance and environment rate limit checks.
- Add RateLimitServiceCollectionExtensions for dependency injection registration.
This commit is contained in:
master
2025-12-17 18:02:37 +02:00
parent 394b57f6bf
commit 8bbfe4d2d2
211 changed files with 47179 additions and 1590 deletions

View File

@@ -0,0 +1,707 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Attestor.ProofChain.Identifiers;
using StellaOps.Attestor.ProofChain.Receipts;
namespace StellaOps.Attestor.ProofChain.Verification;
/// <summary>
/// Implementation of the verification pipeline per advisory §9.1.
/// Executes DSSE signature verification, ID recomputation, Merkle proof
/// verification, and Rekor inclusion proof verification.
/// </summary>
public sealed class VerificationPipeline : IVerificationPipeline
{
private readonly IReadOnlyList<IVerificationStep> _steps;
private readonly ILogger<VerificationPipeline> _logger;
private readonly TimeProvider _timeProvider;
public VerificationPipeline(
IEnumerable<IVerificationStep> steps,
ILogger<VerificationPipeline> logger,
TimeProvider? timeProvider = null)
{
_steps = steps?.ToList() ?? throw new ArgumentNullException(nameof(steps));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <summary>
/// Creates a pipeline with the default verification steps.
/// </summary>
public static VerificationPipeline CreateDefault(
IProofBundleStore proofStore,
IDsseVerifier dsseVerifier,
IRekorVerifier rekorVerifier,
ITrustAnchorResolver trustAnchorResolver,
ILogger<VerificationPipeline> logger,
TimeProvider? timeProvider = null)
{
var steps = new List<IVerificationStep>
{
new DsseSignatureVerificationStep(proofStore, dsseVerifier, logger),
new IdRecomputationVerificationStep(proofStore, logger),
new RekorInclusionVerificationStep(proofStore, rekorVerifier, logger),
new TrustAnchorVerificationStep(trustAnchorResolver, logger)
};
return new VerificationPipeline(steps, logger, timeProvider);
}
/// <inheritdoc />
public async Task<VerificationPipelineResult> VerifyAsync(
VerificationPipelineRequest request,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(request);
var context = new VerificationContext
{
ProofBundleId = request.ProofBundleId,
TrustAnchorId = request.TrustAnchorId,
VerifyRekor = request.VerifyRekor
};
var stepResults = new List<VerificationStepResult>();
var pipelineStartTime = _timeProvider.GetUtcNow();
var overallPassed = true;
string? failureReason = null;
_logger.LogInformation(
"Starting verification pipeline for proof bundle {ProofBundleId}",
request.ProofBundleId);
foreach (var step in _steps)
{
if (ct.IsCancellationRequested)
{
stepResults.Add(CreateCancelledResult(step.Name));
overallPassed = false;
failureReason = "Verification cancelled";
break;
}
try
{
var result = await step.ExecuteAsync(context, ct);
stepResults.Add(result);
if (!result.Passed)
{
overallPassed = false;
failureReason = $"{step.Name}: {result.ErrorMessage}";
_logger.LogWarning(
"Verification step {StepName} failed: {ErrorMessage}",
step.Name, result.ErrorMessage);
// Continue to collect all results, but mark as failed
}
else
{
_logger.LogDebug(
"Verification step {StepName} passed in {Duration}ms",
step.Name, result.Duration.TotalMilliseconds);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Verification step {StepName} threw an exception", step.Name);
stepResults.Add(new VerificationStepResult
{
StepName = step.Name,
Passed = false,
Duration = TimeSpan.Zero,
ErrorMessage = $"Exception: {ex.Message}"
});
overallPassed = false;
failureReason = $"{step.Name}: {ex.Message}";
}
}
var pipelineDuration = _timeProvider.GetUtcNow() - pipelineStartTime;
// Generate receipt
var receipt = new VerificationReceipt
{
ReceiptId = GenerateReceiptId(),
Result = overallPassed ? VerificationResult.Pass : VerificationResult.Fail,
VerifiedAt = pipelineStartTime,
VerifierVersion = request.VerifierVersion,
ProofBundleId = request.ProofBundleId.Value,
FailureReason = failureReason,
StepsSummary = stepResults.Select(s => new VerificationStepSummary
{
StepName = s.StepName,
Passed = s.Passed,
DurationMs = (int)s.Duration.TotalMilliseconds
}).ToList(),
TotalDurationMs = (int)pipelineDuration.TotalMilliseconds
};
_logger.LogInformation(
"Verification pipeline completed for {ProofBundleId}: {Result} in {Duration}ms",
request.ProofBundleId, receipt.Result, pipelineDuration.TotalMilliseconds);
return new VerificationPipelineResult
{
IsValid = overallPassed,
Receipt = receipt,
Steps = stepResults
};
}
private static VerificationStepResult CreateCancelledResult(string stepName) => new()
{
StepName = stepName,
Passed = false,
Duration = TimeSpan.Zero,
ErrorMessage = "Verification cancelled"
};
private static string GenerateReceiptId()
{
var bytes = new byte[16];
RandomNumberGenerator.Fill(bytes);
return $"receipt:{Convert.ToHexString(bytes).ToLowerInvariant()}";
}
}
/// <summary>
/// DSSE signature verification step (PROOF-API-0006).
/// Verifies that all DSSE envelopes in the proof bundle have valid signatures.
/// </summary>
public sealed class DsseSignatureVerificationStep : IVerificationStep
{
private readonly IProofBundleStore _proofStore;
private readonly IDsseVerifier _dsseVerifier;
private readonly ILogger _logger;
public string Name => "dsse_signature";
public DsseSignatureVerificationStep(
IProofBundleStore proofStore,
IDsseVerifier dsseVerifier,
ILogger logger)
{
_proofStore = proofStore ?? throw new ArgumentNullException(nameof(proofStore));
_dsseVerifier = dsseVerifier ?? throw new ArgumentNullException(nameof(dsseVerifier));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<VerificationStepResult> ExecuteAsync(
VerificationContext context,
CancellationToken ct = default)
{
var stopwatch = Stopwatch.StartNew();
try
{
// Get the proof bundle
var bundle = await _proofStore.GetBundleAsync(context.ProofBundleId, ct);
if (bundle is null)
{
return CreateFailedResult(stopwatch.Elapsed, $"Proof bundle {context.ProofBundleId} not found");
}
// Verify each envelope signature
var verifiedKeyIds = new List<string>();
foreach (var envelope in bundle.Envelopes)
{
var verifyResult = await _dsseVerifier.VerifyAsync(envelope, ct);
if (!verifyResult.IsValid)
{
return CreateFailedResult(
stopwatch.Elapsed,
$"DSSE signature verification failed for envelope: {verifyResult.ErrorMessage}",
keyId: verifyResult.KeyId);
}
verifiedKeyIds.Add(verifyResult.KeyId);
}
// Store verified key IDs for trust anchor verification
context.SetData("verifiedKeyIds", verifiedKeyIds);
return new VerificationStepResult
{
StepName = Name,
Passed = true,
Duration = stopwatch.Elapsed,
Details = $"Verified {bundle.Envelopes.Count} envelope(s)"
};
}
catch (Exception ex)
{
_logger.LogError(ex, "DSSE signature verification failed with exception");
return CreateFailedResult(stopwatch.Elapsed, ex.Message);
}
}
private VerificationStepResult CreateFailedResult(TimeSpan duration, string error, string? keyId = null) => new()
{
StepName = Name,
Passed = false,
Duration = duration,
ErrorMessage = error,
KeyId = keyId
};
}
/// <summary>
/// ID recomputation verification step (PROOF-API-0007).
/// Verifies that content-addressed IDs match the actual content.
/// </summary>
public sealed class IdRecomputationVerificationStep : IVerificationStep
{
private readonly IProofBundleStore _proofStore;
private readonly ILogger _logger;
public string Name => "id_recomputation";
public IdRecomputationVerificationStep(
IProofBundleStore proofStore,
ILogger logger)
{
_proofStore = proofStore ?? throw new ArgumentNullException(nameof(proofStore));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<VerificationStepResult> ExecuteAsync(
VerificationContext context,
CancellationToken ct = default)
{
var stopwatch = Stopwatch.StartNew();
try
{
// Get the proof bundle
var bundle = await _proofStore.GetBundleAsync(context.ProofBundleId, ct);
if (bundle is null)
{
return CreateFailedResult(stopwatch.Elapsed, $"Proof bundle {context.ProofBundleId} not found");
}
// Recompute the proof bundle ID from content
var recomputedId = ComputeProofBundleId(bundle);
// Compare with claimed ID
var claimedId = context.ProofBundleId.Value;
if (!recomputedId.Equals(claimedId, StringComparison.OrdinalIgnoreCase))
{
return new VerificationStepResult
{
StepName = Name,
Passed = false,
Duration = stopwatch.Elapsed,
ErrorMessage = "Proof bundle ID does not match content hash",
Expected = claimedId,
Actual = recomputedId
};
}
// Verify each statement ID
foreach (var statement in bundle.Statements)
{
var recomputedStatementId = ComputeStatementId(statement);
if (!recomputedStatementId.Equals(statement.StatementId, StringComparison.OrdinalIgnoreCase))
{
return new VerificationStepResult
{
StepName = Name,
Passed = false,
Duration = stopwatch.Elapsed,
ErrorMessage = $"Statement ID mismatch",
Expected = statement.StatementId,
Actual = recomputedStatementId
};
}
}
return new VerificationStepResult
{
StepName = Name,
Passed = true,
Duration = stopwatch.Elapsed,
Details = $"Verified bundle ID and {bundle.Statements.Count} statement ID(s)"
};
}
catch (Exception ex)
{
_logger.LogError(ex, "ID recomputation verification failed with exception");
return CreateFailedResult(stopwatch.Elapsed, ex.Message);
}
}
private static string ComputeProofBundleId(ProofBundle bundle)
{
// Hash the canonical JSON representation of the bundle
var canonicalJson = JsonSerializer.Serialize(bundle, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
});
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(canonicalJson));
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
private static string ComputeStatementId(ProofStatement statement)
{
// Hash the canonical JSON representation of the statement
var canonicalJson = JsonSerializer.Serialize(statement, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
});
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(canonicalJson));
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
private VerificationStepResult CreateFailedResult(TimeSpan duration, string error) => new()
{
StepName = Name,
Passed = false,
Duration = duration,
ErrorMessage = error
};
}
/// <summary>
/// Rekor inclusion proof verification step (PROOF-API-0008).
/// Verifies that proof bundles are included in Rekor transparency log.
/// </summary>
public sealed class RekorInclusionVerificationStep : IVerificationStep
{
private readonly IProofBundleStore _proofStore;
private readonly IRekorVerifier _rekorVerifier;
private readonly ILogger _logger;
public string Name => "rekor_inclusion";
public RekorInclusionVerificationStep(
IProofBundleStore proofStore,
IRekorVerifier rekorVerifier,
ILogger logger)
{
_proofStore = proofStore ?? throw new ArgumentNullException(nameof(proofStore));
_rekorVerifier = rekorVerifier ?? throw new ArgumentNullException(nameof(rekorVerifier));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<VerificationStepResult> ExecuteAsync(
VerificationContext context,
CancellationToken ct = default)
{
var stopwatch = Stopwatch.StartNew();
// Skip if Rekor verification is disabled
if (!context.VerifyRekor)
{
return new VerificationStepResult
{
StepName = Name,
Passed = true,
Duration = stopwatch.Elapsed,
Details = "Rekor verification skipped (disabled in request)"
};
}
try
{
// Get the proof bundle
var bundle = await _proofStore.GetBundleAsync(context.ProofBundleId, ct);
if (bundle is null)
{
return CreateFailedResult(stopwatch.Elapsed, $"Proof bundle {context.ProofBundleId} not found");
}
// Check if bundle has Rekor log entry
if (bundle.RekorLogEntry is null)
{
return CreateFailedResult(stopwatch.Elapsed, "Proof bundle has no Rekor log entry");
}
// Verify inclusion proof
var verifyResult = await _rekorVerifier.VerifyInclusionAsync(
bundle.RekorLogEntry.LogId,
bundle.RekorLogEntry.LogIndex,
bundle.RekorLogEntry.InclusionProof,
bundle.RekorLogEntry.SignedTreeHead,
ct);
if (!verifyResult.IsValid)
{
return new VerificationStepResult
{
StepName = Name,
Passed = false,
Duration = stopwatch.Elapsed,
ErrorMessage = verifyResult.ErrorMessage,
LogIndex = bundle.RekorLogEntry.LogIndex
};
}
// Store log index for receipt
context.SetData("rekorLogIndex", bundle.RekorLogEntry.LogIndex);
return new VerificationStepResult
{
StepName = Name,
Passed = true,
Duration = stopwatch.Elapsed,
Details = $"Verified inclusion at log index {bundle.RekorLogEntry.LogIndex}",
LogIndex = bundle.RekorLogEntry.LogIndex
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Rekor inclusion verification failed with exception");
return CreateFailedResult(stopwatch.Elapsed, ex.Message);
}
}
private VerificationStepResult CreateFailedResult(TimeSpan duration, string error) => new()
{
StepName = Name,
Passed = false,
Duration = duration,
ErrorMessage = error
};
}
/// <summary>
/// Trust anchor verification step.
/// Verifies that signatures were made by keys authorized in a trust anchor.
/// </summary>
public sealed class TrustAnchorVerificationStep : IVerificationStep
{
private readonly ITrustAnchorResolver _trustAnchorResolver;
private readonly ILogger _logger;
public string Name => "trust_anchor";
public TrustAnchorVerificationStep(
ITrustAnchorResolver trustAnchorResolver,
ILogger logger)
{
_trustAnchorResolver = trustAnchorResolver ?? throw new ArgumentNullException(nameof(trustAnchorResolver));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<VerificationStepResult> ExecuteAsync(
VerificationContext context,
CancellationToken ct = default)
{
var stopwatch = Stopwatch.StartNew();
try
{
// Get verified key IDs from DSSE step
var verifiedKeyIds = context.GetData<List<string>>("verifiedKeyIds");
if (verifiedKeyIds is null || verifiedKeyIds.Count == 0)
{
return CreateFailedResult(stopwatch.Elapsed, "No verified key IDs from DSSE step");
}
// Resolve trust anchor
var anchor = context.TrustAnchorId is not null
? await _trustAnchorResolver.GetAnchorAsync(context.TrustAnchorId.Value, ct)
: await _trustAnchorResolver.FindAnchorForProofAsync(context.ProofBundleId, ct);
if (anchor is null)
{
return CreateFailedResult(stopwatch.Elapsed, "No matching trust anchor found");
}
// Verify all key IDs are authorized
foreach (var keyId in verifiedKeyIds)
{
if (!anchor.AllowedKeyIds.Contains(keyId) && !anchor.RevokedKeyIds.Contains(keyId))
{
return new VerificationStepResult
{
StepName = Name,
Passed = false,
Duration = stopwatch.Elapsed,
ErrorMessage = $"Key {keyId} is not authorized by trust anchor {anchor.AnchorId}",
KeyId = keyId
};
}
}
return new VerificationStepResult
{
StepName = Name,
Passed = true,
Duration = stopwatch.Elapsed,
Details = $"Verified {verifiedKeyIds.Count} key(s) against anchor {anchor.AnchorId}"
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Trust anchor verification failed with exception");
return CreateFailedResult(stopwatch.Elapsed, ex.Message);
}
}
private VerificationStepResult CreateFailedResult(TimeSpan duration, string error) => new()
{
StepName = Name,
Passed = false,
Duration = duration,
ErrorMessage = error
};
}
#region Supporting Interfaces and Types
/// <summary>
/// Store for proof bundles.
/// </summary>
public interface IProofBundleStore
{
Task<ProofBundle?> GetBundleAsync(ProofBundleId bundleId, CancellationToken ct = default);
}
/// <summary>
/// DSSE envelope verifier.
/// </summary>
public interface IDsseVerifier
{
Task<DsseVerificationResult> VerifyAsync(DsseEnvelope envelope, CancellationToken ct = default);
}
/// <summary>
/// Result of DSSE verification.
/// </summary>
public sealed record DsseVerificationResult
{
public required bool IsValid { get; init; }
public required string KeyId { get; init; }
public string? ErrorMessage { get; init; }
}
/// <summary>
/// Rekor transparency log verifier.
/// </summary>
public interface IRekorVerifier
{
Task<RekorVerificationResult> VerifyInclusionAsync(
string logId,
long logIndex,
InclusionProof inclusionProof,
SignedTreeHead signedTreeHead,
CancellationToken ct = default);
}
/// <summary>
/// Result of Rekor verification.
/// </summary>
public sealed record RekorVerificationResult
{
public required bool IsValid { get; init; }
public string? ErrorMessage { get; init; }
}
/// <summary>
/// Trust anchor resolver.
/// </summary>
public interface ITrustAnchorResolver
{
Task<TrustAnchorInfo?> GetAnchorAsync(Guid anchorId, CancellationToken ct = default);
Task<TrustAnchorInfo?> FindAnchorForProofAsync(ProofBundleId proofBundleId, CancellationToken ct = default);
}
/// <summary>
/// Trust anchor information.
/// </summary>
public sealed record TrustAnchorInfo
{
public required Guid AnchorId { get; init; }
public required IReadOnlyList<string> AllowedKeyIds { get; init; }
public required IReadOnlyList<string> RevokedKeyIds { get; init; }
}
/// <summary>
/// A proof bundle containing statements and envelopes.
/// </summary>
public sealed record ProofBundle
{
public required IReadOnlyList<ProofStatement> Statements { get; init; }
public required IReadOnlyList<DsseEnvelope> Envelopes { get; init; }
public RekorLogEntry? RekorLogEntry { get; init; }
}
/// <summary>
/// A statement within a proof bundle.
/// </summary>
public sealed record ProofStatement
{
public required string StatementId { get; init; }
public required string PredicateType { get; init; }
public required object Predicate { get; init; }
}
/// <summary>
/// A DSSE envelope.
/// </summary>
public sealed record DsseEnvelope
{
public required string PayloadType { get; init; }
public required byte[] Payload { get; init; }
public required IReadOnlyList<DsseSignature> Signatures { get; init; }
}
/// <summary>
/// A signature in a DSSE envelope.
/// </summary>
public sealed record DsseSignature
{
public required string KeyId { get; init; }
public required byte[] Sig { get; init; }
}
/// <summary>
/// Rekor log entry information.
/// </summary>
public sealed record RekorLogEntry
{
public required string LogId { get; init; }
public required long LogIndex { get; init; }
public required InclusionProof InclusionProof { get; init; }
public required SignedTreeHead SignedTreeHead { get; init; }
}
/// <summary>
/// Merkle tree inclusion proof.
/// </summary>
public sealed record InclusionProof
{
public required IReadOnlyList<byte[]> Hashes { get; init; }
public required long TreeSize { get; init; }
public required byte[] RootHash { get; init; }
}
/// <summary>
/// Signed tree head from transparency log.
/// </summary>
public sealed record SignedTreeHead
{
public required long TreeSize { get; init; }
public required byte[] RootHash { get; init; }
public required byte[] Signature { get; init; }
}
#endregion