stabilizaiton work - projects rework for maintenanceability and ui livening

This commit is contained in:
master
2026-02-03 23:40:04 +02:00
parent 074ce117ba
commit 557feefdc3
3305 changed files with 186813 additions and 107843 deletions

View File

@@ -0,0 +1,56 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Kms;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Kms.Tests;
public sealed partial class CloudKmsClientTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AwsClient_Signs_Verifies_And_Exports_Metadata_Async()
{
using var fixture = new EcdsaFixture();
using var facade = new TestAwsFacade(fixture, FixedNow);
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);
}
}

View File

@@ -0,0 +1,34 @@
using StellaOps.Cryptography;
using StellaOps.Cryptography.Kms;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Kms.Tests;
public sealed partial class CloudKmsClientTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void KmsCryptoProvider_Skips_NonExportable_Keys()
{
using var fixture = new EcdsaFixture();
var parameters = fixture.Parameters;
var kmsClient = new NonExportingKmsClient(parameters, FixedNow);
var provider = new KmsCryptoProvider(kmsClient);
var signingKey = new CryptoSigningKey(
new CryptoKeyReference("arn:aws:kms:us-east-1:123456789012:key/demo", "kms"),
KmsAlgorithms.Es256,
in parameters,
FixedNow,
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);
}
}

View File

@@ -0,0 +1,53 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Kms;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Kms.Tests;
public sealed partial class CloudKmsClientTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Fido2Client_Signs_Verifies_And_Exports_Async()
{
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 = FixedNow.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);
}
}

View File

@@ -0,0 +1,46 @@
using StellaOps.Cryptography.Kms;
namespace StellaOps.Cryptography.Kms.Tests;
public sealed partial class CloudKmsClientTests
{
private sealed class TestAwsFacade : IAwsKmsFacade
{
private readonly EcdsaFixture _fixture;
private readonly DateTimeOffset _now;
public TestAwsFacade(EcdsaFixture fixture, DateTimeOffset now)
{
_fixture = fixture;
_now = now;
}
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, _now, 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()
{
}
}
}

View File

@@ -0,0 +1,25 @@
using System.Security.Cryptography;
namespace StellaOps.Cryptography.Kms.Tests;
public sealed partial class CloudKmsClientTests
{
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();
}
}
}

View File

@@ -0,0 +1,24 @@
using StellaOps.Cryptography.Kms;
namespace StellaOps.Cryptography.Kms.Tests;
public sealed partial class CloudKmsClientTests
{
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));
}
}
}

View File

@@ -0,0 +1,65 @@
using System.Text;
using StellaOps.Cryptography.Kms;
namespace StellaOps.Cryptography.Kms.Tests;
public sealed partial class CloudKmsClientTests
{
private sealed class TestGcpFacade : IGcpKmsFacade
{
private readonly EcdsaFixture _fixture;
private readonly DateTimeOffset _now;
public TestGcpFacade(EcdsaFixture fixture, DateTimeOffset now)
{
_fixture = fixture;
_now = now;
}
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, _now));
public Task<IReadOnlyList<GcpCryptoKeyVersionMetadata>> ListKeyVersionsAsync(string keyName, CancellationToken cancellationToken)
{
IReadOnlyList<GcpCryptoKeyVersionMetadata> versions = new[]
{
new GcpCryptoKeyVersionMetadata(PrimaryVersion, GcpCryptoKeyVersionState.Enabled, _now.AddDays(-2), null),
new GcpCryptoKeyVersionMetadata(SecondaryVersion, GcpCryptoKeyVersionState.Disabled, _now.AddDays(-10), _now.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();
}
}
}

View File

