- 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.
246 lines
8.7 KiB
C#
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>();
|
|
}
|