audit, advisories and doctors/setup work
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using StellaOps.Attestor.Envelope;
|
||||
|
||||
namespace StellaOps.Attestor.StandardPredicates.BinaryDiff;
|
||||
|
||||
public interface IBinaryDiffDsseSigner
|
||||
{
|
||||
Task<BinaryDiffDsseResult> SignAsync(
|
||||
BinaryDiffPredicate predicate,
|
||||
EnvelopeKey signingKey,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed record BinaryDiffDsseResult
|
||||
{
|
||||
public required string PayloadType { get; init; }
|
||||
|
||||
public required byte[] Payload { get; init; }
|
||||
|
||||
public required ImmutableArray<DsseSignature> Signatures { get; init; }
|
||||
|
||||
public required string EnvelopeJson { get; init; }
|
||||
|
||||
public string? RekorLogIndex { get; init; }
|
||||
|
||||
public string? RekorEntryId { get; init; }
|
||||
}
|
||||
|
||||
public sealed class BinaryDiffDsseSigner : IBinaryDiffDsseSigner
|
||||
{
|
||||
private readonly EnvelopeSignatureService _signatureService;
|
||||
private readonly IBinaryDiffPredicateSerializer _serializer;
|
||||
|
||||
public BinaryDiffDsseSigner(
|
||||
EnvelopeSignatureService signatureService,
|
||||
IBinaryDiffPredicateSerializer serializer)
|
||||
{
|
||||
_signatureService = signatureService ?? throw new ArgumentNullException(nameof(signatureService));
|
||||
_serializer = serializer ?? throw new ArgumentNullException(nameof(serializer));
|
||||
}
|
||||
|
||||
public Task<BinaryDiffDsseResult> SignAsync(
|
||||
BinaryDiffPredicate predicate,
|
||||
EnvelopeKey signingKey,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(predicate);
|
||||
ArgumentNullException.ThrowIfNull(signingKey);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var payloadBytes = _serializer.SerializeToBytes(predicate);
|
||||
var signResult = _signatureService.SignDsse(BinaryDiffPredicate.PredicateType, payloadBytes, signingKey, cancellationToken);
|
||||
if (!signResult.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"BinaryDiff DSSE signing failed: {signResult.Error?.Message}");
|
||||
}
|
||||
|
||||
var signature = DsseSignature.FromBytes(signResult.Value!.Value.Span, signResult.Value.KeyId);
|
||||
var envelope = new DsseEnvelope(BinaryDiffPredicate.PredicateType, payloadBytes, [signature]);
|
||||
var envelopeJson = SerializeEnvelope(envelope);
|
||||
|
||||
var result = new BinaryDiffDsseResult
|
||||
{
|
||||
PayloadType = envelope.PayloadType,
|
||||
Payload = payloadBytes,
|
||||
Signatures = envelope.Signatures.ToImmutableArray(),
|
||||
EnvelopeJson = envelopeJson
|
||||
};
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
private static string SerializeEnvelope(DsseEnvelope envelope)
|
||||
{
|
||||
var serialization = DsseEnvelopeSerializer.Serialize(envelope);
|
||||
if (serialization.CompactJson is null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetString(serialization.CompactJson);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.Attestor.Envelope;
|
||||
|
||||
namespace StellaOps.Attestor.StandardPredicates.BinaryDiff;
|
||||
|
||||
public interface IBinaryDiffDsseVerifier
|
||||
{
|
||||
BinaryDiffVerificationResult Verify(
|
||||
DsseEnvelope envelope,
|
||||
EnvelopeKey publicKey,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed record BinaryDiffVerificationResult
|
||||
{
|
||||
public required bool IsValid { get; init; }
|
||||
|
||||
public string? Error { get; init; }
|
||||
|
||||
public BinaryDiffPredicate? Predicate { get; init; }
|
||||
|
||||
public string? VerifiedKeyId { get; init; }
|
||||
|
||||
public IReadOnlyList<string> SchemaErrors { get; init; } = Array.Empty<string>();
|
||||
|
||||
public static BinaryDiffVerificationResult Success(BinaryDiffPredicate predicate, string keyId) => new()
|
||||
{
|
||||
IsValid = true,
|
||||
Predicate = predicate,
|
||||
VerifiedKeyId = keyId,
|
||||
SchemaErrors = Array.Empty<string>()
|
||||
};
|
||||
|
||||
public static BinaryDiffVerificationResult Failure(string error, IReadOnlyList<string>? schemaErrors = null) => new()
|
||||
{
|
||||
IsValid = false,
|
||||
Error = error,
|
||||
SchemaErrors = schemaErrors ?? Array.Empty<string>()
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class BinaryDiffDsseVerifier : IBinaryDiffDsseVerifier
|
||||
{
|
||||
private readonly EnvelopeSignatureService _signatureService;
|
||||
private readonly IBinaryDiffPredicateSerializer _serializer;
|
||||
|
||||
public BinaryDiffDsseVerifier(
|
||||
EnvelopeSignatureService signatureService,
|
||||
IBinaryDiffPredicateSerializer serializer)
|
||||
{
|
||||
_signatureService = signatureService ?? throw new ArgumentNullException(nameof(signatureService));
|
||||
_serializer = serializer ?? throw new ArgumentNullException(nameof(serializer));
|
||||
}
|
||||
|
||||
public BinaryDiffVerificationResult Verify(
|
||||
DsseEnvelope envelope,
|
||||
EnvelopeKey publicKey,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(envelope);
|
||||
ArgumentNullException.ThrowIfNull(publicKey);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (!string.Equals(envelope.PayloadType, BinaryDiffPredicate.PredicateType, StringComparison.Ordinal))
|
||||
{
|
||||
return BinaryDiffVerificationResult.Failure(
|
||||
$"Invalid payload type: expected '{BinaryDiffPredicate.PredicateType}', got '{envelope.PayloadType}'.");
|
||||
}
|
||||
|
||||
if (!TryVerifySignature(envelope, publicKey, cancellationToken, out var keyId))
|
||||
{
|
||||
return BinaryDiffVerificationResult.Failure("DSSE signature verification failed.");
|
||||
}
|
||||
|
||||
BinaryDiffPredicate predicate;
|
||||
try
|
||||
{
|
||||
predicate = _serializer.Deserialize(envelope.Payload.Span);
|
||||
}
|
||||
catch (Exception ex) when (ex is JsonException or InvalidOperationException)
|
||||
{
|
||||
return BinaryDiffVerificationResult.Failure($"Failed to deserialize predicate: {ex.Message}");
|
||||
}
|
||||
|
||||
if (!string.Equals(predicate.PredicateTypeId, BinaryDiffPredicate.PredicateType, StringComparison.Ordinal))
|
||||
{
|
||||
return BinaryDiffVerificationResult.Failure("Predicate type does not match BinaryDiffV1.");
|
||||
}
|
||||
|
||||
using var document = JsonDocument.Parse(envelope.Payload);
|
||||
var schemaResult = BinaryDiffSchema.Validate(document.RootElement);
|
||||
if (!schemaResult.IsValid)
|
||||
{
|
||||
return BinaryDiffVerificationResult.Failure("Schema validation failed.", schemaResult.Errors);
|
||||
}
|
||||
|
||||
if (!HasDeterministicOrdering(predicate))
|
||||
{
|
||||
return BinaryDiffVerificationResult.Failure("Predicate ordering is not deterministic.");
|
||||
}
|
||||
|
||||
return BinaryDiffVerificationResult.Success(predicate, keyId ?? publicKey.KeyId);
|
||||
}
|
||||
|
||||
private bool TryVerifySignature(
|
||||
DsseEnvelope envelope,
|
||||
EnvelopeKey publicKey,
|
||||
CancellationToken cancellationToken,
|
||||
out string? keyId)
|
||||
{
|
||||
foreach (var signature in envelope.Signatures)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(signature.KeyId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.Equals(signature.KeyId, publicKey.KeyId, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryDecodeSignature(signature.Signature, out var signatureBytes))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var envelopeSignature = new EnvelopeSignature(signature.KeyId, publicKey.AlgorithmId, signatureBytes);
|
||||
var result = _signatureService.VerifyDsse(
|
||||
envelope.PayloadType,
|
||||
envelope.Payload.Span,
|
||||
envelopeSignature,
|
||||
publicKey,
|
||||
cancellationToken);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
keyId = signature.KeyId;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
keyId = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryDecodeSignature(string signature, out byte[] signatureBytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
signatureBytes = Convert.FromBase64String(signature);
|
||||
return signatureBytes.Length > 0;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
signatureBytes = [];
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasDeterministicOrdering(BinaryDiffPredicate predicate)
|
||||
{
|
||||
if (!IsSorted(predicate.Subjects.Select(subject => subject.Name)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsSorted(predicate.Findings.Select(finding => finding.Path)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var finding in predicate.Findings)
|
||||
{
|
||||
if (!IsSorted(finding.SectionDeltas.Select(delta => delta.Section)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsSorted(IEnumerable<string> values)
|
||||
{
|
||||
string? previous = null;
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (previous is not null && string.Compare(previous, value, StringComparison.Ordinal) > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
previous = value;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Attestor.StandardPredicates.BinaryDiff;
|
||||
|
||||
public sealed record BinaryDiffPredicate
|
||||
{
|
||||
public const string PredicateType = "stellaops.binarydiff.v1";
|
||||
|
||||
[JsonPropertyName("predicateType")]
|
||||
public string PredicateTypeId { get; init; } = PredicateType;
|
||||
|
||||
public required ImmutableArray<BinaryDiffSubject> Subjects { get; init; }
|
||||
|
||||
public required BinaryDiffInputs Inputs { get; init; }
|
||||
|
||||
public required ImmutableArray<BinaryDiffFinding> Findings { get; init; }
|
||||
|
||||
public required BinaryDiffMetadata Metadata { get; init; }
|
||||
}
|
||||
|
||||
public sealed record BinaryDiffSubject
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
|
||||
public required ImmutableDictionary<string, string> Digest { get; init; }
|
||||
|
||||
public BinaryDiffPlatform? Platform { get; init; }
|
||||
}
|
||||
|
||||
public sealed record BinaryDiffInputs
|
||||
{
|
||||
public required BinaryDiffImageReference Base { get; init; }
|
||||
|
||||
public required BinaryDiffImageReference Target { get; init; }
|
||||
}
|
||||
|
||||
public sealed record BinaryDiffImageReference
|
||||
{
|
||||
public string? Reference { get; init; }
|
||||
|
||||
public required string Digest { get; init; }
|
||||
|
||||
public string? ManifestDigest { get; init; }
|
||||
|
||||
public BinaryDiffPlatform? Platform { get; init; }
|
||||
}
|
||||
|
||||
public sealed record BinaryDiffPlatform
|
||||
{
|
||||
public required string Os { get; init; }
|
||||
|
||||
public required string Architecture { get; init; }
|
||||
|
||||
public string? Variant { get; init; }
|
||||
}
|
||||
|
||||
public sealed record BinaryDiffFinding
|
||||
{
|
||||
public required string Path { get; init; }
|
||||
|
||||
public required ChangeType ChangeType { get; init; }
|
||||
|
||||
public required BinaryFormat BinaryFormat { get; init; }
|
||||
|
||||
public string? LayerDigest { get; init; }
|
||||
|
||||
public SectionHashSet? BaseHashes { get; init; }
|
||||
|
||||
public SectionHashSet? TargetHashes { get; init; }
|
||||
|
||||
public ImmutableArray<SectionDelta> SectionDeltas { get; init; } = ImmutableArray<SectionDelta>.Empty;
|
||||
|
||||
public double? Confidence { get; init; }
|
||||
|
||||
public Verdict? Verdict { get; init; }
|
||||
}
|
||||
|
||||
public enum ChangeType
|
||||
{
|
||||
Added,
|
||||
Removed,
|
||||
Modified,
|
||||
Unchanged
|
||||
}
|
||||
|
||||
public enum BinaryFormat
|
||||
{
|
||||
Elf,
|
||||
Pe,
|
||||
Macho,
|
||||
Unknown
|
||||
}
|
||||
|
||||
public enum Verdict
|
||||
{
|
||||
Patched,
|
||||
Vanilla,
|
||||
Unknown,
|
||||
Incompatible
|
||||
}
|
||||
|
||||
public sealed record SectionHashSet
|
||||
{
|
||||
public string? BuildId { get; init; }
|
||||
|
||||
public required string FileHash { get; init; }
|
||||
|
||||
public required ImmutableDictionary<string, SectionInfo> Sections { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SectionInfo
|
||||
{
|
||||
public required string Sha256 { get; init; }
|
||||
|
||||
public string? Blake3 { get; init; }
|
||||
|
||||
public required long Size { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SectionDelta
|
||||
{
|
||||
public required string Section { get; init; }
|
||||
|
||||
public required SectionStatus Status { get; init; }
|
||||
|
||||
public string? BaseSha256 { get; init; }
|
||||
|
||||
public string? TargetSha256 { get; init; }
|
||||
|
||||
public long? SizeDelta { get; init; }
|
||||
}
|
||||
|
||||
public enum SectionStatus
|
||||
{
|
||||
Identical,
|
||||
Modified,
|
||||
Added,
|
||||
Removed
|
||||
}
|
||||
|
||||
public sealed record BinaryDiffMetadata
|
||||
{
|
||||
public required string ToolVersion { get; init; }
|
||||
|
||||
public required DateTimeOffset AnalysisTimestamp { get; init; }
|
||||
|
||||
public string? ConfigDigest { get; init; }
|
||||
|
||||
public int TotalBinaries { get; init; }
|
||||
|
||||
public int ModifiedBinaries { get; init; }
|
||||
|
||||
public ImmutableArray<string> AnalyzedSections { get; init; } = ImmutableArray<string>.Empty;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace StellaOps.Attestor.StandardPredicates.BinaryDiff;
|
||||
|
||||
public sealed class BinaryDiffOptions
|
||||
{
|
||||
public const string SectionName = "Attestor:BinaryDiff";
|
||||
|
||||
public string ToolVersion { get; set; } = "1.0.0";
|
||||
|
||||
public string? ConfigDigest { get; set; }
|
||||
|
||||
public IReadOnlyList<string> AnalyzedSections { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Attestor.StandardPredicates.BinaryDiff;
|
||||
|
||||
public interface IBinaryDiffPredicateBuilder
|
||||
{
|
||||
IBinaryDiffPredicateBuilder WithSubject(string name, string digest, BinaryDiffPlatform? platform = null);
|
||||
|
||||
IBinaryDiffPredicateBuilder WithInputs(BinaryDiffImageReference baseImage, BinaryDiffImageReference targetImage);
|
||||
|
||||
IBinaryDiffPredicateBuilder AddFinding(BinaryDiffFinding finding);
|
||||
|
||||
IBinaryDiffPredicateBuilder WithMetadata(Action<BinaryDiffMetadataBuilder> configure);
|
||||
|
||||
BinaryDiffPredicate Build();
|
||||
}
|
||||
|
||||
public sealed class BinaryDiffPredicateBuilder : IBinaryDiffPredicateBuilder
|
||||
{
|
||||
private readonly BinaryDiffOptions _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly List<BinaryDiffSubject> _subjects = [];
|
||||
private readonly List<BinaryDiffFinding> _findings = [];
|
||||
private BinaryDiffInputs? _inputs;
|
||||
private readonly BinaryDiffMetadataBuilder _metadataBuilder;
|
||||
|
||||
public BinaryDiffPredicateBuilder(
|
||||
IOptions<BinaryDiffOptions>? options = null,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_options = options?.Value ?? new BinaryDiffOptions();
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_metadataBuilder = new BinaryDiffMetadataBuilder(_timeProvider, _options);
|
||||
}
|
||||
|
||||
public IBinaryDiffPredicateBuilder WithSubject(string name, string digest, BinaryDiffPlatform? platform = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentException("Subject name must be provided.", nameof(name));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(digest))
|
||||
{
|
||||
throw new ArgumentException("Subject digest must be provided.", nameof(digest));
|
||||
}
|
||||
|
||||
var digestMap = ParseDigest(digest);
|
||||
_subjects.Add(new BinaryDiffSubject
|
||||
{
|
||||
Name = name,
|
||||
Digest = digestMap,
|
||||
Platform = platform
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public IBinaryDiffPredicateBuilder WithInputs(BinaryDiffImageReference baseImage, BinaryDiffImageReference targetImage)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(baseImage);
|
||||
ArgumentNullException.ThrowIfNull(targetImage);
|
||||
|
||||
_inputs = new BinaryDiffInputs
|
||||
{
|
||||
Base = baseImage,
|
||||
Target = targetImage
|
||||
};
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public IBinaryDiffPredicateBuilder AddFinding(BinaryDiffFinding finding)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(finding);
|
||||
_findings.Add(finding);
|
||||
return this;
|
||||
}
|
||||
|
||||
public IBinaryDiffPredicateBuilder WithMetadata(Action<BinaryDiffMetadataBuilder> configure)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(configure);
|
||||
configure(_metadataBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
public BinaryDiffPredicate Build()
|
||||
{
|
||||
if (_subjects.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("At least one subject is required.");
|
||||
}
|
||||
|
||||
if (_inputs is null)
|
||||
{
|
||||
throw new InvalidOperationException("Inputs must be provided.");
|
||||
}
|
||||
|
||||
var metadata = _metadataBuilder.Build();
|
||||
var normalizedSubjects = _subjects
|
||||
.Select(NormalizeSubject)
|
||||
.OrderBy(subject => subject.Name, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
var normalizedFindings = _findings
|
||||
.Select(NormalizeFinding)
|
||||
.OrderBy(finding => finding.Path, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
return new BinaryDiffPredicate
|
||||
{
|
||||
Subjects = normalizedSubjects,
|
||||
Inputs = _inputs,
|
||||
Findings = normalizedFindings,
|
||||
Metadata = metadata
|
||||
};
|
||||
}
|
||||
|
||||
private static BinaryDiffSubject NormalizeSubject(BinaryDiffSubject subject)
|
||||
{
|
||||
var digestBuilder = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
||||
foreach (var (algorithm, value) in subject.Digest)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(algorithm) || string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
digestBuilder[algorithm.Trim().ToLowerInvariant()] = value.Trim();
|
||||
}
|
||||
|
||||
return subject with { Digest = digestBuilder.ToImmutable() };
|
||||
}
|
||||
|
||||
private static BinaryDiffFinding NormalizeFinding(BinaryDiffFinding finding)
|
||||
{
|
||||
var sectionDeltas = finding.SectionDeltas;
|
||||
if (sectionDeltas.IsDefault)
|
||||
{
|
||||
sectionDeltas = ImmutableArray<SectionDelta>.Empty;
|
||||
}
|
||||
|
||||
var normalizedDeltas = sectionDeltas
|
||||
.OrderBy(delta => delta.Section, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
return finding with
|
||||
{
|
||||
SectionDeltas = normalizedDeltas,
|
||||
BaseHashes = NormalizeHashSet(finding.BaseHashes),
|
||||
TargetHashes = NormalizeHashSet(finding.TargetHashes)
|
||||
};
|
||||
}
|
||||
|
||||
private static SectionHashSet? NormalizeHashSet(SectionHashSet? hashSet)
|
||||
{
|
||||
if (hashSet is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var sectionBuilder = ImmutableDictionary.CreateBuilder<string, SectionInfo>(StringComparer.Ordinal);
|
||||
foreach (var (name, info) in hashSet.Sections)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
sectionBuilder[name] = info;
|
||||
}
|
||||
|
||||
return hashSet with
|
||||
{
|
||||
Sections = sectionBuilder.ToImmutable()
|
||||
};
|
||||
}
|
||||
|
||||
private static ImmutableDictionary<string, string> ParseDigest(string digest)
|
||||
{
|
||||
var trimmed = digest.Trim();
|
||||
var colonIndex = trimmed.IndexOf(':');
|
||||
if (colonIndex > 0 && colonIndex < trimmed.Length - 1)
|
||||
{
|
||||
var algorithm = trimmed[..colonIndex].Trim().ToLowerInvariant();
|
||||
var value = trimmed[(colonIndex + 1)..].Trim();
|
||||
return ImmutableDictionary<string, string>.Empty
|
||||
.Add(algorithm, value);
|
||||
}
|
||||
|
||||
return ImmutableDictionary<string, string>.Empty
|
||||
.Add("sha256", trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class BinaryDiffMetadataBuilder
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly BinaryDiffOptions _options;
|
||||
private string? _toolVersion;
|
||||
private DateTimeOffset? _analysisTimestamp;
|
||||
private string? _configDigest;
|
||||
private int? _totalBinaries;
|
||||
private int? _modifiedBinaries;
|
||||
private bool _sectionsConfigured;
|
||||
private readonly List<string> _analyzedSections = [];
|
||||
|
||||
public BinaryDiffMetadataBuilder(TimeProvider timeProvider, BinaryDiffOptions options)
|
||||
{
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
public BinaryDiffMetadataBuilder WithToolVersion(string toolVersion)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(toolVersion))
|
||||
{
|
||||
throw new ArgumentException("ToolVersion must be provided.", nameof(toolVersion));
|
||||
}
|
||||
|
||||
_toolVersion = toolVersion;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BinaryDiffMetadataBuilder WithAnalysisTimestamp(DateTimeOffset analysisTimestamp)
|
||||
{
|
||||
_analysisTimestamp = analysisTimestamp;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BinaryDiffMetadataBuilder WithConfigDigest(string? configDigest)
|
||||
{
|
||||
_configDigest = configDigest;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BinaryDiffMetadataBuilder WithTotals(int totalBinaries, int modifiedBinaries)
|
||||
{
|
||||
if (totalBinaries < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(totalBinaries), "TotalBinaries must be non-negative.");
|
||||
}
|
||||
|
||||
if (modifiedBinaries < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(modifiedBinaries), "ModifiedBinaries must be non-negative.");
|
||||
}
|
||||
|
||||
_totalBinaries = totalBinaries;
|
||||
_modifiedBinaries = modifiedBinaries;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BinaryDiffMetadataBuilder WithAnalyzedSections(IEnumerable<string> sections)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(sections);
|
||||
_sectionsConfigured = true;
|
||||
_analyzedSections.Clear();
|
||||
_analyzedSections.AddRange(sections);
|
||||
return this;
|
||||
}
|
||||
|
||||
internal BinaryDiffMetadata Build()
|
||||
{
|
||||
var toolVersion = _toolVersion ?? _options.ToolVersion;
|
||||
if (string.IsNullOrWhiteSpace(toolVersion))
|
||||
{
|
||||
throw new InvalidOperationException("ToolVersion must be configured.");
|
||||
}
|
||||
|
||||
var analysisTimestamp = _analysisTimestamp ?? _timeProvider.GetUtcNow();
|
||||
var configDigest = _configDigest ?? _options.ConfigDigest;
|
||||
var totalBinaries = _totalBinaries ?? 0;
|
||||
var modifiedBinaries = _modifiedBinaries ?? 0;
|
||||
var analyzedSections = ResolveAnalyzedSections();
|
||||
|
||||
return new BinaryDiffMetadata
|
||||
{
|
||||
ToolVersion = toolVersion,
|
||||
AnalysisTimestamp = analysisTimestamp,
|
||||
ConfigDigest = configDigest,
|
||||
TotalBinaries = totalBinaries,
|
||||
ModifiedBinaries = modifiedBinaries,
|
||||
AnalyzedSections = analyzedSections
|
||||
};
|
||||
}
|
||||
|
||||
private ImmutableArray<string> ResolveAnalyzedSections()
|
||||
{
|
||||
var source = _sectionsConfigured ? _analyzedSections : _options.AnalyzedSections;
|
||||
if (source is null)
|
||||
{
|
||||
return ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
return source
|
||||
.Where(section => !string.IsNullOrWhiteSpace(section))
|
||||
.Select(section => section.Trim())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(section => section, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Attestor.StandardPredicates.BinaryDiff;
|
||||
|
||||
public interface IBinaryDiffPredicateSerializer
|
||||
{
|
||||
string Serialize(BinaryDiffPredicate predicate);
|
||||
|
||||
byte[] SerializeToBytes(BinaryDiffPredicate predicate);
|
||||
|
||||
BinaryDiffPredicate Deserialize(string json);
|
||||
|
||||
BinaryDiffPredicate Deserialize(ReadOnlySpan<byte> json);
|
||||
}
|
||||
|
||||
public sealed class BinaryDiffPredicateSerializer : IBinaryDiffPredicateSerializer
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
|
||||
};
|
||||
|
||||
public string Serialize(BinaryDiffPredicate predicate)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(predicate);
|
||||
|
||||
var normalized = Normalize(predicate);
|
||||
var json = JsonSerializer.Serialize(normalized, SerializerOptions);
|
||||
return JsonCanonicalizer.Canonicalize(json);
|
||||
}
|
||||
|
||||
public byte[] SerializeToBytes(BinaryDiffPredicate predicate)
|
||||
{
|
||||
var json = Serialize(predicate);
|
||||
return Encoding.UTF8.GetBytes(json);
|
||||
}
|
||||
|
||||
public BinaryDiffPredicate Deserialize(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
throw new ArgumentException("JSON must be provided.", nameof(json));
|
||||
}
|
||||
|
||||
var predicate = JsonSerializer.Deserialize<BinaryDiffPredicate>(json, SerializerOptions);
|
||||
return predicate ?? throw new InvalidOperationException("Failed to deserialize BinaryDiff predicate.");
|
||||
}
|
||||
|
||||
public BinaryDiffPredicate Deserialize(ReadOnlySpan<byte> json)
|
||||
{
|
||||
if (json.IsEmpty)
|
||||
{
|
||||
throw new ArgumentException("JSON must be provided.", nameof(json));
|
||||
}
|
||||
|
||||
var predicate = JsonSerializer.Deserialize<BinaryDiffPredicate>(json, SerializerOptions);
|
||||
return predicate ?? throw new InvalidOperationException("Failed to deserialize BinaryDiff predicate.");
|
||||
}
|
||||
|
||||
private static BinaryDiffPredicate Normalize(BinaryDiffPredicate predicate)
|
||||
{
|
||||
var normalizedSubjects = predicate.Subjects
|
||||
.Select(NormalizeSubject)
|
||||
.OrderBy(subject => subject.Name, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
var normalizedFindings = predicate.Findings
|
||||
.Select(NormalizeFinding)
|
||||
.OrderBy(finding => finding.Path, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
return predicate with
|
||||
{
|
||||
Subjects = normalizedSubjects,
|
||||
Findings = normalizedFindings,
|
||||
Metadata = NormalizeMetadata(predicate.Metadata)
|
||||
};
|
||||
}
|
||||
|
||||
private static BinaryDiffSubject NormalizeSubject(BinaryDiffSubject subject)
|
||||
{
|
||||
var digestBuilder = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
||||
foreach (var (algorithm, value) in subject.Digest)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(algorithm) || string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
digestBuilder[algorithm.Trim().ToLowerInvariant()] = value.Trim();
|
||||
}
|
||||
|
||||
return subject with { Digest = digestBuilder.ToImmutable() };
|
||||
}
|
||||
|
||||
private static BinaryDiffFinding NormalizeFinding(BinaryDiffFinding finding)
|
||||
{
|
||||
var sectionDeltas = finding.SectionDeltas;
|
||||
if (sectionDeltas.IsDefault)
|
||||
{
|
||||
sectionDeltas = ImmutableArray<SectionDelta>.Empty;
|
||||
}
|
||||
|
||||
var normalizedDeltas = sectionDeltas
|
||||
.OrderBy(delta => delta.Section, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
return finding with
|
||||
{
|
||||
SectionDeltas = normalizedDeltas,
|
||||
BaseHashes = NormalizeHashSet(finding.BaseHashes),
|
||||
TargetHashes = NormalizeHashSet(finding.TargetHashes)
|
||||
};
|
||||
}
|
||||
|
||||
private static SectionHashSet? NormalizeHashSet(SectionHashSet? hashSet)
|
||||
{
|
||||
if (hashSet is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var sectionBuilder = ImmutableDictionary.CreateBuilder<string, SectionInfo>(StringComparer.Ordinal);
|
||||
foreach (var (name, info) in hashSet.Sections)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
sectionBuilder[name] = info;
|
||||
}
|
||||
|
||||
return hashSet with { Sections = sectionBuilder.ToImmutable() };
|
||||
}
|
||||
|
||||
private static BinaryDiffMetadata NormalizeMetadata(BinaryDiffMetadata metadata)
|
||||
{
|
||||
var analyzedSections = metadata.AnalyzedSections;
|
||||
if (analyzedSections.IsDefault)
|
||||
{
|
||||
analyzedSections = ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
var normalizedSections = analyzedSections
|
||||
.Where(section => !string.IsNullOrWhiteSpace(section))
|
||||
.Select(section => section.Trim())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(section => section, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
return metadata with { AnalyzedSections = normalizedSections };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
using System.Text.Json;
|
||||
using Json.Schema;
|
||||
|
||||
namespace StellaOps.Attestor.StandardPredicates.BinaryDiff;
|
||||
|
||||
public sealed record BinaryDiffSchemaValidationResult
|
||||
{
|
||||
public required bool IsValid { get; init; }
|
||||
|
||||
public IReadOnlyList<string> Errors { get; init; } = Array.Empty<string>();
|
||||
|
||||
public static BinaryDiffSchemaValidationResult Valid() => new()
|
||||
{
|
||||
IsValid = true,
|
||||
Errors = Array.Empty<string>()
|
||||
};
|
||||
|
||||
public static BinaryDiffSchemaValidationResult Invalid(IReadOnlyList<string> errors) => new()
|
||||
{
|
||||
IsValid = false,
|
||||
Errors = errors
|
||||
};
|
||||
}
|
||||
|
||||
public static class BinaryDiffSchema
|
||||
{
|
||||
public const string SchemaId = "https://stellaops.io/schemas/binarydiff-v1.schema.json";
|
||||
|
||||
private static readonly Lazy<JsonSchema> CachedSchema = new(() =>
|
||||
JsonSchema.FromText(SchemaJson, new BuildOptions
|
||||
{
|
||||
SchemaRegistry = new SchemaRegistry()
|
||||
}));
|
||||
|
||||
public static BinaryDiffSchemaValidationResult Validate(JsonElement element)
|
||||
{
|
||||
var schema = CachedSchema.Value;
|
||||
var result = schema.Evaluate(element, new EvaluationOptions
|
||||
{
|
||||
OutputFormat = OutputFormat.List,
|
||||
RequireFormatValidation = true
|
||||
});
|
||||
|
||||
if (result.IsValid)
|
||||
{
|
||||
return BinaryDiffSchemaValidationResult.Valid();
|
||||
}
|
||||
|
||||
var errors = CollectErrors(result);
|
||||
return BinaryDiffSchemaValidationResult.Invalid(errors);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> CollectErrors(EvaluationResults results)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
if (results.Details is null)
|
||||
{
|
||||
return errors;
|
||||
}
|
||||
|
||||
foreach (var detail in results.Details)
|
||||
{
|
||||
if (detail.IsValid || detail.Errors is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var error in detail.Errors)
|
||||
{
|
||||
var message = error.Value ?? "Schema validation error";
|
||||
errors.Add($"{detail.InstanceLocation}: {message}");
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
private const string SchemaJson = """
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://stellaops.io/schemas/binarydiff-v1.schema.json",
|
||||
"title": "BinaryDiffV1",
|
||||
"description": "In-toto predicate for binary-level diff attestations",
|
||||
"type": "object",
|
||||
"required": ["predicateType", "subjects", "inputs", "findings", "metadata"],
|
||||
"properties": {
|
||||
"predicateType": {
|
||||
"const": "stellaops.binarydiff.v1"
|
||||
},
|
||||
"subjects": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/BinaryDiffSubject" },
|
||||
"minItems": 1
|
||||
},
|
||||
"inputs": {
|
||||
"$ref": "#/$defs/BinaryDiffInputs"
|
||||
},
|
||||
"findings": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/BinaryDiffFinding" }
|
||||
},
|
||||
"metadata": {
|
||||
"$ref": "#/$defs/BinaryDiffMetadata"
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"BinaryDiffSubject": {
|
||||
"type": "object",
|
||||
"required": ["name", "digest"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Image reference (e.g., docker://repo/app@sha256:...)"
|
||||
},
|
||||
"digest": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" }
|
||||
},
|
||||
"platform": {
|
||||
"$ref": "#/$defs/Platform"
|
||||
}
|
||||
}
|
||||
},
|
||||
"BinaryDiffInputs": {
|
||||
"type": "object",
|
||||
"required": ["base", "target"],
|
||||
"properties": {
|
||||
"base": { "$ref": "#/$defs/ImageReference" },
|
||||
"target": { "$ref": "#/$defs/ImageReference" }
|
||||
}
|
||||
},
|
||||
"ImageReference": {
|
||||
"type": "object",
|
||||
"required": ["digest"],
|
||||
"properties": {
|
||||
"reference": { "type": "string" },
|
||||
"digest": { "type": "string" },
|
||||
"manifestDigest": { "type": "string" },
|
||||
"platform": { "$ref": "#/$defs/Platform" }
|
||||
}
|
||||
},
|
||||
"Platform": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"os": { "type": "string" },
|
||||
"architecture": { "type": "string" },
|
||||
"variant": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"BinaryDiffFinding": {
|
||||
"type": "object",
|
||||
"required": ["path", "changeType", "binaryFormat"],
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "File path within the image filesystem"
|
||||
},
|
||||
"changeType": {
|
||||
"enum": ["added", "removed", "modified", "unchanged"]
|
||||
},
|
||||
"binaryFormat": {
|
||||
"enum": ["elf", "pe", "macho", "unknown"]
|
||||
},
|
||||
"layerDigest": {
|
||||
"type": "string",
|
||||
"description": "Layer that introduced this change"
|
||||
},
|
||||
"baseHashes": {
|
||||
"$ref": "#/$defs/SectionHashSet"
|
||||
},
|
||||
"targetHashes": {
|
||||
"$ref": "#/$defs/SectionHashSet"
|
||||
},
|
||||
"sectionDeltas": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/SectionDelta" }
|
||||
},
|
||||
"confidence": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1
|
||||
},
|
||||
"verdict": {
|
||||
"enum": ["patched", "vanilla", "unknown", "incompatible"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"SectionHashSet": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"buildId": { "type": "string" },
|
||||
"fileHash": { "type": "string" },
|
||||
"sections": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/SectionInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"SectionInfo": {
|
||||
"type": "object",
|
||||
"required": ["sha256", "size"],
|
||||
"properties": {
|
||||
"sha256": { "type": "string" },
|
||||
"blake3": { "type": "string" },
|
||||
"size": { "type": "integer" }
|
||||
}
|
||||
},
|
||||
"SectionDelta": {
|
||||
"type": "object",
|
||||
"required": ["section", "status"],
|
||||
"properties": {
|
||||
"section": {
|
||||
"type": "string",
|
||||
"description": "Section name (e.g., .text, .rodata)"
|
||||
},
|
||||
"status": {
|
||||
"enum": ["identical", "modified", "added", "removed"]
|
||||
},
|
||||
"baseSha256": { "type": "string" },
|
||||
"targetSha256": { "type": "string" },
|
||||
"sizeDelta": { "type": "integer" }
|
||||
}
|
||||
},
|
||||
"BinaryDiffMetadata": {
|
||||
"type": "object",
|
||||
"required": ["toolVersion", "analysisTimestamp"],
|
||||
"properties": {
|
||||
"toolVersion": { "type": "string" },
|
||||
"analysisTimestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"configDigest": { "type": "string" },
|
||||
"totalBinaries": { "type": "integer" },
|
||||
"modifiedBinaries": { "type": "integer" },
|
||||
"analyzedSections": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace StellaOps.Attestor.StandardPredicates.BinaryDiff;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddBinaryDiffPredicates(
|
||||
this IServiceCollection services,
|
||||
Action<BinaryDiffOptions>? configure = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
if (configure is not null)
|
||||
{
|
||||
services.Configure(configure);
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddOptions<BinaryDiffOptions>();
|
||||
}
|
||||
|
||||
services.TryAddSingleton<IBinaryDiffPredicateSerializer, BinaryDiffPredicateSerializer>();
|
||||
services.TryAddSingleton<IBinaryDiffPredicateBuilder, BinaryDiffPredicateBuilder>();
|
||||
services.TryAddSingleton<IBinaryDiffDsseSigner, BinaryDiffDsseSigner>();
|
||||
services.TryAddSingleton<IBinaryDiffDsseVerifier, BinaryDiffDsseVerifier>();
|
||||
services.TryAddSingleton(TimeProvider.System);
|
||||
services.TryAddSingleton<StellaOps.Attestor.Envelope.EnvelopeSignatureService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,13 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
<PackageReference Include="JsonSchema.Net" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Attestor.Envelope\StellaOps.Attestor.Envelope.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Attestor.ProofChain\StellaOps.Attestor.ProofChain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
# Attestor StandardPredicates Task Board
|
||||
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
Source of truth: `docs/implplan/SPRINT_20260113_001_002_ATTESTOR_binary_diff_predicate.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0064-M | DONE | Revalidated 2026-01-06. |
|
||||
| AUDIT-0064-T | DONE | Revalidated 2026-01-06. |
|
||||
| AUDIT-0064-A | TODO | Reopened after revalidation 2026-01-06. |
|
||||
| BINARYDIFF-SCHEMA-0001 | DONE | Define schema and C# models for BinaryDiffV1. |
|
||||
| BINARYDIFF-MODELS-0001 | DONE | Implement predicate models and enums. |
|
||||
| BINARYDIFF-BUILDER-0001 | DONE | Implement BinaryDiff predicate builder. |
|
||||
| BINARYDIFF-SERIALIZER-0001 | DONE | Implement RFC 8785 serializer and registry registration. |
|
||||
| BINARYDIFF-SIGNER-0001 | DONE | Implement DSSE signer for binary diff predicates. |
|
||||
| BINARYDIFF-VERIFIER-0001 | DONE | Implement DSSE verifier for binary diff predicates. |
|
||||
| BINARYDIFF-DI-0001 | DONE | Register BinaryDiff services and options in DI. |
|
||||
|
||||
@@ -127,7 +127,7 @@ public sealed class BuildProfileValidatorTests
|
||||
SpdxId = "https://stellaops.io/spdx/test/build/123",
|
||||
BuildType = "https://slsa.dev/provenance/v1",
|
||||
BuildId = "build-123",
|
||||
ConfigSourceDigest = ImmutableArray.Create(Spdx3Hash.Sha256("abc123"))
|
||||
ConfigSourceDigest = ImmutableArray.Create(Spdx3BuildHash.Sha256("abc123"))
|
||||
// Note: ConfigSourceUri is empty
|
||||
};
|
||||
|
||||
@@ -149,7 +149,7 @@ public sealed class BuildProfileValidatorTests
|
||||
BuildType = "https://slsa.dev/provenance/v1",
|
||||
BuildId = "build-123",
|
||||
ConfigSourceUri = ImmutableArray.Create("https://github.com/test/repo"),
|
||||
ConfigSourceDigest = ImmutableArray.Create(new Spdx3Hash
|
||||
ConfigSourceDigest = ImmutableArray.Create(new Spdx3BuildHash
|
||||
{
|
||||
Algorithm = "unknown-algo",
|
||||
HashValue = "abc123"
|
||||
@@ -183,3 +183,4 @@ public sealed class BuildProfileValidatorTests
|
||||
result.ErrorsOnly.Should().Contain(e => e.Field == "spdxId");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,22 +32,33 @@ public sealed class BuildProfileIntegrationTests
|
||||
// Arrange: Create a realistic build attestation payload
|
||||
var attestation = new BuildAttestationPayload
|
||||
{
|
||||
Type = "https://in-toto.io/Statement/v1",
|
||||
PredicateType = "https://slsa.dev/provenance/v1",
|
||||
Subject = ImmutableArray.Create(new AttestationSubject
|
||||
BuildType = "https://slsa.dev/provenance/v1",
|
||||
Builder = new BuilderInfo
|
||||
{
|
||||
Name = "pkg:oci/myapp@sha256:abc123",
|
||||
Id = "https://github.com/stellaops/ci-builder@v1"
|
||||
},
|
||||
Invocation = new BuildInvocation
|
||||
{
|
||||
ConfigSource = new BuildConfigSource
|
||||
{
|
||||
Uri = "https://github.com/stellaops/repo",
|
||||
Digest = new Dictionary<string, string>
|
||||
{
|
||||
["sha256"] = "abc123def456"
|
||||
}.ToImmutableDictionary()
|
||||
}
|
||||
},
|
||||
Materials = ImmutableArray.Create(new BuildMaterial
|
||||
{
|
||||
Uri = "pkg:oci/base-image@sha256:base123",
|
||||
Digest = new Dictionary<string, string>
|
||||
{
|
||||
["sha256"] = "abc123def456"
|
||||
["sha256"] = "base123abc"
|
||||
}.ToImmutableDictionary()
|
||||
}),
|
||||
Predicate = new BuildPredicate
|
||||
{
|
||||
BuildDefinition = new BuildDefinitionInfo
|
||||
{
|
||||
BuildType = "https://stellaops.org/build/container-scan/v1",
|
||||
ExternalParameters = new Dictionary<string, object>
|
||||
})
|
||||
};
|
||||
|
||||
// Remove the Subject and PredicateType as they don't exist in BuildAttestationPayload
|
||||
{
|
||||
["imageReference"] = "registry.io/myapp:latest"
|
||||
}.ToImmutableDictionary(),
|
||||
@@ -349,13 +360,13 @@ public sealed class BuildProfileIntegrationTests
|
||||
}
|
||||
|
||||
public Task<bool> VerifyAsync(
|
||||
byte[] payload,
|
||||
byte[] data,
|
||||
byte[] signature,
|
||||
string keyId,
|
||||
DsseVerificationKey key,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var hmac = new System.Security.Cryptography.HMACSHA256(TestKey);
|
||||
var expectedSignature = hmac.ComputeHash(payload);
|
||||
var expectedSignature = hmac.ComputeHash(data);
|
||||
|
||||
return Task.FromResult(signature.SequenceEqual(expectedSignature));
|
||||
}
|
||||
@@ -380,7 +391,7 @@ file sealed class Spdx3JsonSerializer : ISpdx3Serializer
|
||||
return JsonSerializer.SerializeToUtf8Bytes(document, Options);
|
||||
}
|
||||
|
||||
public Spdx3Document? DeserializeFromBytes(byte[] bytes)
|
||||
public Spdx3Document? Deserialize(byte[] bytes)
|
||||
{
|
||||
return JsonSerializer.Deserialize<Spdx3Document>(bytes, Options);
|
||||
}
|
||||
Reference in New Issue
Block a user