Archive completed sprint documentation and deliverables: ## SPRINT_3500 - Proof of Exposure (PoE) Implementation (COMPLETE ✅) - Windows filesystem hash sanitization (colon → underscore) - Namespace conflict resolution (Subgraph → PoESubgraph) - Mock test improvements with It.IsAny<>() - Direct orchestrator unit tests - 8/8 PoE tests passing (100% success) - Archived to: docs/implplan/archived/2025-12-23-sprint-3500-poe/ ## SPRINT_7100.0001 - Proof-Driven Moats Core (COMPLETE ✅) - Four-tier backport detection system - 9 production modules (4,044 LOC) - Binary fingerprinting (TLSH + instruction hashing) - VEX integration with proof-carrying verdicts - 42+ unit tests passing (100% success) - Archived to: docs/implplan/archived/2025-12-23-sprint-7100-proof-moats/ ## SPRINT_7100.0002 - Proof Moats Storage Layer (COMPLETE ✅) - PostgreSQL repository implementations - Database migrations (4 evidence tables + audit) - Test data seed scripts (12 evidence records, 3 CVEs) - Integration tests with Testcontainers - <100ms proof generation performance - Archived to: docs/implplan/archived/2025-12-23-sprint-7100-proof-moats/ ## SPRINT_3000_0200 - Authority Admin & Branding (COMPLETE ✅) - Console admin RBAC UI components - Branding editor with tenant isolation - Authority backend endpoints - Archived to: docs/implplan/archived/ ## Additional Documentation - CLI command reference and compliance guides - Module architecture docs (26 modules documented) - Data schemas and contracts - Operations runbooks - Security risk models - Product roadmap All archived sprints achieved 100% completion of planned deliverables. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1018 lines
27 KiB
Markdown
1018 lines
27 KiB
Markdown
# 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;
|
|
|
|
/// <summary>
|
|
/// Core interface for all cryptographic providers
|
|
/// </summary>
|
|
public interface ICryptoProvider
|
|
{
|
|
/// <summary>
|
|
/// Unique provider name (e.g., "gost", "eidas", "sm", "default")
|
|
/// Used for --provider flag in CLI
|
|
/// </summary>
|
|
string Name { get; }
|
|
|
|
/// <summary>
|
|
/// Supported cryptographic algorithms
|
|
/// (e.g., "GOST12-256", "ECDSA-P256", "SM2")
|
|
/// </summary>
|
|
string[] SupportedAlgorithms { get; }
|
|
|
|
/// <summary>
|
|
/// Sign data with specified algorithm and key
|
|
/// </summary>
|
|
/// <param name="data">Data to sign</param>
|
|
/// <param name="algorithm">Algorithm to use</param>
|
|
/// <param name="keyRef">Key reference</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>Signature bytes</returns>
|
|
Task<byte[]> SignAsync(
|
|
byte[] data,
|
|
string algorithm,
|
|
CryptoKeyReference keyRef,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Verify signature
|
|
/// </summary>
|
|
/// <param name="data">Original data</param>
|
|
/// <param name="signature">Signature to verify</param>
|
|
/// <param name="algorithm">Algorithm used</param>
|
|
/// <param name="keyRef">Key reference (public key)</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>True if valid, false otherwise</returns>
|
|
Task<bool> VerifyAsync(
|
|
byte[] data,
|
|
byte[] signature,
|
|
string algorithm,
|
|
CryptoKeyReference keyRef,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// List available keys in this provider
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>List of available keys</returns>
|
|
Task<IReadOnlyList<CryptoKeyInfo>> ListKeysAsync(
|
|
CancellationToken cancellationToken = default);
|
|
}
|
|
```
|
|
|
|
### ICryptoProviderDiagnostics (Optional)
|
|
|
|
For advanced diagnostics and health checks:
|
|
|
|
```csharp
|
|
namespace StellaOps.Cli.Crypto;
|
|
|
|
/// <summary>
|
|
/// Optional interface for provider diagnostics and health checks
|
|
/// </summary>
|
|
public interface ICryptoProviderDiagnostics
|
|
{
|
|
/// <summary>
|
|
/// Run provider self-test
|
|
/// </summary>
|
|
Task<ProviderHealthCheck> HealthCheckAsync(
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Get provider version and capabilities
|
|
/// </summary>
|
|
ProviderInfo GetInfo();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Health check result
|
|
/// </summary>
|
|
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; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Provider metadata
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Reference to a cryptographic key
|
|
/// </summary>
|
|
public sealed record CryptoKeyReference
|
|
{
|
|
/// <summary>
|
|
/// Key identifier (e.g., "prod-key-2024", file path, HSM slot)
|
|
/// </summary>
|
|
public required string KeyId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Key source type: "file", "hsm", "kms", "csp", "pkcs11"
|
|
/// </summary>
|
|
public required string Source { get; init; }
|
|
|
|
/// <summary>
|
|
/// Additional parameters (e.g., HSM PIN, KMS region, CSP container)
|
|
/// </summary>
|
|
public IReadOnlyDictionary<string, string>? Parameters { get; init; }
|
|
}
|
|
```
|
|
|
|
### CryptoKeyInfo
|
|
|
|
Metadata about an available key:
|
|
|
|
```csharp
|
|
namespace StellaOps.Cli.Crypto;
|
|
|
|
/// <summary>
|
|
/// Information about an available cryptographic key
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Crypto provider for MyProvider algorithm
|
|
/// </summary>
|
|
public class MyProviderCryptoProvider : ICryptoProvider, ICryptoProviderDiagnostics
|
|
{
|
|
private readonly MyProviderOptions _options;
|
|
private readonly ILogger<MyProviderCryptoProvider> _logger;
|
|
|
|
public MyProviderCryptoProvider(
|
|
IOptions<MyProviderOptions> options,
|
|
ILogger<MyProviderCryptoProvider> 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<byte[]> 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<bool> 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<IReadOnlyList<CryptoKeyInfo>> ListKeysAsync(
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
_logger.LogDebug("Listing available keys");
|
|
|
|
var keys = new List<CryptoKeyInfo>();
|
|
|
|
// 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<ProviderHealthCheck> HealthCheckAsync(
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var checks = new List<string>();
|
|
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<byte[]> 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<bool> VerifyWithNativeLibraryAsync(
|
|
byte[] data,
|
|
byte[] signature,
|
|
string algorithm,
|
|
CryptoKeyReference keyRef,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
throw new NotImplementedException("Native library verification not implemented");
|
|
}
|
|
|
|
private async Task<byte[]> 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<bool> IsRemoteSignerReachableAsync(CancellationToken cancellationToken)
|
|
{
|
|
// Ping remote signer
|
|
return true; // Placeholder
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Step 3: Configuration Options
|
|
|
|
```csharp
|
|
namespace StellaOps.Cli.Crypto.MyProvider;
|
|
|
|
/// <summary>
|
|
/// Configuration options for MyProvider crypto provider
|
|
/// </summary>
|
|
public sealed class MyProviderOptions
|
|
{
|
|
/// <summary>
|
|
/// Use native library for crypto operations
|
|
/// </summary>
|
|
public bool UseNativeLibrary { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Path to native library (if not in standard location)
|
|
/// </summary>
|
|
public string? NativeLibraryPath { get; set; }
|
|
|
|
/// <summary>
|
|
/// Use remote signer API
|
|
/// </summary>
|
|
public bool UseRemoteSigner { get; set; } = false;
|
|
|
|
/// <summary>
|
|
/// Remote signer API URL
|
|
/// </summary>
|
|
public string? RemoteSignerUrl { get; set; }
|
|
|
|
/// <summary>
|
|
/// Remote signer API key
|
|
/// </summary>
|
|
public string? RemoteSignerApiKey { get; set; }
|
|
|
|
/// <summary>
|
|
/// Configured keys
|
|
/// </summary>
|
|
public List<KeyConfiguration>? 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;
|
|
|
|
/// <summary>
|
|
/// Service collection extensions for MyProvider crypto provider
|
|
/// </summary>
|
|
public static class ServiceCollectionExtensions
|
|
{
|
|
/// <summary>
|
|
/// Add MyProvider crypto providers to DI container
|
|
/// </summary>
|
|
public static IServiceCollection AddMyProviderCryptoProviders(
|
|
this IServiceCollection services,
|
|
IConfiguration configuration)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(services);
|
|
ArgumentNullException.ThrowIfNull(configuration);
|
|
|
|
// Register provider as ICryptoProvider
|
|
services.AddSingleton<ICryptoProvider, MyProviderCryptoProvider>();
|
|
|
|
// Bind configuration
|
|
services.Configure<MyProviderOptions>(
|
|
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
|
|
<Project Sdk="Microsoft.NET.Sdk">
|
|
<!-- ... -->
|
|
|
|
<!-- MyProvider plugin (custom distribution) -->
|
|
<ItemGroup Condition="'$(StellaOpsEnableMyProvider)' == 'true'">
|
|
<ProjectReference Include="..\StellaOps.Cli.Crypto.MyProvider\StellaOps.Cli.Crypto.MyProvider.csproj" />
|
|
<DefineConstants>$(DefineConstants);STELLAOPS_ENABLE_MYPROVIDER</DefineConstants>
|
|
</ItemGroup>
|
|
</Project>
|
|
```
|
|
|
|
#### 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<int> 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<NotSupportedException>(async () =>
|
|
{
|
|
await provider.SignAsync(data, "UNSUPPORTED-ALG", keyRef);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ListKeysAsync_ReturnsConfiguredKeys()
|
|
{
|
|
var options = new MyProviderOptions
|
|
{
|
|
Keys = new List<MyProviderOptions.KeyConfiguration>
|
|
{
|
|
new()
|
|
{
|
|
KeyId = "key1",
|
|
Algorithm = "MYPROVIDER-ALG1",
|
|
Source = "file",
|
|
FriendlyName = "Test Key 1"
|
|
}
|
|
}
|
|
};
|
|
|
|
var provider = new MyProviderCryptoProvider(
|
|
Options.Create(options),
|
|
NullLogger<MyProviderCryptoProvider>.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<MyProviderCryptoProvider>.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<ICryptoProvider>().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<string, string>
|
|
{
|
|
["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<byte[]> 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<MyProviderOptions> options, ILogger<MyProviderCryptoProvider> 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<byte[]> 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<byte[]> 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<IObjectAttribute>
|
|
{
|
|
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<byte[]> 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<SignResponse>(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
|