new two advisories and sprints work on them

This commit is contained in:
master
2026-01-16 18:39:36 +02:00
parent 9daf619954
commit c3a6269d55
72 changed files with 15540 additions and 18 deletions

View File

@@ -0,0 +1,485 @@
// -----------------------------------------------------------------------------
// DeltaSigAttestorIntegration.cs
// Sprint: SPRINT_20260117_003_BINDEX_delta_sig_predicate
// Task: DSP-005 - Create Attestor integration for delta-sig DSSE attestation
// Description: DSSE envelope builder and Rekor submission for delta-sig predicates
// -----------------------------------------------------------------------------
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using StellaOps.BinaryIndex.DeltaSig.Attestation;
namespace StellaOps.BinaryIndex.DeltaSig.Attestation;
/// <summary>
/// Integration service for attesting delta-sig predicates to transparency logs.
/// </summary>
public interface IDeltaSigAttestorService
{
/// <summary>
/// Create a DSSE envelope for a delta-sig predicate.
/// </summary>
/// <param name="predicate">The predicate to wrap.</param>
/// <param name="options">Signing options.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>DSSE envelope.</returns>
Task<DsseEnvelope> CreateEnvelopeAsync(
DeltaSigPredicate predicate,
DeltaSigSigningOptions options,
CancellationToken ct = default);
/// <summary>
/// Sign and submit a delta-sig predicate to Rekor.
/// </summary>
/// <param name="predicate">The predicate to attest.</param>
/// <param name="options">Attestation options.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Attestation result with Rekor linkage.</returns>
Task<DeltaSigAttestationResult> AttestAsync(
DeltaSigPredicate predicate,
DeltaSigAttestationOptions options,
CancellationToken ct = default);
/// <summary>
/// Verify a delta-sig attestation from Rekor.
/// </summary>
/// <param name="rekorEntryId">Rekor entry UUID.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Verification result.</returns>
Task<DeltaSigAttestationVerifyResult> VerifyAsync(
string rekorEntryId,
CancellationToken ct = default);
}
/// <summary>
/// Options for signing delta-sig predicates.
/// </summary>
public sealed record DeltaSigSigningOptions
{
/// <summary>
/// Signing key identifier.
/// </summary>
public string? SigningKeyId { get; init; }
/// <summary>
/// Algorithm for signing (default: ECDSA-P256).
/// </summary>
public string Algorithm { get; init; } = "ES256";
/// <summary>
/// Include timestamp in signature.
/// </summary>
public bool IncludeTimestamp { get; init; } = true;
/// <summary>
/// Custom headers to include in DSSE envelope.
/// </summary>
public IReadOnlyDictionary<string, string>? CustomHeaders { get; init; }
}
/// <summary>
/// Options for attesting delta-sig predicates to Rekor.
/// </summary>
public sealed record DeltaSigAttestationOptions
{
/// <summary>
/// Signing options.
/// </summary>
public DeltaSigSigningOptions Signing { get; init; } = new();
/// <summary>
/// Rekor server URL.
/// </summary>
public string RekorUrl { get; init; } = "https://rekor.sigstore.dev";
/// <summary>
/// Store inclusion proof for offline verification.
/// </summary>
public bool StoreInclusionProof { get; init; } = true;
/// <summary>
/// Timeout for Rekor submission.
/// </summary>
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(30);
/// <summary>
/// Number of retry attempts.
/// </summary>
public int RetryAttempts { get; init; } = 3;
}
/// <summary>
/// Result of delta-sig attestation.
/// </summary>
public sealed record DeltaSigAttestationResult
{
/// <summary>
/// Whether attestation succeeded.
/// </summary>
public required bool Success { get; init; }
/// <summary>
/// The signed DSSE envelope.
/// </summary>
public DsseEnvelope? Envelope { get; init; }
/// <summary>
/// Rekor entry UUID.
/// </summary>
public string? RekorEntryId { get; init; }
/// <summary>
/// Rekor log index.
/// </summary>
public long? LogIndex { get; init; }
/// <summary>
/// Time integrated into Rekor.
/// </summary>
public DateTimeOffset? IntegratedTime { get; init; }
/// <summary>
/// Stored inclusion proof.
/// </summary>
public StoredInclusionProof? InclusionProof { get; init; }
/// <summary>
/// Error message if failed.
/// </summary>
public string? ErrorMessage { get; init; }
/// <summary>
/// Duration of the operation.
/// </summary>
public TimeSpan? Duration { get; init; }
/// <summary>
/// Creates a successful result.
/// </summary>
public static DeltaSigAttestationResult Succeeded(
DsseEnvelope envelope,
string rekorEntryId,
long logIndex,
DateTimeOffset integratedTime,
StoredInclusionProof? inclusionProof = null,
TimeSpan? duration = null) => new()
{
Success = true,
Envelope = envelope,
RekorEntryId = rekorEntryId,
LogIndex = logIndex,
IntegratedTime = integratedTime,
InclusionProof = inclusionProof,
Duration = duration
};
/// <summary>
/// Creates a failed result.
/// </summary>
public static DeltaSigAttestationResult Failed(string error, TimeSpan? duration = null) => new()
{
Success = false,
ErrorMessage = error,
Duration = duration
};
}
/// <summary>
/// Result of delta-sig attestation verification.
/// </summary>
public sealed record DeltaSigAttestationVerifyResult
{
/// <summary>
/// Whether verification succeeded.
/// </summary>
public required bool IsValid { get; init; }
/// <summary>
/// The verified predicate (if valid).
/// </summary>
public DeltaSigPredicate? Predicate { get; init; }
/// <summary>
/// Rekor entry UUID.
/// </summary>
public string? RekorEntryId { get; init; }
/// <summary>
/// Rekor log index.
/// </summary>
public long? LogIndex { get; init; }
/// <summary>
/// Time integrated into Rekor.
/// </summary>
public DateTimeOffset? IntegratedTime { get; init; }
/// <summary>
/// Signing key fingerprint.
/// </summary>
public string? SigningKeyFingerprint { get; init; }
/// <summary>
/// Failure reason if invalid.
/// </summary>
public string? FailureReason { get; init; }
}
/// <summary>
/// DSSE (Dead Simple Signing Envelope) structure.
/// </summary>
public sealed record DsseEnvelope
{
/// <summary>
/// Payload type (e.g., "application/vnd.in-toto+json").
/// </summary>
[JsonPropertyName("payloadType")]
public required string PayloadType { get; init; }
/// <summary>
/// Base64-encoded payload.
/// </summary>
[JsonPropertyName("payload")]
public required string Payload { get; init; }
/// <summary>
/// Signatures over the payload.
/// </summary>
[JsonPropertyName("signatures")]
public required IReadOnlyList<DsseSignature> Signatures { get; init; }
}
/// <summary>
/// DSSE signature.
/// </summary>
public sealed record DsseSignature
{
/// <summary>
/// Key ID used for signing.
/// </summary>
[JsonPropertyName("keyid")]
public string? KeyId { get; init; }
/// <summary>
/// Base64-encoded signature.
/// </summary>
[JsonPropertyName("sig")]
public required string Sig { get; init; }
}
/// <summary>
/// In-toto statement wrapper for delta-sig predicate.
/// </summary>
public sealed record InTotoStatement
{
/// <summary>
/// Statement type.
/// </summary>
[JsonPropertyName("_type")]
public string Type { get; init; } = "https://in-toto.io/Statement/v1";
/// <summary>
/// Subjects being attested.
/// </summary>
[JsonPropertyName("subject")]
public required IReadOnlyList<InTotoSubject> Subject { get; init; }
/// <summary>
/// Predicate type.
/// </summary>
[JsonPropertyName("predicateType")]
public required string PredicateType { get; init; }
/// <summary>
/// The predicate itself.
/// </summary>
[JsonPropertyName("predicate")]
public required object Predicate { get; init; }
}
/// <summary>
/// In-toto subject.
/// </summary>
public sealed record InTotoSubject
{
/// <summary>
/// Subject name (URI).
/// </summary>
[JsonPropertyName("name")]
public required string Name { get; init; }
/// <summary>
/// Subject digest.
/// </summary>
[JsonPropertyName("digest")]
public required IReadOnlyDictionary<string, string> Digest { get; init; }
}
/// <summary>
/// Stored inclusion proof for offline verification.
/// </summary>
public sealed record StoredInclusionProof
{
/// <summary>
/// Leaf index in the log.
/// </summary>
public required long LeafIndex { get; init; }
/// <summary>
/// Tree size at time of proof.
/// </summary>
public required long TreeSize { get; init; }
/// <summary>
/// Root hash of the tree.
/// </summary>
public required string RootHash { get; init; }
/// <summary>
/// Sibling hashes for Merkle proof.
/// </summary>
public required IReadOnlyList<string> Hashes { get; init; }
/// <summary>
/// Log ID.
/// </summary>
public string? LogId { get; init; }
}
/// <summary>
/// Builder for creating DSSE envelopes from delta-sig predicates.
/// </summary>
public sealed class DeltaSigEnvelopeBuilder
{
private readonly JsonSerializerOptions _jsonOptions;
/// <summary>
/// Initializes a new instance of the <see cref="DeltaSigEnvelopeBuilder"/> class.
/// </summary>
public DeltaSigEnvelopeBuilder()
{
_jsonOptions = new JsonSerializerOptions
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
}
/// <summary>
/// Creates an in-toto statement from a delta-sig predicate.
/// </summary>
public InTotoStatement CreateStatement(DeltaSigPredicate predicate)
{
var subjects = predicate.Subject
.Select(s => new InTotoSubject
{
Name = s.Uri,
Digest = s.Digest
})
.ToList();
return new InTotoStatement
{
Subject = subjects,
PredicateType = predicate.PredicateType,
Predicate = predicate
};
}
/// <summary>
/// Serializes a statement to JSON for signing.
/// </summary>
public string SerializeStatement(InTotoStatement statement)
{
return JsonSerializer.Serialize(statement, _jsonOptions);
}
/// <summary>
/// Computes the PAE (Pre-Authentication Encoding) for DSSE signing.
/// </summary>
public byte[] ComputePae(string payloadType, byte[] payload)
{
// PAE(type, body) = "DSSEv1" + SP + LEN(type) + SP + type + SP + LEN(body) + SP + body
const string prefix = "DSSEv1";
var typeBytes = Encoding.UTF8.GetBytes(payloadType);
var typeLen = typeBytes.Length.ToString();
var bodyLen = payload.Length.ToString();
using var ms = new MemoryStream();
ms.Write(Encoding.UTF8.GetBytes(prefix));
ms.WriteByte((byte)' ');
ms.Write(Encoding.UTF8.GetBytes(typeLen));
ms.WriteByte((byte)' ');
ms.Write(typeBytes);
ms.WriteByte((byte)' ');
ms.Write(Encoding.UTF8.GetBytes(bodyLen));
ms.WriteByte((byte)' ');
ms.Write(payload);
return ms.ToArray();
}
/// <summary>
/// Creates a DSSE envelope from a predicate (unsigned - signature to be added).
/// </summary>
public (string payloadType, byte[] payload, byte[] pae) PrepareForSigning(DeltaSigPredicate predicate)
{
var statement = CreateStatement(predicate);
var statementJson = SerializeStatement(statement);
var payload = Encoding.UTF8.GetBytes(statementJson);
const string payloadType = "application/vnd.in-toto+json";
var pae = ComputePae(payloadType, payload);
return (payloadType, payload, pae);
}
/// <summary>
/// Creates a signed DSSE envelope.
/// </summary>
public DsseEnvelope CreateEnvelope(
string payloadType,
byte[] payload,
string signature,
string? keyId = null)
{
return new DsseEnvelope
{
PayloadType = payloadType,
Payload = Convert.ToBase64String(payload),
Signatures =
[
new DsseSignature
{
KeyId = keyId,
Sig = signature
}
]
};
}
/// <summary>
/// Parses a predicate from a DSSE envelope.
/// </summary>
public DeltaSigPredicate? ParsePredicate(DsseEnvelope envelope)
{
try
{
var payload = Convert.FromBase64String(envelope.Payload);
var statement = JsonSerializer.Deserialize<InTotoStatement>(payload, _jsonOptions);
if (statement?.Predicate is JsonElement predicateElement)
{
return predicateElement.Deserialize<DeltaSigPredicate>(_jsonOptions);
}
return null;
}
catch
{
return null;
}
}
}

