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

@@ -504,6 +504,161 @@ internal static class CanonicalJson
}
```
### 11.1 Full Canonical JSON with Sorted Keys
> **Added**: 2025-12-17 from "Building a Deeper Moat Beyond Reachability" advisory
```csharp
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
public static class CanonJson
{
public static byte[] Canonicalize<T>(T obj)
{
var json = JsonSerializer.SerializeToUtf8Bytes(obj, new JsonSerializerOptions
{
WriteIndented = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
using var doc = JsonDocument.Parse(json);
using var ms = new MemoryStream();
using var writer = new Utf8JsonWriter(ms, new JsonWriterOptions { Indented = false });
WriteElementSorted(doc.RootElement, writer);
writer.Flush();
return ms.ToArray();
}
private static void WriteElementSorted(JsonElement el, Utf8JsonWriter w)
{
switch (el.ValueKind)
{
case JsonValueKind.Object:
w.WriteStartObject();
foreach (var prop in el.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal))
{
w.WritePropertyName(prop.Name);
WriteElementSorted(prop.Value, w);
}
w.WriteEndObject();
break;
case JsonValueKind.Array:
w.WriteStartArray();
foreach (var item in el.EnumerateArray())
WriteElementSorted(item, w);
w.WriteEndArray();
break;
default:
el.WriteTo(w);
break;
}
}
public static string Sha256Hex(ReadOnlySpan<byte> bytes)
=> Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
}
```
## 11.2 SCORE PROOF LEDGER
> **Added**: 2025-12-17 from "Building a Deeper Moat Beyond Reachability" advisory
The Score Proof Ledger provides an append-only trail of scoring decisions with per-node hashing.
### Proof Node Types
```csharp
public enum ProofNodeKind { Input, Transform, Delta, Score }
public sealed record ProofNode(
string Id,
ProofNodeKind Kind,
string RuleId,
string[] ParentIds,
string[] EvidenceRefs, // digests / refs inside bundle
double Delta, // 0 for non-Delta nodes
double Total, // running total at this node
string Actor, // module name
DateTimeOffset TsUtc,
byte[] Seed,
string NodeHash // sha256 over canonical node (excluding NodeHash)
);
```
### Proof Hashing
```csharp
public static class ProofHashing
{
public static ProofNode WithHash(ProofNode n)
{
var canonical = CanonJson.Canonicalize(new
{
n.Id, n.Kind, n.RuleId, n.ParentIds, n.EvidenceRefs, n.Delta, n.Total,
n.Actor, n.TsUtc, Seed = Convert.ToBase64String(n.Seed)
});
return n with { NodeHash = "sha256:" + CanonJson.Sha256Hex(canonical) };
}
public static string ComputeRootHash(IEnumerable<ProofNode> nodesInOrder)
{
// Deterministic: root hash over canonical JSON array of node hashes in order.
var arr = nodesInOrder.Select(n => n.NodeHash).ToArray();
var bytes = CanonJson.Canonicalize(arr);
return "sha256:" + CanonJson.Sha256Hex(bytes);
}
}
```
### Minimal Ledger
```csharp
public sealed class ProofLedger
{
private readonly List<ProofNode> _nodes = new();
public IReadOnlyList<ProofNode> Nodes => _nodes;
public void Append(ProofNode node)
{
_nodes.Add(ProofHashing.WithHash(node));
}
public string RootHash() => ProofHashing.ComputeRootHash(_nodes);
}
```
### Score Replay Invariant
The score replay must produce identical ledger root hashes given:
- Same manifest (artifact, snapshots, policy)
- Same seed
- Same timestamp (or frozen clock)
```csharp
public class DeterminismTests
{
[Fact]
public void Score_Replay_IsBitIdentical()
{
var seed = Enumerable.Repeat((byte)7, 32).ToArray();
var inputs = new ScoreInputs(9.0, 0.50, false, ReachabilityClass.Unknown, new("enforced","ro"));
var (s1, l1) = RiskScoring.Score(inputs, "scanA", seed, DateTimeOffset.Parse("2025-01-01T00:00:00Z"));
var (s2, l2) = RiskScoring.Score(inputs, "scanA", seed, DateTimeOffset.Parse("2025-01-01T00:00:00Z"));
Assert.Equal(s1, s2, 10);
Assert.Equal(l1.RootHash(), l2.RootHash());
Assert.True(l1.Nodes.Zip(l2.Nodes).All(z => z.First.NodeHash == z.Second.NodeHash));
}
}
```
## 12. REPLAY RUNNER
```csharp