@@ -0,0 +1,47 @@
using System.Collections.Immutable;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Cryptography.Kms;
namespace StellaOps.Cryptography.Kms.Tests;
public sealed partial class CloudKmsClientTests
{
private sealed class NonExportingKmsClient : IKmsClient
{
private readonly ECParameters _parameters;
private readonly DateTimeOffset _now;
public NonExportingKmsClient(ECParameters parameters, DateTimeOffset now)
{
_parameters = parameters;
_now = now;
}
public Task<KmsKeyMetadata> GetMetadataAsync(string keyId, CancellationToken cancellationToken = default)
=> Task.FromResult(new KmsKeyMetadata(keyId, KmsAlgorithms.Es256, KmsKeyState.Active, _now, 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."),
_now));
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();
}
}

View File

@@ -0,0 +1,38 @@
using Microsoft.IdentityModel.Tokens;
using StellaOps.Cryptography.Kms;
namespace StellaOps.Cryptography.Kms.Tests;
public sealed partial class CloudKmsClientTests
{
private sealed class TestPkcs11Facade : IPkcs11Facade
{
private readonly EcdsaFixture _fixture;
private readonly DateTimeOffset _now;
public TestPkcs11Facade(EcdsaFixture fixture, DateTimeOffset now)
{
_fixture = fixture;
_now = now;
}
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", _now.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()
{
}
}
}

View File

@@ -0,0 +1,50 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Kms;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Kms.Tests;
public sealed partial class CloudKmsClientTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GcpClient_Uses_Primary_When_Version_Not_Specified_Async()
{
using var fixture = new EcdsaFixture();
using var facade = new TestGcpFacade(fixture, FixedNow);
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);
}
}

View File

@@ -0,0 +1,50 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Kms;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Kms.Tests;
public sealed partial class CloudKmsClientTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Pkcs11Client_Signs_Verifies_And_Exports_Async()
{
using var fixture = new EcdsaFixture();
using var facade = new TestPkcs11Facade(fixture, FixedNow);
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);
}
}

View File

@@ -1,396 +1,6 @@
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;
using StellaOps.TestKit;
namespace StellaOps.Cryptography.Kms.Tests;
public sealed class CloudKmsClientTests
public sealed partial class CloudKmsClientTests
{
[Trait("Category", TestCategories.Unit)]
[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);
}
[Trait("Category", TestCategories.Unit)]
[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);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void KmsCryptoProvider_Skips_NonExportable_Keys()
{
using var fixture = new EcdsaFixture();
var parameters = fixture.Parameters;
var kmsClient = new NonExportingKmsClient(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 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);
}
[Trait("Category", TestCategories.Unit)]
[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);
}
[Trait("Category", TestCategories.Unit)]
[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();
}
}
private static readonly DateTimeOffset FixedNow = new(2024, 1, 15, 10, 30, 0, TimeSpan.Zero);
}

View File

@@ -0,0 +1,27 @@
using StellaOps.Cryptography.Kms;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Kms.Tests;
public sealed partial class FileKmsClientTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExportAsync_ReturnsKeyMaterial_Async()
{
using var workspace = new TestWorkspace(nameof(ExportAsync_ReturnsKeyMaterial_Async));
using var client = workspace.CreateClient();
var keyId = "kms-export";
await client.RotateAsync(keyId);
var material = await client.ExportAsync(keyId, null);
Assert.Equal(keyId, material.KeyId);
Assert.Equal(KmsAlgorithms.Es256, material.Algorithm);
Assert.Equal("nistP256", material.Curve);
Assert.NotEmpty(material.D);
Assert.NotEmpty(material.Qx);
Assert.NotEmpty(material.Qy);
}
}

View File

@@ -0,0 +1,28 @@
using System.Text;
using StellaOps.Cryptography.Kms;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Kms.Tests;
public sealed partial class FileKmsClientTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RevokePreventsSigningAsync()
{
using var workspace = new TestWorkspace(nameof(RevokePreventsSigningAsync));
using var client = workspace.CreateClient();
var keyId = "kms-revoke";
await client.RotateAsync(keyId);
await client.RevokeAsync(keyId);
var metadata = await client.GetMetadataAsync(keyId);
Assert.Equal(KmsKeyState.Revoked, metadata.State);
Assert.All(metadata.Versions, v => Assert.Equal(KmsKeyState.Revoked, v.State));
var data = Encoding.UTF8.GetBytes("kms-revoke-data");
await Assert.ThrowsAsync<InvalidOperationException>(() => client.SignAsync(keyId, null, data));
}
}

