EasySign BETA
Digital Signing Tool
Loading...
Searching...
No Matches
CommandProvider.cs
Go to the documentation of this file.
1using System.CommandLine;
2using System.Diagnostics.Metrics;
3using System.Security.AccessControl;
4using System.Security.Cryptography.X509Certificates;
5using System.Text;
6using System.Text.Json;
7using System.Text.Json.Serialization;
8
9using Microsoft.Extensions.Logging;
10using Microsoft.Extensions.Logging.Abstractions;
11
12using Spectre.Console;
13
15{
21 public abstract partial class CommandProvider<TBundle, TConfiguration>
22 where TBundle : Bundle
23 where TConfiguration : CommandProviderConfiguration, new()
24 {
28 protected ILogger Logger { get; set; }
29
33 public TConfiguration Configuration { get; }
34
45 protected CommandProvider(TConfiguration? configuration, ILogger? logger)
46 {
47 Configuration = configuration ?? new();
48 Logger = logger ?? NullLogger.Instance;
49 }
50
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");
56
61 public abstract RootCommand GetRootCommand();
62
66 public Command Add
67 {
68 get
69 {
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 =>
72 {
73 List<string> result = [];
74 foreach (var file in x.Tokens.Select(t => t.Value))
75 {
76 if (string.IsNullOrEmpty(file)) continue;
77
78 result.Add(Path.GetFullPath(file));
79 }
80
81 return result.ToArray();
82 })
83 {
84 Arity = ArgumentArity.ZeroOrMore,
85 };
86
87 Option<bool> replaceOpt = new Option<bool>("--replace", "Replace existing entries");
88 replaceOpt.AddAlias("-r");
89
90 Option<bool> recursiveOpt = new Option<bool>("--recursive", "Add all files within the bundle root path recursively");
91 recursiveOpt.AddAlias("-R");
92
93 Option<bool> continueOpt = new Option<bool>("--continue", "Continue adding files if an error occurs");
94 continueOpt.AddAlias("-c");
95
96 Option<bool> forceOpt = new Option<bool>("--force", "Add files even if the bundle is signed");
97
98 Command command = new Command("add", "Create new bundle or update an existing one")
99 {
101 filesArg,
102 replaceOpt,
103 recursiveOpt,
104 continueOpt,
105 };
106
107 command.SetHandler((bundlePath, files, replace, recursive, continueOnError, force) =>
108 {
109 InitializeBundle(bundlePath);
110 Utilities.RunInStatusContext("[yellow]Preparing[/]", ctx => RunAdd(ctx, files, replace, recursive, continueOnError, force));
111 }, BundlePath, filesArg, replaceOpt, recursiveOpt, continueOpt, forceOpt);
112
113 return command;
114 }
115 }
116
120 public Command Info
121 {
122 get
123 {
124 Command command = new Command("info", "Show bundle information")
125 {
127 };
128
129 command.SetHandler((bundlePath) =>
130 {
131 InitializeBundle(bundlePath);
132 Utilities.RunInStatusContext("[yellow]Preparing[/]", ctx => RunInfo(ctx));
133 }, BundlePath);
134
135 return command;
136 }
137 }
138
142 public Command Sign
143 {
144 get
145 {
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");
149
150 Option<bool> selfSignOpt = new Option<bool>("--self-sign", "Sign with self-signed certificate");
151 selfSignOpt.AddAlias("-s");
152
153 Option<bool> skipVerifyOpt = new Option<bool>("--skip-verification", "Skip verification of the certificate");
154
155 Command command = new Command("sign", "Sign bundle with certificate")
156 {
158 pfxOpt,
159 pfxPassOpt,
160 pfxNoPassOpt,
161 selfSignOpt,
162 skipVerifyOpt,
163 };
164
165 command.SetHandler((bundlePath, pfxFilePath, pfxFilePassword, pfxNoPasswordPrompt, selfSign, skipVerify) =>
166 {
167 InitializeBundle(bundlePath);
168
169 X509Certificate2Collection collection;
170 X509Certificate2Collection certs;
171
172 if (selfSign)
173 {
174 if (!Configuration.Settings["selfsign.enable"])
175 {
176 AnsiConsole.MarkupLine("[red]Self-Signing feature is disabled[/]");
177 return;
178 }
179
180 X509Certificate2? rootCA = GetSelfSigningRootCA();
181 if (rootCA == null)
182 {
183 AnsiConsole.MarkupLine("[red]Self-Signing Root CA not found[/]");
184 return;
185 }
186
187 string? selectedCert = null;
188 if (Configuration.IssuedCertificates.Count > 0)
189 {
190 selectedCert = AnsiConsole.Prompt<string>(
191 new SelectionPrompt<string>()
192 .PageSize(10)
193 .Title("Select Self-Signing Certificate")
194 .MoreChoicesText("[grey](Move up and down to see more certificates)[/]")
195 .AddChoices(Configuration.IssuedCertificates.Keys)
196 .AddChoices("Issue New Certificate"));
197 }
198
199 if (string.IsNullOrEmpty(selectedCert) || selectedCert == "Issue New Certificate")
200 {
201 var subject = CertificateUtilities.GetSubjectFromUser();
202 var issuedCert = CertificateUtilities.IssueCertificate(subject.ToString(), rootCA);
203
204 Configuration.AddCertificate(CertificateStore.IssuedCertificates, issuedCert, subject.CommonName);
205
206 certs = new X509Certificate2Collection(issuedCert);
207 }
208 else
209 {
210 certs = [Configuration.LoadCertificate(CertificateStore.IssuedCertificates, selectedCert)];
211 }
212 }
213 else
214 {
215 certs = CertificateUtilities.GetCertificates(pfxFilePath, pfxFilePassword, pfxNoPasswordPrompt);
216 }
217
218 if (certs.Count == 0)
219 {
220 AnsiConsole.MarkupLine("[red]No certificates found![/]");
221 return;
222 }
223 else if (certs.Count == 1)
224 {
225 collection = certs;
226 }
227 else
228 {
229 Dictionary<string, X509Certificate2> mapping = [];
230 foreach (X509Certificate2 cert in certs)
231 {
232 mapping[$"{cert.GetNameInfo(X509NameType.SimpleName, false)},{cert.GetNameInfo(X509NameType.SimpleName, true)},{cert.Thumbprint}"] = cert;
233 }
234
235 List<string> selection = AnsiConsole.Prompt(
236 new MultiSelectionPrompt<string>()
237 .PageSize(10)
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));
242
243 collection = new(selection.Select(x => mapping[x]).ToArray());
244 }
245
246 Utilities.RunInStatusContext("[yellow]Preparing[/]", ctx => RunSign(ctx, collection, skipVerify));
247 }, BundlePath, pfxOpt, pfxPassOpt, pfxNoPassOpt, selfSignOpt, skipVerifyOpt);
248
249 return command;
250 }
251 }
252
256 public Command Verify
257 {
258 get
259 {
260 var ignoreTimeOpt = new Option<bool>("--ignore-time", "Ignore time validation");
261 ignoreTimeOpt.AddAlias("-i");
262
263 Command command = new Command("verify", "Verify bundle")
264 {
266 ignoreTimeOpt,
267 };
268
269 command.SetHandler((bundlePath, ignoreTime) =>
270 {
271 InitializeBundle(bundlePath);
272 Utilities.RunInStatusContext("[yellow]Preparing[/]", ctx => RunVerify(ctx, ignoreTime));
273 }, BundlePath, ignoreTimeOpt);
274
275 return command;
276 }
277 }
278
282 public Command SelfSign
283 {
284 get
285 {
286 var forceOpt = new Option<bool>("--force", "Generate new self-signed root CA even if one already exists");
287 forceOpt.AddAlias("-f");
288
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.");
293
294 var emailOption = new Option<string>(
295 aliases: ["--email", "-e"],
296 description: "Email address (e.g., support@example.com)");
297
298 var orgOption = new Option<string>(
299 aliases: ["--organization", "-o"],
300 description: "Organization name (e.g., Example Inc.)");
301
302 var ouOption = new Option<string>(
303 aliases: ["--organizationalUnit", "-ou"],
304 description: "Organizational Unit (e.g., IT Department)");
305
306 var locOption = new Option<string>(
307 aliases: ["--locality", "-l"],
308 description: "Locality (e.g., New York)");
309
310 var stateOption = new Option<string>(
311 aliases: ["--state", "-st"],
312 description: "State or Province (e.g., NY)");
313
314 var countryOption = new Option<string>(
315 aliases: ["--country", "-c"],
316 description: "Country (e.g., US)");
317
318 var command = new Command("self-sign", "Generate self-signed root CA")
319 {
320 forceOpt,
321 cnOption,
322 emailOption,
323 orgOption,
324 ouOption,
325 locOption,
326 stateOption,
327 countryOption,
328 };
329
330 command.SetHandler(RunSelfSign, forceOpt, cnOption, emailOption, orgOption, ouOption, locOption, stateOption, countryOption);
331
332 return command;
333 }
334 }
335
339 public Command Trust
340 {
341 get
342 {
343 var caPathArg = new Argument<string>("path", "Path to the certificate file in PEM or DER format")
344 {
345 Arity = ArgumentArity.ExactlyOne,
346 };
347
348 var interOpt = new Option<bool>("--intermediate", "Run command for Intermediate CA");
349 interOpt.AddAlias("-i");
350
351 Command addCmd = new Command("add", "Add trusted root CA or intermediate CA certificate")
352 {
353 caPathArg,
354 interOpt,
355 };
356
357 addCmd.SetHandler((path, intermediate) =>
358 {
359 if (!File.Exists(path))
360 {
361 AnsiConsole.MarkupLine($"[red]Certificate file not found: {path}[/]");
362 return;
363 }
364
365 var certificate = CertificateUtilities.Import(path);
366 CertificateUtilities.DisplayCertificate(certificate);
367
368 var modifier = intermediate ? "Intermediate" : "Trusted Root";
369 var store = intermediate ? CertificateStore.IntermediateCA : CertificateStore.TrustedRootCA;
370
371 var id = Configuration.AddCertificate(store, certificate);
372 AnsiConsole.MarkupLine($"[green] {modifier} CA certificate added with ID: {id}[/]");
373 }, caPathArg, interOpt);
374
375 var verboseOpt = new Option<bool>("--verbose", "Show detailed information about the certificate");
376 verboseOpt.AddAlias("-v");
377
378 var listCmd = new Command("list", "List trusted root CA and intermediate CA certificates")
379 {
380 interOpt,
381 verboseOpt,
382 };
383
384 listCmd.SetHandler((intermediate, verbose) =>
385 {
386 string modifier = intermediate ? "Intermediate" : "Trusted Root";
387 var store = intermediate ? CertificateStore.IntermediateCA : CertificateStore.TrustedRootCA;
388 var target = intermediate ? Configuration.IntermediateCA : Configuration.TrustedRootCA;
389
390 X509Certificate2Collection certificates = [];
391 AnsiConsole.WriteLine($"{modifier} CA certificates:");
392 foreach (var cert in target)
393 {
394 var certificate = Configuration.LoadCertificate(store, cert.Key);
395 var subject = new CertificateSubject(certificate);
396 var isProtected = Configuration.IsProtected(cert.Key);
397
398 AnsiConsole.MarkupLine($"[{(isProtected ? Color.Green : Color.White)}] {cert.Key}{(isProtected ? " (Protected)" : "")}[/]");
399
400 certificates.Add(certificate);
401 }
402
403 if (verbose)
404 {
405 AnsiConsole.WriteLine();
406 CertificateUtilities.DisplayCertificate(certificates.ToArray());
407 }
408
409 }, interOpt, verboseOpt);
410
411 var idArg = new Argument<string>("ID", "ID of the certificate")
412 {
413 Arity = ArgumentArity.ExactlyOne,
414 };
415
416 var removeCmd = new Command("remove", "Remove trusted root CA or intermediate CA certificate")
417 {
418 idArg,
419 interOpt,
420 };
421
422 removeCmd.SetHandler((id, intermediate) =>
423 {
424 var modifier = intermediate ? "Intermediate" : "Trusted Root";
425 var store = intermediate ? CertificateStore.IntermediateCA : CertificateStore.TrustedRootCA;
426
427 if (Configuration.IsProtected(id))
428 {
429 AnsiConsole.MarkupLine($"[red]This ID is protected and cannot be modified[/]");
430 return;
431 }
432
433 if (Configuration.RemoveCertificate(store, id))
434 {
435 AnsiConsole.MarkupLine($"[green]{modifier} CA certificate removed with ID: {id}[/]");
436 }
437 else
438 {
439 AnsiConsole.MarkupLine($"[red]{modifier} CA certificate with ID: {id} not found![/]");
440 }
441 }, idArg, interOpt);
442
443 Command command = new Command("trust", "Manage trusted root CAs and intermediate CAs");
444
445 if (Configuration.Settings["trust.enable"])
446 {
447 command.AddCommand(addCmd);
448 command.AddCommand(listCmd);
449 command.AddCommand(removeCmd);
450 }
451 else
452 {
453 command.SetHandler(() =>
454 {
455 AnsiConsole.MarkupLine("[red]Custom trust store feature is disabled[/]");
456 return;
457 });
458 }
459
460 return command;
461 }
462 }
463
467 public Command Config
468 {
469 get
470 {
471 var keyArg = new Argument<string>("key", "Key to set or get\n" +
472 "if not specified, will list all keys")
473 {
474 Arity = ArgumentArity.ZeroOrOne,
475 };
476
477 var valueArg = new Argument<string>("value", "Value to set\n" +
478 "if not specified, will get the value of the key")
479 {
480 Arity = ArgumentArity.ZeroOrOne,
481 };
482
483 var forceOpt = new Option<bool>("--force", "Set value even if it is not existing");
484 forceOpt.AddAlias("-f");
485
486 var command = new Command("config", "Get or set configuration values")
487 {
488 keyArg,
489 valueArg,
490 forceOpt,
491 };
492
493 command.SetHandler((key, value, force) =>
494 {
495 if (string.IsNullOrEmpty(value))
496 {
497 var items = string.IsNullOrEmpty(key) ? Configuration.Settings : Configuration.Settings.Where(x => x.Key.StartsWith(key));
498
499 foreach (var item in items)
500 {
501 AnsiConsole.WriteLine($"{item.Key} = {item.Value}");
502 }
503 }
504 else
505 {
506 if (!force && !Configuration.Settings.ContainsKey(key))
507 {
508 AnsiConsole.MarkupLine($"[red]Invalid key: {key}[/]");
509 return;
510 }
511
512 bool bValue;
513 try
514 {
515 bValue = Utilities.ParseToBool(value);
516 }
517 catch
518 {
519 AnsiConsole.MarkupLine($"[red]Invalid value: {value}[/]");
520 return;
521 }
522
523 Configuration.Settings[key] = bValue;
524 AnsiConsole.MarkupLine($"[green]{key} set to {Configuration.Settings[key]}[/]");
525 }
526 }, keyArg, valueArg, forceOpt);
527
528 return command;
529 }
530 }
531
543 public virtual void RunSelfSign(bool force, string? commonName, string? email, string? organization, string? organizationalUnit, string? locality, string? state, string? country)
544 {
545 if (!Configuration.Settings["selfsign.enable"])
546 {
547 AnsiConsole.MarkupLine("[red]Self-Signing feature is disabled[/]");
548 return;
549 }
550
551 Logger.LogInformation("Running self-sign command");
552
553 if (force || Configuration.SelfSignedRootCA != null)
554 {
555 Logger.LogWarning("Root CA already exists");
556 AnsiConsole.MarkupLine("[red]Root CA already exists![/]");
557 return;
558 }
559
560 string subject;
561
562 if (string.IsNullOrEmpty(commonName))
563 {
564 Logger.LogDebug("Getting subject name from user");
565 subject = CertificateUtilities.GetSubjectFromUser().ToString();
566 }
567 else
568 {
569 subject = new CertificateSubject(commonName: commonName,
570 email: email,
571 organization: organization,
572 organizationalUnit: organizationalUnit,
573 locality: locality,
574 state: state,
575 country: country).ToString();
576 }
577
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);
581
582 Logger.LogDebug("Exporting root CA certificate to configuration");
583 Configuration.SelfSignedRootCA = rootCA.Export(X509ContentType.Pfx);
584
585 Logger.LogDebug("Clearing issued certificates");
586 Configuration.IssuedCertificates.Clear();
587
588 CertificateUtilities.DisplayCertificate(rootCA);
589
590 Logger.LogInformation("Root CA created successfully");
591 AnsiConsole.MarkupLine($"[green]Root CA created successfully![/]");
592 }
593
600 protected X509Certificate2? GetSelfSigningRootCA()
601 {
602 if (Configuration.SelfSignedRootCA != null)
603 {
604 return CertificateUtilities.ImportPFX(Configuration.SelfSignedRootCA).Single();
605 }
606
607 return null;
608 }
609 }
610}
Represents a bundle that holds file hashes and signatures.
Definition Bundle.cs:22
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.