using System.Collections.Generic; using System.CommandLine; using System.IO; using System.Linq; using System.Text; using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StellaOps.Cryptography; using StellaOps.Cryptography.DependencyInjection; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; var root = BuildRootCommand(); return await root.InvokeAsync(args); static RootCommand BuildRootCommand() { var configOption = new Option( name: "--config", description: "Path to JSON or YAML file containing the `StellaOps:Crypto` configuration section."); var profileOption = new Option( name: "--profile", description: "Override `StellaOps:Crypto:Registry:ActiveProfile`. Defaults to the profile in the config file."); var root = new RootCommand("StellaOps sovereign crypto diagnostics CLI"); root.AddGlobalOption(configOption); root.AddGlobalOption(profileOption); root.AddCommand(BuildProvidersCommand(configOption, profileOption)); root.AddCommand(BuildSignCommand(configOption, profileOption)); return root; } static Command BuildProvidersCommand(Option configOption, Option profileOption) { var jsonOption = new Option("--json", description: "Emit JSON instead of text output."); var command = new Command("providers", "List registered crypto providers and key descriptors."); command.AddOption(jsonOption); command.SetHandler((string? configPath, string? profile, bool asJson) => ListProvidersAsync(configPath, profile, asJson), configOption, profileOption, jsonOption); return command; } static async Task ListProvidersAsync(string? configPath, string? profile, bool asJson) { using var scope = BuildServiceProvider(configPath, profile).CreateScope(); var providers = scope.ServiceProvider.GetServices(); var registryOptions = scope.ServiceProvider.GetRequiredService>(); var preferred = registryOptions.CurrentValue.ResolvePreferredProviders(); var views = providers.Select(provider => new ProviderView { Name = provider.Name, Keys = (provider as ICryptoProviderDiagnostics)?.DescribeKeys().ToArray() ?? Array.Empty() }).ToArray(); if (asJson) { var payload = new { ActiveProfile = registryOptions.CurrentValue.ActiveProfile, PreferredProviders = preferred, Providers = views }; Console.WriteLine(JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true })); return; } Console.WriteLine($"Active profile: {registryOptions.CurrentValue.ActiveProfile}"); Console.WriteLine("Preferred providers: " + string.Join(", ", preferred)); foreach (var view in views) { Console.WriteLine($"- {view.Name}"); if (view.Keys.Length == 0) { Console.WriteLine(" (no key diagnostics)"); continue; } foreach (var key in view.Keys) { Console.WriteLine($" * {key.KeyId} [{key.AlgorithmId}]"); foreach (var kvp in key.Metadata) { if (!string.IsNullOrWhiteSpace(kvp.Value)) { Console.WriteLine($" {kvp.Key}: {kvp.Value}"); } } } } } static Command BuildSignCommand(Option configOption, Option profileOption) { var keyOption = new Option("--key-id", description: "Key identifier registered in the crypto profile") { IsRequired = true }; var algOption = new Option("--alg", description: "Signature algorithm (e.g. GOST12-256)") { IsRequired = true }; var fileOption = new Option("--file", description: "Path to the file to sign") { IsRequired = true }; var outputOption = new Option("--out", description: "Optional output path for the signature. If omitted, text formats are written to stdout."); var formatOption = new Option("--format", () => "base64", "Output format: base64, hex, or raw."); var command = new Command("sign", "Sign a file with the selected sovereign provider."); command.AddOption(keyOption); command.AddOption(algOption); command.AddOption(fileOption); command.AddOption(outputOption); command.AddOption(formatOption); command.SetHandler((string? configPath, string? profile, string keyId, string alg, string filePath, string? outputPath, string format) => SignAsync(configPath, profile, keyId, alg, filePath, outputPath, format), configOption, profileOption, keyOption, algOption, fileOption, outputOption, formatOption); return command; } static async Task SignAsync(string? configPath, string? profile, string keyId, string alg, string filePath, string? outputPath, string format) { if (!File.Exists(filePath)) { throw new FileNotFoundException("Input file not found.", filePath); } format = format.ToLowerInvariant(); if (format is not ("base64" or "hex" or "raw")) { throw new ArgumentException("--format must be one of base64|hex|raw."); } using var scope = BuildServiceProvider(configPath, profile).CreateScope(); var registry = scope.ServiceProvider.GetRequiredService(); var resolution = registry.ResolveSigner( CryptoCapability.Signing, alg, new CryptoKeyReference(keyId)); var data = await File.ReadAllBytesAsync(filePath); var signature = await resolution.Signer.SignAsync(data); byte[] payload; switch (format) { case "base64": payload = Encoding.UTF8.GetBytes(Convert.ToBase64String(signature)); break; case "hex": payload = Encoding.UTF8.GetBytes(Convert.ToHexString(signature)); break; default: if (string.IsNullOrEmpty(outputPath)) { throw new InvalidOperationException("Raw output requires --out to be specified."); } payload = signature.ToArray(); break; } await WriteOutputAsync(outputPath, payload, format == "raw"); Console.WriteLine($"Provider: {resolution.ProviderName}"); } static IServiceProvider BuildServiceProvider(string? configPath, string? profileOverride) { var configuration = BuildConfiguration(configPath); var services = new ServiceCollection(); services.AddLogging(builder => builder.AddSimpleConsole()); services.AddStellaOpsCryptoRu(configuration); if (!string.IsNullOrWhiteSpace(profileOverride)) { services.PostConfigure(opts => opts.ActiveProfile = profileOverride); } return services.BuildServiceProvider(); } static IConfiguration BuildConfiguration(string? path) { var builder = new ConfigurationBuilder(); if (!string.IsNullOrEmpty(path)) { var extension = Path.GetExtension(path).ToLowerInvariant(); if (extension is ".yaml" or ".yml") { builder.AddJsonStream(ConvertYamlToJsonStream(path)); } else { builder.AddJsonFile(path, optional: false, reloadOnChange: false); } } builder.AddEnvironmentVariables(prefix: "STELLAOPS_"); return builder.Build(); } static Stream ConvertYamlToJsonStream(string path) { var yaml = File.ReadAllText(path); var deserializer = new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .IgnoreUnmatchedProperties() .Build(); var yamlObject = deserializer.Deserialize(yaml); var serializer = new SerializerBuilder() .JsonCompatible() .Build(); var json = serializer.Serialize(yamlObject); return new MemoryStream(Encoding.UTF8.GetBytes(json)); } static async Task WriteOutputAsync(string? outputPath, byte[] payload, bool binary) { if (string.IsNullOrEmpty(outputPath)) { if (binary) { throw new InvalidOperationException("Binary signatures must be written to a file using --out."); } Console.WriteLine(Encoding.UTF8.GetString(payload)); return; } await File.WriteAllBytesAsync(outputPath, payload); Console.WriteLine($"Signature written to {outputPath} ({payload.Length} bytes)."); } file sealed class ProviderView { public required string Name { get; init; } public CryptoProviderKeyDescriptor[] Keys { get; init; } = Array.Empty(); }