Files
git.stella-ops.org/src/Tools/StellaOps.CryptoRu.Cli/Program.cs
master cef4cb2c5a Add support for ГОСТ Р 34.10 digital signatures
- Implemented the GostKeyValue class for handling public key parameters in ГОСТ Р 34.10 digital signatures.
- Created the GostSignedXml class to manage XML signatures using ГОСТ 34.10, including methods for computing and checking signatures.
- Developed the GostSignedXmlImpl class to encapsulate the signature computation logic and public key retrieval.
- Added specific key value classes for ГОСТ Р 34.10-2001, ГОСТ Р 34.10-2012/256, and ГОСТ Р 34.10-2012/512 to support different signature algorithms.
- Ensured compatibility with existing XML signature standards while integrating ГОСТ cryptography.
2025-11-09 21:59:57 +02:00

246 lines
8.7 KiB
C#

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<string?>(
name: "--config",
description: "Path to JSON or YAML file containing the `StellaOps:Crypto` configuration section.");
var profileOption = new Option<string?>(
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<string?> configOption, Option<string?> profileOption)
{
var jsonOption = new Option<bool>("--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<ICryptoProvider>();
var registryOptions = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<CryptoProviderRegistryOptions>>();
var preferred = registryOptions.CurrentValue.ResolvePreferredProviders();
var views = providers.Select(provider => new ProviderView
{
Name = provider.Name,
Keys = (provider as ICryptoProviderDiagnostics)?.DescribeKeys().ToArray() ?? Array.Empty<CryptoProviderKeyDescriptor>()
}).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<string?> configOption, Option<string?> profileOption)
{
var keyOption = new Option<string>("--key-id", description: "Key identifier registered in the crypto profile") { IsRequired = true };
var algOption = new Option<string>("--alg", description: "Signature algorithm (e.g. GOST12-256)") { IsRequired = true };
var fileOption = new Option<string>("--file", description: "Path to the file to sign") { IsRequired = true };
var outputOption = new Option<string?>("--out", description: "Optional output path for the signature. If omitted, text formats are written to stdout.");
var formatOption = new Option<string>("--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<ICryptoProviderRegistry>();
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<CryptoProviderRegistryOptions>(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<object>(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<CryptoProviderKeyDescriptor>();
}