compose and authority fixes. finish sprints.

This commit is contained in:
master
2026-02-17 21:59:47 +02:00
parent fb46a927ad
commit 49cdebe2f1
187 changed files with 23189 additions and 1439 deletions

View File

@@ -63,7 +63,6 @@ if (app.Environment.IsDevelopment())
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseStellaOpsCors();
app.UseAuthentication();
app.UseAuthorization();

View File

@@ -59,6 +59,11 @@ public sealed record BundleData
/// </summary>
public IReadOnlyList<BundleArtifact> ScanResults { get; init; } = [];
/// <summary>
/// Runtime witness triplet artifacts (trace, DSSE, Sigstore bundle).
/// </summary>
public IReadOnlyList<BundleArtifact> RuntimeWitnesses { get; init; } = [];
/// <summary>
/// Public keys for verification.
/// </summary>
@@ -94,6 +99,26 @@ public sealed record BundleArtifact
/// Subject of the artifact.
/// </summary>
public string? Subject { get; init; }
/// <summary>
/// Runtime witness identity this artifact belongs to.
/// </summary>
public string? WitnessId { get; init; }
/// <summary>
/// Runtime witness artifact role (trace, dsse, sigstore_bundle).
/// </summary>
public string? WitnessRole { get; init; }
/// <summary>
/// Deterministic runtime witness lookup keys.
/// </summary>
public RuntimeWitnessIndexKey? WitnessIndex { get; init; }
/// <summary>
/// Related artifact paths for witness-level linkage.
/// </summary>
public IReadOnlyList<string>? LinkedArtifacts { get; init; }
}
/// <summary>

View File

