save progress
This commit is contained in:
295
src/Attestor/StellaOps.Attestation.Tests/DsseVerifierTests.cs
Normal file
295
src/Attestor/StellaOps.Attestation.Tests/DsseVerifierTests.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user