Add unit tests for SBOM ingestion and transformation
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user