sprints and audit work
This commit is contained in:
@@ -26,6 +26,7 @@ public sealed class AirGapTelemetry
|
||||
private readonly Queue<(string Tenant, long Sequence)> _evictionQueue = new();
|
||||
private readonly object _cacheLock = new();
|
||||
private readonly int _maxTenantEntries;
|
||||
private readonly int _maxEvictionQueueSize;
|
||||
private long _sequence;
|
||||
|
||||
private readonly ObservableGauge<long> _anchorAgeGauge;
|
||||
@@ -36,6 +37,8 @@ public sealed class AirGapTelemetry
|
||||
{
|
||||
var maxEntries = options.Value.MaxTenantEntries;
|
||||
_maxTenantEntries = maxEntries > 0 ? maxEntries : 1000;
|
||||
// Bound eviction queue to 3x tenant entries to prevent unbounded memory growth
|
||||
_maxEvictionQueueSize = _maxTenantEntries * 3;
|
||||
_logger = logger;
|
||||
_anchorAgeGauge = Meter.CreateObservableGauge("airgap_time_anchor_age_seconds", ObserveAges);
|
||||
_budgetGauge = Meter.CreateObservableGauge("airgap_staleness_budget_seconds", ObserveBudgets);
|
||||
@@ -146,6 +149,7 @@ public sealed class AirGapTelemetry
|
||||
|
||||
private void TrimCache()
|
||||
{
|
||||
// Evict stale tenant entries when cache is over limit
|
||||
while (_latestByTenant.Count > _maxTenantEntries && _evictionQueue.Count > 0)
|
||||
{
|
||||
var (tenant, sequence) = _evictionQueue.Dequeue();
|
||||
@@ -154,6 +158,19 @@ public sealed class AirGapTelemetry
|
||||
_latestByTenant.TryRemove(tenant, out _);
|
||||
}
|
||||
}
|
||||
|
||||
// Trim eviction queue to prevent unbounded memory growth
|
||||
// Discard stale entries that no longer match current tenant state
|
||||
while (_evictionQueue.Count > _maxEvictionQueueSize)
|
||||
{
|
||||
var (tenant, sequence) = _evictionQueue.Dequeue();
|
||||
// Only actually evict if this is still the current entry for the tenant
|
||||
if (_latestByTenant.TryGetValue(tenant, out var entry) && entry.Sequence == sequence)
|
||||
{
|
||||
_latestByTenant.TryRemove(tenant, out _);
|
||||
}
|
||||
// Otherwise the queue entry is stale and can be discarded
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct TelemetryEntry(long Age, long Budget, long Sequence);
|
||||
|
||||
@@ -209,20 +209,19 @@ public sealed record EvidenceGraphMetadata
|
||||
/// </summary>
|
||||
public sealed class EvidenceGraphSerializer
|
||||
{
|
||||
// Use default escaping for deterministic output (no UnsafeRelaxedJsonEscaping)
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||
{
|
||||
WriteIndented = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions PrettySerializerOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// Part of Step 3: Normalization
|
||||
// =============================================================================
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
@@ -225,7 +226,9 @@ public static class JsonNormalizer
|
||||
char.IsDigit(value[3]) &&
|
||||
value[4] == '-')
|
||||
{
|
||||
return DateTimeOffset.TryParse(value, out _);
|
||||
// Use InvariantCulture for deterministic parsing
|
||||
return DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.RoundtripKind, out _);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -16,11 +16,10 @@ namespace StellaOps.AirGap.Importer.Reconciliation.Parsers;
|
||||
/// </summary>
|
||||
public sealed class CycloneDxParser : ISbomParser
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
private static readonly JsonDocumentOptions DocumentOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
AllowTrailingCommas = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip
|
||||
CommentHandling = JsonCommentHandling.Skip
|
||||
};
|
||||
|
||||
public SbomFormat DetectFormat(string filePath)
|
||||
@@ -87,7 +86,7 @@ public sealed class CycloneDxParser : ISbomParser
|
||||
|
||||
try
|
||||
{
|
||||
using var document = await JsonDocument.ParseAsync(stream, default, cancellationToken);
|
||||
using var document = await JsonDocument.ParseAsync(stream, DocumentOptions, cancellationToken);
|
||||
var root = document.RootElement;
|
||||
|
||||
// Validate bomFormat
|
||||
|
||||
@@ -14,11 +14,10 @@ namespace StellaOps.AirGap.Importer.Reconciliation.Parsers;
|
||||
/// </summary>
|
||||
public sealed class DsseAttestationParser : IAttestationParser
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
private static readonly JsonDocumentOptions DocumentOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
AllowTrailingCommas = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip
|
||||
CommentHandling = JsonCommentHandling.Skip
|
||||
};
|
||||
|
||||
public bool IsAttestation(string filePath)
|
||||
@@ -92,7 +91,7 @@ public sealed class DsseAttestationParser : IAttestationParser
|
||||
|
||||
try
|
||||
{
|
||||
using var document = await JsonDocument.ParseAsync(stream, default, cancellationToken);
|
||||
using var document = await JsonDocument.ParseAsync(stream, DocumentOptions, cancellationToken);
|
||||
var root = document.RootElement;
|
||||
|
||||
// Parse DSSE envelope
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace StellaOps.AirGap.Importer.Reconciliation.Parsers;
|
||||
|
||||
/// <summary>
|
||||
/// Transforms SBOMs into a canonical form for deterministic hashing and comparison.
|
||||
/// Applies normalization rules per advisory §5 step 3.
|
||||
/// Applies normalization rules per advisory section 5 step 3.
|
||||
/// </summary>
|
||||
public sealed class SbomNormalizer
|
||||
{
|
||||
|
||||
@@ -15,11 +15,10 @@ namespace StellaOps.AirGap.Importer.Reconciliation.Parsers;
|
||||
/// </summary>
|
||||
public sealed class SpdxParser : ISbomParser
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
private static readonly JsonDocumentOptions DocumentOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
AllowTrailingCommas = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip
|
||||
CommentHandling = JsonCommentHandling.Skip
|
||||
};
|
||||
|
||||
public SbomFormat DetectFormat(string filePath)
|
||||
@@ -84,7 +83,7 @@ public sealed class SpdxParser : ISbomParser
|
||||
|
||||
try
|
||||
{
|
||||
using var document = await JsonDocument.ParseAsync(stream, default, cancellationToken);
|
||||
using var document = await JsonDocument.ParseAsync(stream, DocumentOptions, cancellationToken);
|
||||
var root = document.RootElement;
|
||||
|
||||
// Validate spdxVersion
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.AirGap.Importer.Validation;
|
||||
@@ -14,7 +15,9 @@ internal static class DssePreAuthenticationEncoding
|
||||
}
|
||||
|
||||
var payloadTypeByteCount = Encoding.UTF8.GetByteCount(payloadType);
|
||||
var header = $"{Prefix} {payloadTypeByteCount} {payloadType} {payload.Length} ";
|
||||
// Use InvariantCulture to ensure ASCII decimal digits per DSSE spec
|
||||
var header = string.Create(CultureInfo.InvariantCulture,
|
||||
$"{Prefix} {payloadTypeByteCount} {payloadType} {payload.Length} ");
|
||||
var headerBytes = Encoding.UTF8.GetBytes(header);
|
||||
|
||||
var buffer = new byte[headerBytes.Length + payload.Length];
|
||||
|
||||
@@ -128,7 +128,14 @@ public sealed class RuleBundleValidator
|
||||
var digestErrors = new List<string>();
|
||||
foreach (var file in manifest.Files)
|
||||
{
|
||||
var filePath = Path.Combine(request.BundleDirectory, file.Name);
|
||||
// Validate path to prevent traversal attacks
|
||||
if (!PathValidation.IsSafeRelativePath(file.Name))
|
||||
{
|
||||
digestErrors.Add($"unsafe-path:{file.Name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var filePath = PathValidation.SafeCombine(request.BundleDirectory, file.Name);
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
digestErrors.Add($"file-missing:{file.Name}");
|
||||
@@ -345,3 +352,81 @@ internal sealed class RuleBundleFileEntry
|
||||
public string Digest { get; set; } = string.Empty;
|
||||
public long SizeBytes { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Utility methods for path validation and security.
|
||||
/// </summary>
|
||||
internal static class PathValidation
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates that a relative path does not escape the bundle root.
|
||||
/// </summary>
|
||||
public static bool IsSafeRelativePath(string? relativePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(relativePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for absolute paths
|
||||
if (Path.IsPathRooted(relativePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for path traversal sequences
|
||||
var normalized = relativePath.Replace('\\', '/');
|
||||
var segments = normalized.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var depth = 0;
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
if (segment == "..")
|
||||
{
|
||||
depth--;
|
||||
if (depth < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (segment != ".")
|
||||
{
|
||||
depth++;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for null bytes
|
||||
if (relativePath.Contains('\0'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines a root path with a relative path, validating that the result does not escape the root.
|
||||
/// </summary>
|
||||
public static string SafeCombine(string rootPath, string relativePath)
|
||||
{
|
||||
if (!IsSafeRelativePath(relativePath))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Invalid relative path: path traversal or absolute path detected in '{relativePath}'",
|
||||
nameof(relativePath));
|
||||
}
|
||||
|
||||
var combined = Path.GetFullPath(Path.Combine(rootPath, relativePath));
|
||||
var normalizedRoot = Path.GetFullPath(rootPath);
|
||||
|
||||
// Ensure the combined path starts with the root path
|
||||
if (!combined.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Path '{relativePath}' escapes root directory",
|
||||
nameof(relativePath));
|
||||
}
|
||||
|
||||
return combined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ public sealed class TimeTelemetry
|
||||
{
|
||||
private static readonly Meter Meter = new("StellaOps.AirGap.Time", "1.0.0");
|
||||
private const int MaxEntries = 1024;
|
||||
// Bound eviction queue to 3x max entries to prevent unbounded memory growth
|
||||
private const int MaxEvictionQueueSize = MaxEntries * 3;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Snapshot> _latest = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentQueue<string> _evictionQueue = new();
|
||||
@@ -71,10 +73,20 @@ public sealed class TimeTelemetry
|
||||
|
||||
private void TrimCache()
|
||||
{
|
||||
// Evict tenant entries when cache is over limit
|
||||
while (_latest.Count > MaxEntries && _evictionQueue.TryDequeue(out var candidate))
|
||||
{
|
||||
_latest.TryRemove(candidate, out _);
|
||||
}
|
||||
|
||||
// Trim eviction queue to prevent unbounded memory growth
|
||||
// Discard stale entries that may no longer be in the cache
|
||||
while (_evictionQueue.Count > MaxEvictionQueueSize && _evictionQueue.TryDequeue(out var stale))
|
||||
{
|
||||
// If the tenant is still in cache, try to remove it
|
||||
// (this helps when we have many updates to the same tenant)
|
||||
_latest.TryRemove(stale, out _);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record Snapshot(long AgeSeconds, bool IsWarning, bool IsBreach);
|
||||
|
||||
@@ -195,7 +195,15 @@ public sealed class KnowledgeSnapshotImporter : IKnowledgeSnapshotImporter
|
||||
{
|
||||
try
|
||||
{
|
||||
var filePath = Path.Combine(bundleDir, entry.RelativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
// Validate path to prevent traversal attacks
|
||||
if (!PathValidation.IsSafeRelativePath(entry.RelativePath))
|
||||
{
|
||||
result.Failed++;
|
||||
result.Errors.Add($"Unsafe path detected: {entry.RelativePath}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var filePath = PathValidation.SafeCombine(bundleDir, entry.RelativePath);
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
result.Failed++;
|
||||
@@ -250,7 +258,15 @@ public sealed class KnowledgeSnapshotImporter : IKnowledgeSnapshotImporter
|
||||
{
|
||||
try
|
||||
{
|
||||
var filePath = Path.Combine(bundleDir, entry.RelativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
// Validate path to prevent traversal attacks
|
||||
if (!PathValidation.IsSafeRelativePath(entry.RelativePath))
|
||||
{
|
||||
result.Failed++;
|
||||
result.Errors.Add($"Unsafe path detected: {entry.RelativePath}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var filePath = PathValidation.SafeCombine(bundleDir, entry.RelativePath);
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
result.Failed++;
|
||||
@@ -305,7 +321,15 @@ public sealed class KnowledgeSnapshotImporter : IKnowledgeSnapshotImporter
|
||||
{
|
||||
try
|
||||
{
|
||||
var filePath = Path.Combine(bundleDir, entry.RelativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
// Validate path to prevent traversal attacks
|
||||
if (!PathValidation.IsSafeRelativePath(entry.RelativePath))
|
||||
{
|
||||
result.Failed++;
|
||||
result.Errors.Add($"Unsafe path detected: {entry.RelativePath}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var filePath = PathValidation.SafeCombine(bundleDir, entry.RelativePath);
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
result.Failed++;
|
||||
@@ -349,9 +373,52 @@ public sealed class KnowledgeSnapshotImporter : IKnowledgeSnapshotImporter
|
||||
|
||||
private static async Task ExtractBundleAsync(string bundlePath, string targetDir, CancellationToken ct)
|
||||
{
|
||||
var normalizedTargetDir = Path.GetFullPath(targetDir);
|
||||
|
||||
await using var fileStream = File.OpenRead(bundlePath);
|
||||
await using var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress);
|
||||
await TarFile.ExtractToDirectoryAsync(gzipStream, targetDir, overwriteFiles: true, ct);
|
||||
await using var tarReader = new TarReader(gzipStream, leaveOpen: false);
|
||||
|
||||
while (await tarReader.GetNextEntryAsync(copyData: true, ct) is { } entry)
|
||||
{
|
||||
if (string.IsNullOrEmpty(entry.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate entry path to prevent traversal attacks
|
||||
if (!PathValidation.IsSafeRelativePath(entry.Name))
|
||||
{
|
||||
throw new InvalidOperationException($"Unsafe tar entry path detected: {entry.Name}");
|
||||
}
|
||||
|
||||
var destinationPath = Path.GetFullPath(Path.Combine(normalizedTargetDir, entry.Name));
|
||||
|
||||
// Verify the path is within the target directory
|
||||
if (!destinationPath.StartsWith(normalizedTargetDir, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException($"Tar entry path escapes target directory: {entry.Name}");
|
||||
}
|
||||
|
||||
// Create directory if needed
|
||||
var entryDir = Path.GetDirectoryName(destinationPath);
|
||||
if (!string.IsNullOrEmpty(entryDir))
|
||||
{
|
||||
Directory.CreateDirectory(entryDir);
|
||||
}
|
||||
|
||||
// Extract based on entry type
|
||||
if (entry.EntryType == TarEntryType.Directory)
|
||||
{
|
||||
Directory.CreateDirectory(destinationPath);
|
||||
}
|
||||
else if (entry.EntryType == TarEntryType.RegularFile ||
|
||||
entry.EntryType == TarEntryType.V7RegularFile)
|
||||
{
|
||||
await entry.ExtractToFileAsync(destinationPath, overwrite: true, ct);
|
||||
}
|
||||
// Skip symbolic links and other special entry types for security
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ModuleImportResult
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// Description: Signs snapshot manifests using DSSE format for integrity verification.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -196,8 +197,9 @@ public sealed class SnapshotManifestSigner : ISnapshotManifestSigner
|
||||
{
|
||||
var typeBytes = Encoding.UTF8.GetBytes(payloadType);
|
||||
var prefixBytes = Encoding.UTF8.GetBytes(PreAuthenticationEncodingPrefix);
|
||||
var typeLenStr = typeBytes.Length.ToString();
|
||||
var payloadLenStr = payload.Length.ToString();
|
||||
// Use InvariantCulture to ensure ASCII decimal digits per DSSE spec
|
||||
var typeLenStr = typeBytes.Length.ToString(CultureInfo.InvariantCulture);
|
||||
var payloadLenStr = payload.Length.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
var totalLen = prefixBytes.Length + 1 +
|
||||
typeLenStr.Length + 1 +
|
||||
|
||||
@@ -178,39 +178,15 @@ public sealed class TimeAnchorService : ITimeAnchorService
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Roughtime is a cryptographic time synchronization protocol
|
||||
// This is a placeholder implementation - full implementation would use a Roughtime client
|
||||
// Full implementation requires a Roughtime client library
|
||||
var serverUrl = request.Source?["roughtime:".Length..] ?? "roughtime.cloudflare.com:2003";
|
||||
|
||||
// For now, fallback to local with indication of intended source
|
||||
var anchorTime = _timeProvider.GetUtcNow();
|
||||
var anchorData = new RoughtimeAnchorData
|
||||
{
|
||||
Timestamp = anchorTime,
|
||||
Server = serverUrl,
|
||||
Midpoint = anchorTime.ToUnixTimeSeconds(),
|
||||
Radius = 1000000, // 1 second radius in microseconds
|
||||
Nonce = _guidProvider.NewGuid().ToString("N"),
|
||||
MerkleRoot = request.MerkleRoot
|
||||
};
|
||||
|
||||
var anchorJson = JsonSerializer.Serialize(anchorData, JsonOptions);
|
||||
var anchorBytes = Encoding.UTF8.GetBytes(anchorJson);
|
||||
var tokenDigest = $"sha256:{Convert.ToHexString(SHA256.HashData(anchorBytes)).ToLowerInvariant()}";
|
||||
|
||||
await Task.CompletedTask;
|
||||
|
||||
return new TimeAnchorResult
|
||||
{
|
||||
Success = true,
|
||||
Content = new TimeAnchorContent
|
||||
{
|
||||
AnchorTime = anchorTime,
|
||||
Source = $"roughtime:{serverUrl}",
|
||||
TokenDigest = tokenDigest
|
||||
},
|
||||
TokenBytes = anchorBytes,
|
||||
Warning = "Roughtime client not implemented; using simulated response"
|
||||
};
|
||||
// Per no-silent-stubs rule: unimplemented paths must fail explicitly
|
||||
return TimeAnchorResult.Failed(
|
||||
$"Roughtime time anchor source '{serverUrl}' is not implemented. " +
|
||||
"Use 'local' source or implement Roughtime client integration.");
|
||||
}
|
||||
|
||||
private async Task<TimeAnchorResult> CreateRfc3161AnchorAsync(
|
||||
@@ -218,37 +194,15 @@ public sealed class TimeAnchorService : ITimeAnchorService
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// RFC 3161 is the Internet X.509 PKI Time-Stamp Protocol (TSP)
|
||||
// This is a placeholder implementation - full implementation would use a TSA client
|
||||
// Full implementation requires a TSA client library
|
||||
var tsaUrl = request.Source?["rfc3161:".Length..] ?? "http://timestamp.digicert.com";
|
||||
|
||||
var anchorTime = _timeProvider.GetUtcNow();
|
||||
var anchorData = new Rfc3161AnchorData
|
||||
{
|
||||
Timestamp = anchorTime,
|
||||
TsaUrl = tsaUrl,
|
||||
SerialNumber = _guidProvider.NewGuid().ToString("N"),
|
||||
PolicyOid = "2.16.840.1.114412.2.1", // DigiCert timestamp policy
|
||||
MerkleRoot = request.MerkleRoot
|
||||
};
|
||||
|
||||
var anchorJson = JsonSerializer.Serialize(anchorData, JsonOptions);
|
||||
var anchorBytes = Encoding.UTF8.GetBytes(anchorJson);
|
||||
var tokenDigest = $"sha256:{Convert.ToHexString(SHA256.HashData(anchorBytes)).ToLowerInvariant()}";
|
||||
|
||||
await Task.CompletedTask;
|
||||
|
||||
return new TimeAnchorResult
|
||||
{
|
||||
Success = true,
|
||||
Content = new TimeAnchorContent
|
||||
{
|
||||
AnchorTime = anchorTime,
|
||||
Source = $"rfc3161:{tsaUrl}",
|
||||
TokenDigest = tokenDigest
|
||||
},
|
||||
TokenBytes = anchorBytes,
|
||||
Warning = "RFC 3161 TSA client not implemented; using simulated response"
|
||||
};
|
||||
// Per no-silent-stubs rule: unimplemented paths must fail explicitly
|
||||
return TimeAnchorResult.Failed(
|
||||
$"RFC 3161 time anchor source '{tsaUrl}' is not implemented. " +
|
||||
"Use 'local' source or implement RFC 3161 TSA client integration.");
|
||||
}
|
||||
|
||||
private sealed record LocalAnchorData
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.AirGap.Sync.Services;
|
||||
using StellaOps.AirGap.Sync.Stores;
|
||||
using StellaOps.AirGap.Sync.Transport;
|
||||
@@ -42,7 +43,8 @@ public static class AirGapSyncServiceCollectionExtensions
|
||||
{
|
||||
var timeProvider = sp.GetService<TimeProvider>() ?? TimeProvider.System;
|
||||
var stateStore = sp.GetRequiredService<IHlcStateStore>();
|
||||
return new HybridLogicalClock.HybridLogicalClock(timeProvider, nodeId, stateStore);
|
||||
var logger = sp.GetRequiredService<ILogger<HybridLogicalClock.HybridLogicalClock>>();
|
||||
return new HybridLogicalClock.HybridLogicalClock(timeProvider, nodeId, stateStore, logger);
|
||||
});
|
||||
|
||||
// Register deterministic GUID provider
|
||||
|
||||
Reference in New Issue
Block a user