up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-26 07:47:08 +02:00
parent 56e2f64d07
commit 1c782897f7
184 changed files with 8991 additions and 649 deletions

View File

@@ -13,15 +13,38 @@ public sealed class CryptoProviderRegistryOptions
private readonly Dictionary<string, CryptoProviderProfileOptions> profiles =
new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Registry configuration factory that aligns with the 2025-11-18 sovereign crypto decision.
/// </summary>
public static CryptoProviderRegistryOptions SovereignDefault()
{
var options = new CryptoProviderRegistryOptions
{
ActiveProfile = "ru-offline"
};
options.PreferredProviders.Add("default");
options.PreferredProviders.Add("ru.openssl.gost");
options.PreferredProviders.Add("ru.pkcs11");
var ruOffline = new CryptoProviderProfileOptions();
ruOffline.PreferredProviders.Add("ru.cryptopro.csp");
ruOffline.PreferredProviders.Add("ru.openssl.gost");
ruOffline.PreferredProviders.Add("ru.pkcs11");
options.Profiles["ru-offline"] = ruOffline;
return options;
}
/// <summary>
/// Ordered list of preferred provider names. Providers appearing here are consulted first.
/// </summary>
public IList<string> PreferredProviders { get; } = new List<string>();
/// <summary>
/// Optional active profile name (e.g. "ru-offline") that overrides <see cref="PreferredProviders"/>.
/// Active profile name (e.g. "ru-offline") that overrides <see cref="PreferredProviders"/>.
/// </summary>
public string? ActiveProfile { get; set; }
public string ActiveProfile { get; set; } = "default";
/// <summary>
/// Regional or environment-specific provider preference profiles.

View File

@@ -9,7 +9,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="IT.GostCryptography" Version="6.0.0.1" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.5.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
@@ -18,6 +17,7 @@
<ItemGroup>
<ProjectReference Include="..\\StellaOps.Cryptography\\StellaOps.Cryptography.csproj" />
<ProjectReference Include="..\\..\\..\\third_party\\forks\\AlexMAS.GostCryptography\\Source\\GostCryptography\\GostCryptography.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
# CryptoPro Plugin Tasks
- [ ] SEC-CRYPTO-90-019: Run fork test suite on Windows runner with CryptoPro CSP; capture results.
- [ ] SEC-CRYPTO-90-020: Run plugin smoke (sign/verify) on Windows runner with CSP; capture results.
- [ ] Add platform gating in CI: ensure `cryptopro-optin` workflow wired to Windows runner that has CSP installed.
- [ ] Publish runbook updates after tests pass (link to docs/security/rootpack_ru_crypto_fork.md).

View File

@@ -14,10 +14,12 @@ public sealed class CryptoProviderRegistry : ICryptoProviderRegistry
private readonly IReadOnlyDictionary<string, ICryptoProvider> providersByName;
private readonly IReadOnlyList<string> preferredOrder;
private readonly HashSet<string> preferredOrderSet;
private readonly CryptoRegistryProfiles profiles;
public CryptoProviderRegistry(
IEnumerable<ICryptoProvider> providers,
IEnumerable<string>? preferredProviderOrder = null)
IEnumerable<string>? preferredProviderOrder = null,
CryptoRegistryProfiles? registryProfiles = null)
{
if (providers is null)
{
@@ -33,10 +35,17 @@ public sealed class CryptoProviderRegistry : ICryptoProviderRegistry
providersByName = providerList.ToDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase);
this.providers = new ReadOnlyCollection<ICryptoProvider>(providerList);
preferredOrder = preferredProviderOrder?
var baseOrder = preferredProviderOrder?
.Where(name => providersByName.ContainsKey(name))
.Select(name => providersByName[name].Name)
.ToArray() ?? Array.Empty<string>();
profiles = registryProfiles ?? new CryptoRegistryProfiles(baseOrder, "default",
new Dictionary<string, IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase)
{
["default"] = baseOrder
});
preferredOrder = profiles.ResolvePreferredOrder();
preferredOrderSet = new HashSet<string>(preferredOrder, StringComparer.OrdinalIgnoreCase);
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace StellaOps.Cryptography;
public sealed class CryptoRegistryProfiles
{
public IReadOnlyList<string> PreferredProviders { get; }
public string ActiveProfile { get; }
public IReadOnlyDictionary<string, IReadOnlyList<string>> Profiles { get; }
public CryptoRegistryProfiles(
IEnumerable<string> preferredProviders,
string activeProfile,
IDictionary<string, IReadOnlyList<string>> profiles)
{
PreferredProviders = (preferredProviders ?? throw new ArgumentNullException(nameof(preferredProviders)))
.Where(p => !string.IsNullOrWhiteSpace(p))
.Select(p => p.Trim())
.ToArray();
ActiveProfile = string.IsNullOrWhiteSpace(activeProfile)
? throw new ArgumentException("Active profile is required", nameof(activeProfile))
: activeProfile.Trim();
Profiles = new ReadOnlyDictionary<string, IReadOnlyList<string>>(profiles ??
throw new ArgumentNullException(nameof(profiles)));
}
public IReadOnlyList<string> ResolvePreferredOrder(string? profileName = null)
{
if (!string.IsNullOrWhiteSpace(profileName) && Profiles.TryGetValue(profileName!, out var specific))
{
return specific;
}
if (Profiles.TryGetValue(ActiveProfile, out var active))
{
return active;
}
return PreferredProviders;
}
}

