|
|
|
|
@@ -0,0 +1,242 @@
|
|
|
|
|
// <copyright file="AirGapBundleDsseSignerTests.cs" company="StellaOps">
|
|
|
|
|
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
|
|
|
|
// </copyright>
|
|
|
|
|
|
|
|
|
|
using System.Security.Cryptography;
|
|
|
|
|
using FluentAssertions;
|
|
|
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
|
using Microsoft.Extensions.Options;
|
|
|
|
|
using StellaOps.AirGap.Sync.Models;
|
|
|
|
|
using StellaOps.AirGap.Sync.Services;
|
|
|
|
|
using StellaOps.TestKit;
|
|
|
|
|
using Xunit;
|
|
|
|
|
|
|
|
|
|
namespace StellaOps.AirGap.Sync.Tests;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Unit tests for <see cref="AirGapBundleDsseSigner"/>.
|
|
|
|
|
/// </summary>
|
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
|
|
|
public sealed class AirGapBundleDsseSignerTests
|
|
|
|
|
{
|
|
|
|
|
private static readonly string TestSecretBase64 = Convert.ToBase64String(
|
|
|
|
|
RandomNumberGenerator.GetBytes(32));
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task SignAsync_WhenDisabled_ReturnsNull()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var options = Options.Create(new AirGapBundleDsseOptions { Mode = "none" });
|
|
|
|
|
var signer = new AirGapBundleDsseSigner(options, NullLogger<AirGapBundleDsseSigner>.Instance);
|
|
|
|
|
var bundle = CreateTestBundle();
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var result = await signer.SignAsync(bundle);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
result.Should().BeNull();
|
|
|
|
|
signer.IsEnabled.Should().BeFalse();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task SignAsync_WhenEnabled_ReturnsValidSignature()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var options = Options.Create(new AirGapBundleDsseOptions
|
|
|
|
|
{
|
|
|
|
|
Mode = "hmac",
|
|
|
|
|
SecretBase64 = TestSecretBase64,
|
|
|
|
|
KeyId = "test-key"
|
|
|
|
|
});
|
|
|
|
|
var signer = new AirGapBundleDsseSigner(options, NullLogger<AirGapBundleDsseSigner>.Instance);
|
|
|
|
|
var bundle = CreateTestBundle();
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var result = await signer.SignAsync(bundle);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
result.Should().NotBeNull();
|
|
|
|
|
result!.KeyId.Should().Be("test-key");
|
|
|
|
|
result.Signature.Should().NotBeEmpty();
|
|
|
|
|
result.SignatureBase64.Should().NotBeNullOrWhiteSpace();
|
|
|
|
|
signer.IsEnabled.Should().BeTrue();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task SignAsync_DeterministicForSameInput()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var options = Options.Create(new AirGapBundleDsseOptions
|
|
|
|
|
{
|
|
|
|
|
Mode = "hmac",
|
|
|
|
|
SecretBase64 = TestSecretBase64
|
|
|
|
|
});
|
|
|
|
|
var signer = new AirGapBundleDsseSigner(options, NullLogger<AirGapBundleDsseSigner>.Instance);
|
|
|
|
|
var bundle = CreateTestBundle();
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var result1 = await signer.SignAsync(bundle);
|
|
|
|
|
var result2 = await signer.SignAsync(bundle);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
result1!.SignatureBase64.Should().Be(result2!.SignatureBase64);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task SignAsync_DifferentForDifferentManifest()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var options = Options.Create(new AirGapBundleDsseOptions
|
|
|
|
|
{
|
|
|
|
|
Mode = "hmac",
|
|
|
|
|
SecretBase64 = TestSecretBase64
|
|
|
|
|
});
|
|
|
|
|
var signer = new AirGapBundleDsseSigner(options, NullLogger<AirGapBundleDsseSigner>.Instance);
|
|
|
|
|
var bundle1 = CreateTestBundle(manifestDigest: "sha256:aaa");
|
|
|
|
|
var bundle2 = CreateTestBundle(manifestDigest: "sha256:bbb");
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var result1 = await signer.SignAsync(bundle1);
|
|
|
|
|
var result2 = await signer.SignAsync(bundle2);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
result1!.SignatureBase64.Should().NotBe(result2!.SignatureBase64);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task VerifyAsync_WhenDisabled_ReturnsSigningDisabled()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var options = Options.Create(new AirGapBundleDsseOptions { Mode = "none" });
|
|
|
|
|
var signer = new AirGapBundleDsseSigner(options, NullLogger<AirGapBundleDsseSigner>.Instance);
|
|
|
|
|
var bundle = CreateTestBundle();
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var result = await signer.VerifyAsync(bundle);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
result.Should().Be(AirGapBundleVerificationResult.SigningDisabled);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task VerifyAsync_WhenNoSignature_ReturnsMissingSignature()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var options = Options.Create(new AirGapBundleDsseOptions
|
|
|
|
|
{
|
|
|
|
|
Mode = "hmac",
|
|
|
|
|
SecretBase64 = TestSecretBase64
|
|
|
|
|
});
|
|
|
|
|
var signer = new AirGapBundleDsseSigner(options, NullLogger<AirGapBundleDsseSigner>.Instance);
|
|
|
|
|
var bundle = CreateTestBundle(signature: null);
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var result = await signer.VerifyAsync(bundle);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
result.Should().Be(AirGapBundleVerificationResult.MissingSignature);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task VerifyAsync_WithValidSignature_ReturnsValid()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var options = Options.Create(new AirGapBundleDsseOptions
|
|
|
|
|
{
|
|
|
|
|
Mode = "hmac",
|
|
|
|
|
SecretBase64 = TestSecretBase64
|
|
|
|
|
});
|
|
|
|
|
var signer = new AirGapBundleDsseSigner(options, NullLogger<AirGapBundleDsseSigner>.Instance);
|
|
|
|
|
var bundle = CreateTestBundle();
|
|
|
|
|
|
|
|
|
|
// Sign the bundle first
|
|
|
|
|
var signResult = await signer.SignAsync(bundle);
|
|
|
|
|
var signedBundle = bundle with { Signature = signResult!.SignatureBase64, SignedBy = signResult.KeyId };
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var verifyResult = await signer.VerifyAsync(signedBundle);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
verifyResult.Should().Be(AirGapBundleVerificationResult.Valid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task VerifyAsync_WithTamperedSignature_ReturnsInvalid()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var options = Options.Create(new AirGapBundleDsseOptions
|
|
|
|
|
{
|
|
|
|
|
Mode = "hmac",
|
|
|
|
|
SecretBase64 = TestSecretBase64
|
|
|
|
|
});
|
|
|
|
|
var signer = new AirGapBundleDsseSigner(options, NullLogger<AirGapBundleDsseSigner>.Instance);
|
|
|
|
|
var bundle = CreateTestBundle();
|
|
|
|
|
|
|
|
|
|
// Sign and then tamper
|
|
|
|
|
var signResult = await signer.SignAsync(bundle);
|
|
|
|
|
var tamperedBundle = bundle with
|
|
|
|
|
{
|
|
|
|
|
Signature = signResult!.SignatureBase64,
|
|
|
|
|
ManifestDigest = "sha256:tampered"
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var verifyResult = await signer.VerifyAsync(tamperedBundle);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
verifyResult.Should().Be(AirGapBundleVerificationResult.InvalidSignature);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task VerifyAsync_WithInvalidBase64Signature_ReturnsInvalid()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var options = Options.Create(new AirGapBundleDsseOptions
|
|
|
|
|
{
|
|
|
|
|
Mode = "hmac",
|
|
|
|
|
SecretBase64 = TestSecretBase64
|
|
|
|
|
});
|
|
|
|
|
var signer = new AirGapBundleDsseSigner(options, NullLogger<AirGapBundleDsseSigner>.Instance);
|
|
|
|
|
var bundle = CreateTestBundle(signature: "not-valid-base64!!!");
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var verifyResult = await signer.VerifyAsync(bundle);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
verifyResult.Should().Be(AirGapBundleVerificationResult.InvalidSignature);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void SignAsync_WithMissingSecret_ThrowsInvalidOperation()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var options = Options.Create(new AirGapBundleDsseOptions
|
|
|
|
|
{
|
|
|
|
|
Mode = "hmac",
|
|
|
|
|
SecretBase64 = null
|
|
|
|
|
});
|
|
|
|
|
var signer = new AirGapBundleDsseSigner(options, NullLogger<AirGapBundleDsseSigner>.Instance);
|
|
|
|
|
var bundle = CreateTestBundle();
|
|
|
|
|
|
|
|
|
|
// Act & Assert
|
|
|
|
|
var act = async () => await signer.SignAsync(bundle);
|
|
|
|
|
act.Should().ThrowAsync<InvalidOperationException>()
|
|
|
|
|
.WithMessage("*SecretBase64*");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static AirGapBundle CreateTestBundle(
|
|
|
|
|
string? manifestDigest = null,
|
|
|
|
|
string? signature = null)
|
|
|
|
|
{
|
|
|
|
|
return new AirGapBundle
|
|
|
|
|
{
|
|
|
|
|
BundleId = Guid.Parse("11111111-1111-1111-1111-111111111111"),
|
|
|
|
|
TenantId = "test-tenant",
|
|
|
|
|
CreatedAt = DateTimeOffset.Parse("2026-01-07T12:00:00Z"),
|
|
|
|
|
CreatedByNodeId = "test-node",
|
|
|
|
|
JobLogs = new List<NodeJobLog>(),
|
|
|
|
|
ManifestDigest = manifestDigest ?? "sha256:abc123def456",
|
|
|
|
|
Signature = signature
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|