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.
This commit is contained in:
245
src/Tools/StellaOps.CryptoRu.Cli/Program.cs
Normal file
245
src/Tools/StellaOps.CryptoRu.Cli/Program.cs
Normal file
@@ -0,0 +1,245 @@
|
||||
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>();
|
||||
}
|
||||
Reference in New Issue
Block a user