save dev progress

This commit is contained in:
StellaOps Bot
2025-12-26 00:32:35 +02:00
parent aa70af062e
commit ed3079543c
142 changed files with 23771 additions and 232 deletions

View File

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

View File

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