View File

@@ -0,0 +1,46 @@
using System.Text.Json;
using StellaOps.Replay.Core;
using Xunit;
public class ReplayManifestTests
{
[Fact]
public void SerializesWithNamespacesAndAnalysis()
{
var manifest = new ReplayManifest
{
SchemaVersion = ReplayManifestVersions.V1,
Reachability = new ReplayReachabilitySection
{
AnalysisId = "analysis-123"
}
};
manifest.AddReachabilityGraph(new ReplayReachabilityGraphReference
{
Kind = "static",
CasUri = "cas://reachability_graphs/aa/aagraph.tar.zst",
Sha256 = "aa",
Namespace = "reachability_graphs",
CallgraphId = "cg-1",
Analyzer = "scanner",
Version = "0.1"
});
manifest.AddReachabilityTrace(new ReplayReachabilityTraceReference
{
Source = "runtime",
CasUri = "cas://runtime_traces/bb/bbtrace.tar.zst",
Sha256 = "bb",
Namespace = "runtime_traces",
RecordedAt = System.DateTimeOffset.Parse("2025-11-26T00:00:00Z")
});
var json = JsonSerializer.Serialize(manifest, new JsonSerializerOptions(JsonSerializerDefaults.Web));
Assert.Contains("\"analysisId\":\"analysis-123\"", json);
Assert.Contains("\"namespace\":\"reachability_graphs\"", json);
Assert.Contains("\"callgraphId\":\"cg-1\"", json);
Assert.Contains("\"namespace\":\"runtime_traces\"", json);
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseConcelierTestInfra>false</UseConcelierTestInfra>
<NoWarn>$(NoWarn);NETSDK1188</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../StellaOps.Replay.Core/StellaOps.Replay.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -13,7 +13,7 @@ Own shared replay domain types, canonicalisation helpers, bundle hashing utiliti
1. Maintain deterministic behaviour (lexicographic ordering, canonical JSON, fixed encodings).
2. Keep APIs offline-friendly; no network dependencies.
3. Coordinate schema and bundle changes with Scanner, Evidence Locker, CLI, and Docs guilds.
4. Update module `TASKS.md` statuses alongside `docs/implplan/SPRINT_185_shared_replay_primitives.md`.
4. Update module `TASKS.md` statuses alongside `docs/implplan/SPRINT_0185_0001_0001_shared_replay_primitives.md`.
## Contacts
- BE-Base Platform Guild (primary)

View File

@@ -0,0 +1,89 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Replay.Core;
/// <summary>
/// Produces deterministic, lexicographically ordered JSON suitable for hashing and DSSE payloads.
/// </summary>
public static class CanonicalJson
{
private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNamingPolicy = null,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
WriteIndented = false
};
public static string Serialize<T>(T value) => Encoding.UTF8.GetString(SerializeToUtf8Bytes(value));
public static byte[] SerializeToUtf8Bytes<T>(T value)
{
ArgumentNullException.ThrowIfNull(value);
var element = JsonSerializer.SerializeToElement(value, SerializerOptions);
var buffer = new ArrayBufferWriter<byte>();
using var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
Indented = false
});
WriteCanonical(element, writer);
writer.Flush();
return buffer.WrittenSpan.ToArray();
}
private static void WriteCanonical(JsonElement element, Utf8JsonWriter writer)
{
switch (element.ValueKind)
{
case JsonValueKind.Object:
writer.WriteStartObject();
foreach (var property in element.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal))
{
writer.WritePropertyName(property.Name);
WriteCanonical(property.Value, writer);
}
writer.WriteEndObject();
break;
case JsonValueKind.Array:
writer.WriteStartArray();
foreach (var item in element.EnumerateArray())
{
WriteCanonical(item, writer);
}
writer.WriteEndArray();
break;
case JsonValueKind.String:
writer.WriteStringValue(element.GetString());
break;
case JsonValueKind.Number:
writer.WriteRawValue(element.GetRawText(), skipInputValidation: true);
break;
case JsonValueKind.True:
case JsonValueKind.False:
writer.WriteBooleanValue(element.GetBoolean());
break;
case JsonValueKind.Null:
case JsonValueKind.Undefined:
writer.WriteNullValue();
break;
default:
throw new NotSupportedException($"Unexpected JSON value kind: {element.ValueKind}");
}
}
}