@@ -79,18 +79,25 @@ public sealed record BundleManifest
[JsonPropertyOrder(8)]
public ImmutableArray<ArtifactEntry> ScanResults { get; init; } = ImmutableArray<ArtifactEntry>.Empty;
/// <summary>
/// Runtime witness artifacts (trace.json/trace.dsse.json/trace.sigstore.json) included in the bundle.
/// </summary>
[JsonPropertyName("runtimeWitnesses")]
[JsonPropertyOrder(9)]
public ImmutableArray<ArtifactEntry> RuntimeWitnesses { get; init; } = ImmutableArray<ArtifactEntry>.Empty;
/// <summary>
/// Public keys for verification.
/// </summary>
[JsonPropertyName("publicKeys")]
[JsonPropertyOrder(9)]
[JsonPropertyOrder(10)]
public ImmutableArray<KeyEntry> PublicKeys { get; init; } = ImmutableArray<KeyEntry>.Empty;
/// <summary>
/// Merkle root hash of all artifacts for integrity verification.
/// </summary>
[JsonPropertyName("merkleRoot")]
[JsonPropertyOrder(10)]
[JsonPropertyOrder(11)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? MerkleRoot { get; init; }
@@ -99,15 +106,20 @@ public sealed record BundleManifest
/// </summary>
[JsonIgnore]
public IEnumerable<ArtifactEntry> AllArtifacts =>
Sboms.Concat(VexStatements).Concat(Attestations).Concat(PolicyVerdicts).Concat(ScanResults);
Sboms
.Concat(VexStatements)
.Concat(Attestations)
.Concat(PolicyVerdicts)
.Concat(ScanResults)
.Concat(RuntimeWitnesses);
/// <summary>
/// Total count of artifacts in the bundle.
/// </summary>
[JsonPropertyName("totalArtifacts")]
[JsonPropertyOrder(11)]
[JsonPropertyOrder(12)]
public int TotalArtifacts => Sboms.Length + VexStatements.Length + Attestations.Length +
PolicyVerdicts.Length + ScanResults.Length;
PolicyVerdicts.Length + ScanResults.Length + RuntimeWitnesses.Length;
}
/// <summary>
@@ -165,6 +177,82 @@ public sealed record ArtifactEntry
[JsonPropertyOrder(6)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Subject { get; init; }
/// <summary>
/// Runtime witness identity this artifact belongs to.
/// </summary>
[JsonPropertyName("witnessId")]
[JsonPropertyOrder(7)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? WitnessId { get; init; }
/// <summary>
/// Runtime witness artifact role (trace, dsse, sigstore_bundle).
/// </summary>
[JsonPropertyName("witnessRole")]
[JsonPropertyOrder(8)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? WitnessRole { get; init; }
/// <summary>
/// Runtime witness lookup keys for deterministic replay.
/// </summary>
[JsonPropertyName("witnessIndex")]
[JsonPropertyOrder(9)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public RuntimeWitnessIndexKey? WitnessIndex { get; init; }
/// <summary>
/// Related artifact paths for this witness artifact.
/// </summary>
[JsonPropertyName("linkedArtifacts")]
[JsonPropertyOrder(10)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public ImmutableArray<string>? LinkedArtifacts { get; init; }
}
/// <summary>
/// Deterministic lookup keys for runtime witness artifacts.
/// </summary>
public sealed record RuntimeWitnessIndexKey
{
/// <summary>
/// Build ID of the observed userspace binary.
/// </summary>
[JsonPropertyName("buildId")]
[JsonPropertyOrder(0)]
public required string BuildId { get; init; }
/// <summary>
/// Kernel release used during runtime collection.
/// </summary>
[JsonPropertyName("kernelRelease")]
[JsonPropertyOrder(1)]
public required string KernelRelease { get; init; }
/// <summary>
/// Probe identifier that produced this runtime witness.
/// </summary>
[JsonPropertyName("probeId")]
[JsonPropertyOrder(2)]
public required string ProbeId { get; init; }
/// <summary>
/// Policy run identifier associated with the runtime evidence.
/// </summary>
[JsonPropertyName("policyRunId")]
[JsonPropertyOrder(3)]
public required string PolicyRunId { get; init; }
}
/// <summary>
/// Runtime witness artifact role values.
/// </summary>
public static class RuntimeWitnessArtifactRoles
{
public const string Trace = "trace";
public const string Dsse = "dsse";
public const string SigstoreBundle = "sigstore_bundle";
}
/// <summary>
@@ -234,6 +322,7 @@ public static class BundlePaths
public const string AttestationsDirectory = "attestations";
public const string PolicyDirectory = "policy";
public const string ScansDirectory = "scans";
public const string RuntimeWitnessesDirectory = "runtime-witnesses";
}
/// <summary>
@@ -249,4 +338,6 @@ public static class BundleMediaTypes
public const string PolicyVerdict = "application/json";
public const string ScanResult = "application/json";
public const string PublicKeyPem = "application/x-pem-file";
public const string RuntimeWitnessTrace = "application/vnd.stellaops.witness.v1+json";
public const string SigstoreBundleV03 = "application/vnd.dev.sigstore.bundle.v0.3+json";
}

View File

@@ -329,32 +329,39 @@ public sealed record ExportConfiguration
[JsonPropertyOrder(4)]
public bool IncludeScanResults { get; init; } = true;
/// <summary>
/// Include runtime witness triplets (trace, DSSE, Sigstore bundle) in export.
/// </summary>
[JsonPropertyName("includeRuntimeWitnesses")]
[JsonPropertyOrder(5)]
public bool IncludeRuntimeWitnesses { get; init; } = true;
/// <summary>
/// Include public keys for offline verification.
/// </summary>
[JsonPropertyName("includeKeys")]
[JsonPropertyOrder(5)]
[JsonPropertyOrder(6)]
public bool IncludeKeys { get; init; } = true;
/// <summary>
/// Include verification scripts.
/// </summary>
[JsonPropertyName("includeVerifyScripts")]
[JsonPropertyOrder(6)]
[JsonPropertyOrder(7)]
public bool IncludeVerifyScripts { get; init; } = true;
/// <summary>
/// Compression algorithm (gzip, brotli, none).
/// </summary>
[JsonPropertyName("compression")]
[JsonPropertyOrder(7)]
[JsonPropertyOrder(8)]
public string Compression { get; init; } = "gzip";
/// <summary>
/// Compression level (1-9).
/// </summary>
[JsonPropertyName("compressionLevel")]
[JsonPropertyOrder(8)]
[JsonPropertyOrder(9)]
public int CompressionLevel { get; init; } = 6;
}

View File

@@ -0,0 +1,300 @@
using StellaOps.EvidenceLocker.Export.Models;
using System.Security.Cryptography;
using System.Text.Json;
namespace StellaOps.EvidenceLocker.Export;
/// <summary>
/// Validates runtime witness triplets for offline replay verification.
/// </summary>
public sealed class RuntimeWitnessOfflineVerifier
{
private static readonly HashSet<string> RequiredRoles = new(StringComparer.Ordinal)
{
RuntimeWitnessArtifactRoles.Trace,
RuntimeWitnessArtifactRoles.Dsse,
RuntimeWitnessArtifactRoles.SigstoreBundle
};
/// <summary>
/// Verifies runtime witness triplets using only bundle-contained artifacts.
/// </summary>
public RuntimeWitnessOfflineVerificationResult Verify(
BundleManifest manifest,
IReadOnlyDictionary<string, byte[]> artifactsByPath)
{
ArgumentNullException.ThrowIfNull(manifest);
ArgumentNullException.ThrowIfNull(artifactsByPath);
var errors = new List<string>();
var witnessArtifacts = manifest.RuntimeWitnesses
.OrderBy(static artifact => artifact.WitnessId, StringComparer.Ordinal)
.ThenBy(static artifact => artifact.Path, StringComparer.Ordinal)
.ToList();
foreach (var artifact in witnessArtifacts)
{
if (string.IsNullOrWhiteSpace(artifact.WitnessId))
{
errors.Add($"runtime witness artifact '{artifact.Path}' is missing witnessId.");
}
if (string.IsNullOrWhiteSpace(artifact.WitnessRole))
{
errors.Add($"runtime witness artifact '{artifact.Path}' is missing witnessRole.");
}
else if (!RequiredRoles.Contains(artifact.WitnessRole))
{
errors.Add($"runtime witness artifact '{artifact.Path}' has unsupported witnessRole '{artifact.WitnessRole}'.");
}
if (artifact.WitnessIndex is null)
{
errors.Add($"runtime witness artifact '{artifact.Path}' is missing witnessIndex.");
}
}
foreach (var group in witnessArtifacts.GroupBy(static artifact => artifact.WitnessId, StringComparer.Ordinal))
{
if (string.IsNullOrWhiteSpace(group.Key))
{
continue;
}
VerifyWitnessTriplet(group.Key!, group.ToList(), artifactsByPath, errors);
}
return errors.Count == 0
? RuntimeWitnessOfflineVerificationResult.Passed()
: RuntimeWitnessOfflineVerificationResult.Failure(errors);
}
private static void VerifyWitnessTriplet(
string witnessId,
IReadOnlyList<ArtifactEntry> artifacts,
IReadOnlyDictionary<string, byte[]> artifactsByPath,
ICollection<string> errors)
{
var errorCountBefore = errors.Count;
var roleMap = artifacts
.Where(static artifact => !string.IsNullOrWhiteSpace(artifact.WitnessRole))
.GroupBy(static artifact => artifact.WitnessRole!, StringComparer.Ordinal)
.ToDictionary(static group => group.Key, static group => group.First(), StringComparer.Ordinal);
foreach (var requiredRole in RequiredRoles)
{
if (!roleMap.ContainsKey(requiredRole))
{
errors.Add($"runtime witness '{witnessId}' is missing '{requiredRole}' artifact.");
}
}
if (errors.Count > errorCountBefore && !roleMap.ContainsKey(RuntimeWitnessArtifactRoles.Trace))
{
return;
}
if (!roleMap.TryGetValue(RuntimeWitnessArtifactRoles.Trace, out var traceArtifact)
|| !roleMap.TryGetValue(RuntimeWitnessArtifactRoles.Dsse, out var dsseArtifact)
|| !roleMap.TryGetValue(RuntimeWitnessArtifactRoles.SigstoreBundle, out var sigstoreArtifact))
{
return;
}
if (!TryGetArtifactBytes(traceArtifact, artifactsByPath, errors, out var traceBytes)
|| !TryGetArtifactBytes(dsseArtifact, artifactsByPath, errors, out var dsseBytes)
|| !TryGetArtifactBytes(sigstoreArtifact, artifactsByPath, errors, out var sigstoreBytes))
{
return;
}
if (!TryGetDssePayload(dsseBytes, dsseArtifact.Path, errors, out var dssePayloadType, out var dssePayloadBase64))
{
return;
}
byte[] dssePayloadBytes;
try
{
dssePayloadBytes = Convert.FromBase64String(dssePayloadBase64!);
}
catch (FormatException)
{
errors.Add($"runtime witness '{witnessId}' DSSE payload is not valid base64 in '{dsseArtifact.Path}'.");
return;
}
if (!traceBytes.SequenceEqual(dssePayloadBytes))
{
errors.Add($"runtime witness '{witnessId}' trace payload bytes do not match DSSE payload.");
}
VerifySigstoreBundle(sigstoreBytes, sigstoreArtifact.Path, witnessId, dssePayloadType!, dssePayloadBase64!, errors);
}
private static bool TryGetArtifactBytes(
ArtifactEntry artifact,
IReadOnlyDictionary<string, byte[]> artifactsByPath,
ICollection<string> errors,
out byte[] bytes)
{
if (!artifactsByPath.TryGetValue(artifact.Path, out bytes!))
{
errors.Add($"runtime witness artifact '{artifact.Path}' is missing from offline artifact set.");
return false;
}
var computedDigest = ComputeSha256Hex(bytes);
var expectedDigest = NormalizeDigest(artifact.Digest);
if (!string.Equals(expectedDigest, computedDigest, StringComparison.Ordinal))
{
errors.Add($"runtime witness artifact '{artifact.Path}' digest mismatch.");
return false;
}
return true;
}
private static bool TryGetDssePayload(
byte[] dsseBytes,
string path,
ICollection<string> errors,
out string? payloadType,
out string? payloadBase64)
{
payloadType = null;
payloadBase64 = null;
try
{
using var document = JsonDocument.Parse(dsseBytes);
var root = document.RootElement;
if (!root.TryGetProperty("payloadType", out var payloadTypeElement)
|| string.IsNullOrWhiteSpace(payloadTypeElement.GetString()))
{
errors.Add($"DSSE envelope '{path}' is missing payloadType.");
return false;
}
if (!root.TryGetProperty("payload", out var payloadElement)
|| string.IsNullOrWhiteSpace(payloadElement.GetString()))
{
errors.Add($"DSSE envelope '{path}' is missing payload.");
return false;
}
payloadType = payloadTypeElement.GetString();
payloadBase64 = payloadElement.GetString();
return true;
}
catch (JsonException)
{
errors.Add($"DSSE envelope '{path}' is not valid JSON.");
return false;
}
}
private static void VerifySigstoreBundle(
byte[] sigstoreBytes,
string path,
string witnessId,
string expectedPayloadType,
string expectedPayloadBase64,
ICollection<string> errors)
{
try
{
using var document = JsonDocument.Parse(sigstoreBytes);
var root = document.RootElement;
var mediaType = root.TryGetProperty("mediaType", out var mediaTypeElement)
? mediaTypeElement.GetString()
: null;
if (!string.Equals(mediaType, BundleMediaTypes.SigstoreBundleV03, StringComparison.Ordinal))
{
errors.Add($"runtime witness '{witnessId}' sigstore bundle '{path}' has unsupported mediaType '{mediaType ?? "<missing>"}'.");
}
if (!root.TryGetProperty("dsseEnvelope", out var dsseEnvelope))
{
errors.Add($"runtime witness '{witnessId}' sigstore bundle '{path}' is missing dsseEnvelope.");
return;
}
var bundlePayloadType = dsseEnvelope.TryGetProperty("payloadType", out var payloadTypeElement)
? payloadTypeElement.GetString()
: null;
var bundlePayload = dsseEnvelope.TryGetProperty("payload", out var payloadElement)
? payloadElement.GetString()
: null;
if (!string.Equals(bundlePayloadType, expectedPayloadType, StringComparison.Ordinal))
{
errors.Add($"runtime witness '{witnessId}' sigstore bundle payloadType does not match trace DSSE envelope.");
}
if (!string.Equals(bundlePayload, expectedPayloadBase64, StringComparison.Ordinal))
{
errors.Add($"runtime witness '{witnessId}' sigstore bundle payload does not match trace DSSE envelope.");
}
if (!dsseEnvelope.TryGetProperty("signatures", out var signatures)
|| signatures.ValueKind != JsonValueKind.Array
|| signatures.GetArrayLength() == 0)
{
errors.Add($"runtime witness '{witnessId}' sigstore bundle '{path}' has no DSSE signatures.");
}
}
catch (JsonException)
{
errors.Add($"runtime witness '{witnessId}' sigstore bundle '{path}' is not valid JSON.");
}
}
private static string ComputeSha256Hex(ReadOnlySpan<byte> bytes)
{
Span<byte> hash = stackalloc byte[32];
SHA256.HashData(bytes, hash);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private static string NormalizeDigest(string digest)
{
const string prefix = "sha256:";
return digest.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
? digest[prefix.Length..].ToLowerInvariant()
: digest.ToLowerInvariant();
}
}
/// <summary>
/// Runtime witness offline verification outcome.
/// </summary>
public sealed record RuntimeWitnessOfflineVerificationResult
{
/// <summary>
/// Whether verification succeeded.
/// </summary>
public required bool Success { get; init; }
/// <summary>
/// Verification errors, if any.
/// </summary>
public IReadOnlyList<string> Errors { get; init; } = [];
public static RuntimeWitnessOfflineVerificationResult Passed()
=> new()
{
Success = true
};
public static RuntimeWitnessOfflineVerificationResult Failure(IReadOnlyList<string> errors)
=> new()
{
Success = false,
Errors = errors
};
}

View File

@@ -165,6 +165,22 @@ public sealed class TarGzBundleExporter : IEvidenceBundleExporter
}
}
// Add runtime witness artifacts (trace / trace.dsse / trace.sigstore)
if (config.IncludeRuntimeWitnesses)
{
foreach (var runtimeWitnessArtifact in bundleData.RuntimeWitnesses)
{
var entry = await AddArtifactAsync(
tarWriter,
runtimeWitnessArtifact,
BundlePaths.RuntimeWitnessesDirectory,
"runtime_witness",
cancellationToken);
manifestBuilder.AddRuntimeWitness(entry);
checksumEntries.Add((entry.Path, entry.Digest));
}
}
// Add public keys
if (config.IncludeKeys)
{
@@ -261,7 +277,13 @@ public sealed class TarGzBundleExporter : IEvidenceBundleExporter
Size = content.Length,
Type = type,
Format = artifact.Format,
Subject = artifact.Subject
Subject = artifact.Subject,
WitnessId = artifact.WitnessId,
WitnessRole = artifact.WitnessRole,
WitnessIndex = artifact.WitnessIndex,
LinkedArtifacts = artifact.LinkedArtifacts is null
? null
: [.. artifact.LinkedArtifacts.Order(StringComparer.Ordinal)]
};
}
@@ -450,6 +472,7 @@ public sealed class TarGzBundleExporter : IEvidenceBundleExporter
- Attestations: {manifest.Attestations.Length}
- Policy Verdicts: {manifest.PolicyVerdicts.Length}
- Scan Results: {manifest.ScanResults.Length}
- Runtime Witness Artifacts: {manifest.RuntimeWitnesses.Length}
- Public Keys: {manifest.PublicKeys.Length}
Total Artifacts: {manifest.TotalArtifacts}
@@ -469,6 +492,7 @@ public sealed class TarGzBundleExporter : IEvidenceBundleExporter
+-- attestations/ # DSSE attestation envelopes
+-- policy/ # Policy verdicts
+-- scans/ # Scan results
+-- runtime-witnesses/ # Runtime witness triplets and index metadata
+-- keys/ # Public keys for verification
```
@@ -515,6 +539,7 @@ internal sealed class BundleManifestBuilder
private readonly List<ArtifactEntry> _attestations = [];
private readonly List<ArtifactEntry> _policyVerdicts = [];
private readonly List<ArtifactEntry> _scanResults = [];
private readonly List<ArtifactEntry> _runtimeWitnesses = [];
private readonly List<KeyEntry> _publicKeys = [];
public BundleManifestBuilder(string bundleId, DateTimeOffset createdAt)
@@ -529,6 +554,7 @@ internal sealed class BundleManifestBuilder
public void AddAttestation(ArtifactEntry entry) => _attestations.Add(entry);
public void AddPolicyVerdict(ArtifactEntry entry) => _policyVerdicts.Add(entry);
public void AddScanResult(ArtifactEntry entry) => _scanResults.Add(entry);
public void AddRuntimeWitness(ArtifactEntry entry) => _runtimeWitnesses.Add(entry);
public void AddPublicKey(KeyEntry entry) => _publicKeys.Add(entry);
public BundleManifest Build() => new()
@@ -541,6 +567,7 @@ internal sealed class BundleManifestBuilder
Attestations = [.. _attestations],
PolicyVerdicts = [.. _policyVerdicts],
ScanResults = [.. _scanResults],
RuntimeWitnesses = [.. _runtimeWitnesses],
PublicKeys = [.. _publicKeys]
};
}

View File

@@ -343,6 +343,7 @@ if __name__ == ""__main__"":
| Attestations | {manifest.Attestations.Length} |
| Policy Verdicts | {manifest.PolicyVerdicts.Length} |
| Scan Results | {manifest.ScanResults.Length} |
| Runtime Witness Artifacts | {manifest.RuntimeWitnesses.Length} |
| Public Keys | {manifest.PublicKeys.Length} |
| **Total Artifacts** | **{manifest.TotalArtifacts}** |
@@ -362,6 +363,7 @@ if __name__ == ""__main__"":
+-- attestations/ # DSSE attestation envelopes
+-- policy/ # Policy verdicts
+-- scans/ # Scan results
+-- runtime-witnesses/ # Runtime witness triplets (trace + DSSE + Sigstore bundle)
+-- keys/ # Public keys for verification
```

