save progress
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Immutable;
|
||||
using System.Formats.Asn1;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
@@ -234,8 +235,7 @@ public sealed class AttestorVerificationEngine : IAttestorVerificationEngine
|
||||
signatures.Add(signatureBytes);
|
||||
}
|
||||
|
||||
var verified = 0;
|
||||
|
||||
var expectedSignatures = new List<byte[]>();
|
||||
foreach (var secret in _options.Security.SignerIdentity.KmsKeys)
|
||||
{
|
||||
if (!TryDecodeSecret(secret, out var secretBytes))
|
||||
@@ -244,14 +244,15 @@ public sealed class AttestorVerificationEngine : IAttestorVerificationEngine
|
||||
}
|
||||
|
||||
using var hmac = new HMACSHA256(secretBytes);
|
||||
var computed = hmac.ComputeHash(preAuthEncoding);
|
||||
expectedSignatures.Add(hmac.ComputeHash(preAuthEncoding));
|
||||
}
|
||||
|
||||
foreach (var candidate in signatures)
|
||||
var verified = 0;
|
||||
foreach (var candidate in signatures)
|
||||
{
|
||||
if (expectedSignatures.Any(expected => CryptographicOperations.FixedTimeEquals(expected, candidate)))
|
||||
{
|
||||
if (CryptographicOperations.FixedTimeEquals(computed, candidate))
|
||||
{
|
||||
verified++;
|
||||
}
|
||||
verified++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,11 +295,11 @@ public sealed class AttestorVerificationEngine : IAttestorVerificationEngine
|
||||
var leafCertificate = certificates[0];
|
||||
var subjectAltName = GetSubjectAlternativeNames(leafCertificate).FirstOrDefault();
|
||||
|
||||
if (_options.Security.SignerIdentity.FulcioRoots.Count > 0)
|
||||
{
|
||||
using var chain = new X509Chain
|
||||
if (_options.Security.SignerIdentity.FulcioRoots.Count > 0)
|
||||
{
|
||||
ChainPolicy =
|
||||
using var chain = new X509Chain
|
||||
{
|
||||
ChainPolicy =
|
||||
{
|
||||
RevocationMode = X509RevocationMode.NoCheck,
|
||||
VerificationFlags = X509VerificationFlags.NoFlag,
|
||||
@@ -306,29 +307,34 @@ public sealed class AttestorVerificationEngine : IAttestorVerificationEngine
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var rootPath in _options.Security.SignerIdentity.FulcioRoots)
|
||||
{
|
||||
try
|
||||
foreach (var rootPath in _options.Security.SignerIdentity.FulcioRoots)
|
||||
{
|
||||
if (File.Exists(rootPath))
|
||||
try
|
||||
{
|
||||
var rootCertificate = X509CertificateLoader.LoadCertificateFromFile(rootPath);
|
||||
chain.ChainPolicy.CustomTrustStore.Add(rootCertificate);
|
||||
if (File.Exists(rootPath))
|
||||
{
|
||||
var rootCertificate = X509CertificateLoader.LoadCertificateFromFile(rootPath);
|
||||
chain.ChainPolicy.CustomTrustStore.Add(rootCertificate);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to load Fulcio root {Root}", rootPath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
for (var i = 1; i < certificates.Count; i++)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to load Fulcio root {Root}", rootPath);
|
||||
chain.ChainPolicy.ExtraStore.Add(certificates[i]);
|
||||
}
|
||||
|
||||
if (!chain.Build(leafCertificate))
|
||||
{
|
||||
var status = string.Join(";", chain.ChainStatus.Select(s => s.StatusInformation.Trim())).Trim(';');
|
||||
issuerIssues.Add(string.IsNullOrEmpty(status) ? "certificate_chain_untrusted" : $"certificate_chain_untrusted:{status}");
|
||||
}
|
||||
}
|
||||
|
||||
if (!chain.Build(leafCertificate))
|
||||
{
|
||||
var status = string.Join(";", chain.ChainStatus.Select(s => s.StatusInformation.Trim())).Trim(';');
|
||||
issuerIssues.Add(string.IsNullOrEmpty(status) ? "certificate_chain_untrusted" : $"certificate_chain_untrusted:{status}");
|
||||
}
|
||||
}
|
||||
|
||||
if (_options.Security.SignerIdentity.AllowedSans.Count > 0)
|
||||
{
|
||||
var sans = GetSubjectAlternativeNames(leafCertificate);
|
||||
@@ -775,14 +781,44 @@ public sealed class AttestorVerificationEngine : IAttestorVerificationEngine
|
||||
{
|
||||
if (string.Equals(extension.Oid?.Value, "2.5.29.17", StringComparison.Ordinal))
|
||||
{
|
||||
var formatted = extension.Format(true);
|
||||
var lines = formatted.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var line in lines)
|
||||
AsnReader reader;
|
||||
try
|
||||
{
|
||||
var parts = line.Split('=');
|
||||
if (parts.Length == 2)
|
||||
reader = new AsnReader(extension.RawData, AsnEncodingRules.DER);
|
||||
}
|
||||
catch (AsnContentException)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var sequence = reader.ReadSequence();
|
||||
while (sequence.HasData)
|
||||
{
|
||||
var tag = sequence.PeekTag();
|
||||
if (tag.TagClass != TagClass.ContextSpecific)
|
||||
{
|
||||
yield return parts[1].Trim();
|
||||
sequence.ReadEncodedValue();
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (tag.TagValue)
|
||||
{
|
||||
case 1:
|
||||
yield return sequence.ReadCharacterString(UniversalTagNumber.IA5String, new Asn1Tag(TagClass.ContextSpecific, 1));
|
||||
break;
|
||||
case 2:
|
||||
yield return sequence.ReadCharacterString(UniversalTagNumber.IA5String, new Asn1Tag(TagClass.ContextSpecific, 2));
|
||||
break;
|
||||
case 6:
|
||||
yield return sequence.ReadCharacterString(UniversalTagNumber.IA5String, new Asn1Tag(TagClass.ContextSpecific, 6));
|
||||
break;
|
||||
case 7:
|
||||
var ipBytes = sequence.ReadOctetString(new Asn1Tag(TagClass.ContextSpecific, 7));
|
||||
yield return new IPAddress(ipBytes).ToString();
|
||||
break;
|
||||
default:
|
||||
sequence.ReadEncodedValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -791,21 +827,32 @@ public sealed class AttestorVerificationEngine : IAttestorVerificationEngine
|
||||
|
||||
private static byte[] ComputePreAuthEncoding(string payloadType, byte[] payload)
|
||||
{
|
||||
var headerBytes = Encoding.UTF8.GetBytes(payloadType ?? string.Empty);
|
||||
var buffer = new byte[6 + 8 + headerBytes.Length + 8 + payload.Length];
|
||||
var payloadTypeValue = payloadType ?? string.Empty;
|
||||
var payloadTypeBytes = Encoding.UTF8.GetBytes(payloadTypeValue);
|
||||
var payloadTypeLength = Encoding.ASCII.GetBytes(payloadTypeBytes.Length.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||
var payloadLength = Encoding.ASCII.GetBytes(payload.Length.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||
var space = new byte[] { (byte)' ' };
|
||||
|
||||
var totalLength = 6 + space.Length + payloadTypeLength.Length + space.Length + payloadTypeBytes.Length
|
||||
+ space.Length + payloadLength.Length + space.Length + payload.Length;
|
||||
var buffer = new byte[totalLength];
|
||||
var offset = 0;
|
||||
|
||||
Encoding.ASCII.GetBytes("DSSEv1", 0, 6, buffer, offset);
|
||||
offset += 6;
|
||||
static void CopyBytes(byte[] source, byte[] destination, ref int index)
|
||||
{
|
||||
Buffer.BlockCopy(source, 0, destination, index, source.Length);
|
||||
index += source.Length;
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteUInt64BigEndian(buffer.AsSpan(offset, 8), (ulong)headerBytes.Length);
|
||||
offset += 8;
|
||||
Buffer.BlockCopy(headerBytes, 0, buffer, offset, headerBytes.Length);
|
||||
offset += headerBytes.Length;
|
||||
|
||||
BinaryPrimitives.WriteUInt64BigEndian(buffer.AsSpan(offset, 8), (ulong)payload.Length);
|
||||
offset += 8;
|
||||
Buffer.BlockCopy(payload, 0, buffer, offset, payload.Length);
|
||||
CopyBytes(Encoding.ASCII.GetBytes("DSSEv1"), buffer, ref offset);
|
||||
CopyBytes(space, buffer, ref offset);
|
||||
CopyBytes(payloadTypeLength, buffer, ref offset);
|
||||
CopyBytes(space, buffer, ref offset);
|
||||
CopyBytes(payloadTypeBytes, buffer, ref offset);
|
||||
CopyBytes(space, buffer, ref offset);
|
||||
CopyBytes(payloadLength, buffer, ref offset);
|
||||
CopyBytes(space, buffer, ref offset);
|
||||
payload.CopyTo(buffer.AsSpan(offset));
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// StellaOps Attestor — Distributed Verification Provider (Resilient, Multi-Node)
|
||||
// -----------------------------------------------------------------------------
|
||||
// StellaOps Attestor - Distributed Verification Provider (Resilient, Multi-Node)
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
#if STELLAOPS_EXPERIMENTAL_DISTRIBUTED_VERIFY
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.Http.Json;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Polly;
|
||||
using Polly.CircuitBreaker;
|
||||
using Polly.Retry;
|
||||
using Polly.Timeout;
|
||||
using StellaOps.Attestor.Verify.Configuration;
|
||||
using StellaOps.Attestor.Verify.Models;
|
||||
|
||||
@@ -32,7 +28,6 @@ public class DistributedVerificationProvider : IVerificationProvider
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ConcurrentDictionary<string, CircuitBreakerState> _circuitStates = new();
|
||||
private readonly ConsistentHashRing _hashRing;
|
||||
private readonly ResiliencePipeline<VerificationResult> _resiliencePipeline;
|
||||
|
||||
public DistributedVerificationProvider(
|
||||
ILogger<DistributedVerificationProvider> logger,
|
||||
@@ -49,7 +44,6 @@ public class DistributedVerificationProvider : IVerificationProvider
|
||||
}
|
||||
|
||||
_hashRing = new ConsistentHashRing(_options.Nodes, _options.VirtualNodeMultiplier);
|
||||
_resiliencePipeline = BuildResiliencePipeline();
|
||||
|
||||
_logger.LogInformation("Initialized distributed verification provider with {NodeCount} nodes", _options.Nodes.Count);
|
||||
}
|
||||
@@ -83,9 +77,7 @@ public class DistributedVerificationProvider : IVerificationProvider
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _resiliencePipeline.ExecuteAsync(
|
||||
async ct => await ExecuteVerificationAsync(node, request, ct),
|
||||
cancellationToken);
|
||||
var result = await ExecuteWithRetriesAsync(node, request, cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Verification request {RequestId} completed on node {NodeId} with result {Status}",
|
||||
@@ -196,37 +188,36 @@ public class DistributedVerificationProvider : IVerificationProvider
|
||||
return result ?? throw new InvalidOperationException("Received null response from verification node");
|
||||
}
|
||||
|
||||
private ResiliencePipeline<VerificationResult> BuildResiliencePipeline()
|
||||
private async Task<VerificationResult> ExecuteWithRetriesAsync(
|
||||
VerificationNode node,
|
||||
VerificationRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return new ResiliencePipelineBuilder<VerificationResult>()
|
||||
.AddTimeout(new TimeoutStrategyOptions
|
||||
Exception? lastError = null;
|
||||
|
||||
for (var attempt = 0; attempt <= _options.MaxRetries; attempt++)
|
||||
{
|
||||
using var attemptCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
attemptCts.CancelAfter(_options.RequestTimeout);
|
||||
|
||||
try
|
||||
{
|
||||
Timeout = _options.RequestTimeout,
|
||||
OnTimeout = args =>
|
||||
{
|
||||
_logger.LogWarning("Request timed out after {Timeout}", args.Timeout);
|
||||
return default;
|
||||
},
|
||||
})
|
||||
.AddRetry(new RetryStrategyOptions<VerificationResult>
|
||||
return await ExecuteVerificationAsync(node, request, attemptCts.Token);
|
||||
}
|
||||
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
|
||||
{
|
||||
MaxRetryAttempts = _options.MaxRetries,
|
||||
Delay = _options.RetryDelay,
|
||||
BackoffType = DelayBackoffType.Exponential,
|
||||
ShouldHandle = new PredicateBuilder<VerificationResult>()
|
||||
.Handle<HttpRequestException>()
|
||||
.Handle<TaskCanceledException>(),
|
||||
OnRetry = args =>
|
||||
lastError = ex;
|
||||
if (attempt >= _options.MaxRetries)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
args.Outcome.Exception,
|
||||
"Retry attempt {AttemptNumber} after delay {Delay}",
|
||||
args.AttemptNumber,
|
||||
args.RetryDelay);
|
||||
return default;
|
||||
},
|
||||
})
|
||||
.Build();
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.LogWarning(ex, "Retry attempt {AttemptNumber} after delay {Delay}", attempt + 1, _options.RetryDelay);
|
||||
await Task.Delay(_options.RetryDelay, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError ?? new InvalidOperationException("Verification retry failed.");
|
||||
}
|
||||
|
||||
private static string ComputeRoutingKey(VerificationRequest request)
|
||||
@@ -342,7 +333,7 @@ internal sealed class ConsistentHashRing
|
||||
private static int ComputeHash(string key)
|
||||
{
|
||||
var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(key));
|
||||
return BitConverter.ToInt32(hashBytes, 0);
|
||||
return BinaryPrimitives.ReadInt32BigEndian(hashBytes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\\StellaOps.Attestor\\StellaOps.Attestor.Core\\StellaOps.Attestor.Core.csproj" />
|
||||
|
||||
@@ -7,4 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0071-M | DONE | Maintainability audit for StellaOps.Attestor.Verify. |
|
||||
| AUDIT-0071-T | DONE | Test coverage audit for StellaOps.Attestor.Verify. |
|
||||
| AUDIT-0071-A | TODO | Pending approval for changes. |
|
||||
| AUDIT-0071-A | DONE | Applied DSSE PAE spec, SAN parsing, keyless chain store fix, KMS count fix, distributed provider cleanup, and tests. |
|
||||
|
||||
Reference in New Issue
Block a user