1using System.CommandLine;
2using System.Diagnostics.Metrics;
3using System.Security.AccessControl;
4using System.Security.Cryptography.X509Certificates;
7using System.Text.Json.Serialization;
9using Microsoft.Extensions.Logging;
10using Microsoft.Extensions.Logging.Abstractions;
28 protected ILogger
Logger {
get;
set; }
48 Logger = logger ?? NullLogger.Instance;
54 protected Argument<string>
BundlePath {
get; } =
new Argument<string>(
"bundle",
"Bundle path or directory containing the bundle\n" +
55 "if the bundle name is not specified, a default name will be used");
70 Argument<string[]> filesArg =
new Argument<string[]>(
"files", description:
"Files to add to the bundle, Must be inside the bundle root path\n" +
71 "if not specified, all files in the bundle root path will be added", parse: x =>
73 List<string> result = [];
74 foreach (var file
in x.Tokens.Select(t => t.Value))
76 if (
string.IsNullOrEmpty(file))
continue;
78 result.Add(Path.GetFullPath(file));
81 return result.ToArray();
84 Arity = ArgumentArity.ZeroOrMore,
87 Option<bool> replaceOpt =
new Option<bool>(
"--replace",
"Replace existing entries");
88 replaceOpt.AddAlias(
"-r");
90 Option<bool> recursiveOpt =
new Option<bool>(
"--recursive",
"Add all files within the bundle root path recursively");
91 recursiveOpt.AddAlias(
"-R");
93 Option<bool> continueOpt =
new Option<bool>(
"--continue",
"Continue adding files if an error occurs");
94 continueOpt.AddAlias(
"-c");
96 Option<bool> forceOpt =
new Option<bool>(
"--force",
"Add files even if the bundle is signed");
98 Command command =
new Command(
"add",
"Create new bundle or update an existing one")
107 command.SetHandler((bundlePath, files, replace, recursive, continueOnError, force) =>
110 Utilities.RunInStatusContext(
"[yellow]Preparing[/]", ctx =>
RunAdd(ctx, files, replace, recursive, continueOnError, force));
111 },
BundlePath, filesArg, replaceOpt, recursiveOpt, continueOpt, forceOpt);
124 Command command =
new Command(
"info",
"Show bundle information")
129 command.SetHandler((bundlePath) =>
132 Utilities.RunInStatusContext(
"[yellow]Preparing[/]", ctx =>
RunInfo(ctx));
146 Option<string> pfxOpt =
new Option<string>(
"--pfx",
"PFX File contains certificate and private key");
147 Option<string> pfxPassOpt =
new Option<string>(
"--pfx-password",
"PFX File password");
148 Option<bool> pfxNoPassOpt =
new Option<bool>(
"--no-password",
"Ignore PFX File password prompt");
150 Option<bool> selfSignOpt =
new Option<bool>(
"--self-sign",
"Sign with self-signed certificate");
151 selfSignOpt.AddAlias(
"-s");
153 Option<bool> skipVerifyOpt =
new Option<bool>(
"--skip-verification",
"Skip verification of the certificate");
155 Command command =
new Command(
"sign",
"Sign bundle with certificate")
165 command.SetHandler((bundlePath, pfxFilePath, pfxFilePassword, pfxNoPasswordPrompt, selfSign, skipVerify) =>
169 X509Certificate2Collection collection;
170 X509Certificate2Collection certs;
176 AnsiConsole.MarkupLine(
"[red]Self-Signing feature is disabled[/]");
183 AnsiConsole.MarkupLine(
"[red]Self-Signing Root CA not found[/]");
187 string? selectedCert =
null;
190 selectedCert = AnsiConsole.Prompt<
string>(
191 new SelectionPrompt<string>()
193 .Title(
"Select Self-Signing Certificate")
194 .MoreChoicesText(
"[grey](Move up and down to see more certificates)[/]")
196 .AddChoices(
"Issue New Certificate"));
199 if (
string.IsNullOrEmpty(selectedCert) || selectedCert ==
"Issue New Certificate")
201 var subject = CertificateUtilities.GetSubjectFromUser();
202 var issuedCert = CertificateUtilities.IssueCertificate(subject.ToString(), rootCA);
206 certs =
new X509Certificate2Collection(issuedCert);
215 certs = CertificateUtilities.GetCertificates(pfxFilePath, pfxFilePassword, pfxNoPasswordPrompt);
218 if (certs.Count == 0)
220 AnsiConsole.MarkupLine(
"[red]No certificates found![/]");
223 else if (certs.Count == 1)
229 Dictionary<string, X509Certificate2> mapping = [];
230 foreach (X509Certificate2 cert
in certs)
232 mapping[$
"{cert.GetNameInfo(X509NameType.SimpleName, false)},{cert.GetNameInfo(X509NameType.SimpleName, true)},{cert.Thumbprint}"] = cert;
235 List<string> selection = AnsiConsole.Prompt(
236 new MultiSelectionPrompt<string>()
238 .Title(
"Select Signing Certificates")
239 .MoreChoicesText(
"[grey](Move up and down to see more certificates)[/]")
240 .InstructionsText(
"[grey](Press [blue]<space>[/] to toggle a certificate, [green]<enter>[/] to accept)[/]")
241 .AddChoices(mapping.Keys));
243 collection =
new(selection.Select(x => mapping[x]).ToArray());
246 Utilities.RunInStatusContext(
"[yellow]Preparing[/]", ctx =>
RunSign(ctx, collection, skipVerify));
247 },
BundlePath, pfxOpt, pfxPassOpt, pfxNoPassOpt, selfSignOpt, skipVerifyOpt);
260 var ignoreTimeOpt =
new Option<bool>(
"--ignore-time",
"Ignore time validation");
261 ignoreTimeOpt.AddAlias(
"-i");
263 Command command =
new Command(
"verify",
"Verify bundle")
269 command.SetHandler((bundlePath, ignoreTime) =>
272 Utilities.RunInStatusContext(
"[yellow]Preparing[/]", ctx =>
RunVerify(ctx, ignoreTime));
286 var forceOpt =
new Option<bool>(
"--force",
"Generate new self-signed root CA even if one already exists");
287 forceOpt.AddAlias(
"-f");
289 var cnOption =
new Option<string>(
290 aliases: [
"--commonName",
"-cn"],
291 description:
"Common Name for the certificate (e.g., example.com)\n" +
292 "If not specified, the user will be prompted for input.");
294 var emailOption =
new Option<string>(
295 aliases: [
"--email",
"-e"],
296 description:
"Email address (e.g., support@example.com)");
298 var orgOption =
new Option<string>(
299 aliases: [
"--organization",
"-o"],
300 description:
"Organization name (e.g., Example Inc.)");
302 var ouOption =
new Option<string>(
303 aliases: [
"--organizationalUnit",
"-ou"],
304 description:
"Organizational Unit (e.g., IT Department)");
306 var locOption =
new Option<string>(
307 aliases: [
"--locality",
"-l"],
308 description:
"Locality (e.g., New York)");
310 var stateOption =
new Option<string>(
311 aliases: [
"--state",
"-st"],
312 description:
"State or Province (e.g., NY)");
314 var countryOption =
new Option<string>(
315 aliases: [
"--country",
"-c"],
316 description:
"Country (e.g., US)");
318 var command =
new Command(
"self-sign",
"Generate self-signed root CA")
330 command.SetHandler(
RunSelfSign, forceOpt, cnOption, emailOption, orgOption, ouOption, locOption, stateOption, countryOption);
343 var caPathArg =
new Argument<string>(
"path",
"Path to the certificate file in PEM or DER format")
345 Arity = ArgumentArity.ExactlyOne,
348 var interOpt =
new Option<bool>(
"--intermediate",
"Run command for Intermediate CA");
349 interOpt.AddAlias(
"-i");
351 Command addCmd =
new Command(
"add",
"Add trusted root CA or intermediate CA certificate")
357 addCmd.SetHandler((path, intermediate) =>
359 if (!File.Exists(path))
361 AnsiConsole.MarkupLine($
"[red]Certificate file not found: {path}[/]");
365 var certificate = CertificateUtilities.Import(path);
366 CertificateUtilities.DisplayCertificate(certificate);
368 var modifier = intermediate ?
"Intermediate" :
"Trusted Root";
369 var store = intermediate ? CertificateStore.IntermediateCA :
CertificateStore.TrustedRootCA;
372 AnsiConsole.MarkupLine($
"[green] {modifier} CA certificate added with ID: {id}[/]");
373 }, caPathArg, interOpt);
375 var verboseOpt =
new Option<bool>(
"--verbose",
"Show detailed information about the certificate");
376 verboseOpt.AddAlias(
"-v");
378 var listCmd =
new Command(
"list",
"List trusted root CA and intermediate CA certificates")
384 listCmd.SetHandler((intermediate, verbose) =>
386 string modifier = intermediate ?
"Intermediate" :
"Trusted Root";
387 var store = intermediate ? CertificateStore.IntermediateCA :
CertificateStore.TrustedRootCA;
388 var target = intermediate ? Configuration.IntermediateCA :
Configuration.TrustedRootCA;
390 X509Certificate2Collection certificates = [];
391 AnsiConsole.WriteLine($
"{modifier} CA certificates:");
392 foreach (var cert
in target)
394 var certificate =
Configuration.LoadCertificate(store, cert.Key);
398 AnsiConsole.MarkupLine($
"[{(isProtected ? Color.Green : Color.White)}] {cert.Key}{(isProtected ? " (Protected)
" : "")}[/]");
400 certificates.Add(certificate);
405 AnsiConsole.WriteLine();
406 CertificateUtilities.DisplayCertificate(certificates.ToArray());
409 }, interOpt, verboseOpt);
411 var idArg =
new Argument<string>(
"ID",
"ID of the certificate")
413 Arity = ArgumentArity.ExactlyOne,
416 var removeCmd =
new Command(
"remove",
"Remove trusted root CA or intermediate CA certificate")
422 removeCmd.SetHandler((
id, intermediate) =>
424 var modifier = intermediate ?
"Intermediate" :
"Trusted Root";
425 var store = intermediate ? CertificateStore.IntermediateCA :
CertificateStore.TrustedRootCA;
429 AnsiConsole.MarkupLine($
"[red]This ID is protected and cannot be modified[/]");
435 AnsiConsole.MarkupLine($
"[green]{modifier} CA certificate removed with ID: {id}[/]");
439 AnsiConsole.MarkupLine($
"[red]{modifier} CA certificate with ID: {id} not found![/]");
443 Command command =
new Command(
"trust",
"Manage trusted root CAs and intermediate CAs");
447 command.AddCommand(addCmd);
448 command.AddCommand(listCmd);
449 command.AddCommand(removeCmd);
453 command.SetHandler(() =>
455 AnsiConsole.MarkupLine(
"[red]Custom trust store feature is disabled[/]");
471 var keyArg =
new Argument<string>(
"key",
"Key to set or get\n" +
472 "if not specified, will list all keys")
474 Arity = ArgumentArity.ZeroOrOne,
477 var valueArg =
new Argument<string>(
"value",
"Value to set\n" +
478 "if not specified, will get the value of the key")
480 Arity = ArgumentArity.ZeroOrOne,
483 var forceOpt =
new Option<bool>(
"--force",
"Set value even if it is not existing");
484 forceOpt.AddAlias(
"-f");
486 var command =
new Command(
"config",
"Get or set configuration values")
493 command.SetHandler((key, value, force) =>
495 if (
string.IsNullOrEmpty(value))
497 var items =
string.IsNullOrEmpty(key) ? Configuration.Settings :
Configuration.Settings.Where(x => x.Key.StartsWith(key));
499 foreach (var item
in items)
501 AnsiConsole.WriteLine($
"{item.Key} = {item.Value}");
508 AnsiConsole.MarkupLine($
"[red]Invalid key: {key}[/]");
515 bValue = Utilities.ParseToBool(value);
519 AnsiConsole.MarkupLine($
"[red]Invalid value: {value}[/]");
524 AnsiConsole.MarkupLine($
"[green]{key} set to {Configuration.Settings[key]}[/]");
526 }, keyArg, valueArg, forceOpt);
543 public virtual void RunSelfSign(
bool force,
string? commonName,
string? email,
string? organization,
string? organizationalUnit,
string? locality,
string? state,
string? country)
547 AnsiConsole.MarkupLine(
"[red]Self-Signing feature is disabled[/]");
551 Logger.LogInformation(
"Running self-sign command");
555 Logger.LogWarning(
"Root CA already exists");
556 AnsiConsole.MarkupLine(
"[red]Root CA already exists![/]");
562 if (
string.IsNullOrEmpty(commonName))
564 Logger.LogDebug(
"Getting subject name from user");
565 subject = CertificateUtilities.GetSubjectFromUser().ToString();
571 organization: organization,
572 organizationalUnit: organizationalUnit,
578 Logger.LogInformation(
"Creating self-signed root CA certificate with subject: {subject}", subject);
579 var rootCA = CertificateUtilities.CreateSelfSignedCACertificate(subject);
580 Logger.LogDebug(
"Root CA certificate issued with subject: {subject}", rootCA.Subject);
582 Logger.LogDebug(
"Exporting root CA certificate to configuration");
583 Configuration.SelfSignedRootCA = rootCA.Export(X509ContentType.Pfx);
585 Logger.LogDebug(
"Clearing issued certificates");
588 CertificateUtilities.DisplayCertificate(rootCA);
590 Logger.LogInformation(
"Root CA created successfully");
591 AnsiConsole.MarkupLine($
"[green]Root CA created successfully![/]");
604 return CertificateUtilities.ImportPFX(
Configuration.SelfSignedRootCA).Single();
Represents a bundle that holds file hashes and signatures.
Represents the subject of a certificate.
override string ToString()
Generates a comma-delimited string representation of the certificate subject.
Represents the configuration for the EasySign command provider.
Provides command definitions and handlers for the EasySign command line interface.
TConfiguration Configuration
Gets the application configurations.
Command SelfSign
Gets the command for generating a self-signed root CA certificate.
Command Info
Gets the command for Showing bundle information.
virtual void RunAdd(StatusContext statusContext, string[] files, bool replace, bool recursive, bool continueOnError, bool force)
Runs the add command.
Command?? Trust
Gets the command for managing trusted root CAs and intermediate CAs.
Command Add
Gets the command for creating a new bundle or updating an existing one.
CommandProvider(TConfiguration? configuration, ILogger? logger)
Initializes a new instance of the CommandProvider<TBundle, TConfiguration> class.
Command Verify
Gets the command for verifying bundle.
virtual void RunSelfSign(bool force, string? commonName, string? email, string? organization, string? organizationalUnit, string? locality, string? state, string? country)
Runs the self-sign command to create a self-signed root CA certificate.
void InitializeBundle(string bundlePath)
Initializes the bundle.
Command Config
Gets the command for managing configuration settings.
virtual void RunInfo(StatusContext statusContext)
Runs the info command.
X509Certificate2? GetSelfSigningRootCA()
Gets the self-signed root CA.
ILogger Logger
Gets or sets the logger to use for logging.
Argument< string > BundlePath
Gets the common argument for the bundle path.
virtual void RunSign(StatusContext statusContext, X509Certificate2Collection certificates, bool skipVerify)
Runs the sign command.
virtual bool RunVerify(StatusContext statusContext, bool ignoreTime)
Runs the verify command.
Command Sign
Gets the command for signing bundle with one or more certificate.
RootCommand GetRootCommand()
Gets the root command for the command line interface.
CertificateStore
Enumeration of certificate stores in the CommandProviderConfiguration.