View File

@@ -0,0 +1,59 @@
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Linq;
namespace StellaOps.Replay.Core;
/// <summary>
/// Deterministic hashing helpers for canonical JSON payloads and Merkle construction.
/// </summary>
public static class DeterministicHash
{
public static string Sha256Hex(ReadOnlySpan<byte> data)
{
Span<byte> hash = stackalloc byte[32];
if (!SHA256.TryHashData(data, hash, out _))
{
throw new InvalidOperationException("Failed to compute SHA-256 hash.");
}
return Convert.ToHexString(hash).ToLowerInvariant();
}
public static string Sha256Hex(string utf8) => Sha256Hex(Encoding.UTF8.GetBytes(utf8));
public static string MerkleRootHex(IEnumerable<byte[]> leaves)
{
ArgumentNullException.ThrowIfNull(leaves);
var currentLevel = leaves.Select(l => l ?? throw new ArgumentNullException(nameof(leaves), "Leaf cannot be null.")).Select(SHA256.HashData).ToList();
if (currentLevel.Count == 0)
{
throw new ArgumentException("At least one leaf is required to compute a Merkle root.", nameof(leaves));
}
while (currentLevel.Count > 1)
{
var nextLevel = new List<byte[]>((currentLevel.Count + 1) / 2);
for (var i = 0; i < currentLevel.Count; i += 2)
{
var left = currentLevel[i];
var right = i + 1 < currentLevel.Count ? currentLevel[i + 1] : left;
var combined = new byte[left.Length + right.Length];
Buffer.BlockCopy(left, 0, combined, 0, left.Length);
Buffer.BlockCopy(right, 0, combined, left.Length, right.Length);
nextLevel.Add(SHA256.HashData(combined));
}
currentLevel = nextLevel;
}
return Convert.ToHexString(currentLevel[0]).ToLowerInvariant();
}
public static string MerkleRootHex(IEnumerable<string> canonicalJsonNodes) =>
MerkleRootHex(canonicalJsonNodes.Select(s => Encoding.UTF8.GetBytes(s ?? string.Empty)));
}

View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Replay.Core;
public sealed record DsseSignature(string KeyId, string Sig);
public sealed record DsseEnvelope(string PayloadType, string Payload, IReadOnlyList<DsseSignature> Signatures)
{
public string DigestSha256 => DeterministicHash.Sha256Hex(Convert.FromBase64String(Payload));
}
public static class DssePayloadBuilder
{
public const string ReplayPayloadType = "application/vnd.stellaops.replay+json";
public static DsseEnvelope BuildUnsigned<T>(T payload, string? payloadType = null)
{
ArgumentNullException.ThrowIfNull(payload);
var bytes = CanonicalJson.SerializeToUtf8Bytes(payload);
var envelope = new DsseEnvelope(payloadType ?? ReplayPayloadType, Convert.ToBase64String(bytes), Array.Empty<DsseSignature>());
return envelope;
}
}