View File

@@ -186,6 +186,7 @@ public class BundleManifestSerializationTests
config.IncludeAttestations.Should().BeTrue();
config.IncludePolicyVerdicts.Should().BeTrue();
config.IncludeScanResults.Should().BeTrue();
config.IncludeRuntimeWitnesses.Should().BeTrue();
config.IncludeKeys.Should().BeTrue();
config.IncludeVerifyScripts.Should().BeTrue();
config.Compression.Should().Be("gzip");
@@ -202,12 +203,13 @@ public class BundleManifestSerializationTests
var allArtifacts = manifest.AllArtifacts.ToList();
// Assert
allArtifacts.Should().HaveCount(5);
allArtifacts.Should().HaveCount(6);
allArtifacts.Select(a => a.Type).Should().Contain("sbom");
allArtifacts.Select(a => a.Type).Should().Contain("vex");
allArtifacts.Select(a => a.Type).Should().Contain("attestation");
allArtifacts.Select(a => a.Type).Should().Contain("policy");
allArtifacts.Select(a => a.Type).Should().Contain("scan");
allArtifacts.Select(a => a.Type).Should().Contain("runtime_witness");
}
[Fact]
@@ -217,7 +219,7 @@ public class BundleManifestSerializationTests
var manifest = CreateTestManifest();
// Act & Assert
manifest.TotalArtifacts.Should().Be(5);
manifest.TotalArtifacts.Should().Be(6);
}
[Fact]
@@ -264,6 +266,7 @@ public class BundleManifestSerializationTests
BundlePaths.AttestationsDirectory.Should().Be("attestations");
BundlePaths.PolicyDirectory.Should().Be("policy");
BundlePaths.ScansDirectory.Should().Be("scans");
BundlePaths.RuntimeWitnessesDirectory.Should().Be("runtime-witnesses");
}
[Fact]
@@ -275,6 +278,8 @@ public class BundleManifestSerializationTests
BundleMediaTypes.VexOpenVex.Should().Be("application/vnd.openvex+json");
BundleMediaTypes.DsseEnvelope.Should().Be("application/vnd.dsse.envelope+json");
BundleMediaTypes.PublicKeyPem.Should().Be("application/x-pem-file");
BundleMediaTypes.RuntimeWitnessTrace.Should().Be("application/vnd.stellaops.witness.v1+json");
BundleMediaTypes.SigstoreBundleV03.Should().Be("application/vnd.dev.sigstore.bundle.v0.3+json");
}
private static BundleManifest CreateTestManifest()
@@ -326,6 +331,28 @@ public class BundleManifestSerializationTests
Size = 10000,
Type = "scan"
}),
RuntimeWitnesses = ImmutableArray.Create(new ArtifactEntry
{
Path = "runtime-witnesses/wit-sha256-001/trace.sigstore.json",
Digest = "sha256:wit123",
MediaType = BundleMediaTypes.SigstoreBundleV03,
Size = 4096,
Type = "runtime_witness",
WitnessId = "wit:sha256:001",
WitnessRole = RuntimeWitnessArtifactRoles.SigstoreBundle,
WitnessIndex = new RuntimeWitnessIndexKey
{
BuildId = "gnu-build-id:abc",
KernelRelease = "6.8.0",
ProbeId = "probe-runtime-core",
PolicyRunId = "policy-run-001"
},
LinkedArtifacts =
[
"runtime-witnesses/wit-sha256-001/trace.json",
"runtime-witnesses/wit-sha256-001/trace.dsse.json"
]
}),
PublicKeys = ImmutableArray.Create(new KeyEntry
{
Path = "keys/signing.pub",

View File

@@ -0,0 +1,499 @@
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using FluentAssertions;
using StellaOps.EvidenceLocker.Export.Models;
using Xunit;
namespace StellaOps.EvidenceLocker.Export.Tests;
[Trait("Category", "Unit")]
public sealed class RuntimeWitnessOfflineVerifierTests
{
private readonly RuntimeWitnessOfflineVerifier _sut = new();
[Fact]
public void Verify_WithValidTriplet_ReturnsSuccess()
{
var fixture = CreateFixture();
var result = _sut.Verify(fixture.Manifest, fixture.ArtifactsByPath);
result.Success.Should().BeTrue();
result.Errors.Should().BeEmpty();
}
[Fact]
public void Verify_WithMissingSigstoreArtifact_ReturnsFailure()
{
var fixture = CreateFixture();
var manifest = fixture.Manifest with
{
RuntimeWitnesses = fixture.Manifest.RuntimeWitnesses
.Where(artifact => artifact.WitnessRole != RuntimeWitnessArtifactRoles.SigstoreBundle)
.ToImmutableArray()
};
var result = _sut.Verify(manifest, fixture.ArtifactsByPath);
result.Success.Should().BeFalse();
result.Errors.Should().Contain(error => error.Contains("sigstore_bundle", StringComparison.Ordinal));
}
[Fact]
public void Verify_WithMismatchedDssePayload_ReturnsFailure()
{
var fixture = CreateFixture();
var mismatchedDsseBytes = Encoding.UTF8.GetBytes("""
{"payloadType":"application/vnd.stellaops.witness.v1+json","payload":"eyJ3aXRuZXNzX2lkIjoid2l0OnNoYTI1NjpESUZGRVJFTlQifQ==","signatures":[{"keyid":"runtime-key","sig":"c2ln"}]}
""");
var artifacts = fixture.ArtifactsByPath
.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
artifacts["runtime-witnesses/wit-001/trace.dsse.json"] = mismatchedDsseBytes;
var manifest = fixture.Manifest with
{
RuntimeWitnesses = fixture.Manifest.RuntimeWitnesses
.Select(artifact => artifact.Path == "runtime-witnesses/wit-001/trace.dsse.json"
? artifact with { Digest = $"sha256:{ComputeSha256Hex(mismatchedDsseBytes)}" }
: artifact)
.ToImmutableArray()
};
var result = _sut.Verify(manifest, artifacts);
result.Success.Should().BeFalse();
result.Errors.Should().Contain(error => error.Contains("do not match DSSE payload", StringComparison.Ordinal));
}
[Fact]
[Trait("Intent", "Regulatory")]
public void ReplayFrames_WithFixedWitnessArtifacts_AreByteIdenticalAcrossKernelLibcMatrix()
{
var fixture = CreateFixture();
var verification = _sut.Verify(fixture.Manifest, fixture.ArtifactsByPath);
verification.Success.Should().BeTrue();
var matrix = CreateReplayMatrix();
matrix.Select(row => row.KernelRelease)
.Distinct(StringComparer.Ordinal)
.Count()
.Should()
.BeGreaterThanOrEqualTo(3);
matrix.Select(row => row.LibcVariant)
.Distinct(StringComparer.Ordinal)
.Should()
.Contain(["glibc", "musl"]);
var projections = matrix
.Select(row => ProjectReplayFrames(fixture.Manifest, fixture.ArtifactsByPath, row))
.ToList();
projections.Should().NotBeEmpty();
projections.Select(projection => projection.FrameCount)
.Should()
.OnlyContain(static count => count > 0);
var baselineBytes = projections[0].FrameBytes;
projections.Select(projection => projection.FrameBytes)
.Should()
.OnlyContain(bytes => bytes.SequenceEqual(baselineBytes));
projections.Select(projection => projection.FrameDigest)
.Distinct(StringComparer.Ordinal)
.Should()
.ContainSingle();
}
[Fact]
[Trait("Intent", "Safety")]
public void BuildReplayFrameBytes_WithReorderedObservations_ProducesIdenticalDigest()
{
var fixture = CreateFixture();
var tracePath = fixture.Manifest.RuntimeWitnesses
.Single(artifact => artifact.WitnessRole == RuntimeWitnessArtifactRoles.Trace)
.Path;
var baselineTraceBytes = fixture.ArtifactsByPath[tracePath];
var reorderedTraceBytes = ReorderObservations(baselineTraceBytes);
var baselineFrames = BuildReplayFrameBytes(baselineTraceBytes);
var reorderedFrames = BuildReplayFrameBytes(reorderedTraceBytes);
baselineFrames.Should().Equal(reorderedFrames);
ComputeSha256Hex(baselineFrames).Should().Be(ComputeSha256Hex(reorderedFrames));
}
[Fact]
[Trait("Intent", "Safety")]
public void BuildReplayFrameBytes_WithMutatedObservation_ProducesDifferentDigest()
{
var fixture = CreateFixture();
var tracePath = fixture.Manifest.RuntimeWitnesses
.Single(artifact => artifact.WitnessRole == RuntimeWitnessArtifactRoles.Trace)
.Path;
var baselineTraceBytes = fixture.ArtifactsByPath[tracePath];
var mutatedTraceBytes = MutateFirstObservationStackHash(baselineTraceBytes, "sha256:ccc");
var baselineFrames = BuildReplayFrameBytes(baselineTraceBytes);
var mutatedFrames = BuildReplayFrameBytes(mutatedTraceBytes);
ComputeSha256Hex(baselineFrames).Should().NotBe(ComputeSha256Hex(mutatedFrames));
}
private static (BundleManifest Manifest, IReadOnlyDictionary<string, byte[]> ArtifactsByPath) CreateFixture()
{
var tracePath = "runtime-witnesses/wit-001/trace.json";
var dssePath = "runtime-witnesses/wit-001/trace.dsse.json";
var sigstorePath = "runtime-witnesses/wit-001/trace.sigstore.json";
var traceBytes = Encoding.UTF8.GetBytes("""
{
"witness_schema":"stellaops.witness.v1",
"witness_id":"wit:sha256:runtime-001",
"claim_id":"claim:sha256:artifact123:pathabcdef123456",
"observation_type":"runtime",
"observations":[
{
"observed_at":"2026-02-17T11:59:01Z",
"observation_count":1,
"stack_sample_hash":"sha256:bbb",
"process_id":4421,
"container_id":"container-a",
"pod_name":"api-0",
"namespace":"prod",
"source_type":"tetragon",
"observation_id":"obs-b"
},
{
"observed_at":"2026-02-17T11:59:00Z",
"observation_count":2,
"stack_sample_hash":"sha256:aaa",
"process_id":4421,
"container_id":"container-a",
"pod_name":"api-0",
"namespace":"prod",
"source_type":"tetragon",
"observation_id":"obs-a"
}
],
"symbolization":{
"build_id":"gnu-build-id:runtime-test",
"debug_artifact_uri":"cas://symbols/runtime-test.debug",
"symbolizer":{
"name":"llvm-symbolizer",
"version":"18.1.7",
"digest":"sha256:symbolizer"
},
"libc_variant":"glibc",
"sysroot_digest":"sha256:sysroot"
}
}
""");
var payloadBase64 = Convert.ToBase64String(traceBytes);
var dsseBytes = Encoding.UTF8.GetBytes(
$"{{\"payloadType\":\"application/vnd.stellaops.witness.v1+json\",\"payload\":\"{payloadBase64}\",\"signatures\":[{{\"keyid\":\"runtime-key\",\"sig\":\"c2ln\"}}]}}");
var sigstoreBytes = Encoding.UTF8.GetBytes(
$"{{\"mediaType\":\"application/vnd.dev.sigstore.bundle.v0.3+json\",\"verificationMaterial\":{{\"publicKey\":{{\"rawBytes\":\"cHVibGlj\"}}}},\"dsseEnvelope\":{{\"payloadType\":\"application/vnd.stellaops.witness.v1+json\",\"payload\":\"{payloadBase64}\",\"signatures\":[{{\"keyid\":\"runtime-key\",\"sig\":\"c2ln\"}}]}}}}");
var index = new RuntimeWitnessIndexKey
{
BuildId = "gnu-build-id:abc123",
KernelRelease = "6.8.0-45-generic",
ProbeId = "probe-runtime-core",
PolicyRunId = "policy-run-42"
};
var manifest = new BundleManifest
{
BundleId = "bundle-runtime-001",
CreatedAt = new DateTimeOffset(2026, 2, 17, 12, 0, 0, TimeSpan.Zero),
Metadata = new BundleMetadata
{
Subject = new BundleSubject
{
Type = SubjectTypes.ContainerImage,
Digest = "sha256:subject"
},
Provenance = new BundleProvenance
{
Creator = new CreatorInfo
{
Name = "StellaOps",
Version = "1.0.0"
},
ExportedAt = new DateTimeOffset(2026, 2, 17, 12, 0, 0, TimeSpan.Zero)
},
TimeWindow = new TimeWindow
{
Earliest = new DateTimeOffset(2026, 2, 17, 11, 0, 0, TimeSpan.Zero),
Latest = new DateTimeOffset(2026, 2, 17, 12, 0, 0, TimeSpan.Zero)
}
},
RuntimeWitnesses =
[
new ArtifactEntry
{
Path = tracePath,
Digest = $"sha256:{ComputeSha256Hex(traceBytes)}",
MediaType = BundleMediaTypes.RuntimeWitnessTrace,
Size = traceBytes.Length,
Type = "runtime_witness",
WitnessId = "wit:sha256:runtime-001",
WitnessRole = RuntimeWitnessArtifactRoles.Trace,
WitnessIndex = index,
LinkedArtifacts = [dssePath, sigstorePath]
},
new ArtifactEntry
{
Path = dssePath,
Digest = $"sha256:{ComputeSha256Hex(dsseBytes)}",
MediaType = BundleMediaTypes.DsseEnvelope,
Size = dsseBytes.Length,
Type = "runtime_witness",
WitnessId = "wit:sha256:runtime-001",
WitnessRole = RuntimeWitnessArtifactRoles.Dsse,
WitnessIndex = index,
LinkedArtifacts = [tracePath, sigstorePath]
},
new ArtifactEntry
{
Path = sigstorePath,
Digest = $"sha256:{ComputeSha256Hex(sigstoreBytes)}",
MediaType = BundleMediaTypes.SigstoreBundleV03,
Size = sigstoreBytes.Length,
Type = "runtime_witness",
WitnessId = "wit:sha256:runtime-001",
WitnessRole = RuntimeWitnessArtifactRoles.SigstoreBundle,
WitnessIndex = index,
LinkedArtifacts = [tracePath, dssePath]
}
]
};
var artifactsByPath = new Dictionary<string, byte[]>(StringComparer.Ordinal)
{
[tracePath] = traceBytes,
[dssePath] = dsseBytes,
[sigstorePath] = sigstoreBytes
};
return (manifest, artifactsByPath);
}
private static IReadOnlyList<ReplayEnvironment> CreateReplayMatrix()
{
return
[
new ReplayEnvironment("5.15.0-1068-azure", "glibc"),
new ReplayEnvironment("6.1.0-21-amd64", "glibc"),
new ReplayEnvironment("6.6.32-0-lts", "musl")
];
}
private static ReplayProjection ProjectReplayFrames(
BundleManifest manifest,
IReadOnlyDictionary<string, byte[]> artifactsByPath,
ReplayEnvironment environment)
{
var dsseArtifact = manifest.RuntimeWitnesses.Single(
artifact => artifact.WitnessRole == RuntimeWitnessArtifactRoles.Dsse);
var dsseBytes = artifactsByPath[dsseArtifact.Path];
using var dsseDocument = JsonDocument.Parse(dsseBytes);
var payload = ReadRequiredString(dsseDocument.RootElement, "payload");
var traceBytes = Convert.FromBase64String(payload);
var frameBytes = BuildReplayFrameBytes(traceBytes);
return new ReplayProjection(
environment.KernelRelease,
environment.LibcVariant,
frameBytes,
$"sha256:{ComputeSha256Hex(frameBytes)}",
GetFrameCount(frameBytes));
}
private static byte[] BuildReplayFrameBytes(byte[] traceBytes)
{
using var traceDocument = JsonDocument.Parse(traceBytes);
var root = traceDocument.RootElement;
var symbolization = root.GetProperty("symbolization");
var frames = root.GetProperty("observations")
.EnumerateArray()
.Select(observation => new ReplayFrame
{
ObservedAt = ReadRequiredString(observation, "observed_at"),
ObservationId = ReadRequiredString(observation, "observation_id"),
StackSampleHash = ReadRequiredString(observation, "stack_sample_hash"),
ProcessId = ReadOptionalInt(observation, "process_id"),
ContainerId = ReadOptionalString(observation, "container_id"),
Namespace = ReadOptionalString(observation, "namespace"),
PodName = ReadOptionalString(observation, "pod_name"),
SourceType = ReadOptionalString(observation, "source_type"),
ObservationCount = ReadOptionalInt(observation, "observation_count")
})
.OrderBy(static frame => frame.ObservedAt, StringComparer.Ordinal)
.ThenBy(static frame => frame.ObservationId, StringComparer.Ordinal)
.ThenBy(static frame => frame.StackSampleHash, StringComparer.Ordinal)
.ThenBy(static frame => frame.ProcessId ?? int.MinValue)
.ThenBy(static frame => frame.ContainerId ?? string.Empty, StringComparer.Ordinal)
.ThenBy(static frame => frame.Namespace ?? string.Empty, StringComparer.Ordinal)
.ThenBy(static frame => frame.PodName ?? string.Empty, StringComparer.Ordinal)
.ThenBy(static frame => frame.SourceType ?? string.Empty, StringComparer.Ordinal)
.ThenBy(static frame => frame.ObservationCount ?? int.MinValue)
.ToList();
var replay = new ReplayFrameDocument
{
WitnessId = ReadRequiredString(root, "witness_id"),
ClaimId = ReadRequiredString(root, "claim_id"),
BuildId = ReadRequiredString(symbolization, "build_id"),
SymbolizerName = ReadRequiredString(symbolization.GetProperty("symbolizer"), "name"),
SymbolizerVersion = ReadRequiredString(symbolization.GetProperty("symbolizer"), "version"),
SymbolizerDigest = ReadRequiredString(symbolization.GetProperty("symbolizer"), "digest"),
LibcVariant = ReadRequiredString(symbolization, "libc_variant"),
SysrootDigest = ReadRequiredString(symbolization, "sysroot_digest"),
Frames = frames
};
return JsonSerializer.SerializeToUtf8Bytes(replay, ReplayJsonOptions);
}
private static int GetFrameCount(byte[] frameBytes)
{
using var frameDocument = JsonDocument.Parse(frameBytes);
return frameDocument.RootElement
.GetProperty("frames")
.GetArrayLength();
}
private static string ReadRequiredString(JsonElement element, string propertyName)
{
var value = ReadOptionalString(element, propertyName);
if (string.IsNullOrWhiteSpace(value))
{
throw new InvalidOperationException($"Required string '{propertyName}' missing from replay fixture.");
}
return value;
}
private static string? ReadOptionalString(JsonElement element, string propertyName)
{
if (!element.TryGetProperty(propertyName, out var property))
{
return null;
}
return property.ValueKind switch
{
JsonValueKind.String => property.GetString(),
JsonValueKind.Number => property.GetRawText(),
_ => null
};
}
private static int? ReadOptionalInt(JsonElement element, string propertyName)
{
if (!element.TryGetProperty(propertyName, out var property))
{
return null;
}
if (property.ValueKind == JsonValueKind.Number && property.TryGetInt32(out var value))
{
return value;
}
return null;
}
private static byte[] ReorderObservations(byte[] traceBytes)
{
var root = JsonNode.Parse(traceBytes)?.AsObject()
?? throw new InvalidOperationException("Trace JSON must parse into an object.");
var observations = root["observations"]?.AsArray()
?? throw new InvalidOperationException("Trace JSON must contain observations.");
var reordered = new JsonArray();
for (var i = observations.Count - 1; i >= 0; i--)
{
reordered.Add(observations[i]?.DeepClone());
}
root["observations"] = reordered;
return Encoding.UTF8.GetBytes(root.ToJsonString());
}
private static byte[] MutateFirstObservationStackHash(byte[] traceBytes, string newHash)
{
var root = JsonNode.Parse(traceBytes)?.AsObject()
?? throw new InvalidOperationException("Trace JSON must parse into an object.");
var observations = root["observations"]?.AsArray()
?? throw new InvalidOperationException("Trace JSON must contain observations.");
if (observations.Count == 0)
{
throw new InvalidOperationException("Trace JSON observations array cannot be empty.");
}
var first = observations[0]?.AsObject()
?? throw new InvalidOperationException("Observation entry must be an object.");
first["stack_sample_hash"] = newHash;
return Encoding.UTF8.GetBytes(root.ToJsonString());
}
private static string ComputeSha256Hex(ReadOnlySpan<byte> bytes)
{
Span<byte> hash = stackalloc byte[32];
SHA256.HashData(bytes, hash);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private static readonly JsonSerializerOptions ReplayJsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false
};
private sealed record ReplayEnvironment(string KernelRelease, string LibcVariant);
private sealed record ReplayProjection(
string KernelRelease,
string LibcVariant,
byte[] FrameBytes,
string FrameDigest,
int FrameCount);
private sealed record ReplayFrameDocument
{
public required string WitnessId { get; init; }
public required string ClaimId { get; init; }
public required string BuildId { get; init; }
public required string SymbolizerName { get; init; }
public required string SymbolizerVersion { get; init; }
public required string SymbolizerDigest { get; init; }
public required string LibcVariant { get; init; }
public required string SysrootDigest { get; init; }
public required IReadOnlyList<ReplayFrame> Frames { get; init; }
}
private sealed record ReplayFrame
{
public required string ObservedAt { get; init; }
public required string ObservationId { get; init; }
public required string StackSampleHash { get; init; }
public int? ProcessId { get; init; }
public string? ContainerId { get; init; }
public string? Namespace { get; init; }
public string? PodName { get; init; }
public string? SourceType { get; init; }
public int? ObservationCount { get; init; }
}
}

View File

@@ -233,6 +233,74 @@ public class TarGzBundleExporterTests
manifest.TotalArtifacts.Should().Be(3);
}
[Fact]
public async Task ExportToStreamAsync_IncludesRuntimeWitnessTriplet_WhenConfigured()
{
// Arrange
var bundleData = CreateTestBundleData() with
{
RuntimeWitnesses = CreateRuntimeWitnessArtifacts()
};
_dataProviderMock
.Setup(x => x.LoadBundleDataAsync("test-bundle", null, It.IsAny<CancellationToken>()))
.ReturnsAsync(bundleData);
var request = new ExportRequest
{
BundleId = "test-bundle",
Configuration = new ExportConfiguration { IncludeRuntimeWitnesses = true }
};
using var stream = new MemoryStream();
// Act
var result = await _exporter.ExportToStreamAsync(request, stream, TestContext.Current.CancellationToken);
// Assert
result.Success.Should().BeTrue();
result.Manifest!.RuntimeWitnesses.Should().HaveCount(3);
result.Manifest.RuntimeWitnesses.Select(a => a.WitnessRole).Should().BeEquivalentTo(
[
RuntimeWitnessArtifactRoles.Trace,
RuntimeWitnessArtifactRoles.Dsse,
RuntimeWitnessArtifactRoles.SigstoreBundle
]);
result.Manifest.RuntimeWitnesses.Should().OnlyContain(a => a.WitnessId == "wit:sha256:runtime-001");
result.Manifest.RuntimeWitnesses.Should().OnlyContain(a => a.WitnessIndex != null);
stream.Position = 0;
var entries = await ExtractTarGzEntries(stream);
entries.Should().Contain("runtime-witnesses/wit-001/trace.json");
entries.Should().Contain("runtime-witnesses/wit-001/trace.dsse.json");
entries.Should().Contain("runtime-witnesses/wit-001/trace.sigstore.json");
}
[Fact]
public async Task ExportToStreamAsync_ExcludesRuntimeWitnessTriplet_WhenDisabled()
{
// Arrange
var bundleData = CreateTestBundleData() with
{
RuntimeWitnesses = CreateRuntimeWitnessArtifacts()
};
_dataProviderMock
.Setup(x => x.LoadBundleDataAsync("test-bundle", null, It.IsAny<CancellationToken>()))
.ReturnsAsync(bundleData);
var request = new ExportRequest
{
BundleId = "test-bundle",
Configuration = new ExportConfiguration { IncludeRuntimeWitnesses = false }
};
using var stream = new MemoryStream();
// Act
var result = await _exporter.ExportToStreamAsync(request, stream, TestContext.Current.CancellationToken);
// Assert
result.Success.Should().BeTrue();
result.Manifest!.RuntimeWitnesses.Should().BeEmpty();
}
[Fact]
public async Task ExportRequest_RequiresBundleId()
{
@@ -388,4 +456,61 @@ public class TarGzBundleExporterTests
}
};
}
private static IReadOnlyList<BundleArtifact> CreateRuntimeWitnessArtifacts()
{
var index = new RuntimeWitnessIndexKey
{
BuildId = "gnu-build-id:runtime-test",
KernelRelease = "6.8.0-45-generic",
ProbeId = "probe-runtime-core",
PolicyRunId = "policy-run-42"
};
return
[
new BundleArtifact
{
FileName = "wit-001/trace.json",
Content = Encoding.UTF8.GetBytes("{\"witness_id\":\"wit:sha256:runtime-001\"}"),
MediaType = BundleMediaTypes.RuntimeWitnessTrace,
WitnessId = "wit:sha256:runtime-001",
WitnessRole = RuntimeWitnessArtifactRoles.Trace,
WitnessIndex = index,
LinkedArtifacts =
[
"runtime-witnesses/wit-001/trace.dsse.json",
"runtime-witnesses/wit-001/trace.sigstore.json"
]
},
new BundleArtifact
{
FileName = "wit-001/trace.dsse.json",
Content = Encoding.UTF8.GetBytes("{\"payloadType\":\"application/vnd.stellaops.witness.v1+json\",\"payload\":\"eyJ3aXRuZXNzX2lkIjoid2l0OnNoYTI1NjpydW50aW1lLTAwMSJ9\",\"signatures\":[{\"keyid\":\"runtime-key\",\"sig\":\"c2ln\"}]}"),
MediaType = BundleMediaTypes.DsseEnvelope,
WitnessId = "wit:sha256:runtime-001",
WitnessRole = RuntimeWitnessArtifactRoles.Dsse,
WitnessIndex = index,
LinkedArtifacts =
[
"runtime-witnesses/wit-001/trace.json",
"runtime-witnesses/wit-001/trace.sigstore.json"
]
},
new BundleArtifact
{
FileName = "wit-001/trace.sigstore.json",
Content = Encoding.UTF8.GetBytes("{\"mediaType\":\"application/vnd.dev.sigstore.bundle.v0.3+json\",\"verificationMaterial\":{\"publicKey\":{\"rawBytes\":\"cHVibGlj\"}},\"dsseEnvelope\":{\"payloadType\":\"application/vnd.stellaops.witness.v1+json\",\"payload\":\"eyJ3aXRuZXNzX2lkIjoid2l0OnNoYTI1NjpydW50aW1lLTAwMSJ9\",\"signatures\":[{\"keyid\":\"runtime-key\",\"sig\":\"c2ln\"}]}}"),
MediaType = BundleMediaTypes.SigstoreBundleV03,
WitnessId = "wit:sha256:runtime-001",
WitnessRole = RuntimeWitnessArtifactRoles.SigstoreBundle,
WitnessIndex = index,
LinkedArtifacts =
[
"runtime-witnesses/wit-001/trace.json",
"runtime-witnesses/wit-001/trace.dsse.json"
]
}
];
}
}

View File

@@ -194,6 +194,7 @@ public class VerifyScriptGeneratorTests
readme.Should().Contain("SBOMs");
readme.Should().Contain("VEX Statements");
readme.Should().Contain("Attestations");
readme.Should().Contain("Runtime Witness Artifacts");
}
[Fact]
@@ -228,6 +229,7 @@ public class VerifyScriptGeneratorTests
readme.Should().Contain("sboms/");
readme.Should().Contain("vex/");
readme.Should().Contain("attestations/");
readme.Should().Contain("runtime-witnesses/");
}
[Fact]