# stella CLI - Crypto Plugin Development Guide **Sprint:** SPRINT_4100_0006_0006 - CLI Documentation Overhaul ## Overview This guide explains how to develop custom cryptographic plugins for the `stella` CLI. Plugins enable support for regional cryptographic algorithms (GOST, eIDAS, SM) and custom signing infrastructure (HSMs, KMS, remote signers). **Prerequisites:** - .NET 10 SDK - Understanding of cryptographic concepts (signing, verification, key management) - Familiarity with Dependency Injection patterns --- ## Plugin Interface Specification ### ICryptoProvider All crypto providers must implement the `ICryptoProvider` interface: ```csharp namespace StellaOps.Cli.Crypto; /// /// Core interface for all cryptographic providers /// public interface ICryptoProvider { /// /// Unique provider name (e.g., "gost", "eidas", "sm", "default") /// Used for --provider flag in CLI /// string Name { get; } /// /// Supported cryptographic algorithms /// (e.g., "GOST12-256", "ECDSA-P256", "SM2") /// string[] SupportedAlgorithms { get; } /// /// Sign data with specified algorithm and key /// /// Data to sign /// Algorithm to use /// Key reference /// Cancellation token /// Signature bytes Task SignAsync( byte[] data, string algorithm, CryptoKeyReference keyRef, CancellationToken cancellationToken = default); /// /// Verify signature /// /// Original data /// Signature to verify /// Algorithm used /// Key reference (public key) /// Cancellation token /// True if valid, false otherwise Task VerifyAsync( byte[] data, byte[] signature, string algorithm, CryptoKeyReference keyRef, CancellationToken cancellationToken = default); /// /// List available keys in this provider /// /// Cancellation token /// List of available keys Task> ListKeysAsync( CancellationToken cancellationToken = default); } ``` ### ICryptoProviderDiagnostics (Optional) For advanced diagnostics and health checks: ```csharp namespace StellaOps.Cli.Crypto; /// /// Optional interface for provider diagnostics and health checks /// public interface ICryptoProviderDiagnostics { /// /// Run provider self-test /// Task HealthCheckAsync( CancellationToken cancellationToken = default); /// /// Get provider version and capabilities /// ProviderInfo GetInfo(); } /// /// Health check result /// public sealed record ProviderHealthCheck { public required string ProviderName { get; init; } public required bool IsHealthy { get; init; } public required string[] Checks { get; init; } public string? ErrorMessage { get; init; } } /// /// Provider metadata /// public sealed record ProviderInfo { public required string Name { get; init; } public required string Version { get; init; } public required string[] Capabilities { get; init; } public required string[] SupportedAlgorithms { get; init; } } ``` --- ## Supporting Types ### CryptoKeyReference Represents a reference to a cryptographic key: ```csharp namespace StellaOps.Cli.Crypto; /// /// Reference to a cryptographic key /// public sealed record CryptoKeyReference { /// /// Key identifier (e.g., "prod-key-2024", file path, HSM slot) /// public required string KeyId { get; init; } /// /// Key source type: "file", "hsm", "kms", "csp", "pkcs11" /// public required string Source { get; init; } /// /// Additional parameters (e.g., HSM PIN, KMS region, CSP container) /// public IReadOnlyDictionary? Parameters { get; init; } } ``` ### CryptoKeyInfo Metadata about an available key: ```csharp namespace StellaOps.Cli.Crypto; /// /// Information about an available cryptographic key /// public sealed record CryptoKeyInfo { public required string KeyId { get; init; } public required string Algorithm { get; init; } public required string Source { get; init; } public string? FriendlyName { get; init; } public DateTimeOffset? ExpiresAt { get; init; } public bool CanSign { get; init; } public bool CanVerify { get; init; } } ``` --- ## Implementation Guide ### Step 1: Create Plugin Project ```bash # Create new library project dotnet new classlib -n StellaOps.Cli.Crypto.MyProvider cd StellaOps.Cli.Crypto.MyProvider # Add reference to interface project dotnet add reference ../StellaOps.Cli.Crypto/StellaOps.Cli.Crypto.csproj # Add required packages dotnet add package Microsoft.Extensions.Options dotnet add package Microsoft.Extensions.Logging ``` **Project structure:** ``` StellaOps.Cli.Crypto.MyProvider/ ├── MyProviderCryptoProvider.cs # ICryptoProvider implementation ├── MyProviderOptions.cs # Configuration options ├── ServiceCollectionExtensions.cs # DI registration ├── Adapters/ │ ├── LibraryAdapter.cs # Native library adapter │ └── RemoteSignerAdapter.cs # Remote signer client └── StellaOps.Cli.Crypto.MyProvider.csproj ``` --- ### Step 2: Implement ICryptoProvider ```csharp using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StellaOps.Cli.Crypto; namespace StellaOps.Cli.Crypto.MyProvider; /// /// Crypto provider for MyProvider algorithm /// public class MyProviderCryptoProvider : ICryptoProvider, ICryptoProviderDiagnostics { private readonly MyProviderOptions _options; private readonly ILogger _logger; public MyProviderCryptoProvider( IOptions options, ILogger logger) { _options = options.Value ?? throw new ArgumentNullException(nameof(options)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger.LogInformation("MyProvider crypto provider initialized"); } public string Name => "myprovider"; public string[] SupportedAlgorithms => new[] { "MYPROVIDER-ALG1", "MYPROVIDER-ALG2" }; public async Task SignAsync( byte[] data, string algorithm, CryptoKeyReference keyRef, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(data); ArgumentNullException.ThrowIfNull(algorithm); ArgumentNullException.ThrowIfNull(keyRef); _logger.LogDebug("Signing {DataLength} bytes with {Algorithm}", data.Length, algorithm); if (!SupportedAlgorithms.Contains(algorithm)) { throw new NotSupportedException($"Algorithm '{algorithm}' is not supported by this provider"); } // Implementation: Call native library, HSM, or remote signer // Example: Use native library if (_options.UseNativeLibrary) { return await SignWithNativeLibraryAsync(data, algorithm, keyRef, cancellationToken); } // Example: Use remote signer if (_options.UseRemoteSigner) { return await SignWithRemoteSignerAsync(data, algorithm, keyRef, cancellationToken); } throw new InvalidOperationException("No signing method configured"); } public async Task VerifyAsync( byte[] data, byte[] signature, string algorithm, CryptoKeyReference keyRef, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(data); ArgumentNullException.ThrowIfNull(signature); ArgumentNullException.ThrowIfNull(algorithm); ArgumentNullException.ThrowIfNull(keyRef); _logger.LogDebug("Verifying signature for {DataLength} bytes with {Algorithm}", data.Length, algorithm); if (!SupportedAlgorithms.Contains(algorithm)) { throw new NotSupportedException($"Algorithm '{algorithm}' is not supported by this provider"); } // Implementation: Verify signature if (_options.UseNativeLibrary) { return await VerifyWithNativeLibraryAsync(data, signature, algorithm, keyRef, cancellationToken); } throw new InvalidOperationException("No verification method configured"); } public async Task> ListKeysAsync( CancellationToken cancellationToken = default) { _logger.LogDebug("Listing available keys"); var keys = new List(); // Example: List keys from configuration if (_options.Keys != null) { foreach (var keyConfig in _options.Keys) { keys.Add(new CryptoKeyInfo { KeyId = keyConfig.KeyId, Algorithm = keyConfig.Algorithm, Source = keyConfig.Source, FriendlyName = keyConfig.FriendlyName, CanSign = true, CanVerify = true }); } } return keys.AsReadOnly(); } public async Task HealthCheckAsync( CancellationToken cancellationToken = default) { var checks = new List(); var isHealthy = true; string? errorMessage = null; try { // Check 1: Native library loaded if (_options.UseNativeLibrary) { if (IsNativeLibraryLoaded()) { checks.Add("✅ Native library loaded"); } else { checks.Add("❌ Native library not loaded"); isHealthy = false; errorMessage = "Native library not found or failed to load"; } } // Check 2: Remote signer reachable if (_options.UseRemoteSigner) { if (await IsRemoteSignerReachableAsync(cancellationToken)) { checks.Add("✅ Remote signer reachable"); } else { checks.Add("❌ Remote signer unreachable"); isHealthy = false; errorMessage = "Remote signer not reachable"; } } // Check 3: Keys accessible var keyList = await ListKeysAsync(cancellationToken); if (keyList.Count > 0) { checks.Add($"✅ {keyList.Count} keys accessible"); } else { checks.Add("⚠️ No keys configured"); } } catch (Exception ex) { checks.Add($"❌ Health check failed: {ex.Message}"); isHealthy = false; errorMessage = ex.Message; } return new ProviderHealthCheck { ProviderName = Name, IsHealthy = isHealthy, Checks = checks.ToArray(), ErrorMessage = errorMessage }; } public ProviderInfo GetInfo() { return new ProviderInfo { Name = Name, Version = "1.0.0", Capabilities = new[] { "sign", "verify", "list-keys" }, SupportedAlgorithms = SupportedAlgorithms }; } // Private helper methods private async Task SignWithNativeLibraryAsync( byte[] data, string algorithm, CryptoKeyReference keyRef, CancellationToken cancellationToken) { // Example: Call native library via P/Invoke or wrapper // This is a placeholder - actual implementation depends on your crypto library throw new NotImplementedException("Native library signing not implemented"); } private async Task VerifyWithNativeLibraryAsync( byte[] data, byte[] signature, string algorithm, CryptoKeyReference keyRef, CancellationToken cancellationToken) { throw new NotImplementedException("Native library verification not implemented"); } private async Task SignWithRemoteSignerAsync( byte[] data, string algorithm, CryptoKeyReference keyRef, CancellationToken cancellationToken) { // Example: Call remote signer API throw new NotImplementedException("Remote signer not implemented"); } private bool IsNativeLibraryLoaded() { // Check if native library is loaded return true; // Placeholder } private async Task IsRemoteSignerReachableAsync(CancellationToken cancellationToken) { // Ping remote signer return true; // Placeholder } } ``` --- ### Step 3: Configuration Options ```csharp namespace StellaOps.Cli.Crypto.MyProvider; /// /// Configuration options for MyProvider crypto provider /// public sealed class MyProviderOptions { /// /// Use native library for crypto operations /// public bool UseNativeLibrary { get; set; } = true; /// /// Path to native library (if not in standard location) /// public string? NativeLibraryPath { get; set; } /// /// Use remote signer API /// public bool UseRemoteSigner { get; set; } = false; /// /// Remote signer API URL /// public string? RemoteSignerUrl { get; set; } /// /// Remote signer API key /// public string? RemoteSignerApiKey { get; set; } /// /// Configured keys /// public List? Keys { get; set; } public sealed class KeyConfiguration { public required string KeyId { get; init; } public required string Algorithm { get; init; } public required string Source { get; init; } public string? FriendlyName { get; init; } public string? FilePath { get; init; } public string? HsmSlot { get; init; } } } ``` --- ### Step 4: DI Registration ```csharp using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace StellaOps.Cli.Crypto.MyProvider; /// /// Service collection extensions for MyProvider crypto provider /// public static class ServiceCollectionExtensions { /// /// Add MyProvider crypto providers to DI container /// public static IServiceCollection AddMyProviderCryptoProviders( this IServiceCollection services, IConfiguration configuration) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(configuration); // Register provider as ICryptoProvider services.AddSingleton(); // Bind configuration services.Configure( configuration.GetSection("StellaOps:Crypto:Providers:MyProvider")); return services; } } ``` --- ### Step 5: Configuration Example ```yaml # appsettings.myprovider.yaml StellaOps: Crypto: Providers: MyProvider: UseNativeLibrary: true NativeLibraryPath: "/usr/lib/libmyprovider.so" UseRemoteSigner: false RemoteSignerUrl: "https://signer.example.com/api/v1/sign" RemoteSignerApiKey: "${MYPROVIDER_API_KEY}" Keys: - KeyId: "prod-key-2024" Algorithm: "MYPROVIDER-ALG1" Source: "file" FilePath: "/etc/stellaops/keys/prod-key.pem" FriendlyName: "Production Signing Key 2024" - KeyId: "hsm-key-001" Algorithm: "MYPROVIDER-ALG2" Source: "hsm" HsmSlot: "0" FriendlyName: "HSM Key Slot 0" ``` --- ### Step 6: Update CLI Project #### Update StellaOps.Cli.csproj ```xml $(DefineConstants);STELLAOPS_ENABLE_MYPROVIDER ``` #### Update Program.cs ```csharp using StellaOps.Cli.Crypto.Default; #if STELLAOPS_ENABLE_GOST using StellaOps.Cli.Crypto.Gost; #endif #if STELLAOPS_ENABLE_MYPROVIDER using StellaOps.Cli.Crypto.MyProvider; #endif namespace StellaOps.Cli; public class Program { public static async Task Main(string[] args) { // ... configuration setup ... // Register default crypto providers (always available) services.AddDefaultCryptoProviders(configuration); #if STELLAOPS_ENABLE_GOST services.AddGostCryptoProviders(configuration); #endif #if STELLAOPS_ENABLE_MYPROVIDER services.AddMyProviderCryptoProviders(configuration); #endif // ... rest of setup ... } } ``` --- ## Testing ### Unit Tests ```csharp using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using StellaOps.Cli.Crypto; using StellaOps.Cli.Crypto.MyProvider; using Xunit; namespace StellaOps.Cli.Crypto.MyProvider.Tests; public class MyProviderCryptoProviderTests { [Fact] public void Name_ReturnsExpectedName() { var provider = CreateProvider(); Assert.Equal("myprovider", provider.Name); } [Fact] public void SupportedAlgorithms_ContainsExpectedAlgorithms() { var provider = CreateProvider(); Assert.Contains("MYPROVIDER-ALG1", provider.SupportedAlgorithms); Assert.Contains("MYPROVIDER-ALG2", provider.SupportedAlgorithms); } [Fact] public async Task SignAsync_WithUnsupportedAlgorithm_ThrowsNotSupportedException() { var provider = CreateProvider(); var data = "test"u8.ToArray(); var keyRef = new CryptoKeyReference { KeyId = "test-key", Source = "file" }; await Assert.ThrowsAsync(async () => { await provider.SignAsync(data, "UNSUPPORTED-ALG", keyRef); }); } [Fact] public async Task ListKeysAsync_ReturnsConfiguredKeys() { var options = new MyProviderOptions { Keys = new List { new() { KeyId = "key1", Algorithm = "MYPROVIDER-ALG1", Source = "file", FriendlyName = "Test Key 1" } } }; var provider = new MyProviderCryptoProvider( Options.Create(options), NullLogger.Instance); var keys = await provider.ListKeysAsync(); Assert.Single(keys); Assert.Equal("key1", keys[0].KeyId); Assert.Equal("MYPROVIDER-ALG1", keys[0].Algorithm); } [Fact] public async Task HealthCheckAsync_ReturnsHealthStatus() { var provider = CreateProvider(); var healthCheck = await provider.HealthCheckAsync(); Assert.NotNull(healthCheck); Assert.Equal("myprovider", healthCheck.ProviderName); } [Fact] public void GetInfo_ReturnsProviderInfo() { var provider = CreateProvider(); var info = provider.GetInfo(); Assert.Equal("myprovider", info.Name); Assert.Equal("1.0.0", info.Version); Assert.Contains("sign", info.Capabilities); Assert.Contains("verify", info.Capabilities); } private static MyProviderCryptoProvider CreateProvider() { var options = Options.Create(new MyProviderOptions { UseNativeLibrary = true }); return new MyProviderCryptoProvider(options, NullLogger.Instance); } } ``` ### Integration Tests ```csharp using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using StellaOps.Cli.Crypto; using Xunit; namespace StellaOps.Cli.Crypto.MyProvider.Tests; public class MyProviderIntegrationTests { [Fact] public void ServiceProvider_ResolvesMyProvider() { var services = new ServiceCollection(); var configuration = new ConfigurationBuilder().Build(); services.AddLogging(); services.AddMyProviderCryptoProviders(configuration); var serviceProvider = services.BuildServiceProvider(); var providers = serviceProvider.GetServices().ToList(); var myProvider = providers.FirstOrDefault(p => p.Name == "myprovider"); Assert.NotNull(myProvider); } [Fact] public async Task EndToEnd_SignAndVerify() { // This test requires actual crypto library or mocking // Example structure: var provider = CreateProvider(); var data = "test data"u8.ToArray(); var keyRef = new CryptoKeyReference { KeyId = "test-key", Source = "file", Parameters = new Dictionary { ["FilePath"] = "/path/to/key.pem" } }; // Sign var signature = await provider.SignAsync(data, "MYPROVIDER-ALG1", keyRef); Assert.NotNull(signature); Assert.NotEmpty(signature); // Verify var isValid = await provider.VerifyAsync(data, signature, "MYPROVIDER-ALG1", keyRef); Assert.True(isValid); } private MyProviderCryptoProvider CreateProvider() { // Setup provider with test configuration throw new NotImplementedException(); } } ``` --- ## Build and Distribution ### Build Plugin ```bash # Build plugin dotnet build src/Cli/StellaOps.Cli.Crypto.MyProvider # Run tests dotnet test src/Cli/StellaOps.Cli.Crypto.MyProvider.Tests ``` ### Build CLI with Plugin ```bash # Build CLI with MyProvider plugin dotnet publish src/Cli/StellaOps.Cli \ --configuration Release \ --runtime linux-x64 \ -p:StellaOpsEnableMyProvider=true ``` ### Verify Plugin Inclusion ```bash # Check available providers ./stella crypto providers # Expected output: # Available Crypto Providers: # - default (.NET Crypto, BouncyCastle) # - myprovider (MYPROVIDER-ALG1, MYPROVIDER-ALG2) ``` --- ## Best Practices ### 1. Error Handling ```csharp public async Task SignAsync(...) { try { // Signing logic } catch (FileNotFoundException ex) { throw new CryptoException($"Key file not found: {keyRef.KeyId}", ex); } catch (UnauthorizedAccessException ex) { throw new CryptoException($"Access denied to key: {keyRef.KeyId}", ex); } catch (Exception ex) { _logger.LogError(ex, "Failed to sign data with {Algorithm}", algorithm); throw; } } ``` ### 2. Logging ```csharp _logger.LogDebug("Signing {DataLength} bytes with {Algorithm}", data.Length, algorithm); _logger.LogInformation("Successfully signed data with {Algorithm}", algorithm); _logger.LogWarning("Key {KeyId} expires soon: {ExpiresAt}", keyRef.KeyId, expiresAt); _logger.LogError(ex, "Failed to sign data with {Algorithm}", algorithm); ``` ### 3. Configuration Validation ```csharp public MyProviderCryptoProvider(IOptions options, ILogger logger) { _options = options.Value ?? throw new ArgumentNullException(nameof(options)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); // Validate configuration if (_options.UseNativeLibrary && string.IsNullOrEmpty(_options.NativeLibraryPath)) { throw new InvalidOperationException("NativeLibraryPath must be set when UseNativeLibrary is true"); } if (_options.UseRemoteSigner && string.IsNullOrEmpty(_options.RemoteSignerUrl)) { throw new InvalidOperationException("RemoteSignerUrl must be set when UseRemoteSigner is true"); } } ``` ### 4. Thread Safety ```csharp private readonly SemaphoreSlim _hsmLock = new(1, 1); public async Task SignAsync(...) { // Protect HSM access with semaphore await _hsmLock.WaitAsync(cancellationToken); try { // HSM signing } finally { _hsmLock.Release(); } } ``` --- ## Advanced Topics ### HSM Integration ```csharp // Example: PKCS#11 HSM integration private async Task SignWithHsmAsync( byte[] data, string algorithm, CryptoKeyReference keyRef, CancellationToken cancellationToken) { var hsmSlot = keyRef.Parameters?["HsmSlot"]; var pin = keyRef.Parameters?["Pin"]; // Initialize PKCS#11 library using var pkcs11 = new Pkcs11(_options.Pkcs11LibraryPath, AppType.MultiThreaded); // Get slot var slot = pkcs11.GetSlotList(SlotsType.WithTokenPresent)[int.Parse(hsmSlot!)]; // Open session using var session = slot.OpenSession(SessionType.ReadOnly); // Login session.Login(CKU.User, pin); // Find private key var template = new List { session.Factories.ObjectAttributeFactory.Create(CKA.Label, keyRef.KeyId) }; var foundObjects = session.FindAllObjects(template); // Sign var mechanism = session.Factories.MechanismFactory.Create(CKM.ECDSA); var signature = session.Sign(mechanism, foundObjects[0], data); return signature; } ``` ### Remote Signer Integration ```csharp private async Task SignWithRemoteSignerAsync( byte[] data, string algorithm, CryptoKeyReference keyRef, CancellationToken cancellationToken) { using var httpClient = new HttpClient(); httpClient.BaseAddress = new Uri(_options.RemoteSignerUrl!); httpClient.DefaultRequestHeaders.Add("X-API-Key", _options.RemoteSignerApiKey); var request = new { keyId = keyRef.KeyId, algorithm = algorithm, data = Convert.ToBase64String(data) }; var response = await httpClient.PostAsJsonAsync("/api/v1/sign", request, cancellationToken); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync(cancellationToken); return Convert.FromBase64String(result!.Signature); } private sealed record SignResponse(string Signature); ``` --- ## See Also - [CLI Architecture](architecture.md) - Plugin architecture overview - [Command Reference](command-reference.md) - Crypto command usage - [Compliance Guide](compliance-guide.md) - Regional crypto requirements - [Distribution Matrix](distribution-matrix.md) - Build and distribution guide - [Troubleshooting](troubleshooting.md) - Common plugin issues