This commit is contained in:
master
2026-02-04 19:59:20 +02:00
parent 557feefdc3
commit 5548cf83bf
1479 changed files with 53557 additions and 40339 deletions

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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";
}
}

View File

@@ -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")
{
}
}

View File

@@ -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). |