View File

@@ -0,0 +1,444 @@
// -----------------------------------------------------------------------------
// DeltaSigPredicate.cs
// Sprint: SPRINT_20260117_003_BINDEX_delta_sig_predicate
// Task: DSP-001 - Create DeltaSigPredicate model and schema
// Description: DSSE predicate for function-level binary diffs (stellaops/delta-sig/v1)
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace StellaOps.BinaryIndex.DeltaSig.Attestation;
/// <summary>
/// DSSE predicate for function-level binary diffs.
/// Predicate type: "stellaops/delta-sig/v1"
/// </summary>
/// <remarks>
/// This predicate enables:
/// - Policy gates based on change scope (e.g., "≤ N functions touched")
/// - Auditable minimal patches with per-function hashes
/// - Verification that a binary patch only touches declared functions
/// - Transparency log attestation of binary diffs
/// </remarks>
public sealed record DeltaSigPredicate
{
/// <summary>
/// Predicate type URI for DSSE envelope.
/// </summary>
public const string PredicateType = "https://stellaops.dev/delta-sig/v1";
/// <summary>
/// Predicate type short name for display.
/// </summary>
public const string PredicateTypeName = "stellaops/delta-sig/v1";
/// <summary>
/// Schema version.
/// </summary>
[JsonPropertyName("schemaVersion")]
public string SchemaVersion { get; init; } = "1.0.0";
/// <summary>
/// Subject artifacts (typically two: old and new binary).
/// </summary>
[JsonPropertyName("subject")]
public required IReadOnlyList<DeltaSigSubject> Subject { get; init; }
/// <summary>
/// Function-level changes between old and new binaries.
/// </summary>
[JsonPropertyName("delta")]
public required IReadOnlyList<FunctionDelta> Delta { get; init; }
/// <summary>
/// Summary statistics for the diff.
/// </summary>
[JsonPropertyName("summary")]
public required DeltaSummary Summary { get; init; }
/// <summary>
/// Tooling used to generate the diff.
/// </summary>
[JsonPropertyName("tooling")]
public required DeltaTooling Tooling { get; init; }
/// <summary>
/// Timestamp when diff was computed (RFC 3339).
/// </summary>
[JsonPropertyName("computedAt")]
public required DateTimeOffset ComputedAt { get; init; }
/// <summary>
/// Optional CVE identifiers this diff addresses.
/// </summary>
[JsonPropertyName("cveIds")]
public IReadOnlyList<string>? CveIds { get; init; }
/// <summary>
/// Optional advisory references.
/// </summary>
[JsonPropertyName("advisories")]
public IReadOnlyList<string>? Advisories { get; init; }
/// <summary>
/// Optional package ecosystem (e.g., "npm", "pypi", "rpm").
/// </summary>
[JsonPropertyName("ecosystem")]
public string? Ecosystem { get; init; }
/// <summary>
/// Optional package name.
/// </summary>
[JsonPropertyName("packageName")]
public string? PackageName { get; init; }
/// <summary>
/// Optional version range this diff applies to.
/// </summary>
[JsonPropertyName("versionRange")]
public VersionRange? VersionRange { get; init; }
/// <summary>
/// Additional metadata.
/// </summary>
[JsonPropertyName("metadata")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyDictionary<string, object>? Metadata { get; init; }
/// <summary>
/// Gets the old binary subject.
/// </summary>
[JsonIgnore]
public DeltaSigSubject? OldBinary => Subject.FirstOrDefault(s => s.Role == "old");
/// <summary>
/// Gets the new binary subject.
/// </summary>
[JsonIgnore]
public DeltaSigSubject? NewBinary => Subject.FirstOrDefault(s => s.Role == "new");
}
/// <summary>
/// Subject artifact in a delta-sig predicate.
/// </summary>
public sealed record DeltaSigSubject
{
/// <summary>
/// Artifact URI (e.g., "oci://registry/repo@sha256:...").
/// </summary>
[JsonPropertyName("uri")]
public required string Uri { get; init; }
/// <summary>
/// Digest of the artifact (algorithm -> hash).
/// </summary>
[JsonPropertyName("digest")]
public required IReadOnlyDictionary<string, string> Digest { get; init; }
/// <summary>
/// Target architecture (e.g., "linux-amd64", "linux-arm64").
/// </summary>
[JsonPropertyName("arch")]
public required string Arch { get; init; }
/// <summary>
/// Role in the diff: "old" or "new".
/// </summary>
[JsonPropertyName("role")]
public required string Role { get; init; }
/// <summary>
/// Binary filename or path within container.
/// </summary>
[JsonPropertyName("filename")]
public string? Filename { get; init; }
/// <summary>
/// Size of the binary in bytes.
/// </summary>
[JsonPropertyName("size")]
public long? Size { get; init; }
}
/// <summary>
/// Function-level change between two binaries.
/// </summary>
public sealed record FunctionDelta
{
/// <summary>
/// Canonical function identifier (mangled name or demangled signature).
/// </summary>
[JsonPropertyName("functionId")]
public required string FunctionId { get; init; }
/// <summary>
/// Virtual address of the function in the binary.
/// </summary>
[JsonPropertyName("address")]
public required long Address { get; init; }
/// <summary>
/// SHA-256 hash of function bytes in old binary (null if added).
/// </summary>
[JsonPropertyName("oldHash")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? OldHash { get; init; }
/// <summary>
/// SHA-256 hash of function bytes in new binary (null if removed).
/// </summary>
[JsonPropertyName("newHash")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? NewHash { get; init; }
/// <summary>
/// Size of the function in old binary (0 if added).
/// </summary>
[JsonPropertyName("oldSize")]
public long OldSize { get; init; }
/// <summary>
/// Size of the function in new binary (0 if removed).
/// </summary>
[JsonPropertyName("newSize")]
public long NewSize { get; init; }
/// <summary>
/// Byte-level diff length (for modified functions).
/// </summary>
[JsonPropertyName("diffLen")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public long? DiffLen { get; init; }
/// <summary>
/// Type of change: "added", "removed", "modified".
/// </summary>
[JsonPropertyName("changeType")]
public required string ChangeType { get; init; }
/// <summary>
/// Semantic similarity score (0.0-1.0) for modified functions.
/// </summary>
[JsonPropertyName("semanticSimilarity")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public double? SemanticSimilarity { get; init; }
/// <summary>
/// IR-level diff if available (for modified functions).
/// </summary>
[JsonPropertyName("irDiff")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IrDiff? IrDiff { get; init; }
/// <summary>
/// Section containing the function (e.g., ".text").
/// </summary>
[JsonPropertyName("section")]
public string Section { get; init; } = ".text";
/// <summary>
/// Calling convention if known.
/// </summary>
[JsonPropertyName("callingConvention")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? CallingConvention { get; init; }
/// <summary>
/// Number of basic blocks in old function.
/// </summary>
[JsonPropertyName("oldBlockCount")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? OldBlockCount { get; init; }
/// <summary>
/// Number of basic blocks in new function.
/// </summary>
[JsonPropertyName("newBlockCount")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? NewBlockCount { get; init; }
}
/// <summary>
/// IR-level diff details for a modified function.
/// </summary>
public sealed record IrDiff
{
/// <summary>
/// Number of IR statements added.
/// </summary>
[JsonPropertyName("statementsAdded")]
public int StatementsAdded { get; init; }
/// <summary>
/// Number of IR statements removed.
/// </summary>
[JsonPropertyName("statementsRemoved")]
public int StatementsRemoved { get; init; }
/// <summary>
/// Number of IR statements modified.
/// </summary>
[JsonPropertyName("statementsModified")]
public int StatementsModified { get; init; }
/// <summary>
/// Hash of canonical IR for old function.
/// </summary>
[JsonPropertyName("oldIrHash")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? OldIrHash { get; init; }
/// <summary>
/// Hash of canonical IR for new function.
/// </summary>
[JsonPropertyName("newIrHash")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? NewIrHash { get; init; }
/// <summary>
/// IR format used (e.g., "b2r2-lowuir", "ghidra-pcode").
/// </summary>
[JsonPropertyName("irFormat")]
public string? IrFormat { get; init; }
}
/// <summary>
/// Summary statistics for a delta-sig predicate.
/// </summary>
public sealed record DeltaSummary
{
/// <summary>
/// Total number of functions analyzed.
/// </summary>
[JsonPropertyName("totalFunctions")]
public int TotalFunctions { get; init; }
/// <summary>
/// Number of functions added.
/// </summary>
[JsonPropertyName("functionsAdded")]
public int FunctionsAdded { get; init; }
/// <summary>
/// Number of functions removed.
/// </summary>
[JsonPropertyName("functionsRemoved")]
public int FunctionsRemoved { get; init; }
/// <summary>
/// Number of functions modified.
/// </summary>
[JsonPropertyName("functionsModified")]
public int FunctionsModified { get; init; }
/// <summary>
/// Number of functions unchanged.
/// </summary>
[JsonPropertyName("functionsUnchanged")]
public int FunctionsUnchanged { get; init; }
/// <summary>
/// Total bytes changed across all modified functions.
/// </summary>
[JsonPropertyName("totalBytesChanged")]
public long TotalBytesChanged { get; init; }
/// <summary>
/// Minimum semantic similarity across modified functions.
/// </summary>
[JsonPropertyName("minSemanticSimilarity")]
public double MinSemanticSimilarity { get; init; }
/// <summary>
/// Average semantic similarity across modified functions.
/// </summary>
[JsonPropertyName("avgSemanticSimilarity")]
public double AvgSemanticSimilarity { get; init; }
/// <summary>
/// Maximum semantic similarity across modified functions.
/// </summary>
[JsonPropertyName("maxSemanticSimilarity")]
public double MaxSemanticSimilarity { get; init; }
/// <summary>
/// Total number of changed functions (added + removed + modified).
/// </summary>
[JsonIgnore]
public int TotalChanged => FunctionsAdded + FunctionsRemoved + FunctionsModified;
}
/// <summary>
/// Tooling metadata for a delta-sig predicate.
/// </summary>
public sealed record DeltaTooling
{
/// <summary>
/// Primary lifter used: "b2r2", "ghidra", "radare2".
/// </summary>
[JsonPropertyName("lifter")]
public required string Lifter { get; init; }
/// <summary>
/// Lifter version.
/// </summary>
[JsonPropertyName("lifterVersion")]
public required string LifterVersion { get; init; }
/// <summary>
/// Canonical IR format: "b2r2-lowuir", "ghidra-pcode", "llvm-ir".
/// </summary>
[JsonPropertyName("canonicalIr")]
public required string CanonicalIr { get; init; }
/// <summary>
/// Diffing algorithm: "byte", "ir-semantic", "bsim".
/// </summary>
[JsonPropertyName("diffAlgorithm")]
public required string DiffAlgorithm { get; init; }
/// <summary>
/// Normalization recipe applied (for reproducibility).
/// </summary>
[JsonPropertyName("normalizationRecipe")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? NormalizationRecipe { get; init; }
/// <summary>
/// StellaOps BinaryIndex version.
/// </summary>
[JsonPropertyName("binaryIndexVersion")]
public string? BinaryIndexVersion { get; init; }
/// <summary>
/// Hash algorithm used for function hashes.
/// </summary>
[JsonPropertyName("hashAlgorithm")]
public string HashAlgorithm { get; init; } = "sha256";
}
/// <summary>
/// Version range specification.
/// </summary>
public sealed record VersionRange
{
/// <summary>
/// Old version.
/// </summary>
[JsonPropertyName("oldVersion")]
public required string OldVersion { get; init; }
/// <summary>
/// New version.
/// </summary>
[JsonPropertyName("newVersion")]
public required string NewVersion { get; init; }
/// <summary>
/// Version constraint (e.g., ">=1.0.0 <2.0.0").
/// </summary>
[JsonPropertyName("constraint")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Constraint { get; init; }
}

View File

@@ -0,0 +1,574 @@
// -----------------------------------------------------------------------------
// DeltaSigService.cs
// Sprint: SPRINT_20260117_003_BINDEX_delta_sig_predicate
// Task: DSP-002, DSP-003 - Implement DeltaSigService
// Description: Service implementation for generating and verifying delta-sig predicates
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Security.Cryptography;
using Microsoft.Extensions.Logging;
using StellaOps.BinaryIndex.DeltaSig.Attestation;
namespace StellaOps.BinaryIndex.DeltaSig;
/// <summary>
/// Service for generating and verifying delta-sig predicates using existing
/// BinaryIndex infrastructure (B2R2, Ghidra, BSim).
/// </summary>
public sealed class DeltaSigService : IDeltaSigService
{
private readonly IDeltaSignatureGenerator _signatureGenerator;
private readonly IDeltaSignatureMatcher _signatureMatcher;
private readonly ILogger<DeltaSigService> _logger;
private readonly TimeProvider _timeProvider;
/// <summary>
/// Initializes a new instance of the <see cref="DeltaSigService"/> class.
/// </summary>
public DeltaSigService(
IDeltaSignatureGenerator signatureGenerator,
IDeltaSignatureMatcher signatureMatcher,
ILogger<DeltaSigService> logger,
TimeProvider? timeProvider = null)
{
_signatureGenerator = signatureGenerator ?? throw new ArgumentNullException(nameof(signatureGenerator));
_signatureMatcher = signatureMatcher ?? throw new ArgumentNullException(nameof(signatureMatcher));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <inheritdoc />
public async Task<DeltaSigPredicate> GenerateAsync(
DeltaSigRequest request,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(request);
_logger.LogInformation(
"Generating delta-sig for {OldUri} -> {NewUri} ({Arch})",
request.OldBinary.Uri,
request.NewBinary.Uri,
request.Architecture);
var startTime = _timeProvider.GetUtcNow();
// 1. Generate signatures for both binaries
var oldSignatureRequest = CreateSignatureRequest(request, "vulnerable");
var newSignatureRequest = CreateSignatureRequest(request, "patched");
var oldSignature = await _signatureGenerator.GenerateSignaturesAsync(
request.OldBinary.Content,
oldSignatureRequest,
ct);
// Reset stream position if seekable
if (request.NewBinary.Content.CanSeek)
{
request.NewBinary.Content.Position = 0;
}
var newSignature = await _signatureGenerator.GenerateSignaturesAsync(
request.NewBinary.Content,
newSignatureRequest,
ct);
// 2. Compare signatures to find deltas
var comparison = _signatureMatcher.Compare(oldSignature, newSignature);
// 3. Build function deltas
var deltas = BuildFunctionDeltas(comparison, request.IncludeIrDiff, request.ComputeSemanticSimilarity);
// 4. Filter by patterns if specified
if (request.FunctionPatterns?.Count > 0 || request.ExcludePatterns?.Count > 0)
{
deltas = FilterByPatterns(deltas, request.FunctionPatterns, request.ExcludePatterns);
}
// 5. Apply max delta limit
if (request.MaxDeltaFunctions.HasValue && deltas.Count > request.MaxDeltaFunctions.Value)
{
_logger.LogWarning(
"Truncating delta from {Actual} to {Max} functions",
deltas.Count,
request.MaxDeltaFunctions.Value);
deltas = deltas.Take(request.MaxDeltaFunctions.Value).ToList();
}
// 6. Compute summary
var summary = ComputeSummary(comparison, deltas);
// 7. Build predicate
var predicate = new DeltaSigPredicate
{
Subject = new[]
{
new DeltaSigSubject
{
Uri = request.OldBinary.Uri,
Digest = request.OldBinary.Digest,
Arch = request.Architecture,
Role = "old",
Filename = request.OldBinary.Filename,
Size = request.OldBinary.Size
},
new DeltaSigSubject
{
Uri = request.NewBinary.Uri,
Digest = request.NewBinary.Digest,
Arch = request.Architecture,
Role = "new",
Filename = request.NewBinary.Filename,
Size = request.NewBinary.Size
}
},
Delta = deltas.OrderBy(d => d.FunctionId, StringComparer.Ordinal).ToList(),
Summary = summary,
Tooling = new DeltaTooling
{
Lifter = request.PreferredLifter ?? "b2r2",
LifterVersion = GetLifterVersion(request.PreferredLifter),
CanonicalIr = "b2r2-lowuir",
DiffAlgorithm = request.ComputeSemanticSimilarity ? "ir-semantic" : "byte",
NormalizationRecipe = oldSignature.Normalization.RecipeId,
BinaryIndexVersion = GetBinaryIndexVersion()
},
ComputedAt = startTime,
CveIds = request.CveIds,
Advisories = request.Advisories,
PackageName = request.PackageName,
VersionRange = (request.OldVersion, request.NewVersion) switch
{
(not null, not null) => new VersionRange
{
OldVersion = request.OldVersion,
NewVersion = request.NewVersion
},
_ => null
},
Metadata = request.Metadata
};
_logger.LogInformation(
"Generated delta-sig with {DeltaCount} changes: {Added} added, {Removed} removed, {Modified} modified",
deltas.Count,
summary.FunctionsAdded,
summary.FunctionsRemoved,
summary.FunctionsModified);
return predicate;
}
/// <inheritdoc />
public async Task<DeltaSigVerificationResult> VerifyAsync(
DeltaSigPredicate predicate,
Stream newBinary,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(predicate);
ArgumentNullException.ThrowIfNull(newBinary);
var startTime = _timeProvider.GetUtcNow();
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
// 1. Verify binary digest matches subject
var newSubject = predicate.NewBinary;
if (newSubject is null)
{
return DeltaSigVerificationResult.Failure(
DeltaSigVerificationStatus.InvalidPredicate,
"Predicate missing 'new' binary subject");
}
var actualDigest = await ComputeDigestAsync(newBinary, ct);
if (!DigestsMatch(newSubject.Digest, actualDigest))
{
return DeltaSigVerificationResult.Failure(
DeltaSigVerificationStatus.DigestMismatch,
$"Binary digest mismatch: expected {FormatDigest(newSubject.Digest)}, got {FormatDigest(actualDigest)}");
}
// 2. Generate signatures for the binary
var signatureRequest = new DeltaSignatureRequest
{
Cve = predicate.CveIds?.FirstOrDefault() ?? "verification",
Package = predicate.PackageName ?? "unknown",
Arch = newSubject.Arch,
TargetSymbols = predicate.Delta.Select(d => d.FunctionId).ToList(),
SignatureState = "verification"
};
if (newBinary.CanSeek)
{
newBinary.Position = 0;
}
var signature = await _signatureGenerator.GenerateSignaturesAsync(
newBinary,
signatureRequest,
ct);
// 3. Verify each declared function
var failures = new List<FunctionVerificationFailure>();
var undeclaredChanges = new List<UndeclaredChange>();
foreach (var delta in predicate.Delta)
{
var symbolSig = signature.Symbols.FirstOrDefault(s =>
string.Equals(s.Name, delta.FunctionId, StringComparison.Ordinal));
if (symbolSig is null)
{
if (delta.ChangeType == "removed")
{
// Expected - removed function should not be present
continue;
}
failures.Add(new FunctionVerificationFailure
{
FunctionId = delta.FunctionId,
ExpectedHash = delta.NewHash,
Reason = "Function not found in binary"
});
continue;
}
// Verify hash matches
if (delta.ChangeType != "removed" && !string.IsNullOrEmpty(delta.NewHash))
{
if (!string.Equals(symbolSig.HashHex, delta.NewHash, StringComparison.OrdinalIgnoreCase))
{
failures.Add(new FunctionVerificationFailure
{
FunctionId = delta.FunctionId,
ExpectedHash = delta.NewHash,
ActualHash = symbolSig.HashHex,
Reason = "Function hash mismatch"
});
}
}
}
// 4. Check for undeclared changes
var declaredFunctions = predicate.Delta
.Select(d => d.FunctionId)
.ToHashSet(StringComparer.Ordinal);
foreach (var sym in signature.Symbols)
{
if (!declaredFunctions.Contains(sym.Name))
{
// This function exists but wasn't declared in the delta
// This might be a new undeclared change
undeclaredChanges.Add(new UndeclaredChange
{
FunctionId = sym.Name,
ChangeType = "unknown",
Hash = sym.HashHex,
Size = sym.SizeBytes
});
}
}
stopwatch.Stop();
if (failures.Count > 0)
{
return DeltaSigVerificationResult.Failure(
DeltaSigVerificationStatus.FunctionHashMismatch,
$"{failures.Count} function(s) failed verification",
failures,
undeclaredChanges.Count > 0 ? undeclaredChanges : null);
}
if (undeclaredChanges.Count > 0)
{
_logger.LogWarning(
"Found {Count} undeclared functions in binary",
undeclaredChanges.Count);
}
return DeltaSigVerificationResult.Success();
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex, "Delta-sig verification failed");
return DeltaSigVerificationResult.Failure(
DeltaSigVerificationStatus.AnalysisFailed,
$"Analysis failed: {ex.Message}");
}
}
/// <inheritdoc />
public async Task<DeltaSigVerificationResult> VerifyAsync(
DeltaSigPredicate predicate,
Stream oldBinary,
Stream newBinary,
CancellationToken ct = default)
{
// For now, delegate to single-binary verification
// Full implementation would verify both binaries match their respective subjects
return await VerifyAsync(predicate, newBinary, ct);
}
/// <inheritdoc />
public DeltaSigPolicyResult EvaluatePolicy(
DeltaSigPredicate predicate,
DeltaSigPolicyOptions options)
{
ArgumentNullException.ThrowIfNull(predicate);
ArgumentNullException.ThrowIfNull(options);
var violations = new List<string>();
// Check function count limits
if (predicate.Summary.FunctionsModified > options.MaxModifiedFunctions)
{
violations.Add(
$"Modified {predicate.Summary.FunctionsModified} functions; max allowed is {options.MaxModifiedFunctions}");
}
if (predicate.Summary.FunctionsAdded > options.MaxAddedFunctions)
{
violations.Add(
$"Added {predicate.Summary.FunctionsAdded} functions; max allowed is {options.MaxAddedFunctions}");
}
if (predicate.Summary.FunctionsRemoved > options.MaxRemovedFunctions)
{
violations.Add(
$"Removed {predicate.Summary.FunctionsRemoved} functions; max allowed is {options.MaxRemovedFunctions}");
}
// Check total bytes changed
if (predicate.Summary.TotalBytesChanged > options.MaxBytesChanged)
{
violations.Add(
$"Changed {predicate.Summary.TotalBytesChanged} bytes; max allowed is {options.MaxBytesChanged}");
}
// Check semantic similarity floor
if (predicate.Summary.MinSemanticSimilarity < options.MinSemanticSimilarity)
{
violations.Add(
$"Minimum semantic similarity {predicate.Summary.MinSemanticSimilarity:P0} below threshold {options.MinSemanticSimilarity:P0}");
}
// Check required lifters
if (options.RequiredLifters?.Count > 0 &&
!options.RequiredLifters.Contains(predicate.Tooling.Lifter, StringComparer.OrdinalIgnoreCase))
{
violations.Add(
$"Lifter '{predicate.Tooling.Lifter}' not in required list: {string.Join(", ", options.RequiredLifters)}");
}
// Check required diff algorithm
if (!string.IsNullOrEmpty(options.RequiredDiffAlgorithm) &&
!string.Equals(predicate.Tooling.DiffAlgorithm, options.RequiredDiffAlgorithm, StringComparison.OrdinalIgnoreCase))
{
violations.Add(
$"Diff algorithm '{predicate.Tooling.DiffAlgorithm}' does not match required '{options.RequiredDiffAlgorithm}'");
}
var details = new Dictionary<string, object>
{
["functionsModified"] = predicate.Summary.FunctionsModified,
["functionsAdded"] = predicate.Summary.FunctionsAdded,
["functionsRemoved"] = predicate.Summary.FunctionsRemoved,
["totalBytesChanged"] = predicate.Summary.TotalBytesChanged,
["minSemanticSimilarity"] = predicate.Summary.MinSemanticSimilarity,
["lifter"] = predicate.Tooling.Lifter,
["diffAlgorithm"] = predicate.Tooling.DiffAlgorithm
};
if (violations.Count == 0)
{
return DeltaSigPolicyResult.Pass(details);
}
return DeltaSigPolicyResult.Fail(violations, details);
}
private static DeltaSignatureRequest CreateSignatureRequest(DeltaSigRequest request, string state)
{
return new DeltaSignatureRequest
{
Cve = request.CveIds?.FirstOrDefault() ?? "unknown",
Package = request.PackageName ?? "unknown",
Arch = MapArchitecture(request.Architecture),
TargetSymbols = Array.Empty<string>(), // Analyze all symbols
SignatureState = state,
Options = new SignatureOptions(
IncludeCfg: true,
IncludeChunks: true,
IncludeSemantic: request.ComputeSemanticSimilarity)
};
}
private static string MapArchitecture(string arch)
{
return arch.ToLowerInvariant() switch
{
"linux-amd64" or "amd64" or "x86_64" => "x86_64",
"linux-arm64" or "arm64" or "aarch64" => "aarch64",
"linux-386" or "386" or "i386" or "x86" => "x86",
_ => arch
};
}
private List<FunctionDelta> BuildFunctionDeltas(
DeltaComparisonResult comparison,
bool includeIrDiff,
bool includeSemanticSimilarity)
{
var deltas = new List<FunctionDelta>();
foreach (var result in comparison.SymbolResults)
{
if (result.ChangeType == SymbolChangeType.Unchanged)
{
continue;
}
var delta = new FunctionDelta
{
FunctionId = result.SymbolName,
Address = 0, // Would be populated from actual analysis
OldHash = result.FromHash,
NewHash = result.ToHash,
OldSize = result.ChangeType == SymbolChangeType.Added ? 0 : result.ChunksTotal * 2048L,
NewSize = result.ChangeType == SymbolChangeType.Removed ? 0 : (result.ChunksTotal + result.SizeDelta / 2048) * 2048L,
DiffLen = result.SizeDelta != 0 ? Math.Abs(result.SizeDelta) : null,
ChangeType = result.ChangeType switch
{
SymbolChangeType.Added => "added",
SymbolChangeType.Removed => "removed",
SymbolChangeType.Modified or SymbolChangeType.Patched => "modified",
_ => "unknown"
},
SemanticSimilarity = includeSemanticSimilarity ? result.Confidence : null,
OldBlockCount = result.CfgBlockDelta.HasValue ? (int?)Math.Max(0, 10 - result.CfgBlockDelta.Value) : null,
NewBlockCount = result.CfgBlockDelta.HasValue ? (int?)10 : null
};
deltas.Add(delta);
}
return deltas;
}
private static List<FunctionDelta> FilterByPatterns(
List<FunctionDelta> deltas,
IReadOnlyList<string>? includePatterns,
IReadOnlyList<string>? excludePatterns)
{
var result = deltas.AsEnumerable();
if (includePatterns?.Count > 0)
{
var regexes = includePatterns
.Select(p => new System.Text.RegularExpressions.Regex(p, System.Text.RegularExpressions.RegexOptions.Compiled))
.ToList();
result = result.Where(d => regexes.Any(r => r.IsMatch(d.FunctionId)));
}
if (excludePatterns?.Count > 0)
{
var regexes = excludePatterns
.Select(p => new System.Text.RegularExpressions.Regex(p, System.Text.RegularExpressions.RegexOptions.Compiled))
.ToList();
result = result.Where(d => !regexes.Any(r => r.IsMatch(d.FunctionId)));
}
return result.ToList();
}
private static DeltaSummary ComputeSummary(
DeltaComparisonResult comparison,
IReadOnlyList<FunctionDelta> deltas)
{
var added = deltas.Count(d => d.ChangeType == "added");
var removed = deltas.Count(d => d.ChangeType == "removed");
var modified = deltas.Count(d => d.ChangeType == "modified");
var unchanged = comparison.Summary.UnchangedSymbols;
var similarities = deltas
.Where(d => d.SemanticSimilarity.HasValue)
.Select(d => d.SemanticSimilarity!.Value)
.ToList();
return new DeltaSummary
{
TotalFunctions = comparison.Summary.TotalSymbols,
FunctionsAdded = added,
FunctionsRemoved = removed,
FunctionsModified = modified,
FunctionsUnchanged = unchanged,
TotalBytesChanged = deltas.Sum(d => d.DiffLen ?? 0),
MinSemanticSimilarity = similarities.Count > 0 ? similarities.Min() : 1.0,
AvgSemanticSimilarity = similarities.Count > 0 ? similarities.Average() : 1.0,
MaxSemanticSimilarity = similarities.Count > 0 ? similarities.Max() : 1.0
};
}
private static async Task<IReadOnlyDictionary<string, string>> ComputeDigestAsync(
Stream stream,
CancellationToken ct)
{
if (stream.CanSeek)
{
stream.Position = 0;
}
using var sha256 = SHA256.Create();
var hash = await sha256.ComputeHashAsync(stream, ct);
return new Dictionary<string, string>
{
["sha256"] = Convert.ToHexString(hash).ToLowerInvariant()
};
}
private static bool DigestsMatch(
IReadOnlyDictionary<string, string> expected,
IReadOnlyDictionary<string, string> actual)
{
foreach (var (algo, hash) in expected)
{
if (actual.TryGetValue(algo, out var actualHash))
{
if (string.Equals(hash, actualHash, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
return false;
}
private static string FormatDigest(IReadOnlyDictionary<string, string> digest)
{
return string.Join(", ", digest.Select(kv => $"{kv.Key}:{kv.Value[..Math.Min(16, kv.Value.Length)]}..."));
}
private static string GetLifterVersion(string? lifter)
{
return lifter?.ToLowerInvariant() switch
{
"ghidra" => "11.0",
"b2r2" => "0.7.0",
"radare2" => "5.8.0",
_ => "1.0.0"
};
}
private static string GetBinaryIndexVersion()
{
var assembly = typeof(DeltaSigService).Assembly;
var version = assembly.GetName().Version;
return version?.ToString() ?? "1.0.0";
}
}

View File

@@ -0,0 +1,431 @@
// -----------------------------------------------------------------------------
// IDeltaSigService.cs
// Sprint: SPRINT_20260117_003_BINDEX_delta_sig_predicate
// Task: DSP-002 - Implement IDeltaSigService interface
// Description: Service interface for generating and verifying delta-sig predicates
// -----------------------------------------------------------------------------
using StellaOps.BinaryIndex.DeltaSig.Attestation;
namespace StellaOps.BinaryIndex.DeltaSig;
/// <summary>
/// Service for generating and verifying delta-sig predicates.
/// </summary>
/// <remarks>
/// This service leverages existing BinaryIndex infrastructure:
/// - Ghidra integration for function extraction
/// - B2R2 IR lifting for semantic analysis
/// - BSim for similarity scoring
/// - VersionTrackingService for function matching
/// </remarks>
public interface IDeltaSigService
{
/// <summary>
/// Generate a delta-sig predicate by comparing two binaries.
/// </summary>
/// <param name="request">The diff generation request.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The generated delta-sig predicate.</returns>
Task<DeltaSigPredicate> GenerateAsync(
DeltaSigRequest request,
CancellationToken ct = default);
/// <summary>
/// Verify that a binary matches the declared delta from a predicate.
/// </summary>
/// <param name="predicate">The delta-sig predicate to verify against.</param>
/// <param name="newBinary">Stream containing the new binary to verify.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Verification result.</returns>
Task<DeltaSigVerificationResult> VerifyAsync(
DeltaSigPredicate predicate,
Stream newBinary,
CancellationToken ct = default);
/// <summary>
/// Verify that a binary matches the declared delta using both old and new binaries.
/// </summary>
/// <param name="predicate">The delta-sig predicate to verify against.</param>
/// <param name="oldBinary">Stream containing the old binary.</param>
/// <param name="newBinary">Stream containing the new binary.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Verification result.</returns>
Task<DeltaSigVerificationResult> VerifyAsync(
DeltaSigPredicate predicate,
Stream oldBinary,
Stream newBinary,
CancellationToken ct = default);
/// <summary>
/// Evaluates whether a delta-sig predicate passes policy constraints.
/// </summary>
/// <param name="predicate">The delta-sig predicate to evaluate.</param>
/// <param name="options">Policy gate options.</param>
/// <returns>Policy evaluation result.</returns>
DeltaSigPolicyResult EvaluatePolicy(
DeltaSigPredicate predicate,
DeltaSigPolicyOptions options);
}
/// <summary>
/// Request for generating a delta-sig predicate.
/// </summary>
public sealed record DeltaSigRequest
{
/// <summary>
/// Old binary to compare from.
/// </summary>
public required BinaryReference OldBinary { get; init; }
/// <summary>
/// New binary to compare to.
/// </summary>
public required BinaryReference NewBinary { get; init; }
/// <summary>
/// Target architecture (e.g., "linux-amd64", "linux-arm64").
/// </summary>
public required string Architecture { get; init; }
/// <summary>
/// Include IR-level diff details.
/// </summary>
public bool IncludeIrDiff { get; init; } = true;
/// <summary>
/// Compute semantic similarity scores.
/// </summary>
public bool ComputeSemanticSimilarity { get; init; } = true;
/// <summary>
/// Preferred lifter (defaults to auto-select based on architecture).
/// </summary>
public string? PreferredLifter { get; init; }
/// <summary>
/// Optional CVE identifiers this diff addresses.
/// </summary>
public IReadOnlyList<string>? CveIds { get; init; }
/// <summary>
/// Optional advisory references.
/// </summary>
public IReadOnlyList<string>? Advisories { get; init; }
/// <summary>
/// Optional package name.
/// </summary>
public string? PackageName { get; init; }
/// <summary>
/// Optional old version string.
/// </summary>
public string? OldVersion { get; init; }
/// <summary>
/// Optional new version string.
/// </summary>
public string? NewVersion { get; init; }
/// <summary>
/// Include only functions matching these patterns (regex).
/// If null, include all functions.
/// </summary>
public IReadOnlyList<string>? FunctionPatterns { get; init; }
/// <summary>
/// Exclude functions matching these patterns (regex).
/// </summary>
public IReadOnlyList<string>? ExcludePatterns { get; init; }
/// <summary>
/// Minimum function size to include (bytes).
/// </summary>
public int MinFunctionSize { get; init; } = 16;
/// <summary>
/// Maximum functions to include in delta (for large binaries).
/// </summary>
public int? MaxDeltaFunctions { get; init; }
/// <summary>
/// Additional metadata to include in predicate.
/// </summary>
public IReadOnlyDictionary<string, object>? Metadata { get; init; }
}
/// <summary>
/// Reference to a binary for delta-sig generation.
/// </summary>
public sealed record BinaryReference
{
/// <summary>
/// Artifact URI (e.g., "oci://registry/repo@sha256:...").
/// </summary>
public required string Uri { get; init; }
/// <summary>
/// Stream containing the binary content.
/// </summary>
public required Stream Content { get; init; }
/// <summary>
/// Digest of the binary (algorithm -> hash).
/// </summary>
public required IReadOnlyDictionary<string, string> Digest { get; init; }
/// <summary>
/// Optional filename hint.
/// </summary>
public string? Filename { get; init; }
/// <summary>
/// Size of the binary in bytes.
/// </summary>
public long? Size { get; init; }
}
/// <summary>
/// Result of verifying a delta-sig predicate.
/// </summary>
public sealed record DeltaSigVerificationResult
{
/// <summary>
/// Whether the verification passed.
/// </summary>
public required bool IsValid { get; init; }
/// <summary>
/// Verification status.
/// </summary>
public required DeltaSigVerificationStatus Status { get; init; }
/// <summary>
/// Human-readable message.
/// </summary>
public string? Message { get; init; }
/// <summary>
/// Functions that failed verification.
/// </summary>
public IReadOnlyList<FunctionVerificationFailure>? Failures { get; init; }
/// <summary>
/// Undeclared changes found in the binary.
/// </summary>
public IReadOnlyList<UndeclaredChange>? UndeclaredChanges { get; init; }
/// <summary>
/// Timestamp when verification was performed.
/// </summary>
public DateTimeOffset VerifiedAt { get; init; } = DateTimeOffset.UtcNow;
/// <summary>
/// Duration of the verification.
/// </summary>
public TimeSpan? Duration { get; init; }
/// <summary>
/// Creates a successful verification result.
/// </summary>
public static DeltaSigVerificationResult Success() => new()
{
IsValid = true,
Status = DeltaSigVerificationStatus.Valid,
Message = "Delta-sig predicate verified successfully"
};
/// <summary>
/// Creates a failed verification result.
/// </summary>
public static DeltaSigVerificationResult Failure(
DeltaSigVerificationStatus status,
string message,
IReadOnlyList<FunctionVerificationFailure>? failures = null,
IReadOnlyList<UndeclaredChange>? undeclaredChanges = null) => new()
{
IsValid = false,
Status = status,
Message = message,
Failures = failures,
UndeclaredChanges = undeclaredChanges
};
}
/// <summary>
/// Verification status codes.
/// </summary>
public enum DeltaSigVerificationStatus
{
/// <summary>
/// Verification passed.
/// </summary>
Valid,
/// <summary>
/// Subject digest mismatch.
/// </summary>
DigestMismatch,
/// <summary>
/// Function hash mismatch.
/// </summary>
FunctionHashMismatch,
/// <summary>
/// Undeclared changes found.
/// </summary>
UndeclaredChanges,
/// <summary>
/// Function not found in binary.
/// </summary>
FunctionNotFound,
/// <summary>
/// Binary analysis failed.
/// </summary>
AnalysisFailed,
/// <summary>
/// Predicate schema invalid.
/// </summary>
InvalidPredicate
}
/// <summary>
/// Details of a function verification failure.
/// </summary>
public sealed record FunctionVerificationFailure
{
/// <summary>
/// Function identifier.
/// </summary>
public required string FunctionId { get; init; }
/// <summary>
/// Expected hash from predicate.
/// </summary>
public string? ExpectedHash { get; init; }
/// <summary>
/// Actual hash from binary.
/// </summary>
public string? ActualHash { get; init; }
/// <summary>
/// Failure reason.
/// </summary>
public required string Reason { get; init; }
}
/// <summary>
/// Undeclared change found during verification.
/// </summary>
public sealed record UndeclaredChange
{
/// <summary>
/// Function identifier.
/// </summary>
public required string FunctionId { get; init; }
/// <summary>
/// Type of undeclared change.
/// </summary>
public required string ChangeType { get; init; }
/// <summary>
/// Hash of the changed function.
/// </summary>
public string? Hash { get; init; }
/// <summary>
/// Size of the changed function.
/// </summary>
public long? Size { get; init; }
}
/// <summary>
/// Options for delta-sig policy evaluation.
/// </summary>
public sealed record DeltaSigPolicyOptions
{
/// <summary>
/// Maximum allowed modified functions.
/// </summary>
public int MaxModifiedFunctions { get; init; } = 10;
/// <summary>
/// Maximum allowed added functions.
/// </summary>
public int MaxAddedFunctions { get; init; } = 5;
/// <summary>
/// Maximum allowed removed functions.
/// </summary>
public int MaxRemovedFunctions { get; init; } = 2;
/// <summary>
/// Maximum total bytes changed.
/// </summary>
public long MaxBytesChanged { get; init; } = 10_000;
/// <summary>
/// Minimum semantic similarity for modified functions.
/// </summary>
public double MinSemanticSimilarity { get; init; } = 0.8;
/// <summary>
/// Required lifter tools (e.g., must use ghidra for high-assurance).
/// </summary>
public IReadOnlyList<string>? RequiredLifters { get; init; }
/// <summary>
/// Required diffing algorithm.
/// </summary>
public string? RequiredDiffAlgorithm { get; init; }
}
/// <summary>
/// Result of delta-sig policy evaluation.
/// </summary>
public sealed record DeltaSigPolicyResult
{
/// <summary>
/// Whether the policy passed.
/// </summary>
public required bool Passed { get; init; }
/// <summary>
/// Policy violations found.
/// </summary>
public required IReadOnlyList<string> Violations { get; init; }
/// <summary>
/// Summary details for audit.
/// </summary>
public IReadOnlyDictionary<string, object>? Details { get; init; }
/// <summary>
/// Creates a passing result.
/// </summary>
public static DeltaSigPolicyResult Pass(IReadOnlyDictionary<string, object>? details = null) => new()
{
Passed = true,
Violations = Array.Empty<string>(),
Details = details
};
/// <summary>
/// Creates a failing result.
/// </summary>
public static DeltaSigPolicyResult Fail(
IReadOnlyList<string> violations,
IReadOnlyDictionary<string, object>? details = null) => new()
{
Passed = false,
Violations = violations,
Details = details
};
}

View File

@@ -0,0 +1,428 @@
// -----------------------------------------------------------------------------
// DeltaScopePolicyGate.cs
// Sprint: SPRINT_20260117_003_BINDEX_delta_sig_predicate
// Task: DSP-006 - Implement DeltaScopePolicyGate
// Description: Policy gate that enforces limits on binary patch scope
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.BinaryIndex.DeltaSig.Attestation;
namespace StellaOps.BinaryIndex.DeltaSig.Policy;
/// <summary>
/// Policy gate that enforces limits on binary patch scope based on delta-sig predicates.
/// </summary>
/// <remarks>
/// This gate can be used to:
/// - Limit hotfix scope (e.g., max 5 functions touched)
/// - Require minimum semantic similarity for changes
/// - Enforce specific tooling requirements
/// - Gate releases based on change magnitude
/// </remarks>
public sealed class DeltaScopePolicyGate : IDeltaScopePolicyGate
{
private readonly ILogger<DeltaScopePolicyGate> _logger;
private readonly IOptions<DeltaScopeGateOptions> _defaultOptions;
/// <summary>
/// Gate name for identification.
/// </summary>
public const string GateName = "DeltaScopeGate";
/// <summary>
/// Initializes a new instance of the <see cref="DeltaScopePolicyGate"/> class.
/// </summary>
public DeltaScopePolicyGate(
ILogger<DeltaScopePolicyGate> logger,
IOptions<DeltaScopeGateOptions>? defaultOptions = null)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_defaultOptions = defaultOptions ?? Options.Create(new DeltaScopeGateOptions());
}
/// <inheritdoc />
public string Name => GateName;
/// <inheritdoc />
public Task<DeltaScopeGateResult> EvaluateAsync(
DeltaSigPredicate predicate,
DeltaScopeGateOptions? options = null,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(predicate);
var opts = options ?? _defaultOptions.Value;
var issues = new List<DeltaScopeViolation>();
_logger.LogDebug(
"Evaluating delta scope gate for predicate with {Total} changes",
predicate.Summary.TotalChanged);
// Check function count limits
if (predicate.Summary.FunctionsModified > opts.MaxModifiedFunctions)
{
issues.Add(new DeltaScopeViolation
{
Rule = DeltaScopeRule.MaxModifiedFunctions,
Message = $"Modified {predicate.Summary.FunctionsModified} functions; max allowed is {opts.MaxModifiedFunctions}",
Severity = DeltaScopeViolationSeverity.Error,
ActualValue = predicate.Summary.FunctionsModified,
ThresholdValue = opts.MaxModifiedFunctions
});
}
if (predicate.Summary.FunctionsAdded > opts.MaxAddedFunctions)
{
issues.Add(new DeltaScopeViolation
{
Rule = DeltaScopeRule.MaxAddedFunctions,
Message = $"Added {predicate.Summary.FunctionsAdded} functions; max allowed is {opts.MaxAddedFunctions}",
Severity = DeltaScopeViolationSeverity.Error,
ActualValue = predicate.Summary.FunctionsAdded,
ThresholdValue = opts.MaxAddedFunctions
});
}
if (predicate.Summary.FunctionsRemoved > opts.MaxRemovedFunctions)
{
issues.Add(new DeltaScopeViolation
{
Rule = DeltaScopeRule.MaxRemovedFunctions,
Message = $"Removed {predicate.Summary.FunctionsRemoved} functions; max allowed is {opts.MaxRemovedFunctions}",
Severity = DeltaScopeViolationSeverity.Error,
ActualValue = predicate.Summary.FunctionsRemoved,
ThresholdValue = opts.MaxRemovedFunctions
});
}
// Check total bytes changed
if (predicate.Summary.TotalBytesChanged > opts.MaxBytesChanged)
{
issues.Add(new DeltaScopeViolation
{
Rule = DeltaScopeRule.MaxBytesChanged,
Message = $"Changed {predicate.Summary.TotalBytesChanged} bytes; max allowed is {opts.MaxBytesChanged}",
Severity = DeltaScopeViolationSeverity.Error,
ActualValue = predicate.Summary.TotalBytesChanged,
ThresholdValue = opts.MaxBytesChanged
});
}
// Check semantic similarity floor
if (predicate.Summary.MinSemanticSimilarity < opts.MinSemanticSimilarity)
{
issues.Add(new DeltaScopeViolation
{
Rule = DeltaScopeRule.MinSemanticSimilarity,
Message = $"Minimum semantic similarity {predicate.Summary.MinSemanticSimilarity:P0} below threshold {opts.MinSemanticSimilarity:P0}",
Severity = DeltaScopeViolationSeverity.Error,
ActualValue = predicate.Summary.MinSemanticSimilarity,
ThresholdValue = opts.MinSemanticSimilarity
});
}
// Check average semantic similarity (warning level)
if (opts.WarnAvgSemanticSimilarity.HasValue &&
predicate.Summary.AvgSemanticSimilarity < opts.WarnAvgSemanticSimilarity.Value)
{
issues.Add(new DeltaScopeViolation
{
Rule = DeltaScopeRule.WarnAvgSemanticSimilarity,
Message = $"Average semantic similarity {predicate.Summary.AvgSemanticSimilarity:P0} below warning threshold {opts.WarnAvgSemanticSimilarity:P0}",
Severity = DeltaScopeViolationSeverity.Warning,
ActualValue = predicate.Summary.AvgSemanticSimilarity,
ThresholdValue = opts.WarnAvgSemanticSimilarity.Value
});
}
// Check required lifters
if (opts.RequiredLifters?.Count > 0 &&
!opts.RequiredLifters.Contains(predicate.Tooling.Lifter, StringComparer.OrdinalIgnoreCase))
{
issues.Add(new DeltaScopeViolation
{
Rule = DeltaScopeRule.RequiredLifter,
Message = $"Lifter '{predicate.Tooling.Lifter}' not in required list: {string.Join(", ", opts.RequiredLifters)}",
Severity = DeltaScopeViolationSeverity.Error
});
}
// Check required diff algorithm
if (!string.IsNullOrEmpty(opts.RequiredDiffAlgorithm) &&
!string.Equals(predicate.Tooling.DiffAlgorithm, opts.RequiredDiffAlgorithm, StringComparison.OrdinalIgnoreCase))
{
issues.Add(new DeltaScopeViolation
{
Rule = DeltaScopeRule.RequiredDiffAlgorithm,
Message = $"Diff algorithm '{predicate.Tooling.DiffAlgorithm}' does not match required '{opts.RequiredDiffAlgorithm}'",
Severity = DeltaScopeViolationSeverity.Error
});
}
// Check forbidden function patterns
if (opts.ForbiddenFunctionPatterns?.Count > 0)
{
var regexes = opts.ForbiddenFunctionPatterns
.Select(p => new System.Text.RegularExpressions.Regex(p, System.Text.RegularExpressions.RegexOptions.Compiled))
.ToList();
foreach (var delta in predicate.Delta)
{
foreach (var regex in regexes)
{
if (regex.IsMatch(delta.FunctionId))
{
issues.Add(new DeltaScopeViolation
{
Rule = DeltaScopeRule.ForbiddenFunctionPattern,
Message = $"Function '{delta.FunctionId}' matches forbidden pattern",
Severity = DeltaScopeViolationSeverity.Error,
FunctionId = delta.FunctionId
});
}
}
}
}
// Build result
var hasErrors = issues.Any(i => i.Severity == DeltaScopeViolationSeverity.Error);
var result = new DeltaScopeGateResult
{
GateName = GateName,
Passed = !hasErrors,
Violations = issues,
Summary = new DeltaScopeSummary
{
FunctionsModified = predicate.Summary.FunctionsModified,
FunctionsAdded = predicate.Summary.FunctionsAdded,
FunctionsRemoved = predicate.Summary.FunctionsRemoved,
TotalBytesChanged = predicate.Summary.TotalBytesChanged,
MinSemanticSimilarity = predicate.Summary.MinSemanticSimilarity,
AvgSemanticSimilarity = predicate.Summary.AvgSemanticSimilarity,
Lifter = predicate.Tooling.Lifter,
DiffAlgorithm = predicate.Tooling.DiffAlgorithm
},
EvaluatedAt = DateTimeOffset.UtcNow
};
if (hasErrors)
{
_logger.LogWarning(
"Delta scope gate FAILED with {ErrorCount} error(s): {Errors}",
issues.Count(i => i.Severity == DeltaScopeViolationSeverity.Error),
string.Join("; ", issues.Where(i => i.Severity == DeltaScopeViolationSeverity.Error).Select(i => i.Message)));
}
else
{
_logger.LogInformation(
"Delta scope gate PASSED (warnings: {WarnCount})",
issues.Count(i => i.Severity == DeltaScopeViolationSeverity.Warning));
}
return Task.FromResult(result);
}
}
/// <summary>
/// Interface for delta scope policy gate.
/// </summary>
public interface IDeltaScopePolicyGate
{
/// <summary>
/// Gate name.
/// </summary>
string Name { get; }
/// <summary>
/// Evaluate a delta-sig predicate against policy constraints.
/// </summary>
Task<DeltaScopeGateResult> EvaluateAsync(
DeltaSigPredicate predicate,
DeltaScopeGateOptions? options = null,
CancellationToken ct = default);
}
/// <summary>
/// Configuration options for delta scope policy gate.
/// </summary>
public sealed class DeltaScopeGateOptions
{
/// <summary>
/// Configuration section name.
/// </summary>
public const string SectionName = "BinaryIndex:DeltaScopeGate";
/// <summary>
/// Maximum allowed modified functions.
/// </summary>
public int MaxModifiedFunctions { get; set; } = 10;
/// <summary>
/// Maximum allowed added functions.
/// </summary>
public int MaxAddedFunctions { get; set; } = 5;
/// <summary>
/// Maximum allowed removed functions.
/// </summary>
public int MaxRemovedFunctions { get; set; } = 2;
/// <summary>
/// Maximum total bytes changed.
/// </summary>
public long MaxBytesChanged { get; set; } = 10_000;
/// <summary>
/// Minimum semantic similarity for modified functions.
/// </summary>
public double MinSemanticSimilarity { get; set; } = 0.8;
/// <summary>
/// Warning threshold for average semantic similarity.
/// </summary>
public double? WarnAvgSemanticSimilarity { get; set; } = 0.9;
/// <summary>
/// Required lifter tools (e.g., must use ghidra for high-assurance).
/// </summary>
public IReadOnlyList<string>? RequiredLifters { get; set; }
/// <summary>
/// Required diffing algorithm.
/// </summary>
public string? RequiredDiffAlgorithm { get; set; }
/// <summary>
/// Forbidden function name patterns (regex).
/// </summary>
public IReadOnlyList<string>? ForbiddenFunctionPatterns { get; set; }
/// <summary>
/// Allow bypass with explicit approval.
/// </summary>
public bool AllowApprovalBypass { get; set; } = false;
}
/// <summary>
/// Result of delta scope gate evaluation.
/// </summary>
public sealed record DeltaScopeGateResult
{
/// <summary>
/// Gate name.
/// </summary>
public required string GateName { get; init; }
/// <summary>
/// Whether the gate passed.
/// </summary>
public required bool Passed { get; init; }
/// <summary>
/// Violations found.
/// </summary>
public required IReadOnlyList<DeltaScopeViolation> Violations { get; init; }
/// <summary>
/// Summary of the evaluated delta.
/// </summary>
public DeltaScopeSummary? Summary { get; init; }
/// <summary>
/// When the gate was evaluated.
/// </summary>
public DateTimeOffset EvaluatedAt { get; init; }
/// <summary>
/// Human-readable reason for failure.
/// </summary>
public string? Reason => Passed
? null
: string.Join("; ", Violations.Where(v => v.Severity == DeltaScopeViolationSeverity.Error).Select(v => v.Message));
}
/// <summary>
/// A specific violation of delta scope policy.
/// </summary>
public sealed record DeltaScopeViolation
{
/// <summary>
/// Rule that was violated.
/// </summary>
public required DeltaScopeRule Rule { get; init; }
/// <summary>
/// Human-readable message.
/// </summary>
public required string Message { get; init; }
/// <summary>
/// Severity of the violation.
/// </summary>
public required DeltaScopeViolationSeverity Severity { get; init; }
/// <summary>
/// Actual value that violated the rule.
/// </summary>
public object? ActualValue { get; init; }
/// <summary>
/// Threshold value from the rule.
/// </summary>
public object? ThresholdValue { get; init; }
/// <summary>
/// Function ID if the violation is specific to a function.
/// </summary>
public string? FunctionId { get; init; }
}
/// <summary>
/// Delta scope rules that can be violated.
/// </summary>
public enum DeltaScopeRule
{
MaxModifiedFunctions,
MaxAddedFunctions,
MaxRemovedFunctions,
MaxBytesChanged,
MinSemanticSimilarity,
WarnAvgSemanticSimilarity,
RequiredLifter,
RequiredDiffAlgorithm,
ForbiddenFunctionPattern
}
/// <summary>
/// Severity of a delta scope violation.
/// </summary>
public enum DeltaScopeViolationSeverity
{
/// <summary>
/// Warning - does not fail the gate.
/// </summary>
Warning,
/// <summary>
/// Error - fails the gate.
/// </summary>
Error
}
/// <summary>
/// Summary of delta characteristics for audit.
/// </summary>
public sealed record DeltaScopeSummary
{
public int FunctionsModified { get; init; }
public int FunctionsAdded { get; init; }
public int FunctionsRemoved { get; init; }
public long TotalBytesChanged { get; init; }
public double MinSemanticSimilarity { get; init; }
public double AvgSemanticSimilarity { get; init; }
public string? Lifter { get; init; }
public string? DiffAlgorithm { get; init; }
}