save dev progress
This commit is contained in:
@@ -0,0 +1,352 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// DsseCosignCompatibilityTestFixture.cs
|
||||
// Sprint: SPRINT_8200_0001_0002_dsse_roundtrip_testing
|
||||
// Tasks: DSSE-8200-013, DSSE-8200-014, DSSE-8200-015
|
||||
// Description: Test fixture for cosign compatibility testing with mock Fulcio/Rekor
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Attestor.Envelope.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Test fixture for cosign compatibility tests.
|
||||
/// Provides mock Fulcio certificates and Rekor entries for offline testing.
|
||||
/// </summary>
|
||||
public sealed class DsseCosignCompatibilityTestFixture : IDisposable
|
||||
{
|
||||
private readonly ECDsa _signingKey;
|
||||
private readonly X509Certificate2 _certificate;
|
||||
private readonly string _keyId;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new fixture with mock Fulcio-style certificate.
|
||||
/// </summary>
|
||||
public DsseCosignCompatibilityTestFixture()
|
||||
{
|
||||
_signingKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
_keyId = $"cosign-test-{Guid.NewGuid():N}";
|
||||
_certificate = CreateMockFulcioCertificate(_signingKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mock Fulcio certificate.
|
||||
/// </summary>
|
||||
public X509Certificate2 Certificate => _certificate;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the signing key.
|
||||
/// </summary>
|
||||
public ECDsa SigningKey => _signingKey;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the key ID.
|
||||
/// </summary>
|
||||
public string KeyId => _keyId;
|
||||
|
||||
// DSSE-8200-014: Mock Fulcio certificate generation
|
||||
|
||||
/// <summary>
|
||||
/// Creates a mock certificate mimicking Fulcio's structure for testing.
|
||||
/// </summary>
|
||||
public static X509Certificate2 CreateMockFulcioCertificate(
|
||||
ECDsa key,
|
||||
string subject = "test@example.com",
|
||||
string issuer = "https://oauth2.sigstore.dev/auth",
|
||||
DateTimeOffset? validFrom = null,
|
||||
DateTimeOffset? validTo = null)
|
||||
{
|
||||
validFrom ??= DateTimeOffset.UtcNow.AddMinutes(-5);
|
||||
validTo ??= DateTimeOffset.UtcNow.AddMinutes(15); // Fulcio certs are short-lived (~20 min)
|
||||
|
||||
var request = new CertificateRequest(
|
||||
new X500DistinguishedName($"CN={subject}"),
|
||||
key,
|
||||
HashAlgorithmName.SHA256);
|
||||
|
||||
// Add extensions similar to Fulcio
|
||||
request.CertificateExtensions.Add(
|
||||
new X509KeyUsageExtension(
|
||||
X509KeyUsageFlags.DigitalSignature,
|
||||
critical: true));
|
||||
|
||||
request.CertificateExtensions.Add(
|
||||
new X509EnhancedKeyUsageExtension(
|
||||
new OidCollection { new Oid("1.3.6.1.5.5.7.3.3") }, // Code Signing
|
||||
critical: false));
|
||||
|
||||
// Add Subject Alternative Name (SAN) for identity
|
||||
var sanBuilder = new SubjectAlternativeNameBuilder();
|
||||
sanBuilder.AddEmailAddress(subject);
|
||||
request.CertificateExtensions.Add(sanBuilder.Build());
|
||||
|
||||
// Create self-signed cert (in real Fulcio this would be CA-signed)
|
||||
return request.CreateSelfSigned(validFrom.Value, validTo.Value);
|
||||
}
|
||||
|
||||
// DSSE-8200-013: Cosign-compatible envelope creation
|
||||
|
||||
/// <summary>
|
||||
/// Signs a payload and creates a cosign-compatible DSSE envelope.
|
||||
/// </summary>
|
||||
public DsseEnvelope SignCosignCompatible(
|
||||
ReadOnlySpan<byte> payload,
|
||||
string payloadType = "application/vnd.in-toto+json")
|
||||
{
|
||||
// Build PAE (Pre-Authentication Encoding)
|
||||
var pae = BuildPae(payloadType, payload);
|
||||
|
||||
// Sign with EC key (ES256 - what cosign uses)
|
||||
var signatureBytes = _signingKey.SignData(pae, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence);
|
||||
|
||||
// Base64 encode signature as cosign expects
|
||||
var signatureBase64 = Convert.ToBase64String(signatureBytes);
|
||||
|
||||
var signature = new DsseSignature(signatureBase64, _keyId);
|
||||
return new DsseEnvelope(payloadType, payload.ToArray(), [signature]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Sigstore bundle structure for testing.
|
||||
/// </summary>
|
||||
public CosignCompatibilityBundle CreateBundle(DsseEnvelope envelope, bool includeRekorEntry = false)
|
||||
{
|
||||
var certPem = ExportCertificateToPem(_certificate);
|
||||
var certChain = new List<string> { certPem };
|
||||
|
||||
MockRekorEntry? rekorEntry = null;
|
||||
if (includeRekorEntry)
|
||||
{
|
||||
rekorEntry = CreateMockRekorEntry(envelope);
|
||||
}
|
||||
|
||||
return new CosignCompatibilityBundle(
|
||||
envelope,
|
||||
certChain,
|
||||
rekorEntry);
|
||||
}
|
||||
|
||||
// DSSE-8200-015: Mock Rekor entry for offline verification
|
||||
|
||||
/// <summary>
|
||||
/// Creates a mock Rekor transparency log entry for testing.
|
||||
/// </summary>
|
||||
public MockRekorEntry CreateMockRekorEntry(
|
||||
DsseEnvelope envelope,
|
||||
long logIndex = 12345678,
|
||||
long? treeSize = null)
|
||||
{
|
||||
treeSize ??= logIndex + 1000;
|
||||
|
||||
// Serialize envelope to get canonicalized body
|
||||
var serializationResult = DsseEnvelopeSerializer.Serialize(envelope, new DsseEnvelopeSerializationOptions
|
||||
{
|
||||
EmitCompactJson = true,
|
||||
EmitExpandedJson = false
|
||||
});
|
||||
|
||||
var canonicalizedBody = serializationResult.CompactJson ?? [];
|
||||
var bodyBase64 = Convert.ToBase64String(canonicalizedBody);
|
||||
|
||||
// Compute leaf hash (SHA256 of the canonicalized body)
|
||||
var leafHash = SHA256.HashData(canonicalizedBody);
|
||||
|
||||
// Generate synthetic Merkle proof
|
||||
var (proofHashes, rootHash) = GenerateSyntheticMerkleProof(leafHash, logIndex, treeSize.Value);
|
||||
|
||||
var integratedTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
|
||||
return new MockRekorEntry(
|
||||
LogIndex: logIndex,
|
||||
LogId: "rekor.sigstore.dev",
|
||||
IntegratedTime: integratedTime,
|
||||
CanonicalizedBody: bodyBase64,
|
||||
InclusionProof: new MockInclusionProof(
|
||||
LogIndex: logIndex,
|
||||
TreeSize: treeSize.Value,
|
||||
RootHash: Convert.ToBase64String(rootHash),
|
||||
Hashes: proofHashes.ConvertAll(h => Convert.ToBase64String(h)),
|
||||
Checkpoint: $"rekor.sigstore.dev - {treeSize}\n{Convert.ToBase64String(rootHash)}"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that an envelope has the structure expected by cosign.
|
||||
/// </summary>
|
||||
public static CosignStructureValidationResult ValidateCosignStructure(DsseEnvelope envelope)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
// Check payload type
|
||||
if (string.IsNullOrEmpty(envelope.PayloadType))
|
||||
{
|
||||
errors.Add("payloadType is required");
|
||||
}
|
||||
|
||||
// Check payload is present
|
||||
if (envelope.Payload.Length == 0)
|
||||
{
|
||||
errors.Add("payload is required");
|
||||
}
|
||||
|
||||
// Check signatures
|
||||
if (envelope.Signatures.Count == 0)
|
||||
{
|
||||
errors.Add("at least one signature is required");
|
||||
}
|
||||
|
||||
foreach (var sig in envelope.Signatures)
|
||||
{
|
||||
// Signature should be base64-encoded
|
||||
if (string.IsNullOrEmpty(sig.Signature))
|
||||
{
|
||||
errors.Add("signature value is required");
|
||||
}
|
||||
else if (!IsValidBase64(sig.Signature))
|
||||
{
|
||||
errors.Add($"signature is not valid base64: {sig.Signature[..Math.Min(20, sig.Signature.Length)]}...");
|
||||
}
|
||||
}
|
||||
|
||||
return new CosignStructureValidationResult(errors.Count == 0, errors);
|
||||
}
|
||||
|
||||
private static byte[] BuildPae(string payloadType, ReadOnlySpan<byte> payload)
|
||||
{
|
||||
// PAE = "DSSEv1" || SP || len(type) || SP || type || SP || len(payload) || SP || payload
|
||||
const string prefix = "DSSEv1 ";
|
||||
var typeBytes = Encoding.UTF8.GetBytes(payloadType);
|
||||
|
||||
var buffer = new List<byte>();
|
||||
buffer.AddRange(Encoding.UTF8.GetBytes(prefix));
|
||||
buffer.AddRange(Encoding.UTF8.GetBytes(typeBytes.Length.ToString()));
|
||||
buffer.Add((byte)' ');
|
||||
buffer.AddRange(typeBytes);
|
||||
buffer.Add((byte)' ');
|
||||
buffer.AddRange(Encoding.UTF8.GetBytes(payload.Length.ToString()));
|
||||
buffer.Add((byte)' ');
|
||||
buffer.AddRange(payload.ToArray());
|
||||
|
||||
return buffer.ToArray();
|
||||
}
|
||||
|
||||
private static string ExportCertificateToPem(X509Certificate2 cert)
|
||||
{
|
||||
var certBytes = cert.Export(X509ContentType.Cert);
|
||||
var base64 = Convert.ToBase64String(certBytes);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("-----BEGIN CERTIFICATE-----");
|
||||
for (var i = 0; i < base64.Length; i += 64)
|
||||
{
|
||||
sb.AppendLine(base64.Substring(i, Math.Min(64, base64.Length - i)));
|
||||
}
|
||||
sb.AppendLine("-----END CERTIFICATE-----");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static (List<byte[]> proofHashes, byte[] rootHash) GenerateSyntheticMerkleProof(
|
||||
byte[] leafHash,
|
||||
long logIndex,
|
||||
long treeSize)
|
||||
{
|
||||
// Generate a synthetic but valid Merkle proof structure
|
||||
var proofHashes = new List<byte[]>();
|
||||
var currentHash = leafHash;
|
||||
|
||||
// Compute tree height
|
||||
var height = (int)Math.Ceiling(Math.Log2(Math.Max(treeSize, 2)));
|
||||
|
||||
// Generate sibling hashes for each level
|
||||
var random = new Random((int)(logIndex % int.MaxValue)); // Deterministic from logIndex
|
||||
var siblingBytes = new byte[32];
|
||||
|
||||
for (var level = 0; level < height; level++)
|
||||
{
|
||||
random.NextBytes(siblingBytes);
|
||||
proofHashes.Add((byte[])siblingBytes.Clone());
|
||||
|
||||
// Compute parent hash (simplified - real Merkle tree would be more complex)
|
||||
var combined = new byte[64];
|
||||
if ((logIndex >> level) % 2 == 0)
|
||||
{
|
||||
currentHash.CopyTo(combined, 0);
|
||||
siblingBytes.CopyTo(combined, 32);
|
||||
}
|
||||
else
|
||||
{
|
||||
siblingBytes.CopyTo(combined, 0);
|
||||
currentHash.CopyTo(combined, 32);
|
||||
}
|
||||
currentHash = SHA256.HashData(combined);
|
||||
}
|
||||
|
||||
return (proofHashes, currentHash);
|
||||
}
|
||||
|
||||
private static bool IsValidBase64(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Convert.FromBase64String(value);
|
||||
return true;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
_signingKey.Dispose();
|
||||
_certificate.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of cosign structure validation.
|
||||
/// </summary>
|
||||
public sealed record CosignStructureValidationResult(bool IsValid, List<string> Errors);
|
||||
|
||||
/// <summary>
|
||||
/// Test bundle with Fulcio certificate chain for cosign compatibility testing.
|
||||
/// </summary>
|
||||
public sealed record CosignCompatibilityBundle(
|
||||
DsseEnvelope Envelope,
|
||||
List<string> CertificateChain,
|
||||
MockRekorEntry? RekorEntry);
|
||||
|
||||
/// <summary>
|
||||
/// Mock Rekor transparency log entry for testing.
|
||||
/// </summary>
|
||||
public sealed record MockRekorEntry(
|
||||
long LogIndex,
|
||||
string LogId,
|
||||
long IntegratedTime,
|
||||
string CanonicalizedBody,
|
||||
MockInclusionProof InclusionProof);
|
||||
|
||||
/// <summary>
|
||||
/// Mock Merkle inclusion proof for testing.
|
||||
/// </summary>
|
||||
public sealed record MockInclusionProof(
|
||||
long LogIndex,
|
||||
long TreeSize,
|
||||
string RootHash,
|
||||
List<string> Hashes,
|
||||
string Checkpoint);
|
||||
@@ -0,0 +1,404 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// DsseCosignCompatibilityTests.cs
|
||||
// Sprint: SPRINT_8200_0001_0002_dsse_roundtrip_testing
|
||||
// Tasks: DSSE-8200-013, DSSE-8200-014, DSSE-8200-015
|
||||
// Description: Cosign compatibility tests with mock Fulcio/Rekor (no CLI required)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Envelope.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for cosign compatibility without requiring external cosign CLI.
|
||||
/// Validates envelope structure, Fulcio certificate handling, and Rekor entry format.
|
||||
/// </summary>
|
||||
public sealed class DsseCosignCompatibilityTests : IDisposable
|
||||
{
|
||||
private readonly DsseCosignCompatibilityTestFixture _fixture;
|
||||
|
||||
public DsseCosignCompatibilityTests()
|
||||
{
|
||||
_fixture = new DsseCosignCompatibilityTestFixture();
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// DSSE-8200-013: Cosign-compatible envelope structure tests
|
||||
// ==========================================================================
|
||||
|
||||
[Fact]
|
||||
public void EnvelopeStructure_HasRequiredFields_ForCosignVerification()
|
||||
{
|
||||
// Arrange
|
||||
var payload = CreateTestInTotoStatement();
|
||||
|
||||
// Act
|
||||
var envelope = _fixture.SignCosignCompatible(payload);
|
||||
|
||||
// Assert - Validate cosign-expected structure
|
||||
var result = DsseCosignCompatibilityTestFixture.ValidateCosignStructure(envelope);
|
||||
Assert.True(result.IsValid, $"Structure validation failed: {string.Join(", ", result.Errors)}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvelopePayload_IsBase64Encoded_InSerializedForm()
|
||||
{
|
||||
// Arrange
|
||||
var payload = CreateTestInTotoStatement();
|
||||
var envelope = _fixture.SignCosignCompatible(payload);
|
||||
|
||||
// Act
|
||||
var serialized = DsseEnvelopeSerializer.Serialize(envelope, new DsseEnvelopeSerializationOptions
|
||||
{
|
||||
EmitCompactJson = true
|
||||
});
|
||||
|
||||
var json = JsonDocument.Parse(serialized.CompactJson!);
|
||||
|
||||
// Assert - payload should be base64-encoded in the JSON
|
||||
var payloadField = json.RootElement.GetProperty("payload").GetString();
|
||||
Assert.NotNull(payloadField);
|
||||
Assert.DoesNotContain("\n", payloadField); // No newlines in base64
|
||||
|
||||
// Verify it decodes back to original
|
||||
var decoded = Convert.FromBase64String(payloadField);
|
||||
Assert.Equal(payload, decoded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvelopeSignature_IsBase64Encoded_InSerializedForm()
|
||||
{
|
||||
// Arrange
|
||||
var payload = CreateTestInTotoStatement();
|
||||
var envelope = _fixture.SignCosignCompatible(payload);
|
||||
|
||||
// Act
|
||||
var serialized = DsseEnvelopeSerializer.Serialize(envelope, new DsseEnvelopeSerializationOptions
|
||||
{
|
||||
EmitCompactJson = true
|
||||
});
|
||||
|
||||
var json = JsonDocument.Parse(serialized.CompactJson!);
|
||||
|
||||
// Assert - signatures array exists with valid base64
|
||||
var signatures = json.RootElement.GetProperty("signatures");
|
||||
Assert.Equal(JsonValueKind.Array, signatures.ValueKind);
|
||||
Assert.True(signatures.GetArrayLength() >= 1);
|
||||
|
||||
var firstSig = signatures[0];
|
||||
var sigValue = firstSig.GetProperty("sig").GetString();
|
||||
Assert.NotNull(sigValue);
|
||||
|
||||
// Verify it's valid base64
|
||||
var sigBytes = Convert.FromBase64String(sigValue);
|
||||
Assert.True(sigBytes.Length > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvelopePayloadType_IsCorrectMimeType_ForInToto()
|
||||
{
|
||||
// Arrange
|
||||
var payload = CreateTestInTotoStatement();
|
||||
|
||||
// Act
|
||||
var envelope = _fixture.SignCosignCompatible(payload, "application/vnd.in-toto+json");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("application/vnd.in-toto+json", envelope.PayloadType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvelopeSerialization_ProducesValidJson_WithoutWhitespace()
|
||||
{
|
||||
// Arrange
|
||||
var payload = CreateTestInTotoStatement();
|
||||
var envelope = _fixture.SignCosignCompatible(payload);
|
||||
|
||||
// Act
|
||||
var serialized = DsseEnvelopeSerializer.Serialize(envelope, new DsseEnvelopeSerializationOptions
|
||||
{
|
||||
EmitCompactJson = true
|
||||
});
|
||||
|
||||
var json = Encoding.UTF8.GetString(serialized.CompactJson!);
|
||||
|
||||
// Assert - compact JSON should not have unnecessary whitespace
|
||||
Assert.DoesNotContain("\n", json);
|
||||
Assert.DoesNotContain(" ", json); // No double spaces
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// DSSE-8200-014: Fulcio certificate chain tests
|
||||
// ==========================================================================
|
||||
|
||||
[Fact]
|
||||
public void FulcioCertificate_HasCodeSigningEku()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cert = _fixture.Certificate;
|
||||
|
||||
// Assert - Certificate should have Code Signing EKU
|
||||
var hasCodeSigning = false;
|
||||
foreach (var ext in cert.Extensions)
|
||||
{
|
||||
if (ext is X509EnhancedKeyUsageExtension eku)
|
||||
{
|
||||
foreach (var oid in eku.EnhancedKeyUsages)
|
||||
{
|
||||
if (oid.Value == "1.3.6.1.5.5.7.3.3") // Code Signing
|
||||
{
|
||||
hasCodeSigning = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Assert.True(hasCodeSigning, "Certificate should have Code Signing EKU");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FulcioCertificate_HasDigitalSignatureKeyUsage()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cert = _fixture.Certificate;
|
||||
|
||||
// Assert
|
||||
var keyUsage = cert.Extensions["2.5.29.15"] as X509KeyUsageExtension;
|
||||
Assert.NotNull(keyUsage);
|
||||
Assert.True(keyUsage.KeyUsages.HasFlag(X509KeyUsageFlags.DigitalSignature));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FulcioCertificate_IsShortLived()
|
||||
{
|
||||
// Arrange - Fulcio certs are typically valid for ~20 minutes
|
||||
|
||||
// Act
|
||||
var cert = _fixture.Certificate;
|
||||
var validity = cert.NotAfter - cert.NotBefore;
|
||||
|
||||
// Assert - Should be less than 24 hours (Fulcio's short-lived nature)
|
||||
Assert.True(validity.TotalHours <= 24, $"Certificate validity ({validity.TotalHours}h) should be <= 24 hours");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BundleWithCertificate_HasValidPemFormat()
|
||||
{
|
||||
// Arrange
|
||||
var payload = CreateTestInTotoStatement();
|
||||
var envelope = _fixture.SignCosignCompatible(payload);
|
||||
|
||||
// Act
|
||||
var bundle = _fixture.CreateBundle(envelope);
|
||||
|
||||
// Assert
|
||||
Assert.NotEmpty(bundle.CertificateChain);
|
||||
var certPem = bundle.CertificateChain[0];
|
||||
Assert.StartsWith("-----BEGIN CERTIFICATE-----", certPem);
|
||||
Assert.Contains("-----END CERTIFICATE-----", certPem);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// DSSE-8200-015: Rekor transparency log offline verification tests
|
||||
// ==========================================================================
|
||||
|
||||
[Fact]
|
||||
public void RekorEntry_HasValidLogIndex()
|
||||
{
|
||||
// Arrange
|
||||
var payload = CreateTestInTotoStatement();
|
||||
var envelope = _fixture.SignCosignCompatible(payload);
|
||||
|
||||
// Act
|
||||
var rekorEntry = _fixture.CreateMockRekorEntry(envelope);
|
||||
|
||||
// Assert
|
||||
Assert.True(rekorEntry.LogIndex >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RekorEntry_HasValidIntegratedTime()
|
||||
{
|
||||
// Arrange
|
||||
var payload = CreateTestInTotoStatement();
|
||||
var envelope = _fixture.SignCosignCompatible(payload);
|
||||
|
||||
// Act
|
||||
var rekorEntry = _fixture.CreateMockRekorEntry(envelope);
|
||||
var integratedTime = DateTimeOffset.FromUnixTimeSeconds(rekorEntry.IntegratedTime);
|
||||
|
||||
// Assert - Should be within reasonable range
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
Assert.True(integratedTime <= now.AddMinutes(1), "Integrated time should not be in the future");
|
||||
Assert.True(integratedTime >= now.AddHours(-1), "Integrated time should not be too old");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RekorEntry_HasValidInclusionProof()
|
||||
{
|
||||
// Arrange
|
||||
var payload = CreateTestInTotoStatement();
|
||||
var envelope = _fixture.SignCosignCompatible(payload);
|
||||
|
||||
// Act
|
||||
var rekorEntry = _fixture.CreateMockRekorEntry(envelope, logIndex: 12345);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(rekorEntry.InclusionProof);
|
||||
Assert.Equal(12345, rekorEntry.InclusionProof.LogIndex);
|
||||
Assert.True(rekorEntry.InclusionProof.TreeSize > rekorEntry.InclusionProof.LogIndex);
|
||||
Assert.NotEmpty(rekorEntry.InclusionProof.RootHash);
|
||||
Assert.NotEmpty(rekorEntry.InclusionProof.Hashes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RekorEntry_CanonicalizedBody_IsBase64Encoded()
|
||||
{
|
||||
// Arrange
|
||||
var payload = CreateTestInTotoStatement();
|
||||
var envelope = _fixture.SignCosignCompatible(payload);
|
||||
|
||||
// Act
|
||||
var rekorEntry = _fixture.CreateMockRekorEntry(envelope);
|
||||
|
||||
// Assert
|
||||
Assert.NotEmpty(rekorEntry.CanonicalizedBody);
|
||||
var decoded = Convert.FromBase64String(rekorEntry.CanonicalizedBody);
|
||||
Assert.True(decoded.Length > 0);
|
||||
|
||||
// Should be valid JSON
|
||||
var json = JsonDocument.Parse(decoded);
|
||||
Assert.NotNull(json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RekorEntry_InclusionProof_HashesAreBase64()
|
||||
{
|
||||
// Arrange
|
||||
var payload = CreateTestInTotoStatement();
|
||||
var envelope = _fixture.SignCosignCompatible(payload);
|
||||
|
||||
// Act
|
||||
var rekorEntry = _fixture.CreateMockRekorEntry(envelope);
|
||||
|
||||
// Assert
|
||||
foreach (var hash in rekorEntry.InclusionProof.Hashes)
|
||||
{
|
||||
var decoded = Convert.FromBase64String(hash);
|
||||
Assert.Equal(32, decoded.Length); // SHA-256 hash length
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BundleWithRekor_ContainsValidTransparencyEntry()
|
||||
{
|
||||
// Arrange
|
||||
var payload = CreateTestInTotoStatement();
|
||||
var envelope = _fixture.SignCosignCompatible(payload);
|
||||
|
||||
// Act
|
||||
var bundle = _fixture.CreateBundle(envelope, includeRekorEntry: true);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(bundle.RekorEntry);
|
||||
Assert.NotEmpty(bundle.RekorEntry.LogId);
|
||||
Assert.True(bundle.RekorEntry.LogIndex >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RekorEntry_CheckpointFormat_IsValid()
|
||||
{
|
||||
// Arrange
|
||||
var payload = CreateTestInTotoStatement();
|
||||
var envelope = _fixture.SignCosignCompatible(payload);
|
||||
|
||||
// Act
|
||||
var rekorEntry = _fixture.CreateMockRekorEntry(envelope);
|
||||
|
||||
// Assert - Checkpoint should contain log ID and root hash
|
||||
Assert.NotEmpty(rekorEntry.InclusionProof.Checkpoint);
|
||||
Assert.Contains("rekor.sigstore.dev", rekorEntry.InclusionProof.Checkpoint);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Integration tests
|
||||
// ==========================================================================
|
||||
|
||||
[Fact]
|
||||
public void FullBundle_SignVerifyRoundtrip_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
var payload = CreateTestInTotoStatement();
|
||||
|
||||
// Act - Create complete bundle
|
||||
var envelope = _fixture.SignCosignCompatible(payload);
|
||||
var bundle = _fixture.CreateBundle(envelope, includeRekorEntry: true);
|
||||
|
||||
// Assert - All components present and valid
|
||||
Assert.NotNull(bundle.Envelope);
|
||||
Assert.NotEmpty(bundle.CertificateChain);
|
||||
Assert.NotNull(bundle.RekorEntry);
|
||||
|
||||
// Verify envelope structure
|
||||
var structureResult = DsseCosignCompatibilityTestFixture.ValidateCosignStructure(envelope);
|
||||
Assert.True(structureResult.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeterministicSigning_SamePayload_ProducesConsistentEnvelope()
|
||||
{
|
||||
// Arrange
|
||||
var payload = CreateTestInTotoStatement();
|
||||
|
||||
// Act - Sign same payload twice with same key
|
||||
var envelope1 = _fixture.SignCosignCompatible(payload);
|
||||
var envelope2 = _fixture.SignCosignCompatible(payload);
|
||||
|
||||
// Assert - Payload type and payload should be identical
|
||||
Assert.Equal(envelope1.PayloadType, envelope2.PayloadType);
|
||||
Assert.Equal(envelope1.Payload.ToArray(), envelope2.Payload.ToArray());
|
||||
|
||||
// Note: Signatures may differ if using randomized ECDSA
|
||||
// (which is the default for security), so we only verify structure
|
||||
Assert.Equal(envelope1.Signatures.Count, envelope2.Signatures.Count);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Helpers
|
||||
// ==========================================================================
|
||||
|
||||
private static byte[] CreateTestInTotoStatement()
|
||||
{
|
||||
var statement = new
|
||||
{
|
||||
_type = "https://in-toto.io/Statement/v0.1",
|
||||
predicateType = "https://stellaops.io/attestations/reachability/v1",
|
||||
subject = new[]
|
||||
{
|
||||
new { name = "test-artifact", digest = new { sha256 = "abc123" } }
|
||||
},
|
||||
predicate = new
|
||||
{
|
||||
graphType = "reachability",
|
||||
nodeCount = 100,
|
||||
edgeCount = 250,
|
||||
timestamp = DateTimeOffset.UtcNow.ToString("O")
|
||||
}
|
||||
};
|
||||
|
||||
return JsonSerializer.SerializeToUtf8Bytes(statement, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = false
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_fixture.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user