View File

@@ -0,0 +1,6 @@
namespace StellaOps.Replay.Core;
public sealed record ReplayBundleEntry(string Path, ReadOnlyMemory<byte> Content, int Mode = 0b110_100_100)
{
public const int DefaultFileMode = 0b110_100_100; // 0644
}

View File

@@ -0,0 +1,88 @@
using System.Formats.Tar;
using System.IO;
using System.Security.Cryptography;
using ZstdSharp;
namespace StellaOps.Replay.Core;
public sealed record ReplayBundleWriteResult(string TarSha256, string ZstSha256, long TarBytes, long ZstBytes, string CasUri);
public static class ReplayBundleWriter
{
private const int DefaultBufferSize = 16 * 1024;
public static async Task<ReplayBundleWriteResult> WriteTarZstAsync(
IEnumerable<ReplayBundleEntry> entries,
Stream destination,
int compressionLevel = 19,
string? casPrefix = "replay",
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(entries);
ArgumentNullException.ThrowIfNull(destination);
var orderedEntries = entries.OrderBy(e => e.Path, StringComparer.Ordinal).ToList();
if (orderedEntries.Count == 0)
{
throw new ArgumentException("At least one bundle entry is required.", nameof(entries));
}
await using var tarBuffer = new MemoryStream();
await WriteDeterministicTarAsync(orderedEntries, tarBuffer, cancellationToken).ConfigureAwait(false);
var tarBytes = tarBuffer.Length;
var tarBytesSpan = tarBuffer.ToArray();
var tarDigest = DeterministicHash.Sha256Hex(tarBytesSpan);
tarBuffer.Position = 0;
using var sha = SHA256.Create();
await using var hashingStream = new CryptoStream(destination, sha, CryptoStreamMode.Write, leaveOpen: true);
await using (var compressor = new CompressionStream(hashingStream, compressionLevel, DefaultBufferSize, leaveOpen: true))
{
await tarBuffer.CopyToAsync(compressor, DefaultBufferSize, cancellationToken).ConfigureAwait(false);
await compressor.FlushAsync(cancellationToken).ConfigureAwait(false);
}
hashingStream.FlushFinalBlock();
var zstDigest = Convert.ToHexString(sha.Hash!).ToLowerInvariant();
var casUri = BuildCasUri(zstDigest, casPrefix);
return new ReplayBundleWriteResult(tarDigest, zstDigest, tarBytes, destination.CanSeek ? destination.Position : -1, casUri);
}
private static async Task WriteDeterministicTarAsync(IReadOnlyCollection<ReplayBundleEntry> entries, Stream tarStream, CancellationToken ct)
{
using var writer = new TarWriter(tarStream, TarEntryFormat.Pax, leaveOpen: true);
foreach (var entry in entries)
{
ct.ThrowIfCancellationRequested();
var tarEntry = new PaxTarEntry(TarEntryType.RegularFile, entry.Path)
{
Mode = (UnixFileMode)entry.Mode,
ModificationTime = DateTimeOffset.UnixEpoch,
DataStream = new MemoryStream(entry.Content.ToArray(), writable: false)
};
writer.WriteEntry(tarEntry);
}
await writer.DisposeAsync().ConfigureAwait(false);
}
public static string BuildCasUri(string sha256Hex, string? prefix = "replay")
{
ArgumentException.ThrowIfNullOrWhiteSpace(sha256Hex);
if (sha256Hex.Length < 2)
{
throw new ArgumentException("Digest must be at least two hex characters for prefixing.", nameof(sha256Hex));
}
var head = sha256Hex[..2];
var label = string.IsNullOrWhiteSpace(prefix) ? "replay" : prefix;
return $"cas://{label}/{head}/{sha256Hex}.tar.zst";
}
}

View File

