tests fixes and some product advisories tunes ups

This commit is contained in:
master
2026-01-30 07:57:43 +02:00
parent 644887997c
commit 55744f6a39
345 changed files with 26290 additions and 2267 deletions

View File

@@ -95,6 +95,14 @@ public sealed record ReleaseEvidencePackManifest
[JsonPropertyName("manifestHash")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ManifestHash { get; init; }
/// <summary>
/// Path to the verification replay log for deterministic offline replay.
/// Advisory: EU CRA/NIS2 compliance - Sealed Audit-Pack replay_log.json
/// </summary>
[JsonPropertyName("replayLogPath")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ReplayLogPath { get; init; }
}
/// <summary>

View File

@@ -0,0 +1,236 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under the BUSL-1.1 license.
// Advisory: Sealed Audit-Pack replay_log.json format per EU CRA/NIS2 compliance
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace StellaOps.Attestor.EvidencePack.Models;
/// <summary>
/// Verification replay log for deterministic offline proof replay.
/// Captures step-by-step verification operations that auditors can replay
/// to recompute canonical_digest → DSSE subject digest → signature verify → Rekor inclusion verify.
/// </summary>
/// <remarks>
/// This format satisfies the EU CRA (Regulation 2024/2847) and NIS2 (Directive 2022/2555)
/// requirements for verifiable supply-chain evidence in procurement scenarios.
/// </remarks>
public sealed record VerificationReplayLog
{
/// <summary>
/// Schema version for the replay log format.
/// </summary>
[JsonPropertyName("schema_version")]
public required string SchemaVersion { get; init; }
/// <summary>
/// Unique identifier for this replay log.
/// </summary>
[JsonPropertyName("replay_id")]
public required string ReplayId { get; init; }
/// <summary>
/// Reference to the artifact being verified.
/// </summary>
[JsonPropertyName("artifact_ref")]
public required string ArtifactRef { get; init; }
/// <summary>
/// Timestamp when verification was performed.
/// </summary>
[JsonPropertyName("verified_at")]
public required DateTimeOffset VerifiedAt { get; init; }
/// <summary>
/// Version of the verifier tool used.
/// </summary>
[JsonPropertyName("verifier_version")]
public required string VerifierVersion { get; init; }
/// <summary>
/// Overall verification result.
/// </summary>
[JsonPropertyName("result")]
public required string Result { get; init; }
/// <summary>
/// Ordered list of verification steps for replay.
/// </summary>
[JsonPropertyName("steps")]
public required ImmutableArray<VerificationReplayStep> Steps { get; init; }
/// <summary>
/// Public keys used for verification.
/// </summary>
[JsonPropertyName("verification_keys")]
public required ImmutableArray<VerificationKeyRef> VerificationKeys { get; init; }
/// <summary>
/// Rekor transparency log information.
/// </summary>
[JsonPropertyName("rekor")]
public RekorVerificationInfo? Rekor { get; init; }
/// <summary>
/// Additional metadata for the replay log.
/// </summary>
[JsonPropertyName("metadata")]
public ImmutableDictionary<string, string>? Metadata { get; init; }
}
/// <summary>
/// A single verification step in the replay log.
/// </summary>
public sealed record VerificationReplayStep
{
/// <summary>
/// Step number (1-indexed).
/// </summary>
[JsonPropertyName("step")]
public required int Step { get; init; }
/// <summary>
/// Action performed (e.g., "compute_canonical_sbom_digest", "verify_dsse_signature").
/// </summary>
[JsonPropertyName("action")]
public required string Action { get; init; }
/// <summary>
/// Description of the action for human readers.
/// </summary>
[JsonPropertyName("description")]
public string? Description { get; init; }
/// <summary>
/// Input file or value for this step.
/// </summary>
[JsonPropertyName("input")]
public string? Input { get; init; }
/// <summary>
/// Output/computed value from this step.
/// </summary>
[JsonPropertyName("output")]
public string? Output { get; init; }
/// <summary>
/// Expected value (for comparison steps).
/// </summary>
[JsonPropertyName("expected")]
public string? Expected { get; init; }
/// <summary>
/// Actual computed value (for comparison steps).
/// </summary>
[JsonPropertyName("actual")]
public string? Actual { get; init; }
/// <summary>
/// Result of this step: "pass", "fail", or "skip".
/// </summary>
[JsonPropertyName("result")]
public required string Result { get; init; }
/// <summary>
/// Duration of this step in milliseconds.
/// </summary>
[JsonPropertyName("duration_ms")]
public double? DurationMs { get; init; }
/// <summary>
/// Error message if the step failed.
/// </summary>
[JsonPropertyName("error")]
public string? Error { get; init; }
/// <summary>
/// Key ID used if this was a signature verification step.
/// </summary>
[JsonPropertyName("key_id")]
public string? KeyId { get; init; }
/// <summary>
/// Algorithm used (e.g., "sha256", "ecdsa-p256").
/// </summary>
[JsonPropertyName("algorithm")]
public string? Algorithm { get; init; }
}
/// <summary>
/// Reference to a verification key.
/// </summary>
public sealed record VerificationKeyRef
{
/// <summary>
/// Key identifier.
/// </summary>
[JsonPropertyName("key_id")]
public required string KeyId { get; init; }
/// <summary>
/// Key type (e.g., "cosign", "rekor", "fulcio").
/// </summary>
[JsonPropertyName("type")]
public required string Type { get; init; }
/// <summary>
/// Path to the public key file in the bundle.
/// </summary>
[JsonPropertyName("path")]
public required string Path { get; init; }
/// <summary>
/// SHA-256 fingerprint of the public key.
/// </summary>
[JsonPropertyName("fingerprint")]
public string? Fingerprint { get; init; }
}
/// <summary>
/// Rekor transparency log verification information.
/// </summary>
public sealed record RekorVerificationInfo
{
/// <summary>
/// Rekor log ID.
/// </summary>
[JsonPropertyName("log_id")]
public required string LogId { get; init; }
/// <summary>
/// Log index of the entry.
/// </summary>
[JsonPropertyName("log_index")]
public required long LogIndex { get; init; }
/// <summary>
/// Tree size at time of inclusion.
/// </summary>
[JsonPropertyName("tree_size")]
public required long TreeSize { get; init; }
/// <summary>
/// Root hash of the Merkle tree.
/// </summary>
[JsonPropertyName("root_hash")]
public required string RootHash { get; init; }
/// <summary>
/// Path to the inclusion proof file.
/// </summary>
[JsonPropertyName("inclusion_proof_path")]
public string? InclusionProofPath { get; init; }
/// <summary>
/// Path to the signed checkpoint file.
/// </summary>
[JsonPropertyName("checkpoint_path")]
public string? CheckpointPath { get; init; }
/// <summary>
/// Integrated time (Unix timestamp).
/// </summary>
[JsonPropertyName("integrated_time")]
public long? IntegratedTime { get; init; }
}

View File

@@ -5,6 +5,7 @@ using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using StellaOps.Attestor.EvidencePack.Models;

View File

@@ -7,6 +7,7 @@ using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using StellaOps.Attestor.EvidencePack.Models;
using StellaOps.Attestor.EvidencePack.Services;
namespace StellaOps.Attestor.EvidencePack;
@@ -104,16 +105,11 @@ public sealed class ReleaseEvidencePackSerializer
verifyMd,
cancellationToken);
// Write verify.sh
var verifyShContent = await LoadTemplateAsync("verify.sh.template");
// Write verify.sh (template renamed to avoid MSBuild treating .sh as culture code)
var verifyShContent = await LoadTemplateAsync("verify-unix.template");
var verifyShPath = Path.Combine(bundleDir, "verify.sh");
await File.WriteAllTextAsync(verifyShPath, verifyShContent, cancellationToken);
#if !WINDOWS
// Make executable on Unix
File.SetUnixFileMode(verifyShPath, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
#endif
SetUnixExecutableIfSupported(verifyShPath);
// Write verify.ps1
var verifyPs1Content = await LoadTemplateAsync("verify.ps1.template");
@@ -125,6 +121,40 @@ public sealed class ReleaseEvidencePackSerializer
_logger.LogInformation("Evidence pack written to: {Path}", bundleDir);
}
/// <summary>
/// Writes the evidence pack to a directory structure with optional replay log.
/// Advisory: EU CRA/NIS2 compliance - includes replay_log.json for deterministic offline verification.
/// </summary>
public async Task SerializeToDirectoryAsync(
ReleaseEvidencePackManifest manifest,
string outputPath,
string artifactsSourcePath,
string publicKeyPath,
string? rekorPublicKeyPath,
VerificationReplayLog? replayLog,
CancellationToken cancellationToken = default)
{
// Update manifest with replay log path if provided
var manifestWithReplayLog = replayLog is not null
? manifest with { ReplayLogPath = "replay_log.json" }
: manifest;
await SerializeToDirectoryAsync(
manifestWithReplayLog,
outputPath,
artifactsSourcePath,
publicKeyPath,
rekorPublicKeyPath,
cancellationToken);
// Write replay_log.json if provided
if (replayLog is not null)
{
var bundleDir = Path.Combine(outputPath, $"stella-release-{manifest.ReleaseVersion}-evidence-pack");
await WriteReplayLogAsync(bundleDir, replayLog, cancellationToken);
}
}
/// <summary>
/// Writes the evidence pack to a directory structure without copying artifacts.
/// This overload is useful for testing and scenarios where artifacts are referenced but not bundled.
@@ -172,16 +202,11 @@ public sealed class ReleaseEvidencePackSerializer
verifyMd,
cancellationToken);
// Write verify.sh
var verifyShContent = await LoadTemplateAsync("verify.sh.template");
// Write verify.sh (template renamed to avoid MSBuild treating .sh as culture code)
var verifyShContent = await LoadTemplateAsync("verify-unix.template");
var verifyShPath = Path.Combine(outputPath, "verify.sh");
await File.WriteAllTextAsync(verifyShPath, verifyShContent, cancellationToken);
#if !WINDOWS
// Make executable on Unix
File.SetUnixFileMode(verifyShPath, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
#endif
SetUnixExecutableIfSupported(verifyShPath);
// Write verify.ps1
var verifyPs1Content = await LoadTemplateAsync("verify.ps1.template");
@@ -193,6 +218,30 @@ public sealed class ReleaseEvidencePackSerializer
_logger.LogInformation("Evidence pack written to: {Path}", outputPath);
}
/// <summary>
/// Writes the evidence pack to a directory structure without copying artifacts, with optional replay log.
/// Advisory: EU CRA/NIS2 compliance - includes replay_log.json for deterministic offline verification.
/// </summary>
public async Task SerializeToDirectoryAsync(
ReleaseEvidencePackManifest manifest,
string outputPath,
VerificationReplayLog? replayLog,
CancellationToken cancellationToken = default)
{
// Update manifest with replay log path if provided
var manifestWithReplayLog = replayLog is not null
? manifest with { ReplayLogPath = "replay_log.json" }
: manifest;
await SerializeToDirectoryAsync(manifestWithReplayLog, outputPath, cancellationToken);
// Write replay_log.json if provided
if (replayLog is not null)
{
await WriteReplayLogAsync(outputPath, replayLog, cancellationToken);
}
}
/// <summary>
/// Writes the evidence pack as a .tar.gz archive.
/// </summary>
@@ -337,6 +386,100 @@ public sealed class ReleaseEvidencePackSerializer
}
}
/// <summary>
/// Writes the evidence pack as a .tar.gz archive with replay log.
/// Advisory: EU CRA/NIS2 compliance - includes replay_log.json for deterministic offline verification.
/// </summary>
public async Task SerializeToTarGzAsync(
ReleaseEvidencePackManifest manifest,
Stream outputStream,
string bundleName,
VerificationReplayLog? replayLog,
CancellationToken cancellationToken = default)
{
// Create temp directory, serialize, then create tar.gz
var tempDir = Path.Combine(Path.GetTempPath(), $"evidence-pack-{Guid.NewGuid():N}");
var bundleDir = Path.Combine(tempDir, bundleName);
try
{
await SerializeToDirectoryAsync(manifest, bundleDir, replayLog, cancellationToken);
// Create tar.gz using GZipStream
await using var gzipStream = new GZipStream(outputStream, CompressionLevel.Optimal, leaveOpen: true);
await CreateTarFromDirectoryAsync(bundleDir, gzipStream, cancellationToken);
_logger.LogInformation("Evidence pack archived as tar.gz with replay_log.json");
}
finally
{
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, recursive: true);
}
}
}
/// <summary>
/// Writes the evidence pack as a .zip archive with replay log.
/// Advisory: EU CRA/NIS2 compliance - includes replay_log.json for deterministic offline verification.
/// </summary>
public async Task SerializeToZipAsync(
ReleaseEvidencePackManifest manifest,
Stream outputStream,
string bundleName,
VerificationReplayLog? replayLog,
CancellationToken cancellationToken = default)
{
// Create temp directory, serialize, then create zip
var tempDir = Path.Combine(Path.GetTempPath(), $"evidence-pack-{Guid.NewGuid():N}");
var bundleDir = Path.Combine(tempDir, bundleName);
try
{
await SerializeToDirectoryAsync(manifest, bundleDir, replayLog, cancellationToken);
using var archive = new ZipArchive(outputStream, ZipArchiveMode.Create, leaveOpen: true);
await AddDirectoryToZipAsync(archive, bundleDir, bundleName, cancellationToken);
_logger.LogInformation("Evidence pack archived as zip with replay_log.json");
}
finally
{
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, recursive: true);
}
}
}
/// <summary>
/// Writes the replay_log.json file to the bundle directory.
/// Advisory: EU CRA/NIS2 compliance - Sealed Audit-Pack replay_log.json
/// </summary>
private async Task WriteReplayLogAsync(
string bundleDir,
VerificationReplayLog replayLog,
CancellationToken cancellationToken)
{
var replayLogJson = JsonSerializer.Serialize(replayLog, ReplayLogSerializerContext.Default.VerificationReplayLog);
var replayLogPath = Path.Combine(bundleDir, "replay_log.json");
await File.WriteAllTextAsync(replayLogPath, replayLogJson, cancellationToken);
_logger.LogDebug("Wrote replay_log.json for deterministic verification replay");
}
/// <summary>
/// Sets Unix executable permissions on a file if running on a Unix-like OS.
/// </summary>
private static void SetUnixExecutableIfSupported(string filePath)
{
if (!OperatingSystem.IsWindows())
{
File.SetUnixFileMode(filePath,
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
}
}
private async Task GenerateChecksumsFilesAsync(
ReleaseEvidencePackManifest manifest,
string bundleDir,
@@ -447,6 +590,31 @@ public sealed class ReleaseEvidencePackSerializer
}
sb.AppendLine();
// Deterministic Replay Verification (CRA/NIS2 compliance)
if (manifest.ReplayLogPath is not null)
{
sb.AppendLine("## Deterministic Replay Verification (EU CRA/NIS2)");
sb.AppendLine();
sb.AppendLine("This bundle includes `replay_log.json` for offline deterministic verification.");
sb.AppendLine("The replay log documents each verification step for auditor replay:");
sb.AppendLine();
sb.AppendLine("```bash");
sb.AppendLine("# View verification steps");
sb.AppendLine("cat replay_log.json | jq '.steps[]'");
sb.AppendLine();
sb.AppendLine("# Verify all steps passed");
sb.AppendLine("cat replay_log.json | jq '.result'");
sb.AppendLine("```");
sb.AppendLine();
sb.AppendLine("Steps typically include:");
sb.AppendLine("1. `compute_canonical_sbom_digest` - RFC 8785 JCS canonicalization");
sb.AppendLine("2. `verify_dsse_subject_match` - SBOM digest matches DSSE subject");
sb.AppendLine("3. `verify_dsse_signature` - DSSE envelope signature validation");
sb.AppendLine("4. `verify_rekor_inclusion` - Merkle proof against transparency log");
sb.AppendLine("5. `verify_rekor_checkpoint` - Signed checkpoint validation (optional)");
sb.AppendLine();
}
sb.AppendLine("## Bundle Contents");
sb.AppendLine();
sb.AppendLine("| File | SHA-256 | Description |");
@@ -488,7 +656,7 @@ public sealed class ReleaseEvidencePackSerializer
private static async Task<string> LoadTemplateAsync(string templateName)
{
var assembly = Assembly.GetExecutingAssembly();
var assembly = typeof(ReleaseEvidencePackSerializer).Assembly;
var resourceName = $"StellaOps.Attestor.EvidencePack.Templates.{templateName}";
await using var stream = assembly.GetManifestResourceStream(resourceName);

View File

@@ -0,0 +1,22 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under the BUSL-1.1 license.
using System.Text.Json.Serialization;
using StellaOps.Attestor.EvidencePack.Models;
namespace StellaOps.Attestor.EvidencePack.Services;
/// <summary>
/// JSON serialization context for verification replay logs.
/// </summary>
[JsonSerializable(typeof(VerificationReplayLog))]
[JsonSerializable(typeof(VerificationReplayStep))]
[JsonSerializable(typeof(VerificationKeyRef))]
[JsonSerializable(typeof(RekorVerificationInfo))]
[JsonSourceGenerationOptions(
WriteIndented = true,
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
internal partial class ReplayLogSerializerContext : JsonSerializerContext
{
}

View File

@@ -0,0 +1,334 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under the BUSL-1.1 license.
// Advisory: Sealed Audit-Pack replay_log.json generation per EU CRA/NIS2 compliance
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using StellaOps.Attestor.EvidencePack.Models;
namespace StellaOps.Attestor.EvidencePack.Services;
/// <summary>
/// Builds verification replay logs for deterministic offline proof replay.
/// </summary>
public sealed class VerificationReplayLogBuilder : IVerificationReplayLogBuilder
{
private const string SchemaVersion = "1.0.0";
private const string VerifierVersion = "stellaops-attestor/1.0.0";
private readonly TimeProvider _timeProvider;
public VerificationReplayLogBuilder(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <summary>
/// Builds a verification replay log from SBOM verification results.
/// </summary>
public VerificationReplayLog Build(VerificationReplayLogRequest request)
{
ArgumentNullException.ThrowIfNull(request);
var now = _timeProvider.GetUtcNow();
var replayId = GenerateReplayId(request.ArtifactRef, now);
var steps = new List<VerificationReplayStep>();
var stepNumber = 1;
// Step 1: Compute canonical SBOM digest
if (request.SbomPath is not null)
{
steps.Add(new VerificationReplayStep
{
Step = stepNumber++,
Action = "compute_canonical_sbom_digest",
Description = "Compute SHA-256 hash of the canonicalized SBOM (RFC 8785 JCS)",
Input = request.SbomPath,
Output = request.CanonicalSbomDigest,
Result = "pass",
Algorithm = "sha256"
});
}
// Step 2: Verify DSSE subject match
if (request.DsseSubjectDigest is not null)
{
var subjectMatch = string.Equals(
request.CanonicalSbomDigest,
request.DsseSubjectDigest,
StringComparison.OrdinalIgnoreCase);
steps.Add(new VerificationReplayStep
{
Step = stepNumber++,
Action = "verify_dsse_subject_match",
Description = "Verify SBOM digest matches DSSE envelope subject[].digest",
Expected = request.DsseSubjectDigest,
Actual = request.CanonicalSbomDigest,
Result = subjectMatch ? "pass" : "fail",
Error = subjectMatch ? null : "SBOM digest does not match DSSE subject digest"
});
}
// Step 3: Verify DSSE signature
if (request.DsseEnvelopePath is not null)
{
steps.Add(new VerificationReplayStep
{
Step = stepNumber++,
Action = "verify_dsse_signature",
Description = "Verify DSSE envelope signature using supplier public key",
Input = request.DsseEnvelopePath,
KeyId = request.SigningKeyId,
Result = request.DsseSignatureValid ? "pass" : "fail",
Error = request.DsseSignatureValid ? null : request.DsseSignatureError,
Algorithm = request.SignatureAlgorithm ?? "ecdsa-p256"
});
}
// Step 4: Verify Rekor inclusion
if (request.RekorLogIndex is not null)
{
steps.Add(new VerificationReplayStep
{
Step = stepNumber++,
Action = "verify_rekor_inclusion",
Description = "Verify Merkle inclusion proof against Rekor transparency log",
Input = request.InclusionProofPath,
Output = $"log_index={request.RekorLogIndex}",
Result = request.RekorInclusionValid ? "pass" : "fail",
Error = request.RekorInclusionValid ? null : request.RekorInclusionError
});
}
// Step 5: Verify Rekor checkpoint signature (if provided)
if (request.CheckpointPath is not null)
{
steps.Add(new VerificationReplayStep
{
Step = stepNumber++,
Action = "verify_rekor_checkpoint",
Description = "Verify signed Rekor checkpoint (tile head)",
Input = request.CheckpointPath,
KeyId = request.RekorPublicKeyId,
Result = request.CheckpointValid ? "pass" : "fail",
Error = request.CheckpointValid ? null : "Checkpoint signature verification failed"
});
}
// Build verification keys list
var keys = new List<VerificationKeyRef>();
if (request.CosignPublicKeyPath is not null)
{
keys.Add(new VerificationKeyRef
{
KeyId = request.SigningKeyId ?? "cosign-key",
Type = "cosign",
Path = request.CosignPublicKeyPath,
Fingerprint = request.SigningKeyFingerprint
});
}
if (request.RekorPublicKeyPath is not null)
{
keys.Add(new VerificationKeyRef
{
KeyId = request.RekorPublicKeyId ?? "rekor-key",
Type = "rekor",
Path = request.RekorPublicKeyPath
});
}
// Build Rekor info
RekorVerificationInfo? rekorInfo = null;
if (request.RekorLogIndex is not null && request.RekorLogId is not null)
{
rekorInfo = new RekorVerificationInfo
{
LogId = request.RekorLogId,
LogIndex = request.RekorLogIndex.Value,
TreeSize = request.RekorTreeSize ?? 0,
RootHash = request.RekorRootHash ?? string.Empty,
InclusionProofPath = request.InclusionProofPath,
CheckpointPath = request.CheckpointPath,
IntegratedTime = request.RekorIntegratedTime
};
}
// Determine overall result
var overallResult = steps.All(s => s.Result == "pass" || s.Result == "skip") ? "pass" : "fail";
return new VerificationReplayLog
{
SchemaVersion = SchemaVersion,
ReplayId = replayId,
ArtifactRef = request.ArtifactRef,
VerifiedAt = now,
VerifierVersion = VerifierVersion,
Result = overallResult,
Steps = steps.ToImmutableArray(),
VerificationKeys = keys.ToImmutableArray(),
Rekor = rekorInfo,
Metadata = request.Metadata
};
}
/// <summary>
/// Serializes the replay log to JSON.
/// </summary>
public string Serialize(VerificationReplayLog log)
{
return JsonSerializer.Serialize(log, ReplayLogSerializerContext.Default.VerificationReplayLog);
}
private static string GenerateReplayId(string artifactRef, DateTimeOffset timestamp)
{
var input = $"{artifactRef}:{timestamp:O}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return $"replay_{Convert.ToHexStringLower(hash)[..16]}";
}
}
/// <summary>
/// Request for building a verification replay log.
/// </summary>
public sealed record VerificationReplayLogRequest
{
/// <summary>
/// Reference to the artifact being verified (e.g., OCI reference, file path).
/// </summary>
public required string ArtifactRef { get; init; }
/// <summary>
/// Path to the SBOM file in the bundle.
/// </summary>
public string? SbomPath { get; init; }
/// <summary>
/// SHA-256 hash of the canonicalized SBOM.
/// </summary>
public string? CanonicalSbomDigest { get; init; }
/// <summary>
/// Path to the DSSE envelope file in the bundle.
/// </summary>
public string? DsseEnvelopePath { get; init; }
/// <summary>
/// Digest from DSSE envelope subject[].digest.
/// </summary>
public string? DsseSubjectDigest { get; init; }
/// <summary>
/// Whether DSSE signature verification passed.
/// </summary>
public bool DsseSignatureValid { get; init; } = true;
/// <summary>
/// Error message if DSSE signature verification failed.
/// </summary>
public string? DsseSignatureError { get; init; }
/// <summary>
/// Key ID used for signing.
/// </summary>
public string? SigningKeyId { get; init; }
/// <summary>
/// Signature algorithm used.
/// </summary>
public string? SignatureAlgorithm { get; init; }
/// <summary>
/// SHA-256 fingerprint of the signing public key.
/// </summary>
public string? SigningKeyFingerprint { get; init; }
/// <summary>
/// Path to the cosign public key in the bundle.
/// </summary>
public string? CosignPublicKeyPath { get; init; }
/// <summary>
/// Rekor log ID.
/// </summary>
public string? RekorLogId { get; init; }
/// <summary>
/// Rekor log index.
/// </summary>
public long? RekorLogIndex { get; init; }
/// <summary>
/// Rekor tree size at time of inclusion.
/// </summary>
public long? RekorTreeSize { get; init; }
/// <summary>
/// Rekor root hash.
/// </summary>
public string? RekorRootHash { get; init; }
/// <summary>
/// Rekor integrated time (Unix timestamp).
/// </summary>
public long? RekorIntegratedTime { get; init; }
/// <summary>
/// Path to the inclusion proof file.
/// </summary>
public string? InclusionProofPath { get; init; }
/// <summary>
/// Whether Rekor inclusion proof verification passed.
/// </summary>
public bool RekorInclusionValid { get; init; } = true;
/// <summary>
/// Error message if Rekor inclusion verification failed.
/// </summary>
public string? RekorInclusionError { get; init; }
/// <summary>
/// Path to the signed checkpoint file.
/// </summary>
public string? CheckpointPath { get; init; }
/// <summary>
/// Whether checkpoint signature verification passed.
/// </summary>
public bool CheckpointValid { get; init; } = true;
/// <summary>
/// Path to the Rekor public key in the bundle.
/// </summary>
public string? RekorPublicKeyPath { get; init; }
/// <summary>
/// Rekor public key ID.
/// </summary>
public string? RekorPublicKeyId { get; init; }
/// <summary>
/// Additional metadata.
/// </summary>
public ImmutableDictionary<string, string>? Metadata { get; init; }
}
/// <summary>
/// Interface for building verification replay logs.
/// </summary>
public interface IVerificationReplayLogBuilder
{
/// <summary>
/// Builds a verification replay log from the request.
/// </summary>
VerificationReplayLog Build(VerificationReplayLogRequest request);
/// <summary>
/// Serializes the replay log to JSON.
/// </summary>
string Serialize(VerificationReplayLog log);
}

View File

@@ -11,7 +11,6 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="System.IO.Compression" />
</ItemGroup>
<ItemGroup>
@@ -21,7 +20,7 @@
<ItemGroup>
<EmbeddedResource Include="Templates\VERIFY.md.template" />
<EmbeddedResource Include="Templates\verify.sh.template" />
<EmbeddedResource Include="Templates\verify-unix.template" />
<EmbeddedResource Include="Templates\verify.ps1.template" />
</ItemGroup>