sprints work
This commit is contained in:
@@ -0,0 +1,354 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// DsseNegativeTests.cs
|
||||
// Sprint: SPRINT_8200_0001_0002_dsse_roundtrip_testing
|
||||
// Tasks: DSSE-8200-016, DSSE-8200-017, DSSE-8200-018
|
||||
// Description: DSSE negative/error handling tests
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Envelope.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Negative tests for DSSE envelope verification.
|
||||
/// Validates error handling for expired certs, wrong keys, and malformed data.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Category", "DsseNegative")]
|
||||
public sealed class DsseNegativeTests : IDisposable
|
||||
{
|
||||
private readonly DsseRoundtripTestFixture _fixture;
|
||||
|
||||
public DsseNegativeTests()
|
||||
{
|
||||
_fixture = new DsseRoundtripTestFixture();
|
||||
}
|
||||
|
||||
// DSSE-8200-016: Expired certificate → verify fails with clear error
|
||||
// Note: Testing certificate expiry requires X.509 certificate infrastructure.
|
||||
// These tests use simulated scenarios or self-signed certs.
|
||||
|
||||
[Fact]
|
||||
public void Verify_WithExpiredCertificateSimulation_FailsGracefully()
|
||||
{
|
||||
// Arrange - Sign with the fixture (simulates current key)
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
var envelope = _fixture.Sign(payload);
|
||||
|
||||
// Simulate "expired" by creating a verification with a different key
|
||||
// In production, certificate expiry would be checked by the verifier
|
||||
using var expiredFixture = new DsseRoundtripTestFixture();
|
||||
|
||||
// Act - Verify with "expired" key (different fixture)
|
||||
var verified = expiredFixture.Verify(envelope);
|
||||
var detailedResult = expiredFixture.VerifyDetailed(envelope);
|
||||
|
||||
// Assert
|
||||
verified.Should().BeFalse("verification with different key should fail");
|
||||
detailedResult.IsValid.Should().BeFalse();
|
||||
detailedResult.SignatureResults.Should().Contain(r => !r.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_SignatureFromRevokedKey_FailsWithDetailedError()
|
||||
{
|
||||
// Arrange - Create envelope with one key
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
using var originalFixture = new DsseRoundtripTestFixture();
|
||||
var envelope = originalFixture.Sign(payload);
|
||||
|
||||
// Act - Try to verify with different key (simulates key revocation scenario)
|
||||
using var differentFixture = new DsseRoundtripTestFixture();
|
||||
var result = differentFixture.VerifyDetailed(envelope);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.SignatureResults.Should().HaveCount(1);
|
||||
result.SignatureResults[0].IsValid.Should().BeFalse();
|
||||
result.SignatureResults[0].FailureReason.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
// DSSE-8200-017: Wrong key type → verify fails
|
||||
|
||||
[Fact]
|
||||
public void Verify_WithWrongKeyType_Fails()
|
||||
{
|
||||
// Arrange - Sign with P-256
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
var envelope = _fixture.Sign(payload);
|
||||
|
||||
// Act - Try to verify with P-384 key (wrong curve)
|
||||
using var wrongCurveKey = ECDsa.Create(ECCurve.NamedCurves.nistP384);
|
||||
using var wrongCurveFixture = new DsseRoundtripTestFixture(wrongCurveKey, "p384-key");
|
||||
var verified = wrongCurveFixture.Verify(envelope);
|
||||
|
||||
// Assert
|
||||
verified.Should().BeFalse("verification with wrong curve should fail");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_WithMismatchedKeyId_SkipsSignature()
|
||||
{
|
||||
// Arrange
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
var envelope = _fixture.Sign(payload);
|
||||
|
||||
// Act - Create fixture with different key ID
|
||||
using var differentKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
using var differentIdFixture = new DsseRoundtripTestFixture(differentKey, "completely-different-key-id");
|
||||
var result = differentIdFixture.VerifyDetailed(envelope);
|
||||
|
||||
// Assert - Should skip due to key ID mismatch (unless keyId is null)
|
||||
result.IsValid.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_WithNullKeyId_MatchesAnyKey()
|
||||
{
|
||||
// Arrange - Create signature with null key ID
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
var pae = BuildPae("application/vnd.in-toto+json", payload);
|
||||
|
||||
using var key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
var signatureBytes = key.SignData(pae, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence);
|
||||
var signature = DsseSignature.FromBytes(signatureBytes, null); // null key ID
|
||||
|
||||
var envelope = new DsseEnvelope("application/vnd.in-toto+json", payload, [signature]);
|
||||
|
||||
// Act - Verify with same key but different fixture (null keyId should still match)
|
||||
using var verifyFixture = new DsseRoundtripTestFixture(key, "any-key-id");
|
||||
var verified = verifyFixture.Verify(envelope);
|
||||
|
||||
// Assert - null keyId in signature should be attempted with any verifying key
|
||||
verified.Should().BeTrue("null keyId should allow verification attempt");
|
||||
}
|
||||
|
||||
// DSSE-8200-018: Truncated/malformed envelope → parse fails gracefully
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_TruncatedJson_ThrowsJsonException()
|
||||
{
|
||||
// Arrange
|
||||
var validJson = """{"payloadType":"application/vnd.in-toto+json","payload":"dGVzdA==","signatures":[{"sig":"YWJj""";
|
||||
|
||||
// Act & Assert
|
||||
var act = () => DsseRoundtripTestFixture.DeserializeFromBytes(Encoding.UTF8.GetBytes(validJson));
|
||||
act.Should().Throw<JsonException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_MissingPayloadType_ThrowsKeyNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
var invalidJson = """{"payload":"dGVzdA==","signatures":[{"sig":"YWJj"}]}""";
|
||||
|
||||
// Act & Assert - GetProperty throws KeyNotFoundException when key is missing
|
||||
var act = () => DsseRoundtripTestFixture.DeserializeFromBytes(Encoding.UTF8.GetBytes(invalidJson));
|
||||
act.Should().Throw<KeyNotFoundException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_MissingPayload_ThrowsKeyNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
var invalidJson = """{"payloadType":"application/vnd.in-toto+json","signatures":[{"sig":"YWJj"}]}""";
|
||||
|
||||
// Act & Assert - GetProperty throws KeyNotFoundException when key is missing
|
||||
var act = () => DsseRoundtripTestFixture.DeserializeFromBytes(Encoding.UTF8.GetBytes(invalidJson));
|
||||
act.Should().Throw<KeyNotFoundException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_MissingSignatures_ThrowsKeyNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
var invalidJson = """{"payloadType":"application/vnd.in-toto+json","payload":"dGVzdA=="}""";
|
||||
|
||||
// Act & Assert - GetProperty throws KeyNotFoundException when key is missing
|
||||
var act = () => DsseRoundtripTestFixture.DeserializeFromBytes(Encoding.UTF8.GetBytes(invalidJson));
|
||||
act.Should().Throw<KeyNotFoundException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_EmptySignaturesArray_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
var invalidJson = """{"payloadType":"application/vnd.in-toto+json","payload":"dGVzdA==","signatures":[]}""";
|
||||
|
||||
// Act & Assert
|
||||
var act = () => DsseRoundtripTestFixture.DeserializeFromBytes(Encoding.UTF8.GetBytes(invalidJson));
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithMessage("*signature*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_InvalidBase64Payload_ThrowsFormatException()
|
||||
{
|
||||
// Arrange
|
||||
var invalidJson = """{"payloadType":"application/vnd.in-toto+json","payload":"not-valid-base64!!!","signatures":[{"sig":"YWJj"}]}""";
|
||||
|
||||
// Act & Assert
|
||||
var act = () => DsseRoundtripTestFixture.DeserializeFromBytes(Encoding.UTF8.GetBytes(invalidJson));
|
||||
act.Should().Throw<FormatException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_MissingSignatureInSignature_ThrowsKeyNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
var invalidJson = """{"payloadType":"application/vnd.in-toto+json","payload":"dGVzdA==","signatures":[{"keyid":"key-1"}]}""";
|
||||
|
||||
// Act & Assert - GetProperty throws KeyNotFoundException when key is missing
|
||||
var act = () => DsseRoundtripTestFixture.DeserializeFromBytes(Encoding.UTF8.GetBytes(invalidJson));
|
||||
act.Should().Throw<KeyNotFoundException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_EmptyPayload_Succeeds()
|
||||
{
|
||||
// Arrange - Empty payload is technically valid base64
|
||||
var validJson = """{"payloadType":"application/vnd.in-toto+json","payload":"","signatures":[{"sig":"YWJj"}]}""";
|
||||
|
||||
// Act
|
||||
var envelope = DsseRoundtripTestFixture.DeserializeFromBytes(Encoding.UTF8.GetBytes(validJson));
|
||||
|
||||
// Assert
|
||||
envelope.Payload.Length.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_InvalidBase64Signature_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
var invalidSig = new DsseSignature("not-valid-base64!!!", _fixture.KeyId);
|
||||
var envelope = new DsseEnvelope("application/vnd.in-toto+json", payload, [invalidSig]);
|
||||
|
||||
// Act
|
||||
var verified = _fixture.Verify(envelope);
|
||||
|
||||
// Assert
|
||||
verified.Should().BeFalse("invalid base64 signature should not verify");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_MalformedSignatureBytes_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
var malformedSig = DsseSignature.FromBytes([0x01, 0x02, 0x03], _fixture.KeyId); // Too short for ECDSA
|
||||
var envelope = new DsseEnvelope("application/vnd.in-toto+json", payload, [malformedSig]);
|
||||
|
||||
// Act
|
||||
var verified = _fixture.Verify(envelope);
|
||||
|
||||
// Assert
|
||||
verified.Should().BeFalse("malformed signature bytes should not verify");
|
||||
}
|
||||
|
||||
// Bundle negative tests
|
||||
|
||||
[Fact]
|
||||
public void BundleDeserialize_TruncatedJson_ThrowsJsonException()
|
||||
{
|
||||
// Arrange
|
||||
var truncated = """{"mediaType":"application/vnd.dev.sigstore""";
|
||||
|
||||
// Act & Assert
|
||||
var act = () => SigstoreTestBundle.Deserialize(Encoding.UTF8.GetBytes(truncated));
|
||||
act.Should().Throw<JsonException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BundleDeserialize_MissingDsseEnvelope_ThrowsKeyNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
var missingEnvelope = """{"mediaType":"test","verificationMaterial":{"publicKey":{"hint":"k","rawBytes":"YWJj"},"algorithm":"ES256"}}""";
|
||||
|
||||
// Act & Assert - GetProperty throws KeyNotFoundException when key is missing
|
||||
var act = () => SigstoreTestBundle.Deserialize(Encoding.UTF8.GetBytes(missingEnvelope));
|
||||
act.Should().Throw<KeyNotFoundException>();
|
||||
}
|
||||
|
||||
// Edge cases
|
||||
|
||||
[Fact]
|
||||
public void Sign_EmptyPayload_FailsValidation()
|
||||
{
|
||||
// Arrange
|
||||
var emptyPayload = Array.Empty<byte>();
|
||||
|
||||
// Act & Assert - DsseEnvelope allows empty payload (technically), but signing behavior depends on PAE
|
||||
// Note: Empty payload is unusual but not necessarily invalid in DSSE spec
|
||||
var envelope = _fixture.Sign(emptyPayload);
|
||||
var verified = _fixture.Verify(envelope);
|
||||
|
||||
envelope.Payload.Length.Should().Be(0);
|
||||
verified.Should().BeTrue("empty payload is valid DSSE");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_ModifiedPayloadType_Fails()
|
||||
{
|
||||
// Arrange
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
var envelope = _fixture.Sign(payload);
|
||||
|
||||
// Act - Create new envelope with modified payloadType
|
||||
var modifiedEnvelope = new DsseEnvelope(
|
||||
"application/vnd.different-type+json", // Different type
|
||||
envelope.Payload,
|
||||
envelope.Signatures);
|
||||
|
||||
// Assert
|
||||
_fixture.Verify(modifiedEnvelope).Should().BeFalse("modified payloadType changes PAE and invalidates signature");
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
private static byte[] BuildPae(string payloadType, byte[] payload)
|
||||
{
|
||||
const string preamble = "DSSEv1 ";
|
||||
|
||||
var payloadTypeBytes = Encoding.UTF8.GetBytes(payloadType);
|
||||
var payloadTypeLenStr = payloadTypeBytes.Length.ToString();
|
||||
var payloadLenStr = payload.Length.ToString();
|
||||
|
||||
var totalLength = preamble.Length
|
||||
+ payloadTypeLenStr.Length + 1 + payloadTypeBytes.Length + 1
|
||||
+ payloadLenStr.Length + 1 + payload.Length;
|
||||
|
||||
var pae = new byte[totalLength];
|
||||
var offset = 0;
|
||||
|
||||
Encoding.UTF8.GetBytes(preamble, pae.AsSpan(offset));
|
||||
offset += preamble.Length;
|
||||
|
||||
Encoding.UTF8.GetBytes(payloadTypeLenStr, pae.AsSpan(offset));
|
||||
offset += payloadTypeLenStr.Length;
|
||||
pae[offset++] = (byte)' ';
|
||||
|
||||
payloadTypeBytes.CopyTo(pae.AsSpan(offset));
|
||||
offset += payloadTypeBytes.Length;
|
||||
pae[offset++] = (byte)' ';
|
||||
|
||||
Encoding.UTF8.GetBytes(payloadLenStr, pae.AsSpan(offset));
|
||||
offset += payloadLenStr.Length;
|
||||
pae[offset++] = (byte)' ';
|
||||
|
||||
payload.CopyTo(pae.AsSpan(offset));
|
||||
|
||||
return pae;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_fixture.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// DsseRebundleTests.cs
|
||||
// Sprint: SPRINT_8200_0001_0002_dsse_roundtrip_testing
|
||||
// Tasks: DSSE-8200-007, DSSE-8200-008, DSSE-8200-009
|
||||
// Description: DSSE re-bundling verification tests
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Envelope.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for DSSE envelope re-bundling operations.
|
||||
/// Validates sign → bundle → extract → re-bundle → verify cycles.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Category", "DsseRebundle")]
|
||||
public sealed class DsseRebundleTests : IDisposable
|
||||
{
|
||||
private readonly DsseRoundtripTestFixture _fixture;
|
||||
|
||||
public DsseRebundleTests()
|
||||
{
|
||||
_fixture = new DsseRoundtripTestFixture();
|
||||
}
|
||||
|
||||
// DSSE-8200-007: Full round-trip through bundle
|
||||
|
||||
[Fact]
|
||||
public void SignBundleExtractRebundleVerify_FullRoundTrip_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
var envelope = _fixture.Sign(payload);
|
||||
_fixture.Verify(envelope).Should().BeTrue("original envelope should verify");
|
||||
|
||||
// Act - Bundle
|
||||
var bundle1 = _fixture.CreateSigstoreBundle(envelope);
|
||||
var bundleBytes = bundle1.Serialize();
|
||||
|
||||
// Act - Extract
|
||||
var extractedBundle = SigstoreTestBundle.Deserialize(bundleBytes);
|
||||
var extractedEnvelope = DsseRoundtripTestFixture.ExtractFromBundle(extractedBundle);
|
||||
|
||||
// Act - Re-bundle
|
||||
var rebundle = _fixture.CreateSigstoreBundle(extractedEnvelope);
|
||||
var rebundleBytes = rebundle.Serialize();
|
||||
|
||||
// Act - Extract again and verify
|
||||
var finalBundle = SigstoreTestBundle.Deserialize(rebundleBytes);
|
||||
var finalEnvelope = DsseRoundtripTestFixture.ExtractFromBundle(finalBundle);
|
||||
var finalVerified = _fixture.Verify(finalEnvelope);
|
||||
|
||||
// Assert
|
||||
finalVerified.Should().BeTrue("re-bundled envelope should verify");
|
||||
finalEnvelope.Payload.ToArray().Should().BeEquivalentTo(envelope.Payload.ToArray());
|
||||
finalEnvelope.PayloadType.Should().Be(envelope.PayloadType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SignBundleExtractRebundleVerify_WithBundleKey_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
var envelope = _fixture.Sign(payload);
|
||||
|
||||
// Act - Bundle with embedded key
|
||||
var bundle = _fixture.CreateSigstoreBundle(envelope);
|
||||
|
||||
// Act - Extract and verify using bundle's embedded key
|
||||
var extractedEnvelope = DsseRoundtripTestFixture.ExtractFromBundle(bundle);
|
||||
var verifiedWithBundleKey = DsseRoundtripTestFixture.VerifyWithBundleKey(extractedEnvelope, bundle);
|
||||
|
||||
// Assert
|
||||
verifiedWithBundleKey.Should().BeTrue("envelope should verify with bundle's embedded key");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bundle_PreservesEnvelopeIntegrity()
|
||||
{
|
||||
// Arrange
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
var envelope = _fixture.Sign(payload);
|
||||
var originalBytes = DsseRoundtripTestFixture.SerializeToBytes(envelope);
|
||||
|
||||
// Act
|
||||
var bundle = _fixture.CreateSigstoreBundle(envelope);
|
||||
var extractedEnvelope = DsseRoundtripTestFixture.ExtractFromBundle(bundle);
|
||||
var extractedBytes = DsseRoundtripTestFixture.SerializeToBytes(extractedEnvelope);
|
||||
|
||||
// Assert - Envelope bytes should be identical
|
||||
extractedBytes.Should().BeEquivalentTo(originalBytes, "bundling should not modify envelope");
|
||||
}
|
||||
|
||||
// DSSE-8200-008: Archive to tar.gz → extract → verify
|
||||
|
||||
[Fact]
|
||||
public async Task SignBundleArchiveExtractVerify_ThroughGzipArchive_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
var envelope = _fixture.Sign(payload);
|
||||
var bundle = _fixture.CreateSigstoreBundle(envelope);
|
||||
var bundleBytes = bundle.Serialize();
|
||||
|
||||
var archivePath = Path.Combine(Path.GetTempPath(), $"dsse-archive-{Guid.NewGuid():N}.tar.gz");
|
||||
var extractPath = Path.Combine(Path.GetTempPath(), $"dsse-extract-{Guid.NewGuid():N}");
|
||||
|
||||
try
|
||||
{
|
||||
// Act - Archive to gzip file
|
||||
await using (var fileStream = File.Create(archivePath))
|
||||
await using (var gzipStream = new GZipStream(fileStream, CompressionLevel.Optimal))
|
||||
{
|
||||
await gzipStream.WriteAsync(bundleBytes);
|
||||
}
|
||||
|
||||
// Act - Extract from gzip file
|
||||
Directory.CreateDirectory(extractPath);
|
||||
await using (var fileStream = File.OpenRead(archivePath))
|
||||
await using (var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress))
|
||||
await using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
await gzipStream.CopyToAsync(memoryStream);
|
||||
var extractedBundleBytes = memoryStream.ToArray();
|
||||
|
||||
// Act - Deserialize and verify
|
||||
var extractedBundle = SigstoreTestBundle.Deserialize(extractedBundleBytes);
|
||||
var extractedEnvelope = DsseRoundtripTestFixture.ExtractFromBundle(extractedBundle);
|
||||
var verified = _fixture.Verify(extractedEnvelope);
|
||||
|
||||
// Assert
|
||||
verified.Should().BeTrue("envelope should verify after archive round-trip");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { File.Delete(archivePath); } catch { }
|
||||
try { Directory.Delete(extractPath, true); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SignBundleArchiveExtractVerify_ThroughMultipleFiles_PreservesIntegrity()
|
||||
{
|
||||
// Arrange
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
var envelope = _fixture.Sign(payload);
|
||||
var bundle = _fixture.CreateSigstoreBundle(envelope);
|
||||
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), $"dsse-multi-{Guid.NewGuid():N}");
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
// Act - Save envelope and bundle as separate files
|
||||
var envelopePath = Path.Combine(tempDir, "envelope.json");
|
||||
var bundlePath = Path.Combine(tempDir, "bundle.json");
|
||||
|
||||
await File.WriteAllBytesAsync(envelopePath, DsseRoundtripTestFixture.SerializeToBytes(envelope));
|
||||
await File.WriteAllBytesAsync(bundlePath, bundle.Serialize());
|
||||
|
||||
// Act - Reload both
|
||||
var reloadedEnvelopeBytes = await File.ReadAllBytesAsync(envelopePath);
|
||||
var reloadedBundleBytes = await File.ReadAllBytesAsync(bundlePath);
|
||||
|
||||
var reloadedEnvelope = DsseRoundtripTestFixture.DeserializeFromBytes(reloadedEnvelopeBytes);
|
||||
var reloadedBundle = SigstoreTestBundle.Deserialize(reloadedBundleBytes);
|
||||
var extractedFromBundle = DsseRoundtripTestFixture.ExtractFromBundle(reloadedBundle);
|
||||
|
||||
// Assert - Both should verify and be equivalent
|
||||
_fixture.Verify(reloadedEnvelope).Should().BeTrue("reloaded envelope should verify");
|
||||
_fixture.Verify(extractedFromBundle).Should().BeTrue("extracted envelope should verify");
|
||||
|
||||
reloadedEnvelope.Payload.ToArray().Should().BeEquivalentTo(extractedFromBundle.Payload.ToArray());
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { Directory.Delete(tempDir, true); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
// DSSE-8200-009: Multi-signature envelope round-trip
|
||||
|
||||
[Fact]
|
||||
public void MultiSignatureEnvelope_BundleExtractVerify_AllSignaturesPreserved()
|
||||
{
|
||||
// Arrange - Create envelope with multiple signatures
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
|
||||
using var key1 = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
using var key2 = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
using var key3 = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
|
||||
var sig1 = CreateSignature(key1, payload, "key-1");
|
||||
var sig2 = CreateSignature(key2, payload, "key-2");
|
||||
var sig3 = CreateSignature(key3, payload, "key-3");
|
||||
|
||||
var multiSigEnvelope = new DsseEnvelope(
|
||||
"application/vnd.in-toto+json",
|
||||
payload,
|
||||
[sig1, sig2, sig3]);
|
||||
|
||||
// Act - Bundle
|
||||
var bundle = _fixture.CreateSigstoreBundle(multiSigEnvelope);
|
||||
var bundleBytes = bundle.Serialize();
|
||||
|
||||
// Act - Extract
|
||||
var extractedBundle = SigstoreTestBundle.Deserialize(bundleBytes);
|
||||
var extractedEnvelope = DsseRoundtripTestFixture.ExtractFromBundle(extractedBundle);
|
||||
|
||||
// Assert - All signatures preserved
|
||||
extractedEnvelope.Signatures.Should().HaveCount(3);
|
||||
extractedEnvelope.Signatures.Select(s => s.KeyId)
|
||||
.Should().BeEquivalentTo(["key-1", "key-2", "key-3"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultiSignatureEnvelope_SignatureOrderIsCanonical()
|
||||
{
|
||||
// Arrange - Create signatures in non-alphabetical order
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
|
||||
using var keyZ = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
using var keyA = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
using var keyM = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
|
||||
var sigZ = CreateSignature(keyZ, payload, "z-key");
|
||||
var sigA = CreateSignature(keyA, payload, "a-key");
|
||||
var sigM = CreateSignature(keyM, payload, "m-key");
|
||||
|
||||
// Act - Create envelope with out-of-order signatures
|
||||
var envelope1 = new DsseEnvelope("application/vnd.in-toto+json", payload, [sigZ, sigA, sigM]);
|
||||
var envelope2 = new DsseEnvelope("application/vnd.in-toto+json", payload, [sigA, sigM, sigZ]);
|
||||
var envelope3 = new DsseEnvelope("application/vnd.in-toto+json", payload, [sigM, sigZ, sigA]);
|
||||
|
||||
// Assert - All should have canonical (alphabetical) signature order
|
||||
var expectedOrder = new[] { "a-key", "m-key", "z-key" };
|
||||
envelope1.Signatures.Select(s => s.KeyId).Should().Equal(expectedOrder);
|
||||
envelope2.Signatures.Select(s => s.KeyId).Should().Equal(expectedOrder);
|
||||
envelope3.Signatures.Select(s => s.KeyId).Should().Equal(expectedOrder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultiSignatureEnvelope_SerializationIsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
|
||||
using var key1 = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
using var key2 = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
|
||||
var sig1 = CreateSignature(key1, payload, "key-1");
|
||||
var sig2 = CreateSignature(key2, payload, "key-2");
|
||||
|
||||
// Act - Create envelopes with different signature order
|
||||
var envelopeA = new DsseEnvelope("application/vnd.in-toto+json", payload, [sig1, sig2]);
|
||||
var envelopeB = new DsseEnvelope("application/vnd.in-toto+json", payload, [sig2, sig1]);
|
||||
|
||||
var bytesA = DsseRoundtripTestFixture.SerializeToBytes(envelopeA);
|
||||
var bytesB = DsseRoundtripTestFixture.SerializeToBytes(envelopeB);
|
||||
|
||||
// Assert - Serialization should be identical due to canonical ordering
|
||||
bytesA.Should().BeEquivalentTo(bytesB, "canonical ordering should produce identical serialization");
|
||||
}
|
||||
|
||||
// Bundle integrity tests
|
||||
|
||||
[Fact]
|
||||
public void Bundle_TamperingDetected_VerificationFails()
|
||||
{
|
||||
// Arrange
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
var envelope = _fixture.Sign(payload);
|
||||
var bundle = _fixture.CreateSigstoreBundle(envelope);
|
||||
|
||||
// Act - Extract and tamper with envelope
|
||||
var extractedEnvelope = DsseRoundtripTestFixture.ExtractFromBundle(bundle);
|
||||
var tamperedPayload = extractedEnvelope.Payload.ToArray();
|
||||
tamperedPayload[0] ^= 0xFF;
|
||||
|
||||
var tamperedEnvelope = new DsseEnvelope(
|
||||
extractedEnvelope.PayloadType,
|
||||
tamperedPayload,
|
||||
extractedEnvelope.Signatures);
|
||||
|
||||
// Assert - Tampered envelope should not verify with bundle key
|
||||
var verifiedWithBundleKey = DsseRoundtripTestFixture.VerifyWithBundleKey(tamperedEnvelope, bundle);
|
||||
verifiedWithBundleKey.Should().BeFalse("tampered envelope should not verify");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bundle_DifferentKey_VerificationFails()
|
||||
{
|
||||
// Arrange
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
var envelope = _fixture.Sign(payload);
|
||||
var bundle = _fixture.CreateSigstoreBundle(envelope);
|
||||
|
||||
// Act - Create a different fixture with different key
|
||||
using var differentFixture = new DsseRoundtripTestFixture();
|
||||
var differentBundle = differentFixture.CreateSigstoreBundle(envelope);
|
||||
|
||||
// Assert - Original envelope should not verify with different key
|
||||
var verified = DsseRoundtripTestFixture.VerifyWithBundleKey(envelope, differentBundle);
|
||||
verified.Should().BeFalse("envelope should not verify with wrong key");
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
private static DsseSignature CreateSignature(ECDsa key, byte[] payload, string keyId)
|
||||
{
|
||||
var pae = BuildPae("application/vnd.in-toto+json", payload);
|
||||
var signatureBytes = key.SignData(pae, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence);
|
||||
return DsseSignature.FromBytes(signatureBytes, keyId);
|
||||
}
|
||||
|
||||
private static byte[] BuildPae(string payloadType, byte[] payload)
|
||||
{
|
||||
const string preamble = "DSSEv1 ";
|
||||
|
||||
var payloadTypeBytes = Encoding.UTF8.GetBytes(payloadType);
|
||||
var payloadTypeLenStr = payloadTypeBytes.Length.ToString();
|
||||
var payloadLenStr = payload.Length.ToString();
|
||||
|
||||
var totalLength = preamble.Length
|
||||
+ payloadTypeLenStr.Length + 1 + payloadTypeBytes.Length + 1
|
||||
+ payloadLenStr.Length + 1 + payload.Length;
|
||||
|
||||
var pae = new byte[totalLength];
|
||||
var offset = 0;
|
||||
|
||||
Encoding.UTF8.GetBytes(preamble, pae.AsSpan(offset));
|
||||
offset += preamble.Length;
|
||||
|
||||
Encoding.UTF8.GetBytes(payloadTypeLenStr, pae.AsSpan(offset));
|
||||
offset += payloadTypeLenStr.Length;
|
||||
pae[offset++] = (byte)' ';
|
||||
|
||||
payloadTypeBytes.CopyTo(pae.AsSpan(offset));
|
||||
offset += payloadTypeBytes.Length;
|
||||
pae[offset++] = (byte)' ';
|
||||
|
||||
Encoding.UTF8.GetBytes(payloadLenStr, pae.AsSpan(offset));
|
||||
offset += payloadLenStr.Length;
|
||||
pae[offset++] = (byte)' ';
|
||||
|
||||
payload.CopyTo(pae.AsSpan(offset));
|
||||
|
||||
return pae;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_fixture.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// DsseRoundtripTestFixture.cs
|
||||
// Sprint: SPRINT_8200_0001_0002_dsse_roundtrip_testing
|
||||
// Tasks: DSSE-8200-001, DSSE-8200-002, DSSE-8200-003
|
||||
// Description: Test fixture providing DSSE signing, verification, and round-trip helpers
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Attestor.Envelope.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Test fixture for DSSE round-trip verification tests.
|
||||
/// Provides key generation, signing, verification, and serialization helpers.
|
||||
/// </summary>
|
||||
public sealed class DsseRoundtripTestFixture : IDisposable
|
||||
{
|
||||
private readonly ECDsa _signingKey;
|
||||
private readonly string _keyId;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new test fixture with a fresh ECDSA P-256 key pair.
|
||||
/// </summary>
|
||||
public DsseRoundtripTestFixture()
|
||||
: this(ECDsa.Create(ECCurve.NamedCurves.nistP256), $"test-key-{Guid.NewGuid():N}")
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a test fixture with a specified key and key ID.
|
||||
/// </summary>
|
||||
public DsseRoundtripTestFixture(ECDsa signingKey, string keyId)
|
||||
{
|
||||
_signingKey = signingKey ?? throw new ArgumentNullException(nameof(signingKey));
|
||||
_keyId = keyId ?? throw new ArgumentNullException(nameof(keyId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the key ID associated with the signing key.
|
||||
/// </summary>
|
||||
public string KeyId => _keyId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the public key bytes in X.509 SubjectPublicKeyInfo format.
|
||||
/// </summary>
|
||||
public ReadOnlyMemory<byte> PublicKeyBytes => _signingKey.ExportSubjectPublicKeyInfo();
|
||||
|
||||
// DSSE-8200-001: Core signing and verification helpers
|
||||
|
||||
/// <summary>
|
||||
/// Signs a payload and creates a DSSE envelope.
|
||||
/// Uses ECDSA P-256 with SHA-256 (ES256).
|
||||
/// </summary>
|
||||
public DsseEnvelope Sign(ReadOnlySpan<byte> payload, string payloadType = "application/vnd.in-toto+json")
|
||||
{
|
||||
// Build PAE (Pre-Authentication Encoding) as per DSSE spec
|
||||
// PAE = "DSSEv1" || len(payloadType) || payloadType || len(payload) || payload
|
||||
var pae = BuildPae(payloadType, payload);
|
||||
|
||||
// Sign the PAE
|
||||
var signatureBytes = _signingKey.SignData(pae, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence);
|
||||
|
||||
var signature = DsseSignature.FromBytes(signatureBytes, _keyId);
|
||||
return new DsseEnvelope(payloadType, payload.ToArray(), [signature]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signs a JSON-serializable payload and creates a DSSE envelope.
|
||||
/// </summary>
|
||||
public DsseEnvelope SignJson<T>(T payload, string payloadType = "application/vnd.in-toto+json")
|
||||
{
|
||||
var payloadBytes = JsonSerializer.SerializeToUtf8Bytes(payload, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false
|
||||
});
|
||||
return Sign(payloadBytes, payloadType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a DSSE envelope signature using the fixture's public key.
|
||||
/// Returns true if at least one signature verifies.
|
||||
/// </summary>
|
||||
public bool Verify(DsseEnvelope envelope)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(envelope);
|
||||
|
||||
var pae = BuildPae(envelope.PayloadType, envelope.Payload.Span);
|
||||
|
||||
foreach (var sig in envelope.Signatures)
|
||||
{
|
||||
// Match by key ID if specified
|
||||
if (sig.KeyId != null && sig.KeyId != _keyId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var signatureBytes = Convert.FromBase64String(sig.Signature);
|
||||
if (_signingKey.VerifyData(pae, signatureBytes, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
// Invalid base64, skip
|
||||
}
|
||||
catch (CryptographicException)
|
||||
{
|
||||
// Invalid signature format, skip
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a verification result with detailed information.
|
||||
/// </summary>
|
||||
public DsseVerificationResult VerifyDetailed(DsseEnvelope envelope)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(envelope);
|
||||
|
||||
var pae = BuildPae(envelope.PayloadType, envelope.Payload.Span);
|
||||
var results = new List<SignatureVerificationResult>();
|
||||
|
||||
foreach (var sig in envelope.Signatures)
|
||||
{
|
||||
var result = VerifySingleSignature(sig, pae);
|
||||
results.Add(result);
|
||||
}
|
||||
|
||||
var anyValid = results.Exists(r => r.IsValid);
|
||||
return new DsseVerificationResult(anyValid, results);
|
||||
}
|
||||
|
||||
// DSSE-8200-002: Serialization and persistence helpers
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a DSSE envelope to canonical JSON bytes.
|
||||
/// </summary>
|
||||
public static byte[] SerializeToBytes(DsseEnvelope envelope)
|
||||
{
|
||||
var result = DsseEnvelopeSerializer.Serialize(envelope, new DsseEnvelopeSerializationOptions
|
||||
{
|
||||
EmitCompactJson = true,
|
||||
EmitExpandedJson = false
|
||||
});
|
||||
|
||||
return result.CompactJson ?? throw new InvalidOperationException("Serialization failed to produce compact JSON.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a DSSE envelope from canonical JSON bytes.
|
||||
/// </summary>
|
||||
public static DsseEnvelope DeserializeFromBytes(ReadOnlySpan<byte> json)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json.ToArray());
|
||||
var root = doc.RootElement;
|
||||
|
||||
var payloadType = root.GetProperty("payloadType").GetString()
|
||||
?? throw new JsonException("Missing payloadType");
|
||||
|
||||
var payloadBase64 = root.GetProperty("payload").GetString()
|
||||
?? throw new JsonException("Missing payload");
|
||||
|
||||
var payload = Convert.FromBase64String(payloadBase64);
|
||||
|
||||
var signatures = new List<DsseSignature>();
|
||||
foreach (var sigElement in root.GetProperty("signatures").EnumerateArray())
|
||||
{
|
||||
var sig = sigElement.GetProperty("sig").GetString()
|
||||
?? throw new JsonException("Missing sig in signature");
|
||||
|
||||
sigElement.TryGetProperty("keyid", out var keyIdElement);
|
||||
var keyId = keyIdElement.ValueKind == JsonValueKind.String ? keyIdElement.GetString() : null;
|
||||
|
||||
signatures.Add(new DsseSignature(sig, keyId));
|
||||
}
|
||||
|
||||
return new DsseEnvelope(payloadType, payload, signatures);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persists a DSSE envelope to a file.
|
||||
/// </summary>
|
||||
public static async Task SaveToFileAsync(DsseEnvelope envelope, string filePath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var bytes = SerializeToBytes(envelope);
|
||||
await File.WriteAllBytesAsync(filePath, bytes, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a DSSE envelope from a file.
|
||||
/// </summary>
|
||||
public static async Task<DsseEnvelope> LoadFromFileAsync(string filePath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var bytes = await File.ReadAllBytesAsync(filePath, cancellationToken);
|
||||
return DeserializeFromBytes(bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a full round-trip: serialize to file, reload, deserialize.
|
||||
/// </summary>
|
||||
public static async Task<DsseEnvelope> RoundtripThroughFileAsync(
|
||||
DsseEnvelope envelope,
|
||||
string? tempPath = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
tempPath ??= Path.Combine(Path.GetTempPath(), $"dsse-roundtrip-{Guid.NewGuid():N}.json");
|
||||
|
||||
try
|
||||
{
|
||||
await SaveToFileAsync(envelope, tempPath, cancellationToken);
|
||||
return await LoadFromFileAsync(tempPath, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { File.Delete(tempPath); } catch { /* Best effort cleanup */ }
|
||||
}
|
||||
}
|
||||
|
||||
// DSSE-8200-003: Sigstore bundle wrapper helpers
|
||||
|
||||
/// <summary>
|
||||
/// Creates a minimal Sigstore-compatible bundle containing the DSSE envelope.
|
||||
/// This is a simplified version for testing; production bundles need additional metadata.
|
||||
/// </summary>
|
||||
public SigstoreTestBundle CreateSigstoreBundle(DsseEnvelope envelope)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(envelope);
|
||||
|
||||
var envelopeJson = SerializeToBytes(envelope);
|
||||
var publicKeyDer = _signingKey.ExportSubjectPublicKeyInfo();
|
||||
|
||||
return new SigstoreTestBundle(
|
||||
MediaType: "application/vnd.dev.sigstore.bundle.v0.3+json",
|
||||
DsseEnvelope: envelopeJson,
|
||||
PublicKey: publicKeyDer,
|
||||
KeyId: _keyId,
|
||||
Algorithm: "ES256");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts a DSSE envelope from a Sigstore test bundle.
|
||||
/// </summary>
|
||||
public static DsseEnvelope ExtractFromBundle(SigstoreTestBundle bundle)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bundle);
|
||||
return DeserializeFromBytes(bundle.DsseEnvelope);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a DSSE envelope using the public key embedded in a bundle.
|
||||
/// </summary>
|
||||
public static bool VerifyWithBundleKey(DsseEnvelope envelope, SigstoreTestBundle bundle)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(envelope);
|
||||
ArgumentNullException.ThrowIfNull(bundle);
|
||||
|
||||
using var publicKey = ECDsa.Create();
|
||||
publicKey.ImportSubjectPublicKeyInfo(bundle.PublicKey, out _);
|
||||
|
||||
var pae = BuildPae(envelope.PayloadType, envelope.Payload.Span);
|
||||
|
||||
foreach (var sig in envelope.Signatures)
|
||||
{
|
||||
if (sig.KeyId != null && sig.KeyId != bundle.KeyId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var signatureBytes = Convert.FromBase64String(sig.Signature);
|
||||
if (publicKey.VerifyData(pae, signatureBytes, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Continue to next signature
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Payload creation helpers for tests
|
||||
|
||||
/// <summary>
|
||||
/// Creates a minimal in-toto statement payload for testing.
|
||||
/// </summary>
|
||||
public static byte[] CreateInTotoPayload(
|
||||
string predicateType = "https://slsa.dev/provenance/v1",
|
||||
string subjectName = "test-artifact",
|
||||
string subjectDigest = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
|
||||
{
|
||||
var statement = new
|
||||
{
|
||||
_type = "https://in-toto.io/Statement/v1",
|
||||
subject = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
name = subjectName,
|
||||
digest = new { sha256 = subjectDigest.Replace("sha256:", "") }
|
||||
}
|
||||
},
|
||||
predicateType,
|
||||
predicate = new { }
|
||||
};
|
||||
|
||||
return JsonSerializer.SerializeToUtf8Bytes(statement, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a deterministic test payload with specified content.
|
||||
/// </summary>
|
||||
public static byte[] CreateTestPayload(string content = "deterministic-test-payload")
|
||||
{
|
||||
return Encoding.UTF8.GetBytes(content);
|
||||
}
|
||||
|
||||
// Private helpers
|
||||
|
||||
private static byte[] BuildPae(string payloadType, ReadOnlySpan<byte> payload)
|
||||
{
|
||||
// PAE(payloadType, payload) = "DSSEv1" + SP + len(payloadType) + SP + payloadType + SP + len(payload) + SP + payload
|
||||
// Where SP is ASCII space (0x20)
|
||||
const string preamble = "DSSEv1 ";
|
||||
|
||||
var payloadTypeBytes = Encoding.UTF8.GetBytes(payloadType);
|
||||
var payloadTypeLenStr = payloadTypeBytes.Length.ToString();
|
||||
var payloadLenStr = payload.Length.ToString();
|
||||
|
||||
var totalLength = preamble.Length
|
||||
+ payloadTypeLenStr.Length + 1 + payloadTypeBytes.Length + 1
|
||||
+ payloadLenStr.Length + 1 + payload.Length;
|
||||
|
||||
var pae = new byte[totalLength];
|
||||
var offset = 0;
|
||||
|
||||
// "DSSEv1 "
|
||||
Encoding.UTF8.GetBytes(preamble, pae.AsSpan(offset));
|
||||
offset += preamble.Length;
|
||||
|
||||
// len(payloadType) + SP
|
||||
Encoding.UTF8.GetBytes(payloadTypeLenStr, pae.AsSpan(offset));
|
||||
offset += payloadTypeLenStr.Length;
|
||||
pae[offset++] = (byte)' ';
|
||||
|
||||
// payloadType + SP
|
||||
payloadTypeBytes.CopyTo(pae.AsSpan(offset));
|
||||
offset += payloadTypeBytes.Length;
|
||||
pae[offset++] = (byte)' ';
|
||||
|
||||
// len(payload) + SP
|
||||
Encoding.UTF8.GetBytes(payloadLenStr, pae.AsSpan(offset));
|
||||
offset += payloadLenStr.Length;
|
||||
pae[offset++] = (byte)' ';
|
||||
|
||||
// payload
|
||||
payload.CopyTo(pae.AsSpan(offset));
|
||||
|
||||
return pae;
|
||||
}
|
||||
|
||||
private SignatureVerificationResult VerifySingleSignature(DsseSignature sig, byte[] pae)
|
||||
{
|
||||
var keyMatches = sig.KeyId == null || sig.KeyId == _keyId;
|
||||
|
||||
if (!keyMatches)
|
||||
{
|
||||
return new SignatureVerificationResult(sig.KeyId, false, "Key ID mismatch");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var signatureBytes = Convert.FromBase64String(sig.Signature);
|
||||
var isValid = _signingKey.VerifyData(pae, signatureBytes, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence);
|
||||
return new SignatureVerificationResult(sig.KeyId, isValid, isValid ? null : "Signature verification failed");
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return new SignatureVerificationResult(sig.KeyId, false, "Invalid base64 signature format");
|
||||
}
|
||||
catch (CryptographicException ex)
|
||||
{
|
||||
return new SignatureVerificationResult(sig.KeyId, false, $"Cryptographic error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
_signingKey.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of DSSE envelope verification with detailed per-signature results.
|
||||
/// </summary>
|
||||
public sealed record DsseVerificationResult(
|
||||
bool IsValid,
|
||||
IReadOnlyList<SignatureVerificationResult> SignatureResults);
|
||||
|
||||
/// <summary>
|
||||
/// Result of verifying a single signature.
|
||||
/// </summary>
|
||||
public sealed record SignatureVerificationResult(
|
||||
string? KeyId,
|
||||
bool IsValid,
|
||||
string? FailureReason);
|
||||
|
||||
/// <summary>
|
||||
/// Minimal Sigstore-compatible bundle for testing DSSE round-trips.
|
||||
/// </summary>
|
||||
public sealed record SigstoreTestBundle(
|
||||
string MediaType,
|
||||
byte[] DsseEnvelope,
|
||||
byte[] PublicKey,
|
||||
string KeyId,
|
||||
string Algorithm)
|
||||
{
|
||||
/// <summary>
|
||||
/// Serializes the bundle to JSON bytes.
|
||||
/// </summary>
|
||||
public byte[] Serialize()
|
||||
{
|
||||
var bundle = new
|
||||
{
|
||||
mediaType = MediaType,
|
||||
dsseEnvelope = Convert.ToBase64String(DsseEnvelope),
|
||||
verificationMaterial = new
|
||||
{
|
||||
publicKey = new
|
||||
{
|
||||
hint = KeyId,
|
||||
rawBytes = Convert.ToBase64String(PublicKey)
|
||||
},
|
||||
algorithm = Algorithm
|
||||
}
|
||||
};
|
||||
|
||||
return JsonSerializer.SerializeToUtf8Bytes(bundle, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a bundle from JSON bytes.
|
||||
/// </summary>
|
||||
public static SigstoreTestBundle Deserialize(ReadOnlySpan<byte> json)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json.ToArray());
|
||||
var root = doc.RootElement;
|
||||
|
||||
var mediaType = root.GetProperty("mediaType").GetString()
|
||||
?? throw new JsonException("Missing mediaType");
|
||||
|
||||
var dsseEnvelopeBase64 = root.GetProperty("dsseEnvelope").GetString()
|
||||
?? throw new JsonException("Missing dsseEnvelope");
|
||||
|
||||
var verificationMaterial = root.GetProperty("verificationMaterial");
|
||||
var publicKeyElement = verificationMaterial.GetProperty("publicKey");
|
||||
|
||||
var keyId = publicKeyElement.GetProperty("hint").GetString()
|
||||
?? throw new JsonException("Missing hint (keyId)");
|
||||
|
||||
var publicKeyBase64 = publicKeyElement.GetProperty("rawBytes").GetString()
|
||||
?? throw new JsonException("Missing rawBytes");
|
||||
|
||||
var algorithm = verificationMaterial.GetProperty("algorithm").GetString()
|
||||
?? throw new JsonException("Missing algorithm");
|
||||
|
||||
return new SigstoreTestBundle(
|
||||
mediaType,
|
||||
Convert.FromBase64String(dsseEnvelopeBase64),
|
||||
Convert.FromBase64String(publicKeyBase64),
|
||||
keyId,
|
||||
algorithm);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// DsseRoundtripTests.cs
|
||||
// Sprint: SPRINT_8200_0001_0002_dsse_roundtrip_testing
|
||||
// Tasks: DSSE-8200-004, DSSE-8200-005, DSSE-8200-006, DSSE-8200-010, DSSE-8200-011, DSSE-8200-012
|
||||
// Description: DSSE round-trip verification tests
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Envelope.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for DSSE envelope round-trip verification.
|
||||
/// Validates sign → serialize → deserialize → verify cycles and determinism.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Category", "DsseRoundtrip")]
|
||||
public sealed class DsseRoundtripTests : IDisposable
|
||||
{
|
||||
private readonly DsseRoundtripTestFixture _fixture;
|
||||
|
||||
public DsseRoundtripTests()
|
||||
{
|
||||
_fixture = new DsseRoundtripTestFixture();
|
||||
}
|
||||
|
||||
// DSSE-8200-004: Basic sign → serialize → deserialize → verify
|
||||
|
||||
[Fact]
|
||||
public void SignSerializeDeserializeVerify_HappyPath_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
|
||||
// Act - Sign
|
||||
var originalEnvelope = _fixture.Sign(payload);
|
||||
var originalVerified = _fixture.Verify(originalEnvelope);
|
||||
|
||||
// Act - Serialize
|
||||
var serializedBytes = DsseRoundtripTestFixture.SerializeToBytes(originalEnvelope);
|
||||
|
||||
// Act - Deserialize
|
||||
var deserializedEnvelope = DsseRoundtripTestFixture.DeserializeFromBytes(serializedBytes);
|
||||
|
||||
// Act - Verify deserialized
|
||||
var deserializedVerified = _fixture.Verify(deserializedEnvelope);
|
||||
|
||||
// Assert
|
||||
originalVerified.Should().BeTrue("original envelope should verify");
|
||||
deserializedVerified.Should().BeTrue("deserialized envelope should verify");
|
||||
|
||||
deserializedEnvelope.PayloadType.Should().Be(originalEnvelope.PayloadType);
|
||||
deserializedEnvelope.Payload.ToArray().Should().BeEquivalentTo(originalEnvelope.Payload.ToArray());
|
||||
deserializedEnvelope.Signatures.Should().HaveCount(originalEnvelope.Signatures.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SignSerializeDeserializeVerify_WithJsonPayload_PreservesContent()
|
||||
{
|
||||
// Arrange
|
||||
var testData = new
|
||||
{
|
||||
_type = "https://in-toto.io/Statement/v1",
|
||||
subject = new[] { new { name = "test", digest = new { sha256 = "abc123" } } },
|
||||
predicateType = "https://slsa.dev/provenance/v1",
|
||||
predicate = new { buildType = "test" }
|
||||
};
|
||||
|
||||
// Act
|
||||
var envelope = _fixture.SignJson(testData);
|
||||
var serialized = DsseRoundtripTestFixture.SerializeToBytes(envelope);
|
||||
var deserialized = DsseRoundtripTestFixture.DeserializeFromBytes(serialized);
|
||||
|
||||
// Assert
|
||||
_fixture.Verify(deserialized).Should().BeTrue();
|
||||
|
||||
var originalPayload = Encoding.UTF8.GetString(envelope.Payload.Span);
|
||||
var deserializedPayload = Encoding.UTF8.GetString(deserialized.Payload.Span);
|
||||
deserializedPayload.Should().Be(originalPayload);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SignSerializeDeserializeVerify_ThroughFile_PreservesIntegrity()
|
||||
{
|
||||
// Arrange
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
var envelope = _fixture.Sign(payload);
|
||||
|
||||
// Act - Full round-trip through file system
|
||||
var roundtrippedEnvelope = await DsseRoundtripTestFixture.RoundtripThroughFileAsync(envelope);
|
||||
|
||||
// Assert
|
||||
_fixture.Verify(roundtrippedEnvelope).Should().BeTrue();
|
||||
roundtrippedEnvelope.Payload.ToArray().Should().BeEquivalentTo(envelope.Payload.ToArray());
|
||||
}
|
||||
|
||||
// DSSE-8200-005: Tamper detection - modified payload
|
||||
|
||||
[Fact]
|
||||
public void Verify_WithModifiedPayload_Fails()
|
||||
{
|
||||
// Arrange
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
var envelope = _fixture.Sign(payload);
|
||||
_fixture.Verify(envelope).Should().BeTrue("unmodified envelope should verify");
|
||||
|
||||
// Act - Tamper with payload
|
||||
var serialized = DsseRoundtripTestFixture.SerializeToBytes(envelope);
|
||||
var tamperedJson = TamperWithPayload(serialized);
|
||||
var tamperedEnvelope = DsseRoundtripTestFixture.DeserializeFromBytes(tamperedJson);
|
||||
|
||||
// Assert
|
||||
_fixture.Verify(tamperedEnvelope).Should().BeFalse("tampered payload should not verify");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_WithSingleBytePayloadChange_Fails()
|
||||
{
|
||||
// Arrange
|
||||
var payload = DsseRoundtripTestFixture.CreateTestPayload("original-content-here");
|
||||
var envelope = _fixture.Sign(payload);
|
||||
|
||||
// Act - Modify a single byte in payload
|
||||
var modifiedPayload = payload.ToArray();
|
||||
modifiedPayload[10] ^= 0x01; // Flip one bit in the middle
|
||||
|
||||
var tamperedEnvelope = new DsseEnvelope(
|
||||
envelope.PayloadType,
|
||||
modifiedPayload,
|
||||
envelope.Signatures);
|
||||
|
||||
// Assert
|
||||
_fixture.Verify(tamperedEnvelope).Should().BeFalse("single bit change should invalidate signature");
|
||||
}
|
||||
|
||||
// DSSE-8200-006: Tamper detection - modified signature
|
||||
|
||||
[Fact]
|
||||
public void Verify_WithModifiedSignature_Fails()
|
||||
{
|
||||
// Arrange
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
var envelope = _fixture.Sign(payload);
|
||||
_fixture.Verify(envelope).Should().BeTrue("unmodified envelope should verify");
|
||||
|
||||
// Act - Tamper with signature
|
||||
var originalSig = envelope.Signatures[0];
|
||||
var tamperedSigBytes = Convert.FromBase64String(originalSig.Signature);
|
||||
tamperedSigBytes[0] ^= 0xFF; // Corrupt first byte
|
||||
|
||||
var tamperedSig = new DsseSignature(Convert.ToBase64String(tamperedSigBytes), originalSig.KeyId);
|
||||
var tamperedEnvelope = new DsseEnvelope(
|
||||
envelope.PayloadType,
|
||||
envelope.Payload,
|
||||
[tamperedSig]);
|
||||
|
||||
// Assert
|
||||
_fixture.Verify(tamperedEnvelope).Should().BeFalse("tampered signature should not verify");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_WithTruncatedSignature_Fails()
|
||||
{
|
||||
// Arrange
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
var envelope = _fixture.Sign(payload);
|
||||
|
||||
// Act - Truncate signature
|
||||
var originalSig = envelope.Signatures[0];
|
||||
var truncatedSigBytes = Convert.FromBase64String(originalSig.Signature).AsSpan(0, 10).ToArray();
|
||||
|
||||
var truncatedSig = new DsseSignature(Convert.ToBase64String(truncatedSigBytes), originalSig.KeyId);
|
||||
var tamperedEnvelope = new DsseEnvelope(
|
||||
envelope.PayloadType,
|
||||
envelope.Payload,
|
||||
[truncatedSig]);
|
||||
|
||||
// Assert
|
||||
_fixture.Verify(tamperedEnvelope).Should().BeFalse("truncated signature should not verify");
|
||||
}
|
||||
|
||||
// DSSE-8200-010: Determinism - same payload signed twice produces identical envelope bytes
|
||||
|
||||
[Fact]
|
||||
public void Sign_SamePayloadTwice_WithSameKey_ProducesConsistentPayloadAndSignatureFormat()
|
||||
{
|
||||
// Arrange - Use the same key instance to sign twice
|
||||
var payload = DsseRoundtripTestFixture.CreateTestPayload("deterministic-payload");
|
||||
|
||||
// Act - Sign the same payload twice with the same key
|
||||
var envelope1 = _fixture.Sign(payload);
|
||||
var envelope2 = _fixture.Sign(payload);
|
||||
|
||||
// Assert - Payloads should be identical
|
||||
envelope1.Payload.ToArray().Should().BeEquivalentTo(envelope2.Payload.ToArray());
|
||||
envelope1.PayloadType.Should().Be(envelope2.PayloadType);
|
||||
|
||||
// Key ID should be the same
|
||||
envelope1.Signatures[0].KeyId.Should().Be(envelope2.Signatures[0].KeyId);
|
||||
|
||||
// Note: ECDSA signatures may differ due to random k value, but they should both verify
|
||||
_fixture.Verify(envelope1).Should().BeTrue();
|
||||
_fixture.Verify(envelope2).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sign_DifferentPayloads_ProducesDifferentSignatures()
|
||||
{
|
||||
// Arrange
|
||||
var payload1 = DsseRoundtripTestFixture.CreateTestPayload("payload-1");
|
||||
var payload2 = DsseRoundtripTestFixture.CreateTestPayload("payload-2");
|
||||
|
||||
// Act
|
||||
var envelope1 = _fixture.Sign(payload1);
|
||||
var envelope2 = _fixture.Sign(payload2);
|
||||
|
||||
// Assert
|
||||
envelope1.Signatures[0].Signature.Should().NotBe(envelope2.Signatures[0].Signature);
|
||||
}
|
||||
|
||||
// DSSE-8200-011: Serialization is canonical (key order, no whitespace variance)
|
||||
|
||||
[Fact]
|
||||
public void Serialize_ProducesCanonicalJson_NoWhitespaceVariance()
|
||||
{
|
||||
// Arrange
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
var envelope = _fixture.Sign(payload);
|
||||
|
||||
// Act - Serialize multiple times
|
||||
var bytes1 = DsseRoundtripTestFixture.SerializeToBytes(envelope);
|
||||
var bytes2 = DsseRoundtripTestFixture.SerializeToBytes(envelope);
|
||||
var bytes3 = DsseRoundtripTestFixture.SerializeToBytes(envelope);
|
||||
|
||||
// Assert - All serializations should be byte-for-byte identical
|
||||
bytes2.Should().BeEquivalentTo(bytes1);
|
||||
bytes3.Should().BeEquivalentTo(bytes1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serialize_OrdersKeysConsistently()
|
||||
{
|
||||
// Arrange
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
var envelope = _fixture.Sign(payload);
|
||||
|
||||
// Act
|
||||
var serialized = DsseRoundtripTestFixture.SerializeToBytes(envelope);
|
||||
var json = Encoding.UTF8.GetString(serialized);
|
||||
|
||||
// Assert - Verify key order in JSON
|
||||
var payloadTypeIndex = json.IndexOf("\"payloadType\"");
|
||||
var payloadIndex = json.IndexOf("\"payload\"");
|
||||
var signaturesIndex = json.IndexOf("\"signatures\"");
|
||||
|
||||
payloadTypeIndex.Should().BeLessThan(payloadIndex, "payloadType should come before payload");
|
||||
payloadIndex.Should().BeLessThan(signaturesIndex, "payload should come before signatures");
|
||||
}
|
||||
|
||||
// DSSE-8200-012: Property test - serialize → deserialize → serialize produces identical bytes
|
||||
|
||||
[Theory]
|
||||
[InlineData("simple-text-payload")]
|
||||
[InlineData("")]
|
||||
[InlineData("unicode: 你好世界 🔐")]
|
||||
[InlineData("{\"key\":\"value\",\"nested\":{\"array\":[1,2,3]}}")]
|
||||
public void SerializeDeserializeSerialize_ProducesIdenticalBytes(string payloadContent)
|
||||
{
|
||||
// Arrange
|
||||
var payload = Encoding.UTF8.GetBytes(payloadContent);
|
||||
if (payload.Length == 0)
|
||||
{
|
||||
// Empty payload needs at least one byte for valid DSSE
|
||||
payload = Encoding.UTF8.GetBytes("{}");
|
||||
}
|
||||
|
||||
var envelope = _fixture.Sign(payload);
|
||||
|
||||
// Act - Triple round-trip
|
||||
var bytes1 = DsseRoundtripTestFixture.SerializeToBytes(envelope);
|
||||
var deserialized1 = DsseRoundtripTestFixture.DeserializeFromBytes(bytes1);
|
||||
var bytes2 = DsseRoundtripTestFixture.SerializeToBytes(deserialized1);
|
||||
var deserialized2 = DsseRoundtripTestFixture.DeserializeFromBytes(bytes2);
|
||||
var bytes3 = DsseRoundtripTestFixture.SerializeToBytes(deserialized2);
|
||||
|
||||
// Assert - All serializations should be identical
|
||||
bytes2.Should().BeEquivalentTo(bytes1, "first round-trip should be stable");
|
||||
bytes3.Should().BeEquivalentTo(bytes1, "second round-trip should be stable");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerializeDeserializeSerialize_LargePayload_ProducesIdenticalBytes()
|
||||
{
|
||||
// Arrange - Create a large payload
|
||||
var largeContent = new string('X', 100_000);
|
||||
var payload = Encoding.UTF8.GetBytes($"{{\"large\":\"{largeContent}\"}}");
|
||||
var envelope = _fixture.Sign(payload);
|
||||
|
||||
// Act
|
||||
var bytes1 = DsseRoundtripTestFixture.SerializeToBytes(envelope);
|
||||
var deserialized = DsseRoundtripTestFixture.DeserializeFromBytes(bytes1);
|
||||
var bytes2 = DsseRoundtripTestFixture.SerializeToBytes(deserialized);
|
||||
|
||||
// Assert
|
||||
bytes2.Should().BeEquivalentTo(bytes1);
|
||||
_fixture.Verify(deserialized).Should().BeTrue();
|
||||
}
|
||||
|
||||
// Verification result tests
|
||||
|
||||
[Fact]
|
||||
public void VerifyDetailed_ValidEnvelope_ReturnsSuccessResult()
|
||||
{
|
||||
// Arrange
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
var envelope = _fixture.Sign(payload);
|
||||
|
||||
// Act
|
||||
var result = _fixture.VerifyDetailed(envelope);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.SignatureResults.Should().HaveCount(1);
|
||||
result.SignatureResults[0].IsValid.Should().BeTrue();
|
||||
result.SignatureResults[0].FailureReason.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyDetailed_InvalidSignature_ReturnsFailureReason()
|
||||
{
|
||||
// Arrange
|
||||
var payload = DsseRoundtripTestFixture.CreateInTotoPayload();
|
||||
var envelope = _fixture.Sign(payload);
|
||||
|
||||
// Tamper with payload
|
||||
var tamperedPayload = payload.ToArray();
|
||||
tamperedPayload[0] ^= 0xFF;
|
||||
var tamperedEnvelope = new DsseEnvelope(
|
||||
envelope.PayloadType,
|
||||
tamperedPayload,
|
||||
envelope.Signatures);
|
||||
|
||||
// Act
|
||||
var result = _fixture.VerifyDetailed(tamperedEnvelope);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.SignatureResults.Should().HaveCount(1);
|
||||
result.SignatureResults[0].IsValid.Should().BeFalse();
|
||||
result.SignatureResults[0].FailureReason.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
private static byte[] TamperWithPayload(byte[] serializedEnvelope)
|
||||
{
|
||||
var json = Encoding.UTF8.GetString(serializedEnvelope);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
|
||||
var payloadBase64 = doc.RootElement.GetProperty("payload").GetString()!;
|
||||
var payloadBytes = Convert.FromBase64String(payloadBase64);
|
||||
|
||||
// Modify payload content
|
||||
payloadBytes[0] ^= 0xFF;
|
||||
var tamperedPayloadBase64 = Convert.ToBase64String(payloadBytes);
|
||||
|
||||
// Reconstruct JSON with tampered payload
|
||||
json = json.Replace(payloadBase64, tamperedPayloadBase64);
|
||||
return Encoding.UTF8.GetBytes(json);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_fixture.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user