@@ -7,14 +7,13 @@ namespace StellaOps.Replay.Core;
public sealed class ReplayManifest
{
[JsonPropertyName("schemaVersion")]
public string SchemaVersion { get; set; } = "1.0";
public string SchemaVersion { get; set; } = ReplayManifestVersions.V1;
[JsonPropertyName("scan")]
public ReplayScanMetadata Scan { get; set; } = new();
[JsonPropertyName("reachability")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public ReplayReachabilitySection? Reachability { get; set; }
public ReplayReachabilitySection Reachability { get; set; } = new();
}
public sealed class ReplayScanMetadata
@@ -23,11 +22,14 @@ public sealed class ReplayScanMetadata
public string Id { get; set; } = string.Empty;
[JsonPropertyName("time")]
public DateTimeOffset Time { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset Time { get; set; } = DateTimeOffset.UnixEpoch;
}
public sealed class ReplayReachabilitySection
{
[JsonPropertyName("analysisId")]
public string? AnalysisId { get; set; }
[JsonPropertyName("graphs")]
public List<ReplayReachabilityGraphReference> Graphs { get; set; } = new();
@@ -46,6 +48,12 @@ public sealed class ReplayReachabilityGraphReference
[JsonPropertyName("sha256")]
public string Sha256 { get; set; } = string.Empty;
[JsonPropertyName("namespace")]
public string Namespace { get; set; } = "reachability_graphs";
[JsonPropertyName("callgraphId")]
public string? CallgraphId { get; set; }
[JsonPropertyName("analyzer")]
public string Analyzer { get; set; } = string.Empty;
@@ -64,6 +72,14 @@ public sealed class ReplayReachabilityTraceReference
[JsonPropertyName("sha256")]
public string Sha256 { get; set; } = string.Empty;
[JsonPropertyName("namespace")]
public string Namespace { get; set; } = "runtime_traces";
[JsonPropertyName("recordedAt")]
public DateTimeOffset RecordedAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset RecordedAt { get; set; } = DateTimeOffset.UnixEpoch;
}
public static class ReplayManifestVersions
{
public const string V1 = "1.0";
}

View File

@@ -19,4 +19,22 @@ public static class ReplayManifestExtensions
manifest.Reachability ??= new ReplayReachabilitySection();
manifest.Reachability.RuntimeTraces.Add(trace);
}
public static byte[] ToCanonicalJson(this ReplayManifest manifest)
{
ArgumentNullException.ThrowIfNull(manifest);
return CanonicalJson.SerializeToUtf8Bytes(manifest);
}
public static string ComputeCanonicalSha256(this ReplayManifest manifest)
{
ArgumentNullException.ThrowIfNull(manifest);
return DeterministicHash.Sha256Hex(manifest.ToCanonicalJson());
}
public static DsseEnvelope ToDsseEnvelope(this ReplayManifest manifest, string? payloadType = null)
{
ArgumentNullException.ThrowIfNull(manifest);
return DssePayloadBuilder.BuildUnsigned(manifest, payloadType);
}
}

View File

