1using System.Security.Cryptography.X509Certificates;
3using Microsoft.Extensions.Logging;
14 public TBundle?
Bundle {
get;
protected set; }
32 throw new ApplicationException(
"Bundle is not initialized");
41 Logger.LogWarning(
"Bundle was created by a different application");
42 AnsiConsole.MarkupLine($
"[{Color.Orange1}]Warning:[/] Bundle was created by a different application");
47 catch (FileNotFoundException fnfex)
50 AnsiConsole.MarkupLine($
"[red]File not found: {Bundle.BundlePath}[/]");
55 AnsiConsole.MarkupLine($
"[{Color.Red}]Failed to load file: {Bundle.BundlePath}[/]");
56 AnsiConsole.MarkupLine($
"[{Color.Red}]Error:[/] {ex.GetType().Name}: {ex.Message}");
83 protected virtual void RunAdd(StatusContext statusContext,
string[] files,
bool replace,
bool recursive,
bool continueOnError,
bool force)
85 Logger.LogInformation(
"Running add command");
89 throw new ApplicationException(
"Bundle is not initialized");
94 Logger.LogDebug(
"A bundle file exists, loading bundle");
95 statusContext.Status(
"[yellow]Loading Bundle[/]");
101 Logger.LogError(
"Bundle is already signed, cannot add files");
102 AnsiConsole.MarkupLine($
"[{Color.Red}]Cannot add files to a signed bundle[/]");
107 statusContext.Status(
"[yellow]Adding Files[/]");
109 if (files.Length == 0)
113 if ((files = Utilities.SafeEnumerateFiles(
Bundle.
RootPath,
"*", recursive).ToArray()).Length == 0)
116 AnsiConsole.MarkupLine($
"[{Color.Red}]No files found in the directory: {Bundle.RootPath}[/]");
121 Logger.LogInformation(
"Discovered {FileCount} files in the directory: {RootPath}", files.Length,
Bundle.
RootPath);
122 AnsiConsole.MarkupLine($
"[{Color.Green}]Discovered {files.Length} files in the directory: {Bundle.RootPath}[/]");
126 Logger.LogInformation(
"Starting file adder multi-thread task");
127 bool errorOccurred =
false;
128 bool bundleUpdated =
false;
130 _ = Parallel.ForEach(files, (file, state) =>
134 Logger.LogWarning(
"File {file} is the bundle file itself", file);
135 AnsiConsole.MarkupLine($
"[{Color.Yellow}]Ignored:[/] File {file} is the bundle file itself");
141 Logger.LogInformation(
"Processing file: {EntryName}", entryName);
147 Logger.LogWarning(
"File {file} is outside the bundle root path", file);
148 AnsiConsole.MarkupLine($
"[{Color.Yellow}]Ignored:[/] File {file} is outside the bundle root path");
156 Logger.LogWarning(
"Entry already exists: {EntryName}", entryName);
157 AnsiConsole.MarkupLine($
"[{Color.Yellow}]Exists:[/] {entryName}");
161 Logger.LogDebug(
"Replacing entry: {EntryName}", entryName);
165 bundleUpdated =
true;
167 Logger.LogInformation(
"Entry: {EntryName} Replaced", entryName);
168 AnsiConsole.MarkupLine($
"[{Color.Cyan2}]Replaced:[/] {entryName}");
172 Logger.LogDebug(
"Adding entry: {EntryName}", entryName);
175 bundleUpdated =
true;
177 Logger.LogInformation(
"Entry: {EntryName} Added", entryName);
178 AnsiConsole.MarkupLine($
"[blue]Added:[/] {entryName}");
183 errorOccurred =
true;
185 Logger.LogError(ex,
"Error occurred while adding entry: {EntryName}", entryName);
186 AnsiConsole.MarkupLine($
"[{Color.Red}]Error:[/] {entryName} ({ex.GetType().Name}: {ex.Message})");
188 if (!continueOnError)
190 Logger.LogWarning(
"Stopping add operation due to error");
196 AnsiConsole.WriteLine();
200 AnsiConsole.MarkupLine(
"[orange]One or more errors occurred, check the console output or logs for more information[/]");
203 if (bundleUpdated && (continueOnError || !errorOccurred))
205 Logger.LogInformation(
"Saving bundle");
206 statusContext.Status(
"[yellow]Saving Bundle[/]");
210 Logger.LogInformation(
"Bundle saved successfully");
211 AnsiConsole.MarkupLine($
"[green]File: {Bundle.BundlePath} Saved successfully[/]");
215 Logger.LogInformation(
"No changes were made to the bundle");
216 AnsiConsole.MarkupLine(
"[yellow]No changes were made to the file[/]");
226 protected virtual void RunInfo(StatusContext statusContext)
228 Logger.LogInformation(
"Running info command");
232 throw new ApplicationException(
"Bundle is not initialized");
235 Logger.LogDebug(
"Loading bundle");
236 statusContext.Status(
"[yellow]Loading Bundle[/]");
239 Grid bundleGrid =
new Grid();
240 bundleGrid.AddColumn(
new GridColumn().NoWrap());
241 bundleGrid.AddColumn(
new GridColumn().PadLeft(2));
243 bundleGrid.AddRow(
"Bundle Info:");
252 AnsiConsole.Write(bundleGrid);
253 AnsiConsole.WriteLine();
255 Grid protectedEntries =
new Grid();
256 protectedEntries.AddColumn(
new GridColumn().NoWrap());
258 protectedEntries.AddRow(
"Protected Entry Names:");
262 protectedEntries.AddRow($
" {entryName}");
265 AnsiConsole.Write(protectedEntries);
266 AnsiConsole.WriteLine();
268 Grid manifestEntries =
new Grid();
269 manifestEntries.AddColumn(
new GridColumn());
270 manifestEntries.AddColumn(
new GridColumn().PadLeft(2).Width(18));
272 manifestEntries.AddRow(
"Manifest Entries:");
273 manifestEntries.AddRow(
" Entry Name",
"Hash");
277 var entryHash = BitConverter.ToString(entry.Value).Replace(
"-",
"");
278 manifestEntries.AddRow($
" {entry.Key}", $
"{entryHash[0..8]}..{entryHash.Substring(entryHash.Length - 8)}");
281 AnsiConsole.Write(manifestEntries);
282 AnsiConsole.WriteLine();
293 protected virtual void RunSign(StatusContext statusContext, X509Certificate2Collection certificates,
bool skipVerify)
295 Logger.LogInformation(
"Running sign command");
299 throw new ApplicationException(
"Bundle is not initialized");
302 if (certificates.Count == 0)
304 Logger.LogWarning(
"No certificates provided for signing");
305 AnsiConsole.MarkupLine(
"[red]No certificates provided for signing[/]");
309 Logger.LogDebug(
"Loading bundle");
310 statusContext.Status(
"[yellow]Loading Bundle[/]");
316 foreach (X509Certificate2 cert
in certificates)
318 if (divider++ > 0) AnsiConsole.WriteLine();
320 Logger.LogDebug(
"Loading certificate information for {Cert}", cert);
321 statusContext.Status(
"[yellow]Loading certificate informations[/]");
323 CertificateUtilities.DisplayCertificate(cert);
327 Logger.LogDebug(
"Verifying certificate {cert}", cert);
328 statusContext.Status(
"[yellow]Verifying Certificate[/]");
333 Logger.LogWarning(
"Skipping signing with {cert}", cert);
338 Logger.LogDebug(
"Acquiring RSA private key for {cert}", cert);
339 statusContext.Status(
"[yellow]Preparing for signing[/]");
341 System.Security.Cryptography.RSA? prvKey = cert.GetRSAPrivateKey();
344 Logger.LogError(
"Failed to acquire RSA private key for {cert}", cert);
345 AnsiConsole.MarkupLine($
"[{Color.Red}] Failed to Acquire RSA Private Key[/]");
349 Logger.LogDebug(
"Signing bundle with {cert}", cert);
350 statusContext.Status(
"[yellow]Signing Bundle[/]");
355 Logger.LogInformation(
"Bundle signed with {cert}", cert);
356 AnsiConsole.MarkupLine($
"[green] Signing Completed Successfully[/]");
361 Logger.LogWarning(
"No certificates were suitable for signing");
362 AnsiConsole.MarkupLine(
"[red]No certificates were suitable for signing[/]");
366 Logger.LogInformation(
"Saving bundle");
367 statusContext.Status(
"[yellow]Updating Bundle[/]");
370 Logger.LogInformation(
"Bundle saved successfully");
371 AnsiConsole.MarkupLine($
"[green]File: {Bundle.BundlePath} Saved successfully[/]");
380 protected virtual bool RunVerify(StatusContext statusContext,
bool ignoreTime)
382 Logger.LogInformation(
"Running verify command");
386 throw new ApplicationException(
"Bundle is not initialized");
389 Dictionary<string, Color> colorDict =
new Dictionary<string, Color>()
391 [
"file_verified"] = Color.MediumSpringGreen,
392 [
"file_failed"] = Color.OrangeRed1,
393 [
"file_missing"] = Color.Grey70,
394 [
"file_error"] = Color.Red3_1,
397 Logger.LogDebug(
"Loading bundle");
398 statusContext.Status(
"[yellow]Loading Bundle[/]");
403 Logger.LogError(
"Bundle is not signed");
404 AnsiConsole.MarkupLine($
"[red]The file is not signed[/]");
408 Logger.LogInformation(
"Starting certificate and signature verification");
409 statusContext.Status(
"[yellow]Verification Phase 1: Certificates and signatures[/]");
411 int verifiedCerts = 0;
416 if (divider++ > 0) AnsiConsole.WriteLine();
420 Logger.LogDebug(
"Verifying certificate {cert}", certificate);
421 AnsiConsole.MarkupLine($
"Verifying Certificate [{Color.Teal}]{certificate.GetNameInfo(X509NameType.SimpleName, false)}[/] Issued by [{Color.Aqua}]{certificate.GetNameInfo(X509NameType.SimpleName, true)}[/]");
426 Logger.LogWarning(
"Skipping signature verification for {cert}", certificate);
430 Logger.LogDebug(
"Verifying signature for certificate {cert}", certificate);
432 AnsiConsole.MarkupLine($
"[{(verifySign ? Color.Green : Color.Red)}] Signature Verification {(verifySign ? "Successful
" : "Failed
")}[/]");
435 Logger.LogWarning(
"Signature verification failed for {cert}", certificate);
439 Logger.LogInformation(
"Certificate and signature verification successful for {cert}", certificate);
443 AnsiConsole.WriteLine();
445 if (verifiedCerts == 0)
447 Logger.LogWarning(
"No certificates were verified");
448 AnsiConsole.MarkupLine($
"[red]Verification failed[/]");
454 Logger.LogInformation(
"All certificates were verified");
455 AnsiConsole.MarkupLine($
"[{Color.Green3}]All Certificates were verified[/]");
460 AnsiConsole.MarkupLine($
"[{Color.Yellow}]{verifiedCerts} out of {Bundle.Signatures.Entries.Count} Certificates were verified[/]");
463 AnsiConsole.WriteLine();
466 statusContext.Status(
"[yellow]Verification Phase 2: Files[/]");
468 bool p2Verified =
true;
477 bool verifyFile = false;
479 Logger.LogDebug(
"Verifying file {file}", entry.Key);
483 verifyFile = Bundle.VerifyFile(entry.Key);
487 Logger.LogInformation(
"File {file} verified", entry.Key);
488 Interlocked.Increment(ref fv);
492 Logger.LogWarning(
"File {file} failed verification", entry.Key);
493 Interlocked.Increment(ref ff);
496 AnsiConsole.MarkupLine($
"[{(verifyFile ? colorDict["file_verified
"] : colorDict["file_failed
"])}]{entry.Key}[/]");
498 catch (FileNotFoundException)
500 Logger.LogWarning(
"File {file} not found", entry.Key);
501 Interlocked.Increment(ref fm);
502 AnsiConsole.MarkupLine($
"[{colorDict["file_missing
"]}]{entry.Key}[/]");
506 Logger.LogError(ex,
"Error occurred while verifying file {file}", entry.Key);
507 Interlocked.Increment(ref fe);
508 AnsiConsole.MarkupLine($
"[{colorDict["file_error
"]}]{entry.Key} - {ex.GetType().Name}: {ex.Message}[/]");
511 if (!verifyFile) p2Verified =
false;
514 AnsiConsole.WriteLine();
518 AnsiConsole.MarkupLine(
"File Verification Summary");
519 AnsiConsole.MarkupLine($
"[{colorDict["file_verified
"]}] {fv} Files verified[/]");
520 if (ff > 0) AnsiConsole.MarkupLine($
"[{colorDict["file_failed
"]}] {ff} Files tampered with[/]");
521 if (fm > 0) AnsiConsole.MarkupLine($
"[{colorDict["file_missing
"]}] {fm} Files not found[/]");
522 if (fe > 0) AnsiConsole.MarkupLine($
"[{colorDict["file_error
"]}] {fe} Files encountered with errors[/]");
524 AnsiConsole.WriteLine();
529 Logger.LogWarning(
"File verification failed");
530 AnsiConsole.MarkupLine($
"[red]File Verification Failed[/]");
534 Logger.LogInformation(
"File verification completed successfully");
535 AnsiConsole.MarkupLine(
"[green]All Files Verified Successfully[/]");
557 bool result = VerifyCertificateImpl(certificate, ignoreTime, out X509ChainStatus[] verificationStatuses);
561 Utilities.EnumerateStatuses(verificationStatuses);
564 AnsiConsole.MarkupLine($
"[{(result ? Color.Green3 : Color.Red)}] Certificate Verification {(result ? "Successful
" : "Failed
")}[/]");
584 protected bool VerifyCertificateImpl(X509Certificate2 certificate,
bool ignoreTime, out X509ChainStatus[] chainStatuses)
588 throw new ApplicationException(
"Bundle is not initialized");
591 List<bool> verificationResults = [];
592 List<X509ChainStatus[]> verificationStatuses = [];
594 if (Configuration.Settings[
"selfsign.enable"] && Configuration.SelfSignedRootCA !=
null)
596 verificationResults.Add(SelfSignVerify(certificate, out X509ChainStatus[] selfSigningStatuses));
597 verificationStatuses.Add(selfSigningStatuses);
600 X509ChainPolicy policy =
new();
601 policy.ExtraStore.AddRange(Configuration.LoadCertificates(
CertificateStore.IntermediateCA));
605 policy.VerificationFlags |= X509VerificationFlags.IgnoreCtlNotTimeValid;
608 if (!verificationResults.Any(x => x))
610 Logger.LogDebug(
"Verifying certificate {cert} with system trust store", certificate);
612 bool defaultVerification =
Bundle.
VerifyCertificate(certificate, out X509ChainStatus[] defaultChainStatuses, policy);
614 Logger.LogInformation(
"Certificate verification with system trust store for {cert}: {result}", certificate, defaultVerification);
616 verificationResults.Add(defaultVerification);
617 verificationStatuses.Add(defaultChainStatuses);
620 if (Configuration.Settings[
"trust.enable"] && !verificationResults.Any(x => x) && Configuration.TrustedRootCA.Count > 0)
622 policy.TrustMode = X509ChainTrustMode.CustomRootTrust;
623 policy.CustomTrustStore.AddRange(Configuration.LoadCertificates(
CertificateStore.TrustedRootCA));
625 Logger.LogDebug(
"Verifying certificate {cert} with custom trust store", certificate);
626 bool customVerification =
Bundle.
VerifyCertificate(certificate, out X509ChainStatus[] customChainStatuses, policy);
627 Logger.LogInformation(
"Certificate verification with custom trust store for {cert}: {result}", certificate, customVerification);
629 verificationResults.Add(customVerification);
630 verificationStatuses.Add(customChainStatuses);
633 chainStatuses = verificationStatuses.Aggregate((prev, next) =>
635 return prev.Intersect(next).ToArray();
638 return verificationResults.Any(x => x);
641 private bool SelfSignVerify(X509Certificate2 certificate, out X509ChainStatus[] chainStatuses)
645 throw new ApplicationException(
"Bundle is not initialized");
649 X509Certificate2? rootCA;
651 if ((rootCA = GetSelfSigningRootCA()) !=
null)
653 Logger.LogDebug(
"Verifying certificate {cert} with self-signing root CA", certificate);
655 X509ChainPolicy selfSignPolicy =
new X509ChainPolicy();
656 selfSignPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
657 selfSignPolicy.CustomTrustStore.Add(rootCA);
658 selfSignPolicy.VerificationFlags |= X509VerificationFlags.IgnoreNotTimeValid;
659 selfSignPolicy.RevocationMode = X509RevocationMode.NoCheck;
662 Logger.LogInformation(
"Certificate verification with self-signing root CA for {cert}: {result}", certificate, selfSignVerification);
664 return selfSignVerification;
667 Logger.LogDebug(
"Self-signing root CA not found");
Represents a bundle that holds file hashes and signatures.
void AddEntry(string path, string destinationPath="./", string? rootPath=null)
Adds a file entry to the bundle.
string BundlePath
Gets the full path of the bundle file.
string RootPath
Gets the root path of the bundle.
void DeleteEntry(string entryName)
Deletes an entry from the bundle.
void Sign(X509Certificate2 certificate, RSA privateKey)
Signs the bundle with the specified certificate and private key.
void Update()
Writes changes to the bundle file.
void LoadFromFile(bool readOnly=true)
Loads the bundle from the file system.
bool VerifyCertificate(string certificateHash, out X509ChainStatus[] statuses, X509ChainPolicy? policy=null)
Verifies the validity of a certificate using the specified certificate hash.
X509Certificate2 GetCertificate(string certificateHash)
Gets a certificate from the bundle using the specified certificate hash.
bool Loaded
Gets a value indicating whether the bundle is loaded.
Manifest Manifest
Gets the manifest of the bundle.
bool VerifySignature(string certificateHash)
Verifies the signature of the bundle using the specified certificate hash.
Signatures Signatures
Gets the signatures of the bundle.
Provides command definitions and handlers for the EasySign command line interface.
virtual void RunAdd(StatusContext statusContext, string[] files, bool replace, bool recursive, bool continueOnError, bool force)
Runs the add command.
bool LoadBundle(bool readOnly=true)
Loads the bundle from file and handles load errors.
bool VerifyCertificateImpl(X509Certificate2 certificate, bool ignoreTime, out X509ChainStatus[] chainStatuses)
Verifies the certificate using the configured trust stores.
void InitializeBundle(string bundlePath)
Initializes the bundle.
virtual void RunInfo(StatusContext statusContext)
Runs the info command.
ILogger Logger
Gets or sets the logger to use for logging.
bool VerifyCertificate(X509Certificate2 certificate, bool ignoreTime)
Verifies the validity of a certificate.
virtual void RunSign(StatusContext statusContext, X509Certificate2Collection certificates, bool skipVerify)
Runs the sign command.
virtual bool RunVerify(StatusContext statusContext, bool ignoreTime)
Runs the verify command.
Represents a manifest that holds entries of file names and their corresponding hashes.
SortedDictionary< string, byte[]> Entries
Gets or sets the entries in the manifest as a sorted dictionary.
static string GetNormalizedEntryName(string path)
Converts the path to an standard zip entry name.
string? UpdatedBy
Gets or sets the full name of the class that updated the manifest.
bool StoreOriginalFiles
Gets or sets a value indicating whether the files should be stored in the bundle.
HashSet< string > ProtectedEntryNames
Gets or sets the list of entry names that should be protected by the bundle from accidental modificat...
Dictionary< string, byte[]> Entries
Gets or sets the signature entries.
CertificateStore
Enumeration of certificate stores in the CommandProviderConfiguration.