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:
@@ -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
|
||||
Reference in New Issue
Block a user