sprints work
This commit is contained in:
389
src/__Libraries/StellaOps.Provcache/VeriKeyBuilder.cs
Normal file
389
src/__Libraries/StellaOps.Provcache/VeriKeyBuilder.cs
Normal file
@@ -0,0 +1,389 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.Provcache;
|
||||
|
||||
/// <summary>
|
||||
/// Fluent builder for constructing a VeriKey (provenance identity key).
|
||||
/// VeriKey is a composite hash that uniquely identifies a provenance decision context.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// VeriKey = SHA256(source_hash || sbom_hash || vex_hash_set_hash || merge_policy_hash || signer_set_hash || time_window)
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Each component ensures cache invalidation when relevant inputs change:
|
||||
/// <list type="bullet">
|
||||
/// <item><c>source_hash</c>: Different artifacts get different keys</item>
|
||||
/// <item><c>sbom_hash</c>: SBOM changes (new packages) create new key</item>
|
||||
/// <item><c>vex_hash_set</c>: VEX updates create new key</item>
|
||||
/// <item><c>policy_hash</c>: Policy changes create new key</item>
|
||||
/// <item><c>signer_set_hash</c>: Key rotation creates new key (security)</item>
|
||||
/// <item><c>time_window</c>: Temporal bucketing enables controlled expiry</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class VeriKeyBuilder
|
||||
{
|
||||
private string? _sourceHash;
|
||||
private string? _sbomHash;
|
||||
private string? _vexHashSetHash;
|
||||
private string? _mergePolicyHash;
|
||||
private string? _signerSetHash;
|
||||
private string? _timeWindow;
|
||||
private readonly ProvcacheOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new VeriKeyBuilder with default options.
|
||||
/// </summary>
|
||||
public VeriKeyBuilder() : this(new ProvcacheOptions())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new VeriKeyBuilder with the specified options.
|
||||
/// </summary>
|
||||
/// <param name="options">Provcache configuration options.</param>
|
||||
public VeriKeyBuilder(ProvcacheOptions options)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the source artifact digest (image/artifact content-addressed hash).
|
||||
/// </summary>
|
||||
/// <param name="sourceHash">The artifact digest (e.g., sha256:abc123...).</param>
|
||||
/// <returns>This builder for fluent chaining.</returns>
|
||||
/// <exception cref="ArgumentException">If sourceHash is null or empty.</exception>
|
||||
public VeriKeyBuilder WithSourceHash(string sourceHash)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sourceHash))
|
||||
throw new ArgumentException("Source hash cannot be null or empty.", nameof(sourceHash));
|
||||
|
||||
_sourceHash = NormalizeHash(sourceHash);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the SBOM canonical hash.
|
||||
/// Automatically canonicalizes the SBOM content before hashing if raw bytes are provided.
|
||||
/// </summary>
|
||||
/// <param name="sbomHash">The SBOM canonical hash.</param>
|
||||
/// <returns>This builder for fluent chaining.</returns>
|
||||
public VeriKeyBuilder WithSbomHash(string sbomHash)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sbomHash))
|
||||
throw new ArgumentException("SBOM hash cannot be null or empty.", nameof(sbomHash));
|
||||
|
||||
_sbomHash = NormalizeHash(sbomHash);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes SBOM hash from raw SBOM bytes using canonical serialization.
|
||||
/// </summary>
|
||||
/// <param name="sbomBytes">Raw SBOM content bytes.</param>
|
||||
/// <returns>This builder for fluent chaining.</returns>
|
||||
public VeriKeyBuilder WithSbomBytes(ReadOnlySpan<byte> sbomBytes)
|
||||
{
|
||||
_sbomHash = ComputeHash(sbomBytes);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the VEX hash set hash (sorted aggregation of VEX statement hashes).
|
||||
/// </summary>
|
||||
/// <param name="vexHashSetHash">The pre-computed VEX hash set hash.</param>
|
||||
/// <returns>This builder for fluent chaining.</returns>
|
||||
public VeriKeyBuilder WithVexHashSet(string vexHashSetHash)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(vexHashSetHash))
|
||||
throw new ArgumentException("VEX hash set hash cannot be null or empty.", nameof(vexHashSetHash));
|
||||
|
||||
_vexHashSetHash = NormalizeHash(vexHashSetHash);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes VEX hash set from individual VEX statement hashes.
|
||||
/// Hashes are sorted lexicographically before aggregation for determinism.
|
||||
/// </summary>
|
||||
/// <param name="vexStatementHashes">Individual VEX statement hashes.</param>
|
||||
/// <returns>This builder for fluent chaining.</returns>
|
||||
public VeriKeyBuilder WithVexStatementHashes(IEnumerable<string> vexStatementHashes)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(vexStatementHashes);
|
||||
|
||||
// Sort hashes for deterministic aggregation
|
||||
var sortedHashes = vexStatementHashes
|
||||
.Where(h => !string.IsNullOrWhiteSpace(h))
|
||||
.Select(NormalizeHash)
|
||||
.Order(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
if (sortedHashes.Count == 0)
|
||||
{
|
||||
// Empty VEX set gets a well-known hash
|
||||
_vexHashSetHash = ComputeHash(Encoding.UTF8.GetBytes("empty-vex-set"));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Concatenate sorted hashes and hash the result
|
||||
var concatenated = string.Join("|", sortedHashes);
|
||||
_vexHashSetHash = ComputeHash(Encoding.UTF8.GetBytes(concatenated));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the merge policy hash (PolicyBundle digest).
|
||||
/// </summary>
|
||||
/// <param name="policyHash">The policy bundle hash.</param>
|
||||
/// <returns>This builder for fluent chaining.</returns>
|
||||
public VeriKeyBuilder WithMergePolicyHash(string policyHash)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(policyHash))
|
||||
throw new ArgumentException("Policy hash cannot be null or empty.", nameof(policyHash));
|
||||
|
||||
_mergePolicyHash = NormalizeHash(policyHash);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes policy hash from raw policy bundle bytes.
|
||||
/// </summary>
|
||||
/// <param name="policyBytes">Raw policy bundle content bytes.</param>
|
||||
/// <returns>This builder for fluent chaining.</returns>
|
||||
public VeriKeyBuilder WithMergePolicyBytes(ReadOnlySpan<byte> policyBytes)
|
||||
{
|
||||
_mergePolicyHash = ComputeHash(policyBytes);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the signer set hash (sorted certificate chain hashes).
|
||||
/// </summary>
|
||||
/// <param name="signerSetHash">The pre-computed signer set hash.</param>
|
||||
/// <returns>This builder for fluent chaining.</returns>
|
||||
public VeriKeyBuilder WithSignerSetHash(string signerSetHash)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(signerSetHash))
|
||||
throw new ArgumentException("Signer set hash cannot be null or empty.", nameof(signerSetHash));
|
||||
|
||||
_signerSetHash = NormalizeHash(signerSetHash);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes signer set hash from individual certificate hashes.
|
||||
/// Hashes are sorted lexicographically before aggregation for determinism.
|
||||
/// </summary>
|
||||
/// <param name="certificateHashes">Individual certificate hashes.</param>
|
||||
/// <returns>This builder for fluent chaining.</returns>
|
||||
public VeriKeyBuilder WithCertificateHashes(IEnumerable<string> certificateHashes)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(certificateHashes);
|
||||
|
||||
// Sort hashes for deterministic aggregation
|
||||
var sortedHashes = certificateHashes
|
||||
.Where(h => !string.IsNullOrWhiteSpace(h))
|
||||
.Select(NormalizeHash)
|
||||
.Order(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
if (sortedHashes.Count == 0)
|
||||
{
|
||||
// Empty signer set gets a well-known hash
|
||||
_signerSetHash = ComputeHash(Encoding.UTF8.GetBytes("empty-signer-set"));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Concatenate sorted hashes and hash the result
|
||||
var concatenated = string.Join("|", sortedHashes);
|
||||
_signerSetHash = ComputeHash(Encoding.UTF8.GetBytes(concatenated));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the time window for epoch bucketing.
|
||||
/// </summary>
|
||||
/// <param name="timeWindow">The time window identifier (e.g., "2024-12-24T12:00:00Z").</param>
|
||||
/// <returns>This builder for fluent chaining.</returns>
|
||||
public VeriKeyBuilder WithTimeWindow(string timeWindow)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(timeWindow))
|
||||
throw new ArgumentException("Time window cannot be null or empty.", nameof(timeWindow));
|
||||
|
||||
_timeWindow = timeWindow;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes time window from a timestamp using the configured bucket size.
|
||||
/// </summary>
|
||||
/// <param name="timestamp">The timestamp to bucket.</param>
|
||||
/// <returns>This builder for fluent chaining.</returns>
|
||||
public VeriKeyBuilder WithTimeWindow(DateTimeOffset timestamp)
|
||||
{
|
||||
_timeWindow = _options.ComputeTimeWindow(timestamp);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the final VeriKey by hashing all components together.
|
||||
/// </summary>
|
||||
/// <returns>The computed VeriKey in format "sha256:<hex>".</returns>
|
||||
/// <exception cref="InvalidOperationException">If required components are missing.</exception>
|
||||
public string Build()
|
||||
{
|
||||
ValidateRequiredComponents();
|
||||
|
||||
// Build composite hash input: all components concatenated with delimiters
|
||||
var components = new StringBuilder();
|
||||
components.Append("v1|"); // Version prefix for future compatibility
|
||||
components.Append(_sourceHash);
|
||||
components.Append('|');
|
||||
components.Append(_sbomHash);
|
||||
components.Append('|');
|
||||
components.Append(_vexHashSetHash);
|
||||
components.Append('|');
|
||||
components.Append(_mergePolicyHash);
|
||||
components.Append('|');
|
||||
components.Append(_signerSetHash);
|
||||
components.Append('|');
|
||||
components.Append(_timeWindow);
|
||||
|
||||
var compositeBytes = Encoding.UTF8.GetBytes(components.ToString());
|
||||
return ComputeHash(compositeBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a <see cref="VeriKeyComponents"/> record with all individual components.
|
||||
/// Useful for debugging and serialization.
|
||||
/// </summary>
|
||||
/// <returns>A record containing all VeriKey components.</returns>
|
||||
public VeriKeyComponents BuildWithComponents()
|
||||
{
|
||||
ValidateRequiredComponents();
|
||||
|
||||
return new VeriKeyComponents
|
||||
{
|
||||
VeriKey = Build(),
|
||||
SourceHash = _sourceHash!,
|
||||
SbomHash = _sbomHash!,
|
||||
VexHashSetHash = _vexHashSetHash!,
|
||||
MergePolicyHash = _mergePolicyHash!,
|
||||
SignerSetHash = _signerSetHash!,
|
||||
TimeWindow = _timeWindow!
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the builder to its initial state.
|
||||
/// </summary>
|
||||
/// <returns>This builder for fluent chaining.</returns>
|
||||
public VeriKeyBuilder Reset()
|
||||
{
|
||||
_sourceHash = null;
|
||||
_sbomHash = null;
|
||||
_vexHashSetHash = null;
|
||||
_mergePolicyHash = null;
|
||||
_signerSetHash = null;
|
||||
_timeWindow = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
private void ValidateRequiredComponents()
|
||||
{
|
||||
var missing = new List<string>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_sourceHash))
|
||||
missing.Add("SourceHash");
|
||||
if (string.IsNullOrWhiteSpace(_sbomHash))
|
||||
missing.Add("SbomHash");
|
||||
if (string.IsNullOrWhiteSpace(_vexHashSetHash))
|
||||
missing.Add("VexHashSetHash");
|
||||
if (string.IsNullOrWhiteSpace(_mergePolicyHash))
|
||||
missing.Add("MergePolicyHash");
|
||||
if (string.IsNullOrWhiteSpace(_signerSetHash))
|
||||
missing.Add("SignerSetHash");
|
||||
if (string.IsNullOrWhiteSpace(_timeWindow))
|
||||
missing.Add("TimeWindow");
|
||||
|
||||
if (missing.Count > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot build VeriKey: missing required components: {string.Join(", ", missing)}. " +
|
||||
"Use the With* methods to set all required components before calling Build().");
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeHash(ReadOnlySpan<byte> data)
|
||||
{
|
||||
Span<byte> hash = stackalloc byte[32];
|
||||
SHA256.HashData(data, hash);
|
||||
return $"sha256:{Convert.ToHexStringLower(hash)}";
|
||||
}
|
||||
|
||||
private static string NormalizeHash(string hash)
|
||||
{
|
||||
// If hash already has algorithm prefix, validate and return lowercase
|
||||
if (hash.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return $"sha256:{hash[7..].ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
// Assume SHA256 if no prefix and looks like a hex string
|
||||
if (hash.Length == 64 && hash.All(c => char.IsAsciiHexDigit(c)))
|
||||
{
|
||||
return $"sha256:{hash.ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
// Return as-is if not recognized (might be other hash format)
|
||||
return hash.ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record containing all VeriKey components for debugging and serialization.
|
||||
/// </summary>
|
||||
public sealed record VeriKeyComponents
|
||||
{
|
||||
/// <summary>
|
||||
/// The final computed VeriKey.
|
||||
/// </summary>
|
||||
public required string VeriKey { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Source artifact digest.
|
||||
/// </summary>
|
||||
public required string SourceHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SBOM canonical hash.
|
||||
/// </summary>
|
||||
public required string SbomHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// VEX hash set hash.
|
||||
/// </summary>
|
||||
public required string VexHashSetHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Policy bundle hash.
|
||||
/// </summary>
|
||||
public required string MergePolicyHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Signer certificate set hash.
|
||||
/// </summary>
|
||||
public required string SignerSetHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Time window identifier.
|
||||
/// </summary>
|
||||
public required string TimeWindow { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user