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,388 @@
using System.Collections.Immutable;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Kms;
namespace StellaOps.Cryptography.Kms.Tests;
public sealed class CloudKmsClientTests
{
[Fact]
public async Task AwsClient_Signs_Verifies_And_Exports_Metadata()
{
using var fixture = new EcdsaFixture();
var facade = new TestAwsFacade(fixture);
var client = new AwsKmsClient(facade, new AwsKmsOptions
{
MetadataCacheDuration = TimeSpan.FromMinutes(30),
PublicKeyCacheDuration = TimeSpan.FromMinutes(30),
});
var payload = Encoding.UTF8.GetBytes("stella-ops");
var expectedDigest = SHA256.HashData(payload);
var signResult = await client.SignAsync(facade.KeyId, facade.VersionId, payload);
Assert.Equal(KmsAlgorithms.Es256, signResult.Algorithm);
Assert.Equal(facade.VersionId, signResult.VersionId);
Assert.NotEmpty(signResult.Signature);
Assert.Equal(expectedDigest, facade.LastDigest);
var verified = await client.VerifyAsync(facade.KeyId, facade.VersionId, payload, signResult.Signature);
Assert.True(verified);
Assert.Equal(expectedDigest, facade.LastVerifyDigest);
var metadata = await client.GetMetadataAsync(facade.KeyId);
Assert.Equal(facade.KeyId, metadata.KeyId);
Assert.Equal(KmsAlgorithms.Es256, metadata.Algorithm);
Assert.Equal(KmsKeyState.Active, metadata.State);
Assert.Single(metadata.Versions);
var version = metadata.Versions[0];
Assert.Equal(facade.VersionId, version.VersionId);
Assert.Equal(JsonWebKeyECTypes.P256, version.Curve);
Assert.Equal(Convert.ToBase64String(fixture.PublicSubjectInfo), version.PublicKey);
var exported = await client.ExportAsync(facade.KeyId, facade.VersionId);
Assert.Equal(facade.KeyId, exported.KeyId);
Assert.Equal(facade.VersionId, exported.VersionId);
Assert.Empty(exported.D);
Assert.Equal(fixture.Parameters.Q.X, exported.Qx);
Assert.Equal(fixture.Parameters.Q.Y, exported.Qy);
}
[Fact]
public async Task GcpClient_Uses_Primary_When_Version_Not_Specified()
{
using var fixture = new EcdsaFixture();
var facade = new TestGcpFacade(fixture);
var client = new GcpKmsClient(facade, new GcpKmsOptions
{
MetadataCacheDuration = TimeSpan.FromMinutes(30),
PublicKeyCacheDuration = TimeSpan.FromMinutes(30),
});
var payload = Encoding.UTF8.GetBytes("cloud-gcp");
var expectedDigest = SHA256.HashData(payload);
var signResult = await client.SignAsync(facade.KeyName, keyVersion: null, payload);
Assert.Equal(facade.PrimaryVersion, signResult.VersionId);
Assert.Equal(expectedDigest, facade.LastDigest);
var verified = await client.VerifyAsync(facade.KeyName, null, payload, signResult.Signature);
Assert.True(verified);
var metadata = await client.GetMetadataAsync(facade.KeyName);
Assert.Equal(facade.KeyName, metadata.KeyId);
Assert.Equal(KmsKeyState.Active, metadata.State);
Assert.Equal(2, metadata.Versions.Length);
var primaryVersion = metadata.Versions.First(v => v.VersionId == facade.PrimaryVersion);
Assert.Equal(JsonWebKeyECTypes.P256, primaryVersion.Curve);
Assert.Equal(Convert.ToBase64String(fixture.PublicSubjectInfo), primaryVersion.PublicKey);
var exported = await client.ExportAsync(facade.KeyName, null);
Assert.Equal(facade.PrimaryVersion, exported.VersionId);
Assert.Empty(exported.D);
Assert.Equal(fixture.Parameters.Q.X, exported.Qx);
Assert.Equal(fixture.Parameters.Q.Y, exported.Qy);
}
[Fact]
public void KmsCryptoProvider_Skips_NonExportable_Keys()
{
using var fixture = new EcdsaFixture();
var kmsClient = new NonExportingKmsClient(fixture.Parameters);
var provider = new KmsCryptoProvider(kmsClient);
var signingKey = new CryptoSigningKey(
new CryptoKeyReference("arn:aws:kms:us-east-1:123456789012:key/demo", "kms"),
KmsAlgorithms.Es256,
in fixture.Parameters,
DateTimeOffset.UtcNow,
metadata: new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
{
["kms.version"] = "arn:aws:kms:us-east-1:123456789012:key/demo",
});
provider.UpsertSigningKey(signingKey);
var keys = provider.GetSigningKeys();
Assert.Empty(keys);
}
[Fact]
public async Task Pkcs11Client_Signs_Verifies_And_Exports()
{
using var fixture = new EcdsaFixture();
var facade = new TestPkcs11Facade(fixture);
var client = new Pkcs11KmsClient(facade, new Pkcs11Options
{
MetadataCacheDuration = TimeSpan.FromMinutes(15),
PublicKeyCacheDuration = TimeSpan.FromMinutes(15),
});
var payload = Encoding.UTF8.GetBytes("pkcs11");
var expectedDigest = SHA256.HashData(payload);
var signResult = await client.SignAsync("ignored", null, payload);
Assert.Equal(KmsAlgorithms.Es256, signResult.Algorithm);
Assert.Equal(facade.KeyId, signResult.KeyId);
Assert.NotEmpty(signResult.Signature);
Assert.Equal(expectedDigest, facade.LastDigest);
var verified = await client.VerifyAsync("ignored", null, payload, signResult.Signature);
Assert.True(verified);
var metadata = await client.GetMetadataAsync("ignored");
Assert.Equal(facade.KeyId, metadata.KeyId);
Assert.Equal(KmsKeyState.Active, metadata.State);
var version = Assert.Single(metadata.Versions);
Assert.Equal(JsonWebKeyECTypes.P256, version.Curve);
Assert.Equal(Convert.ToBase64String(fixture.PublicSubjectInfo), version.PublicKey);
var exported = await client.ExportAsync("ignored", null);
Assert.Equal(facade.KeyId, exported.KeyId);
Assert.Empty(exported.D);
Assert.Equal(fixture.Parameters.Q.X, exported.Qx);
Assert.Equal(fixture.Parameters.Q.Y, exported.Qy);
}
[Fact]
public async Task Fido2Client_Signs_Verifies_And_Exports()
{
using var fixture = new EcdsaFixture();
var authenticator = new TestFidoAuthenticator(fixture);
var options = new Fido2Options
{
CredentialId = "cred-demo",
RelyingPartyId = "stellaops.test",
PublicKeyPem = TestGcpFacade.ToPem(fixture.PublicSubjectInfo),
CreatedAt = DateTimeOffset.UtcNow.AddDays(-1),
MetadataCacheDuration = TimeSpan.FromMinutes(10),
};
var client = new Fido2KmsClient(authenticator, options);
var payload = Encoding.UTF8.GetBytes("fido2-data");
var expectedDigest = SHA256.HashData(payload);
var signResult = await client.SignAsync(options.CredentialId, null, payload);
Assert.Equal(KmsAlgorithms.Es256, signResult.Algorithm);
Assert.NotEmpty(signResult.Signature);
Assert.Equal(expectedDigest, authenticator.LastDigest);
var verified = await client.VerifyAsync(options.CredentialId, null, payload, signResult.Signature);
Assert.True(verified);
var metadata = await client.GetMetadataAsync(options.CredentialId);
Assert.Equal(options.CredentialId, metadata.KeyId);
Assert.Equal(KmsKeyState.Active, metadata.State);
var version = Assert.Single(metadata.Versions);
Assert.Equal(JsonWebKeyECTypes.P256, version.Curve);
Assert.Equal(Convert.ToBase64String(fixture.PublicSubjectInfo), version.PublicKey);
var material = await client.ExportAsync(options.CredentialId, null);
Assert.Empty(material.D);
Assert.Equal(fixture.Parameters.Q.X, material.Qx);
Assert.Equal(fixture.Parameters.Q.Y, material.Qy);
}
private sealed class TestAwsFacade : IAwsKmsFacade
{
private readonly EcdsaFixture _fixture;
public TestAwsFacade(EcdsaFixture fixture)
{
_fixture = fixture;
}
public string KeyId { get; } = "arn:aws:kms:us-east-1:111122223333:key/demo";
public string VersionId { get; } = "arn:aws:kms:us-east-1:111122223333:key/demo/123";
public byte[] LastDigest { get; private set; } = Array.Empty<byte>();
public byte[] LastVerifyDigest { get; private set; } = Array.Empty<byte>();
public Task<AwsKeyMetadata> GetMetadataAsync(string keyId, CancellationToken cancellationToken)
=> Task.FromResult(new AwsKeyMetadata(KeyId, KeyId, DateTimeOffset.UtcNow, AwsKeyStatus.Enabled));
public Task<AwsPublicKeyMaterial> GetPublicKeyAsync(string keyResource, CancellationToken cancellationToken)
=> Task.FromResult(new AwsPublicKeyMaterial(KeyId, VersionId, "ECC_NIST_P256", _fixture.PublicSubjectInfo));
public Task<AwsSignResult> SignAsync(string keyResource, ReadOnlyMemory<byte> digest, CancellationToken cancellationToken)
{
LastDigest = digest.ToArray();
var signature = _fixture.SignDigest(digest.Span);
return Task.FromResult(new AwsSignResult(KeyId, VersionId, signature));
}
public Task<bool> VerifyAsync(string keyResource, ReadOnlyMemory<byte> digest, ReadOnlyMemory<byte> signature, CancellationToken cancellationToken)
{
LastVerifyDigest = digest.ToArray();
return Task.FromResult(_fixture.VerifyDigest(digest.Span, signature.Span));
}
public void Dispose()
{
}
}
private sealed class TestGcpFacade : IGcpKmsFacade
{
private readonly EcdsaFixture _fixture;
public TestGcpFacade(EcdsaFixture fixture)
{
_fixture = fixture;
}
public string KeyName { get; } = "projects/demo/locations/global/keyRings/sample/cryptoKeys/attestor";
public string PrimaryVersion { get; } = "projects/demo/locations/global/keyRings/sample/cryptoKeys/attestor/cryptoKeyVersions/1";
public string SecondaryVersion { get; } = "projects/demo/locations/global/keyRings/sample/cryptoKeys/attestor/cryptoKeyVersions/2";
public byte[] LastDigest { get; private set; } = Array.Empty<byte>();
public Task<GcpCryptoKeyMetadata> GetCryptoKeyMetadataAsync(string keyName, CancellationToken cancellationToken)
=> Task.FromResult(new GcpCryptoKeyMetadata(KeyName, PrimaryVersion, DateTimeOffset.UtcNow));
public Task<IReadOnlyList<GcpCryptoKeyVersionMetadata>> ListKeyVersionsAsync(string keyName, CancellationToken cancellationToken)
{
IReadOnlyList<GcpCryptoKeyVersionMetadata> versions = new[]
{
new GcpCryptoKeyVersionMetadata(PrimaryVersion, GcpCryptoKeyVersionState.Enabled, DateTimeOffset.UtcNow.AddDays(-2), null),
new GcpCryptoKeyVersionMetadata(SecondaryVersion, GcpCryptoKeyVersionState.Disabled, DateTimeOffset.UtcNow.AddDays(-10), DateTimeOffset.UtcNow.AddDays(-1)),
};
return Task.FromResult(versions);
}
public Task<GcpPublicKeyMaterial> GetPublicKeyAsync(string versionName, CancellationToken cancellationToken)
{
var pem = ToPem(_fixture.PublicSubjectInfo);
return Task.FromResult(new GcpPublicKeyMaterial(versionName, "EC_SIGN_P256_SHA256", pem));
}
public Task<GcpSignResult> SignAsync(string versionName, ReadOnlyMemory<byte> digest, CancellationToken cancellationToken)
{
LastDigest = digest.ToArray();
var signature = _fixture.SignDigest(digest.Span);
return Task.FromResult(new GcpSignResult(versionName, signature));
}
public void Dispose()
{
}
internal static string ToPem(byte[] subjectPublicKeyInfo)
{
var builder = new StringBuilder();
builder.AppendLine("-----BEGIN PUBLIC KEY-----");
builder.AppendLine(Convert.ToBase64String(subjectPublicKeyInfo, Base64FormattingOptions.InsertLineBreaks));
builder.AppendLine("-----END PUBLIC KEY-----");
return builder.ToString();
}
}
private sealed class TestPkcs11Facade : IPkcs11Facade
{
private readonly EcdsaFixture _fixture;
public TestPkcs11Facade(EcdsaFixture fixture)
{
_fixture = fixture;
}
public string KeyId { get; } = "pkcs11-key-1";
public byte[] LastDigest { get; private set; } = Array.Empty<byte>();
public Task<Pkcs11KeyDescriptor> GetKeyAsync(CancellationToken cancellationToken)
=> Task.FromResult(new Pkcs11KeyDescriptor(KeyId, "attestor", DateTimeOffset.UtcNow.AddDays(-7)));
public Task<Pkcs11PublicKeyMaterial> GetPublicKeyAsync(CancellationToken cancellationToken)
=> Task.FromResult(new Pkcs11PublicKeyMaterial(KeyId, JsonWebKeyECTypes.P256, _fixture.Parameters.Q.X!, _fixture.Parameters.Q.Y!));
public Task<byte[]> SignDigestAsync(ReadOnlyMemory<byte> digest, CancellationToken cancellationToken)
{
LastDigest = digest.ToArray();
return Task.FromResult(_fixture.SignDigest(digest.Span));
}
public void Dispose()
{
}
}
private sealed class TestFidoAuthenticator : IFido2Authenticator
{
private readonly EcdsaFixture _fixture;
public TestFidoAuthenticator(EcdsaFixture fixture)
{
_fixture = fixture;
}
public byte[] LastDigest { get; private set; } = Array.Empty<byte>();
public Task<byte[]> SignAsync(string credentialId, ReadOnlyMemory<byte> digest, CancellationToken cancellationToken = default)
{
LastDigest = digest.ToArray();
return Task.FromResult(_fixture.SignDigest(digest.Span));
}
}
private sealed class NonExportingKmsClient : IKmsClient
{
private readonly ECParameters _parameters;
public NonExportingKmsClient(ECParameters parameters)
{
_parameters = parameters;
}
public Task<KmsKeyMetadata> GetMetadataAsync(string keyId, CancellationToken cancellationToken = default)
=> Task.FromResult(new KmsKeyMetadata(keyId, KmsAlgorithms.Es256, KmsKeyState.Active, DateTimeOffset.UtcNow, ImmutableArray<KmsKeyVersionMetadata>.Empty));
public Task<KmsKeyMaterial> ExportAsync(string keyId, string? keyVersion, CancellationToken cancellationToken = default)
=> Task.FromResult(new KmsKeyMaterial(
keyId,
keyVersion ?? keyId,
KmsAlgorithms.Es256,
JsonWebKeyECTypes.P256,
Array.Empty<byte>(),
_parameters.Q.X ?? throw new InvalidOperationException("Qx missing."),
_parameters.Q.Y ?? throw new InvalidOperationException("Qy missing."),
DateTimeOffset.UtcNow));
public Task<KmsSignResult> SignAsync(string keyId, string? keyVersion, ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task<bool> VerifyAsync(string keyId, string? keyVersion, ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task<KmsKeyMetadata> RotateAsync(string keyId, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task RevokeAsync(string keyId, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
}
private sealed class EcdsaFixture : IDisposable
{
private readonly ECDsa _ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
public ECParameters Parameters => _ecdsa.ExportParameters(true);
public byte[] PublicSubjectInfo => _ecdsa.ExportSubjectPublicKeyInfo();
public byte[] SignDigest(ReadOnlySpan<byte> digest) => _ecdsa.SignHash(digest.ToArray());
public bool VerifyDigest(ReadOnlySpan<byte> digest, ReadOnlySpan<byte> signature)
=> _ecdsa.VerifyHash(digest.ToArray(), signature.ToArray());
public void Dispose()
{
_ecdsa.Dispose();
}
}
}