@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace StellaOps.Replay.Core;
public static class ReplayCollections
{
public const string Runs = "replay_runs";
public const string Bundles = "replay_bundles";
public const string Subjects = "replay_subjects";
}
[BsonIgnoreExtraElements]
public sealed class ReplayRunRecord
{
[BsonId]
public string Id { get; set; } = string.Empty; // scan UUID
public string ManifestHash { get; set; } = string.Empty; // sha256:...
public string Status { get; set; } = "pending"; // verified|failed|replayed
public DateTime CreatedAt { get; set; } = DateTime.SpecifyKind(DateTime.UnixEpoch, DateTimeKind.Utc);
public DateTime UpdatedAt { get; set; } = DateTime.SpecifyKind(DateTime.UnixEpoch, DateTimeKind.Utc);
public ReplayRunOutputs Outputs { get; set; } = new();
public List<ReplaySignatureRecord> Signatures { get; set; } = new();
}
public sealed class ReplayRunOutputs
{
public string Sbom { get; set; } = string.Empty; // sha256:...
public string Findings { get; set; } = string.Empty;
public string? Vex { get; set; }
= null;
public string? Log { get; set; }
= null;
}
public sealed class ReplaySignatureRecord
{
public string Profile { get; set; } = string.Empty; // e.g., FIPS, GOST
public bool Verified { get; set; }
= false;
}
[BsonIgnoreExtraElements]
public sealed class ReplayBundleRecord
{
[BsonId]
public string Id { get; set; } = string.Empty; // sha256 hex
public string Type { get; set; } = string.Empty; // input|output|rootpack|reachability
public long Size { get; set; }
= 0;
public string Location { get; set; } = string.Empty; // cas://.../digest.tar.zst
public DateTime CreatedAt { get; set; } = DateTime.SpecifyKind(DateTime.UnixEpoch, DateTimeKind.Utc);
}
[BsonIgnoreExtraElements]
public sealed class ReplaySubjectRecord
{
[BsonId]
public string OciDigest { get; set; } = string.Empty;
public List<ReplayLayerRecord> Layers { get; set; } = new();
}
public sealed class ReplayLayerRecord
{
public string LayerDigest { get; set; } = string.Empty;
public string MerkleRoot { get; set; } = string.Empty;
public int LeafCount { get; set; }
= 0;
}
/// <summary>
/// Index names to keep mongod migrations deterministic.
/// </summary>
public static class ReplayIndexes
{
public const string Runs_ManifestHash = "runs_manifestHash_unique";
public const string Runs_Status_CreatedAt = "runs_status_createdAt";
public const string Bundles_Type = "bundles_type";
public const string Bundles_Location = "bundles_location";
public const string Subjects_LayerDigest = "subjects_layerDigest";
}

View File

@@ -5,6 +5,7 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="10.0.0-preview.7.25380.108" />
<PackageReference Include="ZstdSharp.Port" Version="0.8.6" />
<PackageReference Include="MongoDB.Bson" Version="2.25.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,16 @@
# StellaOps.Replay.Core task board
Keep this table in sync with `docs/implplan/SPRINT_0185_0001_0001_shared_replay_primitives.md`.
| Task ID | Status | Owners | Notes |
| --- | --- | --- | --- |
| REPLAY-CORE-185-001 | DONE (2025-11-25) | BE-Base Platform Guild | Library scaffolding: manifest schema types, canonical JSON rules, Merkle utilities, DSSE payload builders. |
| REPLAY-CORE-185-002 | DONE (2025-11-25) | Platform Guild | Deterministic bundle writer (tar.zst, CAS naming) and hashing abstractions; update platform architecture doc with “Replay CAS” subsection. |
| REPLAY-CORE-185-003 | DONE (2025-11-25) | Platform Data Guild | Mongo collections (`replay_runs`, `replay_bundles`, `replay_subjects`) and indices aligned with schema doc. |
| DOCS-REPLAY-185-003 | DONE (2025-11-25) | Docs Guild · Platform Data Guild | `docs/data/replay_schema.md` detailing collections, index guidance, offline sync strategy. |
| DOCS-REPLAY-185-004 | DONE (2025-11-25) | Docs Guild | Expand `docs/replay/DEVS_GUIDE_REPLAY.md` with integration guidance and deterministic replay checklist. |
## Status rules
- Use TODO → DOING → DONE/BLOCKED and mirror every change in the sprint Delivery Tracker.
- Note dates in parentheses when flipping to DOING/DONE for traceability.
- Capture contract or runbook changes in the relevant docs under `docs/replay` or `docs/data`.

View File

@@ -14,6 +14,16 @@ public class CryptoProGostSignerTests
[Fact]
public void ExportPublicJsonWebKey_ContainsCertificateChain()
{
if (!OperatingSystem.IsWindows())
{
return; // CryptoPro CSP is Windows-only; skip on other platforms
}
if (!string.Equals(Environment.GetEnvironmentVariable("STELLAOPS_CRYPTO_PRO_ENABLED"), "1", StringComparison.Ordinal))
{
return; // opt-in only when a Windows agent has CryptoPro CSP installed
}
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var request = new CertificateRequest("CN=stellaops.test", ecdsa, HashAlgorithmName.SHA256);
using var cert = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1));