View File

@@ -0,0 +1,49 @@
using System.Text;
using StellaOps.Cryptography.Kms;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Kms.Tests;
public sealed partial class FileKmsClientTests
{
private static readonly byte[] FirstData = Encoding.UTF8.GetBytes("kms-rotate-first");
private static readonly byte[] NewData = Encoding.UTF8.GetBytes("kms-rotate-new");
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RotateSignVerifyLifecycle_WorksAsync()
{
using var workspace = new TestWorkspace(nameof(RotateSignVerifyLifecycle_WorksAsync));
using var client = workspace.CreateClient();
var keyId = "kms-test-key";
// Initial rotate creates the key.
var metadata = await client.RotateAsync(keyId);
Assert.Equal(keyId, metadata.KeyId);
Assert.Single(metadata.Versions);
Assert.Equal(KmsKeyState.Active, metadata.State);
var version = metadata.Versions[0];
Assert.Equal(KmsKeyState.Active, version.State);
var firstSignature = await client.SignAsync(keyId, null, FirstData);
Assert.Equal(keyId, firstSignature.KeyId);
Assert.Equal(KmsAlgorithms.Es256, firstSignature.Algorithm);
Assert.True(await client.VerifyAsync(keyId, firstSignature.VersionId, FirstData, firstSignature.Signature));
// Rotate again and ensure metadata reflects both versions.
var rotated = await client.RotateAsync(keyId);
Assert.Equal(2, rotated.Versions.Length);
var activeVersion = rotated.Versions.Single(v => v.State == KmsKeyState.Active);
Assert.Equal(rotated.Versions.Max(v => v.VersionId), activeVersion.VersionId);
var previousVersion = rotated.Versions.Single(v => v.State != KmsKeyState.Active);
Assert.Equal(KmsKeyState.PendingRotation, previousVersion.State);
var activeSignature = await client.SignAsync(keyId, null, NewData);
Assert.Equal(activeVersion.VersionId, activeSignature.VersionId);
Assert.True(await client.VerifyAsync(keyId, null, NewData, activeSignature.Signature));
// Explicit version verify should still pass for previous version using the old signature.
Assert.True(await client.VerifyAsync(keyId, previousVersion.VersionId, FirstData, firstSignature.Signature));
}
}

View File

