stabilizaiton work - projects rework for maintenanceability and ui livening
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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). |
|
||||
|
||||
Reference in New Issue
Block a user