sprints work

This commit is contained in:
StellaOps Bot
2025-12-25 12:19:12 +02:00
parent 223843f1d1
commit 2a06f780cf
224 changed files with 41796 additions and 1515 deletions

View File

@@ -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);
}
}

View File

@@ -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
};
}
}

View File

@@ -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);
}
}

View File

@@ -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>