sprints work
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// SigstoreBundleBuilderTests.cs
|
||||
// Sprint: SPRINT_8200_0001_0005 - Sigstore Bundle Implementation
|
||||
// Task: BUNDLE-8200-019 - Add unit tests for bundle builder
|
||||
// Description: Unit tests for Sigstore bundle builder
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Attestor.Bundle.Builder;
|
||||
using StellaOps.Attestor.Bundle.Models;
|
||||
using StellaOps.Attestor.Bundle.Serialization;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Bundle.Tests;
|
||||
|
||||
public class SigstoreBundleBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
public void Build_WithAllComponents_CreatesBundleSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new SigstoreBundleBuilder()
|
||||
.WithDsseEnvelope(
|
||||
"application/vnd.in-toto+json",
|
||||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
new[] { new BundleSignature { Sig = Convert.ToBase64String(new byte[64]) } })
|
||||
.WithCertificateBase64(Convert.ToBase64String(new byte[100]));
|
||||
|
||||
// Act
|
||||
var bundle = builder.Build();
|
||||
|
||||
// Assert
|
||||
bundle.Should().NotBeNull();
|
||||
bundle.MediaType.Should().Be(SigstoreBundleConstants.MediaTypeV03);
|
||||
bundle.DsseEnvelope.Should().NotBeNull();
|
||||
bundle.DsseEnvelope.PayloadType.Should().Be("application/vnd.in-toto+json");
|
||||
bundle.VerificationMaterial.Should().NotBeNull();
|
||||
bundle.VerificationMaterial.Certificate.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithPublicKeyInsteadOfCertificate_CreatesBundleSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new SigstoreBundleBuilder()
|
||||
.WithDsseEnvelope(
|
||||
"application/vnd.in-toto+json",
|
||||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
new[] { new BundleSignature { Sig = Convert.ToBase64String(new byte[64]) } })
|
||||
.WithPublicKey(new byte[32], "test-hint");
|
||||
|
||||
// Act
|
||||
var bundle = builder.Build();
|
||||
|
||||
// Assert
|
||||
bundle.Should().NotBeNull();
|
||||
bundle.VerificationMaterial.PublicKey.Should().NotBeNull();
|
||||
bundle.VerificationMaterial.PublicKey!.Hint.Should().Be("test-hint");
|
||||
bundle.VerificationMaterial.Certificate.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithRekorEntry_IncludesTlogEntry()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new SigstoreBundleBuilder()
|
||||
.WithDsseEnvelope(
|
||||
"application/vnd.in-toto+json",
|
||||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
new[] { new BundleSignature { Sig = Convert.ToBase64String(new byte[64]) } })
|
||||
.WithCertificateBase64(Convert.ToBase64String(new byte[100]))
|
||||
.WithRekorEntry(
|
||||
logIndex: "12345",
|
||||
logIdKeyId: Convert.ToBase64String(new byte[32]),
|
||||
integratedTime: "1703500000",
|
||||
canonicalizedBody: Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")));
|
||||
|
||||
// Act
|
||||
var bundle = builder.Build();
|
||||
|
||||
// Assert
|
||||
bundle.VerificationMaterial.TlogEntries.Should().HaveCount(1);
|
||||
var entry = bundle.VerificationMaterial.TlogEntries![0];
|
||||
entry.LogIndex.Should().Be("12345");
|
||||
entry.KindVersion.Kind.Should().Be("dsse");
|
||||
entry.KindVersion.Version.Should().Be("0.0.1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithMultipleRekorEntries_IncludesAllEntries()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new SigstoreBundleBuilder()
|
||||
.WithDsseEnvelope(
|
||||
"application/vnd.in-toto+json",
|
||||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
new[] { new BundleSignature { Sig = Convert.ToBase64String(new byte[64]) } })
|
||||
.WithCertificateBase64(Convert.ToBase64String(new byte[100]))
|
||||
.WithRekorEntry("1", Convert.ToBase64String(new byte[32]), "1000", Convert.ToBase64String(new byte[10]))
|
||||
.WithRekorEntry("2", Convert.ToBase64String(new byte[32]), "2000", Convert.ToBase64String(new byte[10]));
|
||||
|
||||
// Act
|
||||
var bundle = builder.Build();
|
||||
|
||||
// Assert
|
||||
bundle.VerificationMaterial.TlogEntries.Should().HaveCount(2);
|
||||
bundle.VerificationMaterial.TlogEntries![0].LogIndex.Should().Be("1");
|
||||
bundle.VerificationMaterial.TlogEntries![1].LogIndex.Should().Be("2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithInclusionProof_AddsToLastEntry()
|
||||
{
|
||||
// Arrange
|
||||
var proof = new InclusionProof
|
||||
{
|
||||
LogIndex = "12345",
|
||||
RootHash = Convert.ToBase64String(new byte[32]),
|
||||
TreeSize = "100000",
|
||||
Hashes = new[] { Convert.ToBase64String(new byte[32]) },
|
||||
Checkpoint = new Checkpoint { Envelope = "checkpoint-data" }
|
||||
};
|
||||
|
||||
var builder = new SigstoreBundleBuilder()
|
||||
.WithDsseEnvelope(
|
||||
"application/vnd.in-toto+json",
|
||||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
new[] { new BundleSignature { Sig = Convert.ToBase64String(new byte[64]) } })
|
||||
.WithCertificateBase64(Convert.ToBase64String(new byte[100]))
|
||||
.WithRekorEntry("12345", Convert.ToBase64String(new byte[32]), "1000", Convert.ToBase64String(new byte[10]))
|
||||
.WithInclusionProof(proof);
|
||||
|
||||
// Act
|
||||
var bundle = builder.Build();
|
||||
|
||||
// Assert
|
||||
bundle.VerificationMaterial.TlogEntries![0].InclusionProof.Should().NotBeNull();
|
||||
bundle.VerificationMaterial.TlogEntries![0].InclusionProof!.TreeSize.Should().Be("100000");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithTimestamps_IncludesTimestampData()
|
||||
{
|
||||
// Arrange
|
||||
var timestamps = new[] { Convert.ToBase64String(new byte[100]), Convert.ToBase64String(new byte[100]) };
|
||||
|
||||
var builder = new SigstoreBundleBuilder()
|
||||
.WithDsseEnvelope(
|
||||
"application/vnd.in-toto+json",
|
||||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
new[] { new BundleSignature { Sig = Convert.ToBase64String(new byte[64]) } })
|
||||
.WithCertificateBase64(Convert.ToBase64String(new byte[100]))
|
||||
.WithTimestamps(timestamps);
|
||||
|
||||
// Act
|
||||
var bundle = builder.Build();
|
||||
|
||||
// Assert
|
||||
bundle.VerificationMaterial.TimestampVerificationData.Should().NotBeNull();
|
||||
bundle.VerificationMaterial.TimestampVerificationData!.Rfc3161Timestamps.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithCustomMediaType_UsesCustomType()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new SigstoreBundleBuilder()
|
||||
.WithDsseEnvelope(
|
||||
"application/vnd.in-toto+json",
|
||||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
new[] { new BundleSignature { Sig = Convert.ToBase64String(new byte[64]) } })
|
||||
.WithCertificateBase64(Convert.ToBase64String(new byte[100]))
|
||||
.WithMediaType("application/vnd.dev.sigstore.bundle.v0.2+json");
|
||||
|
||||
// Act
|
||||
var bundle = builder.Build();
|
||||
|
||||
// Assert
|
||||
bundle.MediaType.Should().Be("application/vnd.dev.sigstore.bundle.v0.2+json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_MissingDsseEnvelope_ThrowsSigstoreBundleException()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new SigstoreBundleBuilder()
|
||||
.WithCertificateBase64(Convert.ToBase64String(new byte[100]));
|
||||
|
||||
// Act
|
||||
var act = () => builder.Build();
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<SigstoreBundleException>()
|
||||
.WithMessage("*DSSE*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_MissingCertificateAndPublicKey_ThrowsSigstoreBundleException()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new SigstoreBundleBuilder()
|
||||
.WithDsseEnvelope(
|
||||
"application/vnd.in-toto+json",
|
||||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
new[] { new BundleSignature { Sig = Convert.ToBase64String(new byte[64]) } });
|
||||
|
||||
// Act
|
||||
var act = () => builder.Build();
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<SigstoreBundleException>()
|
||||
.WithMessage("*certificate*public key*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithInclusionProof_WithoutRekorEntry_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var proof = new InclusionProof
|
||||
{
|
||||
LogIndex = "12345",
|
||||
RootHash = Convert.ToBase64String(new byte[32]),
|
||||
TreeSize = "100000",
|
||||
Hashes = new[] { Convert.ToBase64String(new byte[32]) },
|
||||
Checkpoint = new Checkpoint { Envelope = "checkpoint-data" }
|
||||
};
|
||||
|
||||
var builder = new SigstoreBundleBuilder();
|
||||
|
||||
// Act
|
||||
var act = () => builder.WithInclusionProof(proof);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("*Rekor entry*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildJson_ReturnsSerializedBundle()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new SigstoreBundleBuilder()
|
||||
.WithDsseEnvelope(
|
||||
"application/vnd.in-toto+json",
|
||||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
new[] { new BundleSignature { Sig = Convert.ToBase64String(new byte[64]) } })
|
||||
.WithCertificateBase64(Convert.ToBase64String(new byte[100]));
|
||||
|
||||
// Act
|
||||
var json = builder.BuildJson();
|
||||
|
||||
// Assert
|
||||
json.Should().NotBeNullOrWhiteSpace();
|
||||
json.Should().Contain("\"mediaType\"");
|
||||
json.Should().Contain("\"dsseEnvelope\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildUtf8Bytes_ReturnsSerializedBytes()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new SigstoreBundleBuilder()
|
||||
.WithDsseEnvelope(
|
||||
"application/vnd.in-toto+json",
|
||||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
new[] { new BundleSignature { Sig = Convert.ToBase64String(new byte[64]) } })
|
||||
.WithCertificateBase64(Convert.ToBase64String(new byte[100]));
|
||||
|
||||
// Act
|
||||
var bytes = builder.BuildUtf8Bytes();
|
||||
|
||||
// Assert
|
||||
bytes.Should().NotBeNullOrEmpty();
|
||||
var json = System.Text.Encoding.UTF8.GetString(bytes);
|
||||
json.Should().Contain("\"mediaType\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithDsseEnvelope_FromObject_SetsEnvelopeCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var envelope = new BundleDsseEnvelope
|
||||
{
|
||||
PayloadType = "custom/type",
|
||||
Payload = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("test")),
|
||||
Signatures = new[] { new BundleSignature { Sig = Convert.ToBase64String(new byte[32]) } }
|
||||
};
|
||||
|
||||
var builder = new SigstoreBundleBuilder()
|
||||
.WithDsseEnvelope(envelope)
|
||||
.WithCertificateBase64(Convert.ToBase64String(new byte[100]));
|
||||
|
||||
// Act
|
||||
var bundle = builder.Build();
|
||||
|
||||
// Assert
|
||||
bundle.DsseEnvelope.PayloadType.Should().Be("custom/type");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithCertificate_FromBytes_SetsCertificateCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var certBytes = new byte[] { 0x30, 0x82, 0x01, 0x00 };
|
||||
|
||||
var builder = new SigstoreBundleBuilder()
|
||||
.WithDsseEnvelope(
|
||||
"application/vnd.in-toto+json",
|
||||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
new[] { new BundleSignature { Sig = Convert.ToBase64String(new byte[64]) } })
|
||||
.WithCertificate(certBytes);
|
||||
|
||||
// Act
|
||||
var bundle = builder.Build();
|
||||
|
||||
// Assert
|
||||
bundle.VerificationMaterial.Certificate.Should().NotBeNull();
|
||||
var decoded = Convert.FromBase64String(bundle.VerificationMaterial.Certificate!.RawBytes);
|
||||
decoded.Should().BeEquivalentTo(certBytes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// SigstoreBundleSerializerTests.cs
|
||||
// Sprint: SPRINT_8200_0001_0005 - Sigstore Bundle Implementation
|
||||
// Task: BUNDLE-8200-019 - Add unit test: serialize → deserialize round-trip
|
||||
// Description: Unit tests for Sigstore bundle serialization
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Attestor.Bundle.Builder;
|
||||
using StellaOps.Attestor.Bundle.Models;
|
||||
using StellaOps.Attestor.Bundle.Serialization;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Bundle.Tests;
|
||||
|
||||
public class SigstoreBundleSerializerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Serialize_ValidBundle_ProducesValidJson()
|
||||
{
|
||||
// Arrange
|
||||
var bundle = CreateValidBundle();
|
||||
|
||||
// Act
|
||||
var json = SigstoreBundleSerializer.Serialize(bundle);
|
||||
|
||||
// Assert
|
||||
json.Should().NotBeNullOrWhiteSpace();
|
||||
json.Should().Contain("\"mediaType\"");
|
||||
json.Should().Contain("\"verificationMaterial\"");
|
||||
json.Should().Contain("\"dsseEnvelope\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerializeToUtf8Bytes_ValidBundle_ProducesValidBytes()
|
||||
{
|
||||
// Arrange
|
||||
var bundle = CreateValidBundle();
|
||||
|
||||
// Act
|
||||
var bytes = SigstoreBundleSerializer.SerializeToUtf8Bytes(bundle);
|
||||
|
||||
// Assert
|
||||
bytes.Should().NotBeNullOrEmpty();
|
||||
var json = System.Text.Encoding.UTF8.GetString(bytes);
|
||||
json.Should().Contain("\"mediaType\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_ValidJson_ReturnsBundle()
|
||||
{
|
||||
// Arrange
|
||||
var json = CreateValidBundleJson();
|
||||
|
||||
// Act
|
||||
var bundle = SigstoreBundleSerializer.Deserialize(json);
|
||||
|
||||
// Assert
|
||||
bundle.Should().NotBeNull();
|
||||
bundle.MediaType.Should().Be(SigstoreBundleConstants.MediaTypeV03);
|
||||
bundle.DsseEnvelope.Should().NotBeNull();
|
||||
bundle.VerificationMaterial.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_Utf8Bytes_ReturnsBundle()
|
||||
{
|
||||
// Arrange
|
||||
var json = CreateValidBundleJson();
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(json);
|
||||
|
||||
// Act
|
||||
var bundle = SigstoreBundleSerializer.Deserialize(bytes);
|
||||
|
||||
// Assert
|
||||
bundle.Should().NotBeNull();
|
||||
bundle.MediaType.Should().Be(SigstoreBundleConstants.MediaTypeV03);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTrip_SerializeDeserialize_PreservesData()
|
||||
{
|
||||
// Arrange
|
||||
var original = CreateValidBundle();
|
||||
|
||||
// Act
|
||||
var json = SigstoreBundleSerializer.Serialize(original);
|
||||
var deserialized = SigstoreBundleSerializer.Deserialize(json);
|
||||
|
||||
// Assert
|
||||
deserialized.MediaType.Should().Be(original.MediaType);
|
||||
deserialized.DsseEnvelope.PayloadType.Should().Be(original.DsseEnvelope.PayloadType);
|
||||
deserialized.DsseEnvelope.Payload.Should().Be(original.DsseEnvelope.Payload);
|
||||
deserialized.DsseEnvelope.Signatures.Should().HaveCount(original.DsseEnvelope.Signatures.Count);
|
||||
deserialized.VerificationMaterial.Certificate.Should().NotBeNull();
|
||||
deserialized.VerificationMaterial.Certificate!.RawBytes
|
||||
.Should().Be(original.VerificationMaterial.Certificate!.RawBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTrip_WithTlogEntries_PreservesEntries()
|
||||
{
|
||||
// Arrange
|
||||
var original = CreateBundleWithTlogEntry();
|
||||
|
||||
// Act
|
||||
var json = SigstoreBundleSerializer.Serialize(original);
|
||||
var deserialized = SigstoreBundleSerializer.Deserialize(json);
|
||||
|
||||
// Assert
|
||||
deserialized.VerificationMaterial.TlogEntries.Should().HaveCount(1);
|
||||
var entry = deserialized.VerificationMaterial.TlogEntries![0];
|
||||
entry.LogIndex.Should().Be("12345");
|
||||
entry.LogId.KeyId.Should().NotBeNullOrEmpty();
|
||||
entry.KindVersion.Kind.Should().Be("dsse");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryDeserialize_ValidJson_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var json = CreateValidBundleJson();
|
||||
|
||||
// Act
|
||||
var result = SigstoreBundleSerializer.TryDeserialize(json, out var bundle);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
bundle.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryDeserialize_InvalidJson_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var json = "{ invalid json }";
|
||||
|
||||
// Act
|
||||
var result = SigstoreBundleSerializer.TryDeserialize(json, out var bundle);
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
bundle.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryDeserialize_NullOrEmpty_ReturnsFalse()
|
||||
{
|
||||
// Act & Assert
|
||||
SigstoreBundleSerializer.TryDeserialize(null!, out _).Should().BeFalse();
|
||||
SigstoreBundleSerializer.TryDeserialize("", out _).Should().BeFalse();
|
||||
SigstoreBundleSerializer.TryDeserialize(" ", out _).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_MissingMediaType_ThrowsSigstoreBundleException()
|
||||
{
|
||||
// Arrange - JSON that deserializes but fails validation
|
||||
var json = """{"mediaType":"","verificationMaterial":{"certificate":{"rawBytes":"AAAA"}},"dsseEnvelope":{"payloadType":"test","payload":"e30=","signatures":[{"sig":"AAAA"}]}}""";
|
||||
|
||||
// Act
|
||||
var act = () => SigstoreBundleSerializer.Deserialize(json);
|
||||
|
||||
// Assert - Validation catches empty mediaType
|
||||
act.Should().Throw<SigstoreBundleException>()
|
||||
.WithMessage("*mediaType*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_MissingDsseEnvelope_ThrowsSigstoreBundleException()
|
||||
{
|
||||
// Arrange - JSON with null dsseEnvelope
|
||||
var json = """{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"certificate":{"rawBytes":"AAAA"}},"dsseEnvelope":null}""";
|
||||
|
||||
// Act
|
||||
var act = () => SigstoreBundleSerializer.Deserialize(json);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<SigstoreBundleException>()
|
||||
.WithMessage("*dsseEnvelope*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serialize_NullBundle_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act
|
||||
var act = () => SigstoreBundleSerializer.Serialize(null!);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
private static SigstoreBundle CreateValidBundle()
|
||||
{
|
||||
return new SigstoreBundleBuilder()
|
||||
.WithDsseEnvelope(
|
||||
"application/vnd.in-toto+json",
|
||||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
new[] { new BundleSignature { Sig = Convert.ToBase64String(new byte[64]) } })
|
||||
.WithCertificateBase64(Convert.ToBase64String(CreateTestCertificateBytes()))
|
||||
.Build();
|
||||
}
|
||||
|
||||
private static SigstoreBundle CreateBundleWithTlogEntry()
|
||||
{
|
||||
return new SigstoreBundleBuilder()
|
||||
.WithDsseEnvelope(
|
||||
"application/vnd.in-toto+json",
|
||||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
new[] { new BundleSignature { Sig = Convert.ToBase64String(new byte[64]) } })
|
||||
.WithCertificateBase64(Convert.ToBase64String(CreateTestCertificateBytes()))
|
||||
.WithRekorEntry(
|
||||
logIndex: "12345",
|
||||
logIdKeyId: Convert.ToBase64String(new byte[32]),
|
||||
integratedTime: "1703500000",
|
||||
canonicalizedBody: Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")))
|
||||
.Build();
|
||||
}
|
||||
|
||||
private static string CreateValidBundleJson()
|
||||
{
|
||||
var bundle = CreateValidBundle();
|
||||
return SigstoreBundleSerializer.Serialize(bundle);
|
||||
}
|
||||
|
||||
private static byte[] CreateTestCertificateBytes()
|
||||
{
|
||||
// Minimal DER-encoded certificate placeholder
|
||||
// In real tests, use a proper test certificate
|
||||
return new byte[]
|
||||
{
|
||||
0x30, 0x82, 0x01, 0x00, // SEQUENCE, length
|
||||
0x30, 0x81, 0xB0, // TBSCertificate SEQUENCE
|
||||
0x02, 0x01, 0x01, // Version
|
||||
0x02, 0x01, 0x01, // Serial number
|
||||
0x30, 0x0D, // Algorithm ID
|
||||
0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B,
|
||||
0x05, 0x00
|
||||
// ... truncated for test purposes
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// SigstoreBundleVerifierTests.cs
|
||||
// Sprint: SPRINT_8200_0001_0005 - Sigstore Bundle Implementation
|
||||
// Tasks: BUNDLE-8200-020, BUNDLE-8200-021 - Bundle verification tests
|
||||
// Description: Unit tests for Sigstore bundle verification
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Attestor.Bundle.Builder;
|
||||
using StellaOps.Attestor.Bundle.Models;
|
||||
using StellaOps.Attestor.Bundle.Verification;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Bundle.Tests;
|
||||
|
||||
public class SigstoreBundleVerifierTests
|
||||
{
|
||||
private readonly SigstoreBundleVerifier _verifier = new();
|
||||
|
||||
[Fact]
|
||||
public async Task Verify_MissingDsseEnvelope_ReturnsFailed()
|
||||
{
|
||||
// Arrange
|
||||
var bundle = new SigstoreBundle
|
||||
{
|
||||
MediaType = SigstoreBundleConstants.MediaTypeV03,
|
||||
VerificationMaterial = new VerificationMaterial
|
||||
{
|
||||
Certificate = new CertificateInfo { RawBytes = Convert.ToBase64String(new byte[32]) }
|
||||
},
|
||||
DsseEnvelope = null!
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _verifier.VerifyAsync(bundle);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Code == BundleVerificationErrorCode.MissingDsseEnvelope);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Verify_MissingCertificateAndPublicKey_ReturnsFailed()
|
||||
{
|
||||
// Arrange
|
||||
var bundle = new SigstoreBundle
|
||||
{
|
||||
MediaType = SigstoreBundleConstants.MediaTypeV03,
|
||||
VerificationMaterial = new VerificationMaterial(),
|
||||
DsseEnvelope = new BundleDsseEnvelope
|
||||
{
|
||||
PayloadType = "application/vnd.in-toto+json",
|
||||
Payload = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
Signatures = new[] { new BundleSignature { Sig = Convert.ToBase64String(new byte[64]) } }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _verifier.VerifyAsync(bundle);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Code == BundleVerificationErrorCode.MissingCertificate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Verify_EmptyMediaType_ReturnsFailed()
|
||||
{
|
||||
// Arrange
|
||||
var bundle = new SigstoreBundle
|
||||
{
|
||||
MediaType = "",
|
||||
VerificationMaterial = new VerificationMaterial
|
||||
{
|
||||
Certificate = new CertificateInfo { RawBytes = Convert.ToBase64String(new byte[32]) }
|
||||
},
|
||||
DsseEnvelope = new BundleDsseEnvelope
|
||||
{
|
||||
PayloadType = "application/vnd.in-toto+json",
|
||||
Payload = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
Signatures = new[] { new BundleSignature { Sig = Convert.ToBase64String(new byte[64]) } }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _verifier.VerifyAsync(bundle);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Code == BundleVerificationErrorCode.InvalidBundleStructure);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Verify_NoSignaturesInEnvelope_ReturnsFailed()
|
||||
{
|
||||
// Arrange
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
var certBytes = CreateSelfSignedCertificateBytes(ecdsa);
|
||||
|
||||
var bundle = new SigstoreBundleBuilder()
|
||||
.WithDsseEnvelope(
|
||||
"application/vnd.in-toto+json",
|
||||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
Array.Empty<BundleSignature>())
|
||||
.WithCertificateBase64(Convert.ToBase64String(certBytes))
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var result = await _verifier.VerifyAsync(bundle);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Code == BundleVerificationErrorCode.DsseSignatureInvalid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Verify_InvalidSignature_ReturnsFailed()
|
||||
{
|
||||
// Arrange
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
var certBytes = CreateSelfSignedCertificateBytes(ecdsa);
|
||||
|
||||
var bundle = new SigstoreBundleBuilder()
|
||||
.WithDsseEnvelope(
|
||||
"application/vnd.in-toto+json",
|
||||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
new[] { new BundleSignature { Sig = Convert.ToBase64String(new byte[64]) } })
|
||||
.WithCertificateBase64(Convert.ToBase64String(certBytes))
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var result = await _verifier.VerifyAsync(bundle);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Code == BundleVerificationErrorCode.DsseSignatureInvalid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Verify_ValidEcdsaSignature_ReturnsPassed()
|
||||
{
|
||||
// Arrange
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
var certBytes = CreateSelfSignedCertificateBytes(ecdsa);
|
||||
var payload = System.Text.Encoding.UTF8.GetBytes("{}");
|
||||
var payloadType = "application/vnd.in-toto+json";
|
||||
|
||||
// Create PAE message for signing
|
||||
var paeMessage = ConstructPae(payloadType, payload);
|
||||
var signature = ecdsa.SignData(paeMessage, HashAlgorithmName.SHA256);
|
||||
|
||||
var bundle = new SigstoreBundleBuilder()
|
||||
.WithDsseEnvelope(
|
||||
payloadType,
|
||||
Convert.ToBase64String(payload),
|
||||
new[] { new BundleSignature { Sig = Convert.ToBase64String(signature) } })
|
||||
.WithCertificateBase64(Convert.ToBase64String(certBytes))
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var result = await _verifier.VerifyAsync(bundle);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.Checks.DsseSignature.Should().Be(CheckResult.Passed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Verify_TamperedPayload_ReturnsFailed()
|
||||
{
|
||||
// Arrange
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
var certBytes = CreateSelfSignedCertificateBytes(ecdsa);
|
||||
var originalPayload = System.Text.Encoding.UTF8.GetBytes("{}");
|
||||
var payloadType = "application/vnd.in-toto+json";
|
||||
|
||||
// Sign the original payload
|
||||
var paeMessage = ConstructPae(payloadType, originalPayload);
|
||||
var signature = ecdsa.SignData(paeMessage, HashAlgorithmName.SHA256);
|
||||
|
||||
// Build bundle with tampered payload
|
||||
var tamperedPayload = System.Text.Encoding.UTF8.GetBytes("{\"tampered\":true}");
|
||||
var bundle = new SigstoreBundleBuilder()
|
||||
.WithDsseEnvelope(
|
||||
payloadType,
|
||||
Convert.ToBase64String(tamperedPayload),
|
||||
new[] { new BundleSignature { Sig = Convert.ToBase64String(signature) } })
|
||||
.WithCertificateBase64(Convert.ToBase64String(certBytes))
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var result = await _verifier.VerifyAsync(bundle);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Code == BundleVerificationErrorCode.DsseSignatureInvalid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Verify_WithVerificationTimeInPast_ValidatesCertificate()
|
||||
{
|
||||
// Arrange
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
var certBytes = CreateSelfSignedCertificateBytes(ecdsa);
|
||||
var payload = System.Text.Encoding.UTF8.GetBytes("{}");
|
||||
var payloadType = "application/vnd.in-toto+json";
|
||||
|
||||
var paeMessage = ConstructPae(payloadType, payload);
|
||||
var signature = ecdsa.SignData(paeMessage, HashAlgorithmName.SHA256);
|
||||
|
||||
var bundle = new SigstoreBundleBuilder()
|
||||
.WithDsseEnvelope(
|
||||
payloadType,
|
||||
Convert.ToBase64String(payload),
|
||||
new[] { new BundleSignature { Sig = Convert.ToBase64String(signature) } })
|
||||
.WithCertificateBase64(Convert.ToBase64String(certBytes))
|
||||
.Build();
|
||||
|
||||
var options = new BundleVerificationOptions
|
||||
{
|
||||
VerificationTime = DateTimeOffset.UtcNow.AddYears(-10) // Before cert was valid
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _verifier.VerifyAsync(bundle, options);
|
||||
|
||||
// Assert
|
||||
result.Checks.CertificateChain.Should().Be(CheckResult.Failed);
|
||||
result.Errors.Should().Contain(e => e.Code == BundleVerificationErrorCode.CertificateNotYetValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Verify_SkipsInclusionProofWhenNotPresent()
|
||||
{
|
||||
// Arrange
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
var certBytes = CreateSelfSignedCertificateBytes(ecdsa);
|
||||
var payload = System.Text.Encoding.UTF8.GetBytes("{}");
|
||||
var payloadType = "application/vnd.in-toto+json";
|
||||
|
||||
var paeMessage = ConstructPae(payloadType, payload);
|
||||
var signature = ecdsa.SignData(paeMessage, HashAlgorithmName.SHA256);
|
||||
|
||||
var bundle = new SigstoreBundleBuilder()
|
||||
.WithDsseEnvelope(
|
||||
payloadType,
|
||||
Convert.ToBase64String(payload),
|
||||
new[] { new BundleSignature { Sig = Convert.ToBase64String(signature) } })
|
||||
.WithCertificateBase64(Convert.ToBase64String(certBytes))
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var result = await _verifier.VerifyAsync(bundle);
|
||||
|
||||
// Assert
|
||||
result.Checks.InclusionProof.Should().Be(CheckResult.Skipped);
|
||||
result.Checks.TransparencyLog.Should().Be(CheckResult.Skipped);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Verify_NullBundle_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act
|
||||
var act = async () => await _verifier.VerifyAsync(null!);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<ArgumentNullException>();
|
||||
}
|
||||
|
||||
private static byte[] ConstructPae(string payloadType, byte[] payload)
|
||||
{
|
||||
const string DssePrefix = "DSSEv1";
|
||||
const byte Space = 0x20;
|
||||
|
||||
var typeBytes = System.Text.Encoding.UTF8.GetBytes(payloadType);
|
||||
var typeLenBytes = System.Text.Encoding.UTF8.GetBytes(typeBytes.Length.ToString());
|
||||
var payloadLenBytes = System.Text.Encoding.UTF8.GetBytes(payload.Length.ToString());
|
||||
var prefixBytes = System.Text.Encoding.UTF8.GetBytes(DssePrefix);
|
||||
|
||||
var totalLength = prefixBytes.Length + 1 + typeLenBytes.Length + 1 +
|
||||
typeBytes.Length + 1 + payloadLenBytes.Length + 1 + payload.Length;
|
||||
|
||||
var pae = new byte[totalLength];
|
||||
var offset = 0;
|
||||
|
||||
Buffer.BlockCopy(prefixBytes, 0, pae, offset, prefixBytes.Length);
|
||||
offset += prefixBytes.Length;
|
||||
pae[offset++] = Space;
|
||||
|
||||
Buffer.BlockCopy(typeLenBytes, 0, pae, offset, typeLenBytes.Length);
|
||||
offset += typeLenBytes.Length;
|
||||
pae[offset++] = Space;
|
||||
|
||||
Buffer.BlockCopy(typeBytes, 0, pae, offset, typeBytes.Length);
|
||||
offset += typeBytes.Length;
|
||||
pae[offset++] = Space;
|
||||
|
||||
Buffer.BlockCopy(payloadLenBytes, 0, pae, offset, payloadLenBytes.Length);
|
||||
offset += payloadLenBytes.Length;
|
||||
pae[offset++] = Space;
|
||||
|
||||
Buffer.BlockCopy(payload, 0, pae, offset, payload.Length);
|
||||
|
||||
return pae;
|
||||
}
|
||||
|
||||
private static byte[] CreateSelfSignedCertificateBytes(ECDsa ecdsa)
|
||||
{
|
||||
var request = new System.Security.Cryptography.X509Certificates.CertificateRequest(
|
||||
"CN=Test",
|
||||
ecdsa,
|
||||
HashAlgorithmName.SHA256);
|
||||
|
||||
using var cert = request.CreateSelfSigned(
|
||||
DateTimeOffset.UtcNow.AddDays(-1),
|
||||
DateTimeOffset.UtcNow.AddYears(1));
|
||||
|
||||
return cert.Export(System.Security.Cryptography.X509Certificates.X509ContentType.Cert);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="FluentAssertions" Version="8.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Attestor.Bundle\StellaOps.Attestor.Bundle.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user