Add tests for SBOM generation determinism across multiple formats

- Created `StellaOps.TestKit.Tests` project for unit tests related to determinism.
- Implemented `DeterminismManifestTests` to validate deterministic output for canonical bytes and strings, file read/write operations, and error handling for invalid schema versions.
- Added `SbomDeterminismTests` to ensure identical inputs produce consistent SBOMs across SPDX 3.0.1 and CycloneDX 1.6/1.7 formats, including parallel execution tests.
- Updated project references in `StellaOps.Integration.Determinism` to include the new determinism testing library.
This commit is contained in:
master
2025-12-23 18:56:12 +02:00
committed by StellaOps Bot
parent 7ac70ece71
commit 491e883653
409 changed files with 23797 additions and 17779 deletions

View File

@@ -20,7 +20,7 @@ public sealed class DefaultCryptoHashTests
var hash = CryptoHashFactory.CreateDefault();
var expected = SHA256.HashData(Sample);
var actual = hash.ComputeHash(Sample, HashAlgorithms.Sha256);
Assert.Equal(Convert.ToHexString(expected).ToLowerInvariant(), Convert.ToHexString(actual).ToLowerInvariant());
Assert.Equal(Convert.ToHexStringLower(expected), Convert.ToHexStringLower(actual));
}
[Fact]
@@ -29,7 +29,7 @@ public sealed class DefaultCryptoHashTests
var hash = CryptoHashFactory.CreateDefault();
var expected = SHA512.HashData(Sample);
var actual = hash.ComputeHash(Sample, HashAlgorithms.Sha512);
Assert.Equal(Convert.ToHexString(expected).ToLowerInvariant(), Convert.ToHexString(actual).ToLowerInvariant());
Assert.Equal(Convert.ToHexStringLower(expected), Convert.ToHexStringLower(actual));
}
[Fact]
@@ -38,7 +38,7 @@ public sealed class DefaultCryptoHashTests
var hash = CryptoHashFactory.CreateDefault();
var expected = ComputeGostDigest(use256: true);
var actual = hash.ComputeHash(Sample, HashAlgorithms.Gost3411_2012_256);
Assert.Equal(Convert.ToHexString(expected).ToLowerInvariant(), Convert.ToHexString(actual).ToLowerInvariant());
Assert.Equal(Convert.ToHexStringLower(expected), Convert.ToHexStringLower(actual));
}
[Fact]
@@ -47,7 +47,7 @@ public sealed class DefaultCryptoHashTests
var hash = CryptoHashFactory.CreateDefault();
var expected = ComputeGostDigest(use256: false);
var actual = hash.ComputeHash(Sample, HashAlgorithms.Gost3411_2012_512);
Assert.Equal(Convert.ToHexString(expected).ToLowerInvariant(), Convert.ToHexString(actual).ToLowerInvariant());
Assert.Equal(Convert.ToHexStringLower(expected), Convert.ToHexStringLower(actual));
}
[Fact]
@@ -60,6 +60,25 @@ public sealed class DefaultCryptoHashTests
Assert.Equal(Convert.ToHexString(bufferDigest), Convert.ToHexString(streamDigest));
}
[Fact]
public void ComputeHashHex_Sha256_MatchesBclLowerHex()
{
var hash = CryptoHashFactory.CreateDefault();
var expected = Convert.ToHexStringLower(SHA256.HashData(Sample));
var actual = hash.ComputeHashHex(Sample, HashAlgorithms.Sha256);
Assert.Equal(expected, actual);
}
[Fact]
public async Task ComputeHashHexAsync_Sha256_MatchesBclLowerHex()
{
var hash = CryptoHashFactory.CreateDefault();
var expected = Convert.ToHexStringLower(SHA256.HashData(Sample));
await using var stream = new MemoryStream(Sample);
var actual = await hash.ComputeHashHexAsync(stream, HashAlgorithms.Sha256);
Assert.Equal(expected, actual);
}
private static byte[] ComputeGostDigest(bool use256)
{
Org.BouncyCastle.Crypto.IDigest digest = use256

View File

@@ -0,0 +1,34 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed class DefaultCryptoHmacTests
{
private static readonly byte[] Sample = Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog");
private static readonly byte[] Key = Encoding.UTF8.GetBytes("test-key");
[Fact]
public void ComputeHmacHexForPurpose_WebhookInterop_MatchesBclLowerHex()
{
var hmac = DefaultCryptoHmac.CreateForTests();
var expected = Convert.ToHexStringLower(HMACSHA256.HashData(Key, Sample));
var actual = hmac.ComputeHmacHexForPurpose(Key, Sample, HmacPurpose.WebhookInterop);
Assert.Equal(expected, actual);
}
[Fact]
public async Task ComputeHmacHexForPurposeAsync_WebhookInterop_MatchesBclLowerHex()
{
var hmac = DefaultCryptoHmac.CreateForTests();
var expected = Convert.ToHexStringLower(HMACSHA256.HashData(Key, Sample));
await using var stream = new MemoryStream(Sample);
var actual = await hmac.ComputeHmacHexForPurposeAsync(Key, stream, HmacPurpose.WebhookInterop);
Assert.Equal(expected, actual);
}
}

View File

@@ -0,0 +1,52 @@
using System.Security.Cryptography;
using System.Text;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Digests;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed class Sha256DigestTests
{
[Fact]
public void Normalize_AllowsBareHex_WhenPrefixNotRequired()
{
var hex = new string('a', Sha256Digest.HexLength);
Assert.Equal($"sha256:{hex}", Sha256Digest.Normalize(hex));
}
[Fact]
public void Normalize_NormalizesPrefixAndHexToLower()
{
var hexUpper = new string('A', Sha256Digest.HexLength);
Assert.Equal(
$"sha256:{new string('a', Sha256Digest.HexLength)}",
Sha256Digest.Normalize($"SHA256:{hexUpper}"));
}
[Fact]
public void Normalize_RequiresPrefix_WhenConfigured()
{
var hex = new string('a', Sha256Digest.HexLength);
var ex = Assert.Throws<FormatException>(() => Sha256Digest.Normalize(hex, requirePrefix: true, parameterName: "sbomDigest"));
Assert.Contains("sbomDigest", ex.Message, StringComparison.Ordinal);
Assert.Contains("sha256:", ex.Message, StringComparison.Ordinal);
}
[Fact]
public void ExtractHex_ReturnsLowercaseHex()
{
var hexUpper = new string('A', Sha256Digest.HexLength);
Assert.Equal(new string('a', Sha256Digest.HexLength), Sha256Digest.ExtractHex($"sha256:{hexUpper}"));
}
[Fact]
public void Compute_UsesCryptoHashStack()
{
var hash = CryptoHashFactory.CreateDefault();
var content = Encoding.UTF8.GetBytes("hello");
var expectedHex = Convert.ToHexStringLower(SHA256.HashData(content));
Assert.Equal($"sha256:{expectedHex}", Sha256Digest.Compute(hash, content));
}
}

View File

@@ -16,6 +16,10 @@
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
<ProjectReference Include="..\..\StellaOps.Cryptography.DependencyInjection\StellaOps.Cryptography.DependencyInjection.csproj" />
<ProjectReference Include="..\..\StellaOps.Cryptography.Plugin.BouncyCastle\StellaOps.Cryptography.Plugin.BouncyCastle.csproj" />
<ProjectReference Include="..\..\StellaOps.Cryptography.Plugin.OfflineVerification\StellaOps.Cryptography.Plugin.OfflineVerification.csproj" />
<ProjectReference Include="..\..\StellaOps.Cryptography.Plugin.OpenSslGost\StellaOps.Cryptography.Plugin.OpenSslGost.csproj" />
<ProjectReference Include="..\..\StellaOps.Cryptography.Plugin.SmSoft\StellaOps.Cryptography.Plugin.SmSoft.csproj" />
</ItemGroup>
</Project>