save progress

This commit is contained in:
StellaOps Bot
2026-01-06 09:42:02 +02:00
parent 94d68bee8b
commit 37e11918e0
443 changed files with 85863 additions and 897 deletions

View File

@@ -0,0 +1,295 @@
// <copyright file="DsseVerifierTests.cs" company="Stella Operations">
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
// </copyright>
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace StellaOps.Attestation.Tests;
/// <summary>
/// Unit tests for DsseVerifier.
/// Sprint: SPRINT_20260105_002_001_REPLAY, Tasks RPL-006 through RPL-010.
/// </summary>
[Trait("Category", "Unit")]
public class DsseVerifierTests
{
private readonly DsseVerifier _verifier;
public DsseVerifierTests()
{
_verifier = new DsseVerifier(NullLogger<DsseVerifier>.Instance);
}
[Fact]
public async Task VerifyAsync_WithValidEcdsaSignature_ReturnsSuccess()
{
// Arrange
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var (envelope, publicKeyPem) = CreateSignedEnvelope(ecdsa);
// Act
var result = await _verifier.VerifyAsync(envelope, publicKeyPem, TestContext.Current.CancellationToken);
// Assert
result.IsValid.Should().BeTrue();
result.ValidSignatureCount.Should().Be(1);
result.TotalSignatureCount.Should().Be(1);
result.PayloadType.Should().Be("https://in-toto.io/Statement/v1");
result.Issues.Should().BeEmpty();
}
[Fact]
public async Task VerifyAsync_WithInvalidSignature_ReturnsFail()
{
// Arrange
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var (envelope, _) = CreateSignedEnvelope(ecdsa);
// Use a different key for verification
using var differentKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var differentPublicKeyPem = ExportPublicKeyPem(differentKey);
// Act
var result = await _verifier.VerifyAsync(envelope, differentPublicKeyPem, TestContext.Current.CancellationToken);
// Assert
result.IsValid.Should().BeFalse();
result.ValidSignatureCount.Should().Be(0);
result.Issues.Should().NotBeEmpty();
}
[Fact]
public async Task VerifyAsync_WithMalformedJson_ReturnsParseError()
{
// Arrange
var malformedJson = "{ not valid json }";
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var publicKeyPem = ExportPublicKeyPem(ecdsa);
// Act
var result = await _verifier.VerifyAsync(malformedJson, publicKeyPem, TestContext.Current.CancellationToken);
// Assert
result.IsValid.Should().BeFalse();
result.Issues.Should().Contain(i => i.Contains("envelope_parse_error"));
}
[Fact]
public async Task VerifyAsync_WithMissingPayload_ReturnsFail()
{
// Arrange
var envelope = JsonSerializer.Serialize(new
{
payloadType = "https://in-toto.io/Statement/v1",
signatures = new[] { new { keyId = "key-001", sig = "YWJj" } }
});
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var publicKeyPem = ExportPublicKeyPem(ecdsa);
// Act
var result = await _verifier.VerifyAsync(envelope, publicKeyPem, TestContext.Current.CancellationToken);
// Assert
result.IsValid.Should().BeFalse();
result.Issues.Should().Contain(i => i.Contains("envelope_missing_payload"));
}
[Fact]
public async Task VerifyAsync_WithMissingSignatures_ReturnsFail()
{
// Arrange
var payload = Convert.ToBase64String(Encoding.UTF8.GetBytes("{}"));
var envelope = JsonSerializer.Serialize(new
{
payloadType = "https://in-toto.io/Statement/v1",
payload,
signatures = Array.Empty<object>()
});
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var publicKeyPem = ExportPublicKeyPem(ecdsa);
// Act
var result = await _verifier.VerifyAsync(envelope, publicKeyPem, TestContext.Current.CancellationToken);
// Assert
result.IsValid.Should().BeFalse();
result.Issues.Should().Contain("envelope_missing_signatures");
}
[Fact]
public async Task VerifyAsync_WithNoTrustedKeys_ReturnsFail()
{
// Arrange
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var (envelope, _) = CreateSignedEnvelope(ecdsa);
// Act
var result = await _verifier.VerifyAsync(envelope, Array.Empty<string>(), TestContext.Current.CancellationToken);
// Assert
result.IsValid.Should().BeFalse();
result.Issues.Should().Contain("no_trusted_keys_provided");
}
[Fact]
public async Task VerifyAsync_WithMultipleTrustedKeys_SucceedsWithMatchingKey()
{
// Arrange
using var signingKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
using var otherKey1 = ECDsa.Create(ECCurve.NamedCurves.nistP256);
using var otherKey2 = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var (envelope, signingKeyPem) = CreateSignedEnvelope(signingKey);
var trustedKeys = new[]
{
ExportPublicKeyPem(otherKey1),
signingKeyPem,
ExportPublicKeyPem(otherKey2),
};
// Act
var result = await _verifier.VerifyAsync(envelope, trustedKeys, TestContext.Current.CancellationToken);
// Assert
result.IsValid.Should().BeTrue();
result.ValidSignatureCount.Should().Be(1);
}
[Fact]
public async Task VerifyAsync_WithKeyResolver_UsesResolverForVerification()
{
// Arrange
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var (envelope, publicKeyPem) = CreateSignedEnvelope(ecdsa);
Task<string?> KeyResolver(string? keyId, CancellationToken ct)
{
return Task.FromResult<string?>(publicKeyPem);
}
// Act
var result = await _verifier.VerifyAsync(envelope, KeyResolver, TestContext.Current.CancellationToken);
// Assert
result.IsValid.Should().BeTrue();
}
[Fact]
public async Task VerifyAsync_WithKeyResolverReturningNull_ReturnsFail()
{
// Arrange
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var (envelope, _) = CreateSignedEnvelope(ecdsa);
static Task<string?> KeyResolver(string? keyId, CancellationToken ct)
{
return Task.FromResult<string?>(null);
}
// Act
var result = await _verifier.VerifyAsync(envelope, KeyResolver, TestContext.Current.CancellationToken);
// Assert
result.IsValid.Should().BeFalse();
result.Issues.Should().Contain(i => i.Contains("key_not_found"));
}
[Fact]
public async Task VerifyAsync_ReturnsPayloadHash()
{
// Arrange
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var (envelope, publicKeyPem) = CreateSignedEnvelope(ecdsa);
// Act
var result = await _verifier.VerifyAsync(envelope, publicKeyPem, TestContext.Current.CancellationToken);
// Assert
result.PayloadHash.Should().StartWith("sha256:");
result.PayloadHash.Should().HaveLength("sha256:".Length + 64);
}
[Fact]
public async Task VerifyAsync_ThrowsOnNullEnvelope()
{
// Arrange
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var publicKeyPem = ExportPublicKeyPem(ecdsa);
// Act & Assert - null envelope throws ArgumentNullException
await Assert.ThrowsAsync<ArgumentNullException>(
() => _verifier.VerifyAsync(null!, publicKeyPem, TestContext.Current.CancellationToken));
// Empty envelope throws ArgumentException (whitespace check)
await Assert.ThrowsAsync<ArgumentException>(
() => _verifier.VerifyAsync("", publicKeyPem, TestContext.Current.CancellationToken));
}
[Fact]
public async Task VerifyAsync_ThrowsOnNullKeys()
{
// Arrange
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var (envelope, _) = CreateSignedEnvelope(ecdsa);
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(
() => _verifier.VerifyAsync(envelope, (IEnumerable<string>)null!, TestContext.Current.CancellationToken));
await Assert.ThrowsAsync<ArgumentNullException>(
() => _verifier.VerifyAsync(envelope, (Func<string?, CancellationToken, Task<string?>>)null!, TestContext.Current.CancellationToken));
}
private static (string EnvelopeJson, string PublicKeyPem) CreateSignedEnvelope(ECDsa signingKey)
{
var payloadType = "https://in-toto.io/Statement/v1";
var payloadContent = "{\"_type\":\"https://in-toto.io/Statement/v1\",\"subject\":[]}";
var payloadBytes = Encoding.UTF8.GetBytes(payloadContent);
var payloadBase64 = Convert.ToBase64String(payloadBytes);
// Compute PAE
var pae = DsseHelper.PreAuthenticationEncoding(payloadType, payloadBytes);
// Sign
var signatureBytes = signingKey.SignData(pae, HashAlgorithmName.SHA256);
var signatureBase64 = Convert.ToBase64String(signatureBytes);
// Build envelope
var envelope = JsonSerializer.Serialize(new
{
payloadType,
payload = payloadBase64,
signatures = new[]
{
new { keyId = "test-key-001", sig = signatureBase64 }
}
});
var publicKeyPem = ExportPublicKeyPem(signingKey);
return (envelope, publicKeyPem);
}
private static string ExportPublicKeyPem(ECDsa key)
{
var publicKeyBytes = key.ExportSubjectPublicKeyInfo();
var base64 = Convert.ToBase64String(publicKeyBytes);
var builder = new StringBuilder();
builder.AppendLine("-----BEGIN PUBLIC KEY-----");
for (var i = 0; i < base64.Length; i += 64)
{
builder.AppendLine(base64.Substring(i, Math.Min(64, base64.Length - i)));
}
builder.AppendLine("-----END PUBLIC KEY-----");
return builder.ToString();
}
}