203 lines
7.4 KiB
C#
203 lines
7.4 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using StellaOps.Attestor.Core.Rekor;
|
|
using StellaOps.Attestor.Core.Submission;
|
|
using StellaOps.Cryptography;
|
|
using StellaOps.Scanner.ProofSpine;
|
|
using StellaOps.Scanner.ProofSpine.Options;
|
|
using StellaOps.Scanner.Reachability.Attestation;
|
|
using Xunit;
|
|
|
|
using StellaOps.TestKit;
|
|
namespace StellaOps.Scanner.Reachability.Tests;
|
|
|
|
public sealed class ReachabilityWitnessPublisherIntegrationTests
|
|
{
|
|
private static CancellationToken TestCancellationToken => TestContext.Current.CancellationToken;
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task PublishAsync_WhenStoreInCasEnabled_StoresGraphAndEnvelopeInCas()
|
|
{
|
|
var options = Options.Create(new ReachabilityWitnessOptions
|
|
{
|
|
Enabled = true,
|
|
StoreInCas = true,
|
|
PublishToRekor = false,
|
|
});
|
|
|
|
var cas = new FakeFileContentAddressableStore();
|
|
var cryptoHash = CryptoHashFactory.CreateDefault();
|
|
var publisher = new ReachabilityWitnessPublisher(
|
|
options,
|
|
cryptoHash,
|
|
NullLogger<ReachabilityWitnessPublisher>.Instance,
|
|
cas: cas);
|
|
|
|
var graph = CreateTestGraph();
|
|
var graphBytes = System.Text.Encoding.UTF8.GetBytes("{\"schema\":\"richgraph-v1\",\"nodes\":[],\"edges\":[]}");
|
|
|
|
var result = await publisher.PublishAsync(
|
|
graph,
|
|
graphBytes,
|
|
graphHash: "blake3:abc123",
|
|
subjectDigest: "sha256:def456",
|
|
cancellationToken: TestCancellationToken);
|
|
|
|
Assert.Equal("cas://reachability/graphs/abc123", result.CasUri);
|
|
Assert.Equal(graphBytes, cas.GetBytes("abc123"));
|
|
Assert.NotNull(cas.GetBytes("abc123.dsse"));
|
|
Assert.NotEmpty(result.DsseEnvelopeBytes);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task PublishAsync_WhenRekorEnabled_SubmitsDsseEnvelope()
|
|
{
|
|
var rekor = new CapturingRekorClient();
|
|
var signer = CreateDeterministicSigner(keyId: "reachability-test-key");
|
|
var cryptoProfile = new TestCryptoProfile("reachability-test-key", "hs256");
|
|
|
|
var options = Options.Create(new ReachabilityWitnessOptions
|
|
{
|
|
Enabled = true,
|
|
StoreInCas = false,
|
|
PublishToRekor = true,
|
|
RekorUrl = new Uri("https://rekor.test"),
|
|
RekorBackendName = "primary",
|
|
SigningKeyId = "reachability-test-key",
|
|
Tier = AttestationTier.Standard
|
|
});
|
|
|
|
var cryptoHash = CryptoHashFactory.CreateDefault();
|
|
var publisher = new ReachabilityWitnessPublisher(
|
|
options,
|
|
cryptoHash,
|
|
NullLogger<ReachabilityWitnessPublisher>.Instance,
|
|
dsseSigningService: signer,
|
|
cryptoProfile: cryptoProfile,
|
|
rekorClient: rekor);
|
|
|
|
var graph = CreateTestGraph();
|
|
var result = await publisher.PublishAsync(
|
|
graph,
|
|
graphBytes: Array.Empty<byte>(),
|
|
graphHash: "blake3:abc123",
|
|
subjectDigest: "sha256:def456",
|
|
cancellationToken: TestCancellationToken);
|
|
|
|
Assert.NotNull(rekor.LastRequest);
|
|
Assert.NotNull(rekor.LastBackend);
|
|
Assert.Equal("primary", rekor.LastBackend!.Name);
|
|
Assert.Equal(new Uri("https://rekor.test"), rekor.LastBackend.Url);
|
|
|
|
var request = rekor.LastRequest!;
|
|
Assert.Equal("application/vnd.in-toto+json", request.Bundle.Dsse.PayloadType);
|
|
Assert.False(string.IsNullOrWhiteSpace(request.Bundle.Dsse.PayloadBase64));
|
|
Assert.NotEmpty(request.Bundle.Dsse.Signatures);
|
|
Assert.Equal("reachability-test-key", request.Bundle.Dsse.Signatures[0].KeyId);
|
|
Assert.False(string.IsNullOrWhiteSpace(request.Meta.BundleSha256));
|
|
|
|
Assert.Equal(1234, result.RekorLogIndex);
|
|
Assert.Equal("rekor-uuid-1234", result.RekorLogId);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task PublishAsync_WhenAirGapped_SkipsRekorSubmission()
|
|
{
|
|
var rekor = new CapturingRekorClient();
|
|
|
|
var options = Options.Create(new ReachabilityWitnessOptions
|
|
{
|
|
Enabled = true,
|
|
StoreInCas = false,
|
|
PublishToRekor = true,
|
|
RekorUrl = new Uri("https://rekor.test"),
|
|
Tier = AttestationTier.AirGapped,
|
|
});
|
|
|
|
var cryptoHash = CryptoHashFactory.CreateDefault();
|
|
var publisher = new ReachabilityWitnessPublisher(
|
|
options,
|
|
cryptoHash,
|
|
NullLogger<ReachabilityWitnessPublisher>.Instance,
|
|
rekorClient: rekor);
|
|
|
|
var graph = CreateTestGraph();
|
|
var result = await publisher.PublishAsync(
|
|
graph,
|
|
graphBytes: Array.Empty<byte>(),
|
|
graphHash: "blake3:abc123",
|
|
subjectDigest: "sha256:def456",
|
|
cancellationToken: TestCancellationToken);
|
|
|
|
Assert.Null(rekor.LastRequest);
|
|
Assert.Null(result.RekorLogIndex);
|
|
Assert.Null(result.RekorLogId);
|
|
}
|
|
|
|
private static RichGraph CreateTestGraph()
|
|
{
|
|
return new RichGraph(
|
|
Schema: "richgraph-v1",
|
|
Analyzer: new RichGraphAnalyzer("test-analyzer", "1.0.0", null),
|
|
Nodes: new[]
|
|
{
|
|
new RichGraphNode("n1", "sym:dotnet:A", null, null, "dotnet", "method", "A", null, null, null, null),
|
|
new RichGraphNode("n2", "sym:dotnet:B", null, null, "dotnet", "sink", "B", null, null, null, null)
|
|
},
|
|
Edges: new[]
|
|
{
|
|
new RichGraphEdge("n1", "n2", "call", null, null, null, 1.0, null)
|
|
},
|
|
Roots: null!);
|
|
}
|
|
|
|
private static IDsseSigningService CreateDeterministicSigner(string keyId)
|
|
{
|
|
var options = Options.Create(new ProofSpineDsseSigningOptions
|
|
{
|
|
Mode = "hash",
|
|
KeyId = keyId,
|
|
Algorithm = "hs256",
|
|
AllowDeterministicFallback = true,
|
|
});
|
|
|
|
return new HmacDsseSigningService(
|
|
options,
|
|
DefaultCryptoHmac.CreateForTests(),
|
|
DefaultCryptoHash.CreateForTests());
|
|
}
|
|
|
|
private sealed record TestCryptoProfile(string KeyId, string Algorithm) : ICryptoProfile;
|
|
|
|
private sealed class CapturingRekorClient : IRekorClient
|
|
{
|
|
public AttestorSubmissionRequest? LastRequest { get; private set; }
|
|
|
|
public RekorBackend? LastBackend { get; private set; }
|
|
|
|
public Task<RekorSubmissionResponse> SubmitAsync(AttestorSubmissionRequest request, RekorBackend backend, CancellationToken cancellationToken = default)
|
|
{
|
|
LastRequest = request;
|
|
LastBackend = backend;
|
|
|
|
return Task.FromResult(new RekorSubmissionResponse
|
|
{
|
|
Uuid = "rekor-uuid-1234",
|
|
Index = 1234,
|
|
LogUrl = backend.Url.ToString(),
|
|
Status = "included",
|
|
Proof = null
|
|
});
|
|
}
|
|
|
|
public Task<RekorProofResponse?> GetProofAsync(string rekorUuid, RekorBackend backend, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<RekorProofResponse?>(null);
|
|
|
|
public Task<RekorInclusionVerificationResult> VerifyInclusionAsync(string rekorUuid, byte[] payloadDigest, RekorBackend backend, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(RekorInclusionVerificationResult.Failure("not_implemented"));
|
|
}
|
|
}
|
|
|