part #2
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.Plugin.BouncyCastle;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public sealed class BouncyCastleKeyNormalizationTests
|
||||
{
|
||||
private static readonly DateTimeOffset FixedNow = new(2024, 1, 15, 10, 30, 0, TimeSpan.Zero);
|
||||
|
||||
[Fact]
|
||||
public void UpsertSigningKey_With64BytePrivateKey_NormalizesTo32Bytes()
|
||||
{
|
||||
var provider = new BouncyCastleEd25519CryptoProvider();
|
||||
var privateKey = Enumerable.Range(1, 64).Select(i => (byte)i).ToArray();
|
||||
var keyReference = new CryptoKeyReference("key-64", provider.Name);
|
||||
var signingKey = new CryptoSigningKey(
|
||||
keyReference,
|
||||
SignatureAlgorithms.Ed25519,
|
||||
privateKey,
|
||||
createdAt: FixedNow);
|
||||
|
||||
provider.UpsertSigningKey(signingKey);
|
||||
|
||||
var stored = provider.GetSigningKeys().Single();
|
||||
stored.PrivateKey.Length.Should().Be(32);
|
||||
stored.PrivateKey.ToArray().Should().Equal(privateKey.Take(32).ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpsertSigningKey_EmptyPublicKey_DerivesPublicKey()
|
||||
{
|
||||
var provider = new BouncyCastleEd25519CryptoProvider();
|
||||
var privateKey = Enumerable.Range(10, 32).Select(i => (byte)i).ToArray();
|
||||
var keyReference = new CryptoKeyReference("key-derived-public", provider.Name);
|
||||
var signingKey = new CryptoSigningKey(
|
||||
keyReference,
|
||||
SignatureAlgorithms.Ed25519,
|
||||
privateKey,
|
||||
createdAt: FixedNow);
|
||||
|
||||
provider.UpsertSigningKey(signingKey);
|
||||
|
||||
var stored = provider.GetSigningKeys().Single();
|
||||
stored.PublicKey.Length.Should().Be(32);
|
||||
stored.PublicKey.ToArray().Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpsertSigningKey_InvalidPublicKeyLength_Throws()
|
||||
{
|
||||
var provider = new BouncyCastleEd25519CryptoProvider();
|
||||
var privateKey = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray();
|
||||
var publicKey = new byte[31];
|
||||
var keyReference = new CryptoKeyReference("key-invalid-public", provider.Name);
|
||||
var signingKey = new CryptoSigningKey(
|
||||
keyReference,
|
||||
SignatureAlgorithms.Ed25519,
|
||||
privateKey,
|
||||
createdAt: FixedNow,
|
||||
publicKey: publicKey);
|
||||
|
||||
Action act = () => provider.UpsertSigningKey(signingKey);
|
||||
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("*public key must be 32 bytes*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpsertSigningKey_EdDsaAlgorithm_NormalizesToEd25519()
|
||||
{
|
||||
var provider = new BouncyCastleEd25519CryptoProvider();
|
||||
var privateKey = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray();
|
||||
var keyReference = new CryptoKeyReference("key-eddsa", provider.Name);
|
||||
var signingKey = new CryptoSigningKey(
|
||||
keyReference,
|
||||
SignatureAlgorithms.EdDsa,
|
||||
privateKey,
|
||||
createdAt: FixedNow);
|
||||
|
||||
provider.UpsertSigningKey(signingKey);
|
||||
|
||||
var stored = provider.GetSigningKeys().Single();
|
||||
stored.AlgorithmId.Should().Be(SignatureAlgorithms.Ed25519);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
internal sealed class OrderedTestCryptoProvider : ICryptoProvider
|
||||
{
|
||||
internal const string Algorithm = "test-hash";
|
||||
private readonly ICryptoHasher _hasher;
|
||||
|
||||
public OrderedTestCryptoProvider(string name)
|
||||
{
|
||||
Name = name;
|
||||
_hasher = new OrderedTestHasher(Algorithm);
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public bool Supports(CryptoCapability capability, string algorithmId)
|
||||
=> capability == CryptoCapability.ContentHashing &&
|
||||
string.Equals(algorithmId, Algorithm, StringComparison.Ordinal);
|
||||
|
||||
public IPasswordHasher GetPasswordHasher(string algorithmId)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public ICryptoHasher GetHasher(string algorithmId)
|
||||
=> Supports(CryptoCapability.ContentHashing, algorithmId)
|
||||
? _hasher
|
||||
: throw new InvalidOperationException("Unsupported hash algorithm.");
|
||||
|
||||
public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public void UpsertSigningKey(CryptoSigningKey signingKey)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public bool RemoveSigningKey(string keyId) => false;
|
||||
|
||||
public IReadOnlyCollection<CryptoSigningKey> GetSigningKeys()
|
||||
=> Array.Empty<CryptoSigningKey>();
|
||||
|
||||
private sealed class OrderedTestHasher : ICryptoHasher
|
||||
{
|
||||
public OrderedTestHasher(string algorithmId)
|
||||
{
|
||||
AlgorithmId = algorithmId;
|
||||
}
|
||||
|
||||
public string AlgorithmId { get; }
|
||||
|
||||
public byte[] ComputeHash(ReadOnlySpan<byte> data) => Array.Empty<byte>();
|
||||
|
||||
public string ComputeHashHex(ReadOnlySpan<byte> data) => string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.DependencyInjection;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public sealed class CryptoDependencyInjectionTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddStellaOpsCrypto_ResolvesPreferredProviderOrder()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<ICryptoProvider>(new OrderedTestCryptoProvider("alpha"));
|
||||
services.AddSingleton<ICryptoProvider>(new OrderedTestCryptoProvider("beta"));
|
||||
services.AddStellaOpsCrypto(options =>
|
||||
{
|
||||
options.PreferredProviders.Add("beta");
|
||||
options.PreferredProviders.Add("alpha");
|
||||
});
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
var registry = provider.GetRequiredService<ICryptoProviderRegistry>();
|
||||
|
||||
var resolved = registry.ResolveOrThrow(CryptoCapability.ContentHashing, OrderedTestCryptoProvider.Algorithm);
|
||||
|
||||
Assert.Equal("beta", resolved.Name);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddStellaOpsCryptoFromConfiguration_LoadsPluginProvidersByPriority()
|
||||
{
|
||||
var tempRoot = Path.Combine(Path.GetTempPath(), "stellaops-crypto-tests", $"crypto-di-{Environment.ProcessId}");
|
||||
Directory.CreateDirectory(tempRoot);
|
||||
|
||||
try
|
||||
{
|
||||
var assemblyPath = typeof(TestPluginAlphaProvider).Assembly.Location;
|
||||
var assemblyFileName = Path.GetFileName(assemblyPath);
|
||||
File.Copy(assemblyPath, Path.Combine(tempRoot, assemblyFileName), overwrite: true);
|
||||
|
||||
var manifestPath = Path.Combine(tempRoot, "crypto-plugins-manifest.json");
|
||||
WriteManifest(manifestPath, assemblyFileName, GetCurrentPlatform());
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["StellaOps:Crypto:Plugins:ManifestPath"] = manifestPath,
|
||||
["StellaOps:Crypto:Plugins:DiscoveryMode"] = "explicit",
|
||||
["StellaOps:Crypto:Plugins:Enabled:0:Id"] = "test.plugin.alpha",
|
||||
["StellaOps:Crypto:Plugins:Enabled:0:Priority"] = "10",
|
||||
["StellaOps:Crypto:Plugins:Enabled:1:Id"] = "test.plugin.beta",
|
||||
["StellaOps:Crypto:Plugins:Enabled:1:Priority"] = "90",
|
||||
})
|
||||
.Build();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddStellaOpsCryptoFromConfiguration(configuration, tempRoot);
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
var registry = provider.GetRequiredService<ICryptoProviderRegistry>();
|
||||
|
||||
var resolved = registry.ResolveOrThrow(CryptoCapability.ContentHashing, TestPluginCryptoProviderBase.TestAlgorithm);
|
||||
|
||||
Assert.Equal("test.plugin.beta", resolved.Name);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup is best-effort because the plugin assembly can remain locked on Windows.
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(tempRoot))
|
||||
{
|
||||
Directory.Delete(tempRoot, recursive: true);
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteManifest(string manifestPath, string assemblyFileName, string platform)
|
||||
{
|
||||
var manifest = new
|
||||
{
|
||||
version = "1.0",
|
||||
plugins = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
id = "test.plugin.alpha",
|
||||
name = "Test Plugin Alpha",
|
||||
assembly = assemblyFileName,
|
||||
type = typeof(TestPluginAlphaProvider).FullName!,
|
||||
platforms = new[] { platform },
|
||||
},
|
||||
new
|
||||
{
|
||||
id = "test.plugin.beta",
|
||||
name = "Test Plugin Beta",
|
||||
assembly = assemblyFileName,
|
||||
type = typeof(TestPluginBetaProvider).FullName!,
|
||||
platforms = new[] { platform },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(manifest, new JsonSerializerOptions { WriteIndented = true });
|
||||
File.WriteAllText(manifestPath, json);
|
||||
}
|
||||
|
||||
private static string GetCurrentPlatform()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return "linux";
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return "windows";
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
return "osx";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public abstract class TestPluginCryptoProviderBase : ICryptoProvider
|
||||
{
|
||||
public const string TestAlgorithm = "plugin-hash";
|
||||
private static readonly ICryptoHasher Hasher = new TestPluginHasher();
|
||||
|
||||
protected TestPluginCryptoProviderBase(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public bool Supports(CryptoCapability capability, string algorithmId)
|
||||
=> capability == CryptoCapability.ContentHashing &&
|
||||
string.Equals(algorithmId, TestAlgorithm, StringComparison.Ordinal);
|
||||
|
||||
public IPasswordHasher GetPasswordHasher(string algorithmId)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public ICryptoHasher GetHasher(string algorithmId)
|
||||
=> Supports(CryptoCapability.ContentHashing, algorithmId)
|
||||
? Hasher
|
||||
: throw new InvalidOperationException("Unsupported hash algorithm.");
|
||||
|
||||
public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public void UpsertSigningKey(CryptoSigningKey signingKey)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public bool RemoveSigningKey(string keyId) => false;
|
||||
|
||||
public IReadOnlyCollection<CryptoSigningKey> GetSigningKeys()
|
||||
=> Array.Empty<CryptoSigningKey>();
|
||||
|
||||
private sealed class TestPluginHasher : ICryptoHasher
|
||||
{
|
||||
public string AlgorithmId => TestAlgorithm;
|
||||
public byte[] ComputeHash(ReadOnlySpan<byte> data) => Array.Empty<byte>();
|
||||
public string ComputeHashHex(ReadOnlySpan<byte> data) => string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestPluginAlphaProvider : TestPluginCryptoProviderBase
|
||||
{
|
||||
public TestPluginAlphaProvider()
|
||||
: base("test.plugin.alpha")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestPluginBetaProvider : TestPluginCryptoProviderBase
|
||||
{
|
||||
public TestPluginBetaProvider()
|
||||
: base("test.plugin.beta")
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -13,3 +13,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| AUDIT-0271-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
| REMED-08 | DONE | Remediated (deterministic fixtures, async naming, file split <= 100 lines); `dotnet test` passed (312 tests). |
|
||||
| REMED-05 | DONE | Added DI ordering and plugin-loading tests for crypto registration paths; `dotnet test src/__Libraries/__Tests/StellaOps.Cryptography.Tests/StellaOps.Cryptography.Tests.csproj -p:BuildInParallel=false -p:UseSharedCompilation=false` passed (326 tests). |
|
||||
|
||||
Reference in New Issue
Block a user