# 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