Add unit tests for SBOM ingestion and transformation
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Implement `SbomIngestServiceCollectionExtensionsTests` to verify the SBOM ingestion pipeline exports snapshots correctly.
- Create `SbomIngestTransformerTests` to ensure the transformation produces expected nodes and edges, including deduplication of license nodes and normalization of timestamps.
- Add `SbomSnapshotExporterTests` to test the export functionality for manifest, adjacency, nodes, and edges.
- Introduce `VexOverlayTransformerTests` to validate the transformation of VEX nodes and edges.
- Set up project file for the test project with necessary dependencies and configurations.
- Include JSON fixture files for testing purposes.
This commit is contained in:
master
2025-11-04 07:49:39 +02:00
parent f72c5c513a
commit 2eb6852d34
491 changed files with 39445 additions and 3917 deletions

View File

@@ -0,0 +1,228 @@
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Security.Cryptography;
namespace StellaOps.Cryptography.Kms;
/// <summary>
/// PKCS#11-backed implementation of <see cref="IKmsClient"/>.
/// </summary>
public sealed class Pkcs11KmsClient : IKmsClient
{
private readonly IPkcs11Facade _facade;
private readonly TimeSpan _metadataCacheDuration;
private readonly TimeSpan _publicKeyCacheDuration;
private readonly ConcurrentDictionary<string, CachedMetadata> _metadataCache = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, CachedPublicKey> _publicKeyCache = new(StringComparer.Ordinal);
private bool _disposed;
public Pkcs11KmsClient(IPkcs11Facade facade, Pkcs11Options options)
{
_facade = facade ?? throw new ArgumentNullException(nameof(facade));
ArgumentNullException.ThrowIfNull(options);
_metadataCacheDuration = options.MetadataCacheDuration;
_publicKeyCacheDuration = options.PublicKeyCacheDuration;
}
public async Task<KmsSignResult> SignAsync(
string keyId,
string? keyVersion,
ReadOnlyMemory<byte> data,
CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
ArgumentException.ThrowIfNullOrWhiteSpace(keyId);
if (data.IsEmpty)
{
throw new ArgumentException("Signing payload cannot be empty.", nameof(data));
}
var digest = ComputeSha256(data);
try
{
var descriptor = await GetCachedMetadataAsync(keyId, cancellationToken).ConfigureAwait(false);
var signature = await _facade.SignDigestAsync(digest, cancellationToken).ConfigureAwait(false);
return new KmsSignResult(
descriptor.Descriptor.KeyId,
descriptor.Descriptor.KeyId,
KmsAlgorithms.Es256,
signature);
}
finally
{
CryptographicOperations.ZeroMemory(digest.AsSpan());
}
}
public async Task<bool> VerifyAsync(
string keyId,
string? keyVersion,
ReadOnlyMemory<byte> data,
ReadOnlyMemory<byte> signature,
CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
ArgumentException.ThrowIfNullOrWhiteSpace(keyId);
if (data.IsEmpty || signature.IsEmpty)
{
return false;
}
var digest = ComputeSha256(data);
try
{
var publicMaterial = await GetCachedPublicKeyAsync(keyId, cancellationToken).ConfigureAwait(false);
using var ecdsa = ECDsa.Create(new ECParameters
{
Curve = ResolveCurve(publicMaterial.Material.Curve),
Q =
{
X = publicMaterial.Material.Qx,
Y = publicMaterial.Material.Qy,
},
});
return ecdsa.VerifyHash(digest, signature.ToArray());
}
finally
{
CryptographicOperations.ZeroMemory(digest.AsSpan());
}
}
public async Task<KmsKeyMetadata> GetMetadataAsync(string keyId, CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
ArgumentException.ThrowIfNullOrWhiteSpace(keyId);
var descriptor = await GetCachedMetadataAsync(keyId, cancellationToken).ConfigureAwait(false);
var publicMaterial = await GetCachedPublicKeyAsync(keyId, cancellationToken).ConfigureAwait(false);
using var ecdsa = ECDsa.Create(new ECParameters
{
Curve = ResolveCurve(publicMaterial.Material.Curve),
Q =
{
X = publicMaterial.Material.Qx,
Y = publicMaterial.Material.Qy,
},
});
var subjectInfo = Convert.ToBase64String(ecdsa.ExportSubjectPublicKeyInfo());
var version = new KmsKeyVersionMetadata(
descriptor.Descriptor.KeyId,
KmsKeyState.Active,
descriptor.Descriptor.CreatedAt,
null,
subjectInfo,
publicMaterial.Material.Curve);
return new KmsKeyMetadata(
descriptor.Descriptor.KeyId,
KmsAlgorithms.Es256,
KmsKeyState.Active,
descriptor.Descriptor.CreatedAt,
ImmutableArray.Create(version));
}
public async Task<KmsKeyMaterial> ExportAsync(string keyId, string? keyVersion, CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
ArgumentException.ThrowIfNullOrWhiteSpace(keyId);
var descriptor = await GetCachedMetadataAsync(keyId, cancellationToken).ConfigureAwait(false);
var publicMaterial = await GetCachedPublicKeyAsync(keyId, cancellationToken).ConfigureAwait(false);
return new KmsKeyMaterial(
descriptor.Descriptor.KeyId,
descriptor.Descriptor.KeyId,
KmsAlgorithms.Es256,
publicMaterial.Material.Curve,
Array.Empty<byte>(),
publicMaterial.Material.Qx,
publicMaterial.Material.Qy,
descriptor.Descriptor.CreatedAt);
}
public Task<KmsKeyMetadata> RotateAsync(string keyId, CancellationToken cancellationToken = default)
=> throw new NotSupportedException("PKCS#11 rotation requires HSM administrative tooling.");
public Task RevokeAsync(string keyId, CancellationToken cancellationToken = default)
=> throw new NotSupportedException("PKCS#11 revocation must be handled by HSM policies.");
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_facade.Dispose();
}
private async Task<CachedMetadata> GetCachedMetadataAsync(string keyId, CancellationToken cancellationToken)
{
var now = DateTimeOffset.UtcNow;
if (_metadataCache.TryGetValue(keyId, out var cached) && cached.ExpiresAt > now)
{
return cached;
}
var descriptor = await _facade.GetKeyAsync(cancellationToken).ConfigureAwait(false);
var entry = new CachedMetadata(descriptor, now.Add(_metadataCacheDuration));
_metadataCache[keyId] = entry;
return entry;
}
private async Task<CachedPublicKey> GetCachedPublicKeyAsync(string keyId, CancellationToken cancellationToken)
{
var now = DateTimeOffset.UtcNow;
if (_publicKeyCache.TryGetValue(keyId, out var cached) && cached.ExpiresAt > now)
{
return cached;
}
var material = await _facade.GetPublicKeyAsync(cancellationToken).ConfigureAwait(false);
var entry = new CachedPublicKey(material, now.Add(_publicKeyCacheDuration));
_publicKeyCache[keyId] = entry;
return entry;
}
private static byte[] ComputeSha256(ReadOnlyMemory<byte> data)
{
var digest = new byte[32];
if (!SHA256.TryHashData(data.Span, digest, out _))
{
throw new InvalidOperationException("Failed to hash payload with SHA-256.");
}
return digest;
}
private static ECCurve ResolveCurve(string curve)
=> curve switch
{
JsonWebKeyECTypes.P256 => ECCurve.NamedCurves.nistP256,
JsonWebKeyECTypes.P384 => ECCurve.NamedCurves.nistP384,
JsonWebKeyECTypes.P521 => ECCurve.NamedCurves.nistP521,
_ => throw new InvalidOperationException($"Unsupported EC curve '{curve}'."),
};
private void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(Pkcs11KmsClient));
}
}
private sealed record CachedMetadata(Pkcs11KeyDescriptor Descriptor, DateTimeOffset ExpiresAt);
private sealed record CachedPublicKey(Pkcs11PublicKeyMaterial Material, DateTimeOffset ExpiresAt);
}