new two advisories and sprints work on them
This commit is contained in:
@@ -0,0 +1,372 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// DeltaSigAttestorIntegrationTests.cs
|
||||
// Sprint: SPRINT_20260117_003_BINDEX_delta_sig_predicate
|
||||
// Task: DSP-008 - Unit tests for DeltaSig attestation
|
||||
// Description: Unit tests for delta-sig attestation integration
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.BinaryIndex.DeltaSig.Attestation;
|
||||
|
||||
namespace StellaOps.BinaryIndex.DeltaSig.Tests.Attestation;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for delta-sig attestation integration.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class DeltaSigAttestorIntegrationTests
|
||||
{
|
||||
private static readonly DateTimeOffset FixedTimestamp = new(2026, 1, 16, 12, 0, 0, TimeSpan.Zero);
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
|
||||
public DeltaSigAttestorIntegrationTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(FixedTimestamp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreatePredicate_ValidInput_CreatesPredicateWithCorrectType()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var request = CreateValidPredicateRequest();
|
||||
|
||||
// Act
|
||||
var predicate = service.CreatePredicate(request);
|
||||
|
||||
// Assert
|
||||
predicate.PredicateType.Should().Be("https://stellaops.io/delta-sig/v1");
|
||||
predicate.Subject.Should().NotBeEmpty();
|
||||
predicate.DeltaSignatures.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreatePredicate_WithSymbols_IncludesAllSymbols()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var request = CreateValidPredicateRequest(symbolCount: 5);
|
||||
|
||||
// Act
|
||||
var predicate = service.CreatePredicate(request);
|
||||
|
||||
// Assert
|
||||
predicate.DeltaSignatures.Should().HaveCount(5);
|
||||
predicate.Statistics.TotalSymbols.Should().Be(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreatePredicate_IncludesTimestamp()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var request = CreateValidPredicateRequest();
|
||||
|
||||
// Act
|
||||
var predicate = service.CreatePredicate(request);
|
||||
|
||||
// Assert
|
||||
predicate.Timestamp.Should().Be(FixedTimestamp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreatePredicate_ComputesContentDigest()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var request = CreateValidPredicateRequest();
|
||||
|
||||
// Act
|
||||
var predicate = service.CreatePredicate(request);
|
||||
|
||||
// Assert
|
||||
predicate.Subject.Should().ContainSingle();
|
||||
predicate.Subject.First().Digest.Should().ContainKey("sha256");
|
||||
predicate.Subject.First().Digest["sha256"].Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreatePredicate_DeterministicOutput()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var request = CreateValidPredicateRequest();
|
||||
|
||||
// Act
|
||||
var predicate1 = service.CreatePredicate(request);
|
||||
var predicate2 = service.CreatePredicate(request);
|
||||
|
||||
// Assert
|
||||
predicate1.DeltaSignatures.Should().BeEquivalentTo(predicate2.DeltaSignatures);
|
||||
predicate1.Subject.First().Digest["sha256"].Should().Be(predicate2.Subject.First().Digest["sha256"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateEnvelope_ValidPredicate_CreatesDsseEnvelope()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var request = CreateValidPredicateRequest();
|
||||
var predicate = service.CreatePredicate(request);
|
||||
|
||||
// Act
|
||||
var envelope = service.CreateEnvelope(predicate);
|
||||
|
||||
// Assert
|
||||
envelope.PayloadType.Should().Be("application/vnd.in-toto+json");
|
||||
envelope.Payload.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateEnvelope_PayloadIsBase64Encoded()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var request = CreateValidPredicateRequest();
|
||||
var predicate = service.CreatePredicate(request);
|
||||
|
||||
// Act
|
||||
var envelope = service.CreateEnvelope(predicate);
|
||||
|
||||
// Assert
|
||||
var decoded = Convert.FromBase64String(envelope.Payload);
|
||||
decoded.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerializePredicate_ProducesValidJson()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var request = CreateValidPredicateRequest();
|
||||
var predicate = service.CreatePredicate(request);
|
||||
|
||||
// Act
|
||||
var json = service.SerializePredicate(predicate);
|
||||
|
||||
// Assert
|
||||
json.Should().Contain("\"predicateType\"");
|
||||
json.Should().Contain("\"subject\"");
|
||||
json.Should().Contain("\"deltaSignatures\"");
|
||||
json.Should().Contain("delta-sig/v1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatePredicate_ValidPredicate_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var request = CreateValidPredicateRequest();
|
||||
var predicate = service.CreatePredicate(request);
|
||||
|
||||
// Act
|
||||
var result = service.ValidatePredicate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.Errors.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatePredicate_EmptySubject_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var predicate = new DeltaSigPredicate(
|
||||
PredicateType: "https://stellaops.io/delta-sig/v1",
|
||||
Subject: Array.Empty<InTotoSubject>(),
|
||||
DeltaSignatures: new[] { CreateTestDeltaSig() },
|
||||
Timestamp: FixedTimestamp,
|
||||
Statistics: new DeltaSigStatistics(1, 0, 0));
|
||||
|
||||
// Act
|
||||
var result = service.ValidatePredicate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("subject", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatePredicate_EmptyDeltaSignatures_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var predicate = new DeltaSigPredicate(
|
||||
PredicateType: "https://stellaops.io/delta-sig/v1",
|
||||
Subject: new[] { CreateTestSubject() },
|
||||
DeltaSignatures: Array.Empty<DeltaSignatureEntry>(),
|
||||
Timestamp: FixedTimestamp,
|
||||
Statistics: new DeltaSigStatistics(0, 0, 0));
|
||||
|
||||
// Act
|
||||
var result = service.ValidatePredicate(predicate);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("signature", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComparePredicate_SameContent_ReturnsNoDifferences()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var request = CreateValidPredicateRequest();
|
||||
var predicate1 = service.CreatePredicate(request);
|
||||
var predicate2 = service.CreatePredicate(request);
|
||||
|
||||
// Act
|
||||
var diff = service.ComparePredicate(predicate1, predicate2);
|
||||
|
||||
// Assert
|
||||
diff.HasDifferences.Should().BeFalse();
|
||||
diff.AddedSymbols.Should().BeEmpty();
|
||||
diff.RemovedSymbols.Should().BeEmpty();
|
||||
diff.ModifiedSymbols.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComparePredicate_AddedSymbol_DetectsAddition()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var request1 = CreateValidPredicateRequest(symbolCount: 3);
|
||||
var request2 = CreateValidPredicateRequest(symbolCount: 4);
|
||||
var predicate1 = service.CreatePredicate(request1);
|
||||
var predicate2 = service.CreatePredicate(request2);
|
||||
|
||||
// Act
|
||||
var diff = service.ComparePredicate(predicate1, predicate2);
|
||||
|
||||
// Assert
|
||||
diff.HasDifferences.Should().BeTrue();
|
||||
diff.AddedSymbols.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComparePredicate_RemovedSymbol_DetectsRemoval()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var request1 = CreateValidPredicateRequest(symbolCount: 4);
|
||||
var request2 = CreateValidPredicateRequest(symbolCount: 3);
|
||||
var predicate1 = service.CreatePredicate(request1);
|
||||
var predicate2 = service.CreatePredicate(request2);
|
||||
|
||||
// Act
|
||||
var diff = service.ComparePredicate(predicate1, predicate2);
|
||||
|
||||
// Assert
|
||||
diff.HasDifferences.Should().BeTrue();
|
||||
diff.RemovedSymbols.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
private IDeltaSigAttestorIntegration CreateService()
|
||||
{
|
||||
return new DeltaSigAttestorIntegration(
|
||||
Options.Create(new DeltaSigAttestorOptions
|
||||
{
|
||||
PredicateType = "https://stellaops.io/delta-sig/v1",
|
||||
IncludeStatistics = true
|
||||
}),
|
||||
_timeProvider,
|
||||
NullLogger<DeltaSigAttestorIntegration>.Instance);
|
||||
}
|
||||
|
||||
private static DeltaSigPredicateRequest CreateValidPredicateRequest(int symbolCount = 3)
|
||||
{
|
||||
var signatures = Enumerable.Range(0, symbolCount)
|
||||
.Select(i => CreateTestDeltaSig(i))
|
||||
.ToArray();
|
||||
|
||||
return new DeltaSigPredicateRequest(
|
||||
BinaryDigest: $"sha256:abc123def456{symbolCount:D4}",
|
||||
BinaryName: "libtest.so",
|
||||
Signatures: signatures);
|
||||
}
|
||||
|
||||
private static DeltaSignatureEntry CreateTestDeltaSig(int index = 0)
|
||||
{
|
||||
return new DeltaSignatureEntry(
|
||||
SymbolName: $"test_function_{index}",
|
||||
HashAlgorithm: "sha256",
|
||||
HashHex: $"abcdef{index:D8}0123456789abcdef0123456789abcdef0123456789abcdef01234567",
|
||||
SizeBytes: 128 + index * 16,
|
||||
Scope: ".text");
|
||||
}
|
||||
|
||||
private static InTotoSubject CreateTestSubject()
|
||||
{
|
||||
return new InTotoSubject(
|
||||
Name: "libtest.so",
|
||||
Digest: new Dictionary<string, string>
|
||||
{
|
||||
["sha256"] = "abc123def4560000"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Supporting types for tests (would normally be in main project)
|
||||
|
||||
public record DeltaSigPredicate(
|
||||
string PredicateType,
|
||||
IReadOnlyList<InTotoSubject> Subject,
|
||||
IReadOnlyList<DeltaSignatureEntry> DeltaSignatures,
|
||||
DateTimeOffset Timestamp,
|
||||
DeltaSigStatistics Statistics);
|
||||
|
||||
public record InTotoSubject(
|
||||
string Name,
|
||||
IReadOnlyDictionary<string, string> Digest);
|
||||
|
||||
public record DeltaSignatureEntry(
|
||||
string SymbolName,
|
||||
string HashAlgorithm,
|
||||
string HashHex,
|
||||
int SizeBytes,
|
||||
string Scope);
|
||||
|
||||
public record DeltaSigStatistics(
|
||||
int TotalSymbols,
|
||||
int AddedSymbols,
|
||||
int ModifiedSymbols);
|
||||
|
||||
public record DeltaSigPredicateRequest(
|
||||
string BinaryDigest,
|
||||
string BinaryName,
|
||||
IReadOnlyList<DeltaSignatureEntry> Signatures);
|
||||
|
||||
public record DeltaSigPredicateDiff(
|
||||
bool HasDifferences,
|
||||
IReadOnlyList<string> AddedSymbols,
|
||||
IReadOnlyList<string> RemovedSymbols,
|
||||
IReadOnlyList<string> ModifiedSymbols);
|
||||
|
||||
public record PredicateValidationResult(
|
||||
bool IsValid,
|
||||
IReadOnlyList<string> Errors);
|
||||
|
||||
public record DsseEnvelope(
|
||||
string PayloadType,
|
||||
string Payload);
|
||||
|
||||
public record DeltaSigAttestorOptions
|
||||
{
|
||||
public string PredicateType { get; init; } = "https://stellaops.io/delta-sig/v1";
|
||||
public bool IncludeStatistics { get; init; } = true;
|
||||
}
|
||||
|
||||
public interface IDeltaSigAttestorIntegration
|
||||
{
|
||||
DeltaSigPredicate CreatePredicate(DeltaSigPredicateRequest request);
|
||||
DsseEnvelope CreateEnvelope(DeltaSigPredicate predicate);
|
||||
string SerializePredicate(DeltaSigPredicate predicate);
|
||||
PredicateValidationResult ValidatePredicate(DeltaSigPredicate predicate);
|
||||
DeltaSigPredicateDiff ComparePredicate(DeltaSigPredicate before, DeltaSigPredicate after);
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// DeltaSigEndToEndTests.cs
|
||||
// Sprint: SPRINT_20260117_003_BINDEX_delta_sig_predicate
|
||||
// Task: DSP-009 - Integration tests for delta-sig predicate E2E flow
|
||||
// Description: End-to-end tests for delta-sig generation, signing, submission, and verification
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.BinaryIndex.DeltaSig.Tests.Integration;
|
||||
|
||||
[Trait("Category", TestCategories.Integration)]
|
||||
public sealed class DeltaSigEndToEndTests
|
||||
{
|
||||
private static readonly DateTimeOffset FixedTimestamp = new(2026, 1, 16, 12, 0, 0, TimeSpan.Zero);
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly MockRekorClient _rekorClient;
|
||||
private readonly MockSigningService _signingService;
|
||||
|
||||
public DeltaSigEndToEndTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(FixedTimestamp);
|
||||
_rekorClient = new MockRekorClient();
|
||||
_signingService = new MockSigningService();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullFlow_GenerateSignSubmitVerify_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var beforeBinary = CreateTestBinary("libtest-1.0.so", 10);
|
||||
var afterBinary = CreateTestBinary("libtest-1.1.so", 12); // 2 new functions
|
||||
|
||||
// Act - Step 1: Generate delta-sig predicate
|
||||
var predicate = await service.GenerateAsync(beforeBinary, afterBinary, CancellationToken.None);
|
||||
|
||||
// Assert - predicate created correctly
|
||||
predicate.Should().NotBeNull();
|
||||
predicate.PredicateType.Should().Contain("delta-sig");
|
||||
predicate.Summary.FunctionsAdded.Should().Be(2);
|
||||
predicate.Summary.FunctionsModified.Should().Be(0);
|
||||
|
||||
// Act - Step 2: Sign the predicate
|
||||
var envelope = await service.SignAsync(predicate, CancellationToken.None);
|
||||
|
||||
// Assert - envelope created
|
||||
envelope.Should().NotBeNull();
|
||||
envelope.PayloadType.Should().Be("application/vnd.in-toto+json");
|
||||
envelope.Signatures.Should().NotBeEmpty();
|
||||
|
||||
// Act - Step 3: Submit to Rekor
|
||||
var submission = await service.SubmitToRekorAsync(envelope, CancellationToken.None);
|
||||
|
||||
// Assert - submission successful
|
||||
submission.Success.Should().BeTrue();
|
||||
submission.EntryId.Should().NotBeNullOrEmpty();
|
||||
submission.LogIndex.Should().BeGreaterThan(0);
|
||||
|
||||
// Act - Step 4: Verify from Rekor
|
||||
var verification = await service.VerifyFromRekorAsync(submission.EntryId!, CancellationToken.None);
|
||||
|
||||
// Assert - verification successful
|
||||
verification.IsValid.Should().BeTrue();
|
||||
verification.PredicateType.Should().Contain("delta-sig");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Generate_IdenticalBinaries_ReturnsEmptyDiff()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var binary = CreateTestBinary("libtest.so", 5);
|
||||
|
||||
// Act
|
||||
var predicate = await service.GenerateAsync(binary, binary, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
predicate.Summary.FunctionsAdded.Should().Be(0);
|
||||
predicate.Summary.FunctionsModified.Should().Be(0);
|
||||
predicate.Summary.FunctionsRemoved.Should().Be(0);
|
||||
predicate.Diff.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Generate_RemovedFunctions_TracksRemovals()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var beforeBinary = CreateTestBinary("libtest-1.0.so", 10);
|
||||
var afterBinary = CreateTestBinary("libtest-1.1.so", 7); // 3 removed
|
||||
|
||||
// Act
|
||||
var predicate = await service.GenerateAsync(beforeBinary, afterBinary, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
predicate.Summary.FunctionsRemoved.Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Generate_ModifiedFunctions_TracksModifications()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var beforeBinary = CreateTestBinaryWithModifications("libtest-1.0.so", 5, modifyIndices: new[] { 1, 3 });
|
||||
var afterBinary = CreateTestBinaryWithModifications("libtest-1.1.so", 5, modifyIndices: new[] { 1, 3 }, modified: true);
|
||||
|
||||
// Act
|
||||
var predicate = await service.GenerateAsync(beforeBinary, afterBinary, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
predicate.Summary.FunctionsModified.Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Verify_TamperedPredicate_FailsVerification()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var beforeBinary = CreateTestBinary("libtest-1.0.so", 5);
|
||||
var afterBinary = CreateTestBinary("libtest-1.1.so", 6);
|
||||
|
||||
var predicate = await service.GenerateAsync(beforeBinary, afterBinary, CancellationToken.None);
|
||||
var envelope = await service.SignAsync(predicate, CancellationToken.None);
|
||||
|
||||
// Tamper with the envelope
|
||||
var tamperedEnvelope = envelope with
|
||||
{
|
||||
Payload = Convert.ToBase64String(Encoding.UTF8.GetBytes("tampered content"))
|
||||
};
|
||||
|
||||
// Act
|
||||
var verification = await service.VerifyEnvelopeAsync(tamperedEnvelope, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
verification.IsValid.Should().BeFalse();
|
||||
verification.FailureReason.Should().Contain("signature");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PolicyGate_WithinLimits_Passes()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var beforeBinary = CreateTestBinary("libtest-1.0.so", 10);
|
||||
var afterBinary = CreateTestBinary("libtest-1.1.so", 12); // 2 added
|
||||
|
||||
var predicate = await service.GenerateAsync(beforeBinary, afterBinary, CancellationToken.None);
|
||||
|
||||
var policyOptions = new DeltaScopePolicyOptions
|
||||
{
|
||||
MaxAddedFunctions = 5,
|
||||
MaxRemovedFunctions = 5,
|
||||
MaxModifiedFunctions = 10,
|
||||
MaxBytesChanged = 10000
|
||||
};
|
||||
|
||||
// Act
|
||||
var gateResult = await service.EvaluatePolicyAsync(predicate, policyOptions, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
gateResult.Passed.Should().BeTrue();
|
||||
gateResult.Violations.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PolicyGate_ExceedsLimits_FailsWithViolations()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var beforeBinary = CreateTestBinary("libtest-1.0.so", 10);
|
||||
var afterBinary = CreateTestBinary("libtest-1.1.so", 20); // 10 added
|
||||
|
||||
var predicate = await service.GenerateAsync(beforeBinary, afterBinary, CancellationToken.None);
|
||||
|
||||
var policyOptions = new DeltaScopePolicyOptions
|
||||
{
|
||||
MaxAddedFunctions = 5, // Exceeded
|
||||
MaxRemovedFunctions = 5,
|
||||
MaxModifiedFunctions = 10,
|
||||
MaxBytesChanged = 10000
|
||||
};
|
||||
|
||||
// Act
|
||||
var gateResult = await service.EvaluatePolicyAsync(predicate, policyOptions, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
gateResult.Passed.Should().BeFalse();
|
||||
gateResult.Violations.Should().ContainSingle();
|
||||
gateResult.Violations.First().Should().Contain("added");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SerializeDeserialize_RoundTrip_PreservesData()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var beforeBinary = CreateTestBinary("libtest-1.0.so", 5);
|
||||
var afterBinary = CreateTestBinary("libtest-1.1.so", 7);
|
||||
|
||||
var originalPredicate = await service.GenerateAsync(beforeBinary, afterBinary, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var json = service.SerializePredicate(originalPredicate);
|
||||
var deserialized = service.DeserializePredicate(json);
|
||||
|
||||
// Assert
|
||||
deserialized.PredicateType.Should().Be(originalPredicate.PredicateType);
|
||||
deserialized.Summary.FunctionsAdded.Should().Be(originalPredicate.Summary.FunctionsAdded);
|
||||
deserialized.Subject.Should().HaveCount(originalPredicate.Subject.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Generate_WithSemanticSimilarity_IncludesSimilarityScores()
|
||||
{
|
||||
// Arrange
|
||||
var options = CreateOptions();
|
||||
options.Value.IncludeSemanticSimilarity = true;
|
||||
var service = CreateService(options);
|
||||
|
||||
var beforeBinary = CreateTestBinaryWithModifications("libtest-1.0.so", 5, modifyIndices: new[] { 2 });
|
||||
var afterBinary = CreateTestBinaryWithModifications("libtest-1.1.so", 5, modifyIndices: new[] { 2 }, modified: true);
|
||||
|
||||
// Act
|
||||
var predicate = await service.GenerateAsync(beforeBinary, afterBinary, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
var modifiedFunc = predicate.Diff.FirstOrDefault(d => d.ChangeType == "modified");
|
||||
modifiedFunc.Should().NotBeNull();
|
||||
modifiedFunc!.SemanticSimilarity.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SubmitToRekor_Offline_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
_rekorClient.SetOffline(true);
|
||||
var service = CreateService();
|
||||
var predicate = CreateMinimalPredicate();
|
||||
var envelope = await service.SignAsync(predicate, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var submission = await service.SubmitToRekorAsync(envelope, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
submission.Success.Should().BeFalse();
|
||||
submission.Error.Should().Contain("offline");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Verify_StoredOfflineProof_SucceedsWithoutNetwork()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var predicate = CreateMinimalPredicate();
|
||||
var envelope = await service.SignAsync(predicate, CancellationToken.None);
|
||||
|
||||
// Submit and get proof
|
||||
var submission = await service.SubmitToRekorAsync(envelope, CancellationToken.None);
|
||||
var proof = await service.GetInclusionProofAsync(submission.EntryId!, CancellationToken.None);
|
||||
|
||||
// Go offline
|
||||
_rekorClient.SetOffline(true);
|
||||
|
||||
// Act - verify using stored proof
|
||||
var verification = await service.VerifyWithStoredProofAsync(envelope, proof, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
verification.IsValid.Should().BeTrue();
|
||||
verification.VerificationMode.Should().Be("offline");
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
private IDeltaSigService CreateService(IOptions<DeltaSigServiceOptions>? options = null)
|
||||
{
|
||||
return new DeltaSigService(
|
||||
options ?? CreateOptions(),
|
||||
_rekorClient,
|
||||
_signingService,
|
||||
_timeProvider,
|
||||
NullLogger<DeltaSigService>.Instance);
|
||||
}
|
||||
|
||||
private static IOptions<DeltaSigServiceOptions> CreateOptions()
|
||||
{
|
||||
return Options.Create(new DeltaSigServiceOptions
|
||||
{
|
||||
PredicateType = "https://stellaops.io/delta-sig/v1",
|
||||
IncludeSemanticSimilarity = false,
|
||||
RekorUrl = "https://rekor.sigstore.dev"
|
||||
});
|
||||
}
|
||||
|
||||
private static TestBinaryData CreateTestBinary(string name, int functionCount)
|
||||
{
|
||||
var functions = Enumerable.Range(0, functionCount)
|
||||
.Select(i => new TestFunction(
|
||||
Name: $"func_{i:D3}",
|
||||
Hash: ComputeHash($"{name}-func-{i}"),
|
||||
Size: 100 + i * 10))
|
||||
.ToImmutableArray();
|
||||
|
||||
return new TestBinaryData(
|
||||
Name: name,
|
||||
Digest: $"sha256:{ComputeHash(name)}",
|
||||
Functions: functions);
|
||||
}
|
||||
|
||||
private static TestBinaryData CreateTestBinaryWithModifications(
|
||||
string name, int functionCount, int[] modifyIndices, bool modified = false)
|
||||
{
|
||||
var functions = Enumerable.Range(0, functionCount)
|
||||
.Select(i =>
|
||||
{
|
||||
var suffix = modified && modifyIndices.Contains(i) ? "-modified" : "";
|
||||
return new TestFunction(
|
||||
Name: $"func_{i:D3}",
|
||||
Hash: ComputeHash($"{name}-func-{i}{suffix}"),
|
||||
Size: 100 + i * 10);
|
||||
})
|
||||
.ToImmutableArray();
|
||||
|
||||
return new TestBinaryData(
|
||||
Name: name,
|
||||
Digest: $"sha256:{ComputeHash(name)}",
|
||||
Functions: functions);
|
||||
}
|
||||
|
||||
private DeltaSigPredicate CreateMinimalPredicate()
|
||||
{
|
||||
return new DeltaSigPredicate(
|
||||
PredicateType: "https://stellaops.io/delta-sig/v1",
|
||||
Subject: ImmutableArray.Create(new InTotoSubject(
|
||||
Name: "test.so",
|
||||
Digest: ImmutableDictionary<string, string>.Empty.Add("sha256", "abc123"))),
|
||||
Diff: ImmutableArray<DeltaSigDiffEntry>.Empty,
|
||||
Summary: new DeltaSigSummary(0, 0, 0, 0),
|
||||
Timestamp: FixedTimestamp,
|
||||
BeforeDigest: "sha256:before",
|
||||
AfterDigest: "sha256:after");
|
||||
}
|
||||
|
||||
private static string ComputeHash(string input)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(input);
|
||||
var hash = SHA256.HashData(bytes);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
// Supporting types for tests
|
||||
|
||||
public record TestBinaryData(
|
||||
string Name,
|
||||
string Digest,
|
||||
ImmutableArray<TestFunction> Functions);
|
||||
|
||||
public record TestFunction(
|
||||
string Name,
|
||||
string Hash,
|
||||
int Size);
|
||||
|
||||
public record DeltaSigPredicate(
|
||||
string PredicateType,
|
||||
ImmutableArray<InTotoSubject> Subject,
|
||||
ImmutableArray<DeltaSigDiffEntry> Diff,
|
||||
DeltaSigSummary Summary,
|
||||
DateTimeOffset Timestamp,
|
||||
string BeforeDigest,
|
||||
string AfterDigest);
|
||||
|
||||
public record InTotoSubject(
|
||||
string Name,
|
||||
ImmutableDictionary<string, string> Digest);
|
||||
|
||||
public record DeltaSigDiffEntry(
|
||||
string FunctionName,
|
||||
string ChangeType,
|
||||
string? BeforeHash,
|
||||
string? AfterHash,
|
||||
int BytesDelta,
|
||||
double? SemanticSimilarity);
|
||||
|
||||
public record DeltaSigSummary(
|
||||
int FunctionsAdded,
|
||||
int FunctionsRemoved,
|
||||
int FunctionsModified,
|
||||
int TotalBytesChanged);
|
||||
|
||||
public record DsseEnvelope(
|
||||
string PayloadType,
|
||||
string Payload,
|
||||
ImmutableArray<DsseSignature> Signatures);
|
||||
|
||||
public record DsseSignature(
|
||||
string KeyId,
|
||||
string Sig);
|
||||
|
||||
public record RekorSubmissionResult(
|
||||
bool Success,
|
||||
string? EntryId,
|
||||
long LogIndex,
|
||||
string? Error);
|
||||
|
||||
public record VerificationResult(
|
||||
bool IsValid,
|
||||
string? PredicateType,
|
||||
string? FailureReason,
|
||||
string? VerificationMode);
|
||||
|
||||
public record PolicyGateResult(
|
||||
bool Passed,
|
||||
ImmutableArray<string> Violations);
|
||||
|
||||
public record InclusionProof(
|
||||
long TreeSize,
|
||||
string RootHash,
|
||||
ImmutableArray<string> Hashes);
|
||||
|
||||
public record DeltaScopePolicyOptions
|
||||
{
|
||||
public int MaxAddedFunctions { get; init; }
|
||||
public int MaxRemovedFunctions { get; init; }
|
||||
public int MaxModifiedFunctions { get; init; }
|
||||
public int MaxBytesChanged { get; init; }
|
||||
}
|
||||
|
||||
public record DeltaSigServiceOptions
|
||||
{
|
||||
public string PredicateType { get; init; } = "https://stellaops.io/delta-sig/v1";
|
||||
public bool IncludeSemanticSimilarity { get; init; }
|
||||
public string RekorUrl { get; init; } = "https://rekor.sigstore.dev";
|
||||
}
|
||||
|
||||
public interface IDeltaSigService
|
||||
{
|
||||
Task<DeltaSigPredicate> GenerateAsync(TestBinaryData before, TestBinaryData after, CancellationToken ct);
|
||||
Task<DsseEnvelope> SignAsync(DeltaSigPredicate predicate, CancellationToken ct);
|
||||
Task<RekorSubmissionResult> SubmitToRekorAsync(DsseEnvelope envelope, CancellationToken ct);
|
||||
Task<VerificationResult> VerifyFromRekorAsync(string entryId, CancellationToken ct);
|
||||
Task<VerificationResult> VerifyEnvelopeAsync(DsseEnvelope envelope, CancellationToken ct);
|
||||
Task<PolicyGateResult> EvaluatePolicyAsync(DeltaSigPredicate predicate, DeltaScopePolicyOptions options, CancellationToken ct);
|
||||
string SerializePredicate(DeltaSigPredicate predicate);
|
||||
DeltaSigPredicate DeserializePredicate(string json);
|
||||
Task<InclusionProof> GetInclusionProofAsync(string entryId, CancellationToken ct);
|
||||
Task<VerificationResult> VerifyWithStoredProofAsync(DsseEnvelope envelope, InclusionProof proof, CancellationToken ct);
|
||||
}
|
||||
|
||||
public sealed class MockRekorClient
|
||||
{
|
||||
private bool _offline;
|
||||
private long _nextLogIndex = 10000;
|
||||
private readonly Dictionary<string, InclusionProof> _proofs = new();
|
||||
|
||||
public void SetOffline(bool offline) => _offline = offline;
|
||||
|
||||
public Task<RekorSubmissionResult> SubmitAsync(byte[] payload, CancellationToken ct)
|
||||
{
|
||||
if (_offline)
|
||||
return Task.FromResult(new RekorSubmissionResult(false, null, 0, "offline"));
|
||||
|
||||
var entryId = Guid.NewGuid().ToString("N");
|
||||
var logIndex = _nextLogIndex++;
|
||||
_proofs[entryId] = new InclusionProof(logIndex, "root-hash", ImmutableArray.Create("h1", "h2"));
|
||||
|
||||
return Task.FromResult(new RekorSubmissionResult(true, entryId, logIndex, null));
|
||||
}
|
||||
|
||||
public Task<InclusionProof?> GetProofAsync(string entryId, CancellationToken ct)
|
||||
{
|
||||
if (_offline) return Task.FromResult<InclusionProof?>(null);
|
||||
_proofs.TryGetValue(entryId, out var proof);
|
||||
return Task.FromResult(proof);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class MockSigningService
|
||||
{
|
||||
public Task<DsseEnvelope> SignAsync(string payload, CancellationToken ct)
|
||||
{
|
||||
var signature = Convert.ToBase64String(
|
||||
SHA256.HashData(Encoding.UTF8.GetBytes(payload)));
|
||||
|
||||
return Task.FromResult(new DsseEnvelope(
|
||||
PayloadType: "application/vnd.in-toto+json",
|
||||
Payload: Convert.ToBase64String(Encoding.UTF8.GetBytes(payload)),
|
||||
Signatures: ImmutableArray.Create(new DsseSignature("key-1", signature))));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user