@@ -1,98 +1,16 @@
using System.Security.Cryptography;
using StellaOps.Cryptography.Kms;
using StellaOps.TestKit;
namespace StellaOps.Cryptography.Kms.Tests;
public sealed class FileKmsClientTests : IDisposable
public sealed partial class FileKmsClientTests
{
private readonly string _rootPath;
private static readonly string RootBasePath = Path.Combine(Path.GetTempPath(), "stellaops-tests", "kms", nameof(FileKmsClientTests));
public FileKmsClientTests()
{
_rootPath = Path.Combine(Path.GetTempPath(), $"kms-tests-{Guid.NewGuid():N}");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RotateSignVerifyLifecycle_Works()
{
using var client = CreateClient();
var keyId = "kms-test-key";
// Initial rotate creates the key.
var metadata = await client.RotateAsync(keyId);
Assert.Equal(keyId, metadata.KeyId);
Assert.Single(metadata.Versions);
Assert.Equal(KmsKeyState.Active, metadata.State);
var version = metadata.Versions[0];
Assert.Equal(KmsKeyState.Active, version.State);
var firstData = RandomNumberGenerator.GetBytes(256);
var firstSignature = await client.SignAsync(keyId, null, firstData);
Assert.Equal(keyId, firstSignature.KeyId);
Assert.Equal(KmsAlgorithms.Es256, firstSignature.Algorithm);
Assert.True(await client.VerifyAsync(keyId, firstSignature.VersionId, firstData, firstSignature.Signature));
// Rotate again and ensure metadata reflects both versions.
var rotated = await client.RotateAsync(keyId);
Assert.Equal(2, rotated.Versions.Length);
var activeVersion = rotated.Versions.Single(v => v.State == KmsKeyState.Active);
Assert.Equal(rotated.Versions.Max(v => v.VersionId), activeVersion.VersionId);
var previousVersion = rotated.Versions.Single(v => v.State != KmsKeyState.Active);
Assert.Equal(KmsKeyState.PendingRotation, previousVersion.State);
var newData = RandomNumberGenerator.GetBytes(128);
var activeSignature = await client.SignAsync(keyId, null, newData);
Assert.Equal(activeVersion.VersionId, activeSignature.VersionId);
Assert.True(await client.VerifyAsync(keyId, null, newData, activeSignature.Signature));
// Explicit version verify should still pass for previous version using the old signature.
Assert.True(await client.VerifyAsync(keyId, previousVersion.VersionId, firstData, firstSignature.Signature));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RevokePreventsSigning()
{
using var client = CreateClient();
var keyId = "kms-revoke";
await client.RotateAsync(keyId);
await client.RevokeAsync(keyId);
var metadata = await client.GetMetadataAsync(keyId);
Assert.Equal(KmsKeyState.Revoked, metadata.State);
Assert.All(metadata.Versions, v => Assert.Equal(KmsKeyState.Revoked, v.State));
var data = RandomNumberGenerator.GetBytes(32);
await Assert.ThrowsAsync<InvalidOperationException>(() => client.SignAsync(keyId, null, data));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExportAsync_ReturnsKeyMaterial()
{
using var client = CreateClient();
var keyId = "kms-export";
await client.RotateAsync(keyId);
var material = await client.ExportAsync(keyId, null);
Assert.Equal(keyId, material.KeyId);
Assert.Equal(KmsAlgorithms.Es256, material.Algorithm);
Assert.Equal("nistP256", material.Curve);
Assert.NotEmpty(material.D);
Assert.NotEmpty(material.Qx);
Assert.NotEmpty(material.Qy);
}
private FileKmsClient CreateClient()
private static FileKmsClient CreateClient(string rootPath)
{
var options = new FileKmsOptions
{
RootPath = _rootPath,
RootPath = rootPath,
Password = "P@ssw0rd!",
Algorithm = KmsAlgorithms.Es256,
};
@@ -100,18 +18,35 @@ public sealed class FileKmsClientTests : IDisposable
return new FileKmsClient(options);
}
public void Dispose()
private sealed class TestWorkspace : IDisposable
{
try
private readonly string _rootPath;
public TestWorkspace(string testName)
{
_rootPath = Path.Combine(RootBasePath, testName);
if (Directory.Exists(_rootPath))
{
Directory.Delete(_rootPath, recursive: true);
}
Directory.CreateDirectory(_rootPath);
}
catch
public FileKmsClient CreateClient() => FileKmsClientTests.CreateClient(_rootPath);
public void Dispose()
{
// ignore cleanup errors
try
{
if (Directory.Exists(_rootPath))
{
Directory.Delete(_rootPath, recursive: true);
}
}
catch
{
// ignore cleanup errors
}
}
}
}

View File

@@ -12,3 +12,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0250-T | DONE | Revalidated 2026-01-07. |
| AUDIT-0250-A | DONE | Waived (test project; revalidated 2026-01-07). |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| REMED-08 | DONE | Async naming fixed; deterministic timestamps/data; files split <= 100 lines; ConfigureAwait(false) skipped per xUnit1030; dotnet test passed 2026-02-03 (8 tests, MTP0001 warning). |