From e57164183e9cb70648db85bc8856112d038f2d9c Mon Sep 17 00:00:00 2001 From: Fernando Ribeiro Date: Tue, 17 Mar 2026 19:04:18 -0300 Subject: [PATCH 1/3] feat: Implement a console application to download, validate, and install X.509 certificates from specified sources with argument parsing and service-oriented architecture. --- WinCertInstaller.Tests/ProgramTests.cs | 64 +++++ .../WinCertInstaller.Tests.csproj | 25 ++ WinCertInstaller.sln | 26 ++ WinCertInstaller/Configuration/AppSettings.cs | 8 + WinCertInstaller/Models/CertSource.cs | 13 + WinCertInstaller/Program.cs | 258 ++++++++---------- WinCertInstaller/Properties/AssemblyInfo.cs | 18 -- .../Properties/Settings.Designer.cs | 26 -- WinCertInstaller/Properties/Settings.settings | 6 - .../Properties/WinCertInstaller.manifest | 15 - .../Services/CertificateDownloader.cs | 114 ++++++++ .../Services/CertificateInstaller.cs | 123 +++++++++ .../Services/CertificateValidator.cs | 36 +++ .../Services/ICertificateDownloader.cs | 12 + .../Services/ICertificateInstaller.cs | 9 + .../Services/ICertificateValidator.cs | 11 + WinCertInstaller/WinCertInstaller.csproj | 96 +------ WinCertInstaller/app.config | 3 - 18 files changed, 566 insertions(+), 297 deletions(-) create mode 100644 WinCertInstaller.Tests/ProgramTests.cs create mode 100644 WinCertInstaller.Tests/WinCertInstaller.Tests.csproj create mode 100644 WinCertInstaller/Configuration/AppSettings.cs create mode 100644 WinCertInstaller/Models/CertSource.cs delete mode 100644 WinCertInstaller/Properties/AssemblyInfo.cs delete mode 100644 WinCertInstaller/Properties/Settings.Designer.cs delete mode 100644 WinCertInstaller/Properties/Settings.settings delete mode 100644 WinCertInstaller/Properties/WinCertInstaller.manifest create mode 100644 WinCertInstaller/Services/CertificateDownloader.cs create mode 100644 WinCertInstaller/Services/CertificateInstaller.cs create mode 100644 WinCertInstaller/Services/CertificateValidator.cs create mode 100644 WinCertInstaller/Services/ICertificateDownloader.cs create mode 100644 WinCertInstaller/Services/ICertificateInstaller.cs create mode 100644 WinCertInstaller/Services/ICertificateValidator.cs delete mode 100644 WinCertInstaller/app.config diff --git a/WinCertInstaller.Tests/ProgramTests.cs b/WinCertInstaller.Tests/ProgramTests.cs new file mode 100644 index 0000000..7baf3d6 --- /dev/null +++ b/WinCertInstaller.Tests/ProgramTests.cs @@ -0,0 +1,64 @@ +using System; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Xunit; +using WinCertInstaller.Models; +using WinCertInstaller.Services; + +namespace WinCertInstaller.Tests +{ + public class ProgramTests + { + [Fact] + public void ParseArguments_DefaultsToAll() + { + var result = WinCertInstaller.Program.ParseArguments(Array.Empty()); + + Assert.Equal(CertSource.All, result.source); + Assert.False(result.dryRun); + Assert.False(result.quiet); + Assert.False(result.showHelp); + } + + [Fact] + public void ParseArguments_MpfAndDryRunAndQuiet_SetCorrectFlags() + { + var result = WinCertInstaller.Program.ParseArguments(new[] { "--mpf", "--dry-run", "-q" }); + + Assert.Equal(CertSource.MPF, result.source); + Assert.True(result.dryRun); + Assert.True(result.quiet); + Assert.False(result.showHelp); + } + + [Fact] + public void ParseArguments_HelpSet_ShowHelpTrue() + { + var result = WinCertInstaller.Program.ParseArguments(new[] { "--help" }); + + Assert.Equal(CertSource.None, result.source); + Assert.True(result.showHelp); + } + + [Fact] + public void ParseArguments_InvalidArgument_ThrowsArgumentException() + { + Assert.Throws(() => WinCertInstaller.Program.ParseArguments(new[] { "--unknown" })); + } + + [Fact] + public void IsCertificateAuthorityAndSelfSigned_TrueForSelfSignedCA() + { + using var rsa = RSA.Create(2048); + var request = new CertificateRequest("CN=TestCA", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + request.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true)); + request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false)); + + using var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(365)); + var validator = new CertificateValidator(); + + Assert.True(validator.IsCertificateAuthority(certificate)); + Assert.True(validator.IsSelfSigned(certificate)); + } + } +} diff --git a/WinCertInstaller.Tests/WinCertInstaller.Tests.csproj b/WinCertInstaller.Tests/WinCertInstaller.Tests.csproj new file mode 100644 index 0000000..9924ba7 --- /dev/null +++ b/WinCertInstaller.Tests/WinCertInstaller.Tests.csproj @@ -0,0 +1,25 @@ + + + + net10.0-windows7.0 + enable + enable + false + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WinCertInstaller.sln b/WinCertInstaller.sln index b0b044d..bc2cd24 100644 --- a/WinCertInstaller.sln +++ b/WinCertInstaller.sln @@ -10,16 +10,42 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinCertInstaller.Tests", "WinCertInstaller.Tests\WinCertInstaller.Tests.csproj", "{9C5EF0B3-D039-4278-BA67-F47AFB11364D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {C22FA746-778E-4806-BD82-F4E0FCE53763}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C22FA746-778E-4806-BD82-F4E0FCE53763}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C22FA746-778E-4806-BD82-F4E0FCE53763}.Debug|x64.ActiveCfg = Debug|Any CPU + {C22FA746-778E-4806-BD82-F4E0FCE53763}.Debug|x64.Build.0 = Debug|Any CPU + {C22FA746-778E-4806-BD82-F4E0FCE53763}.Debug|x86.ActiveCfg = Debug|Any CPU + {C22FA746-778E-4806-BD82-F4E0FCE53763}.Debug|x86.Build.0 = Debug|Any CPU {C22FA746-778E-4806-BD82-F4E0FCE53763}.Release|Any CPU.ActiveCfg = Release|Any CPU {C22FA746-778E-4806-BD82-F4E0FCE53763}.Release|Any CPU.Build.0 = Release|Any CPU + {C22FA746-778E-4806-BD82-F4E0FCE53763}.Release|x64.ActiveCfg = Release|Any CPU + {C22FA746-778E-4806-BD82-F4E0FCE53763}.Release|x64.Build.0 = Release|Any CPU + {C22FA746-778E-4806-BD82-F4E0FCE53763}.Release|x86.ActiveCfg = Release|Any CPU + {C22FA746-778E-4806-BD82-F4E0FCE53763}.Release|x86.Build.0 = Release|Any CPU + {9C5EF0B3-D039-4278-BA67-F47AFB11364D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C5EF0B3-D039-4278-BA67-F47AFB11364D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C5EF0B3-D039-4278-BA67-F47AFB11364D}.Debug|x64.ActiveCfg = Debug|Any CPU + {9C5EF0B3-D039-4278-BA67-F47AFB11364D}.Debug|x64.Build.0 = Debug|Any CPU + {9C5EF0B3-D039-4278-BA67-F47AFB11364D}.Debug|x86.ActiveCfg = Debug|Any CPU + {9C5EF0B3-D039-4278-BA67-F47AFB11364D}.Debug|x86.Build.0 = Debug|Any CPU + {9C5EF0B3-D039-4278-BA67-F47AFB11364D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C5EF0B3-D039-4278-BA67-F47AFB11364D}.Release|Any CPU.Build.0 = Release|Any CPU + {9C5EF0B3-D039-4278-BA67-F47AFB11364D}.Release|x64.ActiveCfg = Release|Any CPU + {9C5EF0B3-D039-4278-BA67-F47AFB11364D}.Release|x64.Build.0 = Release|Any CPU + {9C5EF0B3-D039-4278-BA67-F47AFB11364D}.Release|x86.ActiveCfg = Release|Any CPU + {9C5EF0B3-D039-4278-BA67-F47AFB11364D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/WinCertInstaller/Configuration/AppSettings.cs b/WinCertInstaller/Configuration/AppSettings.cs new file mode 100644 index 0000000..5944e43 --- /dev/null +++ b/WinCertInstaller/Configuration/AppSettings.cs @@ -0,0 +1,8 @@ +namespace WinCertInstaller.Configuration +{ + public static class AppSettings + { + public const string ITICertUrl = "http://acraiz.icpbrasil.gov.br/credenciadas/CertificadosAC-ICP-Brasil/ACcompactado.zip"; + public const string MPFCertUrl = "http://repositorio.acinterna.mpf.mp.br/ejbca/ra/downloads/ACIMPF-cadeia-completa.p7b"; + } +} diff --git a/WinCertInstaller/Models/CertSource.cs b/WinCertInstaller/Models/CertSource.cs new file mode 100644 index 0000000..6f181d9 --- /dev/null +++ b/WinCertInstaller/Models/CertSource.cs @@ -0,0 +1,13 @@ +using System; + +namespace WinCertInstaller.Models +{ + [Flags] + public enum CertSource + { + None = 0, + ITI = 1, + MPF = 2, + All = ITI | MPF + } +} diff --git a/WinCertInstaller/Program.cs b/WinCertInstaller/Program.cs index c17b829..b9e1325 100644 --- a/WinCertInstaller/Program.cs +++ b/WinCertInstaller/Program.cs @@ -1,190 +1,162 @@ -using System; -using System.Net; -using System.IO; -using System.IO.Compression; +using System; +using System.Threading; +using System.Threading.Tasks; using System.Security.Cryptography.X509Certificates; -using System.Linq; +using WinCertInstaller.Models; +using WinCertInstaller.Configuration; +using WinCertInstaller.Services; namespace WinCertInstaller { - class Program + public class Program { - static Stream DownloadFile(String url) + static void PrintUsage() { - WebClient client = new WebClient(); - Stream stream = null; - try - { - stream = client.OpenRead(url); - } catch (System.Net.WebException ex) - { - Console.WriteLine("ERROR: Unable to download certificates."); - Console.WriteLine("ERROR: {0}", ex.Message); - } - - return stream; + Console.WriteLine("Usage: WinCertInstaller [options]"); + Console.WriteLine("Options:"); + Console.WriteLine(" --iti Install certificates from ITI"); + Console.WriteLine(" --mpf Install certificates from MPF"); + Console.WriteLine(" --all Install certificates from ITI and MPF (default)"); + Console.WriteLine(" --dry-run Run without writing certificates to store"); + Console.WriteLine(" -q Quiet mode (no pause at exit)"); + Console.WriteLine(" -h,--help Show this help message"); + Console.WriteLine("Example: WinCertInstaller --iti --dry-run"); } - static X509Certificate2Collection GetZIPCertificates(String url) + public static (CertSource source, bool dryRun, bool quiet, bool showHelp) ParseArguments(string[] args) { - X509Certificate2Collection certCollection = new X509Certificate2Collection(); - Console.WriteLine("Getting certificates from {0} please wait.", url); - Stream stream = DownloadFile(url); - - if (stream != null) { - ZipArchive archive = new ZipArchive(stream); + bool quiet = false; + bool dryRun = false; + CertSource selectedSources = CertSource.None; + bool showHelp = false; - foreach (ZipArchiveEntry certificate in archive.Entries) + if (args.Length == 0) + { + selectedSources = CertSource.All; + } + else + { + foreach (string arg in args) { - Stream certStrean = certificate.Open(); - MemoryStream ms = new MemoryStream(); - X509Certificate2 cert = new X509Certificate2(); - certStrean.CopyTo(ms); - cert.Import(ms.ToArray()); - certCollection.Add(cert); + switch (arg.ToLowerInvariant()) + { + case "-q": + quiet = true; + break; + case "--dry-run": + dryRun = true; + break; + case "--iti": + selectedSources |= CertSource.ITI; + break; + case "--mpf": + selectedSources |= CertSource.MPF; + break; + case "--all": + selectedSources = CertSource.All; + break; + case "-h": + case "--help": + showHelp = true; + break; + default: + throw new ArgumentException($"Wrong parameter: {arg}"); + } } - Console.WriteLine("{0} certificates found.", certCollection.Count); } - return certCollection; - } - static X509Certificate2Collection GetP7BCertificates(String url) - { - X509Certificate2Collection certCollection = new X509Certificate2Collection(); - Console.WriteLine("Getting certificates from {0} please wait.", url); - Stream stream = DownloadFile(url); - if (stream != null) { - MemoryStream ms = new MemoryStream(); - stream.CopyTo(ms); - certCollection.Import(ms.ToArray()); - Console.WriteLine("{0} certificates found.", certCollection.Count); + if (selectedSources == CertSource.None && !showHelp) + { + throw new ArgumentException("No certificate source selected."); } - return certCollection; + + return (selectedSources, dryRun, quiet, showHelp); } - static void Add(X509Certificate2Collection certificates, StoreName storeName, StoreLocation location) + static void DisposeCertificates(X509Certificate2Collection collection) { - X509Store store = new X509Store(storeName, location); - store.Open(OpenFlags.MaxAllowed); - Console.WriteLine("Installing certificates."); - store.AddRange(certificates); - Console.WriteLine("Added {0} certificates to {1}.", certificates.Count, storeName); - store.Close(); + if (collection == null) return; + foreach (var cert in collection) + { + cert.Dispose(); + } } - static void InstallCertificates(X509Certificate2Collection certificates) { - X509Certificate2Collection CACertificates = new X509Certificate2Collection(); - X509Certificate2Collection CAIntermediateCertificates = new X509Certificate2Collection(); - - foreach (X509Certificate2 cert in certificates) + static async Task Main(string[] args) + { + using var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (s, e) => { - bool isCA = IsCertificateAuthority(cert); - bool isSelfSigned = IsSelfSigned(cert); - if (isCA) - { - if (isSelfSigned) - { - CACertificates.Add(cert); - } - else - { - CAIntermediateCertificates.Add(cert); - } - } - else - { - Console.WriteLine("{0} is not a CA. Ignoring.", cert.Subject); - } - } + Console.WriteLine("\nCanceling..."); + e.Cancel = true; + cts.Cancel(); + }; try { - if (CACertificates.Count > 0) - { - Add(CACertificates, StoreName.Root, StoreLocation.LocalMachine); - } else + var (selectedSources, dryRun, quiet, showHelp) = ParseArguments(args); + + if (showHelp) { - Console.WriteLine("No CA certificates to import."); + PrintUsage(); + return 0; } - if (CAIntermediateCertificates.Count > 0) + if (dryRun) { - Add(CAIntermediateCertificates, StoreName.CertificateAuthority, StoreLocation.LocalMachine); + Console.WriteLine("Dry run mode enabled: no certificates will be added to the store."); } - else + + // Dependency Injection Composition Root + ICertificateDownloader downloader = new CertificateDownloader(); + ICertificateValidator validator = new CertificateValidator(); + ICertificateInstaller installer = new CertificateInstaller(validator); + + if (selectedSources.HasFlag(CertSource.ITI)) { - Console.WriteLine("No Intermediate CA certificates to import."); + Console.WriteLine("====================== ITI ======================"); + X509Certificate2Collection itiCertificates = await downloader.GetZIPCertificatesAsync(AppSettings.ITICertUrl, cts.Token); + if (itiCertificates.Count > 0) + { + installer.InstallCertificates(itiCertificates, dryRun); + DisposeCertificates(itiCertificates); + } } - } - catch (System.Security.Cryptography.CryptographicException ex) - { - Console.WriteLine("Error: {0}", ex.Message); - System.Environment.Exit(-1); - } - - } - public static bool IsCertificateAuthority(X509Certificate2 certificate) - { - foreach (X509BasicConstraintsExtension basic_constraints in certificate.Extensions.OfType()) - { - if (basic_constraints.CertificateAuthority) + if (selectedSources.HasFlag(CertSource.MPF)) { - return true; + Console.WriteLine("====================== MPF ======================"); + X509Certificate2Collection mpfCertificates = await downloader.GetP7BCertificatesAsync(AppSettings.MPFCertUrl, cts.Token); + if (mpfCertificates.Count > 0) + { + installer.InstallCertificates(mpfCertificates, dryRun); + DisposeCertificates(mpfCertificates); + } } - } - return false; - } - public static bool IsSelfSigned(X509Certificate2 certificate) - { - if (certificate.Issuer == certificate.Subject) - { - return true; - } - return false; - } + Console.WriteLine("================================================="); + Console.WriteLine("Finished!"); - static void Main(string[] args) - { - bool quiet = false; - if(args.Length > 0) - { - if (args[0] == "-q") - { - quiet = true; - } else + if (!quiet) { - Console.WriteLine("Wrong parameter. Aborting."); - Console.WriteLine("Valid parameters are only: -q"); - System.Environment.Exit(-1); + Console.WriteLine("To run in quiet mode use -q parameter."); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(true); } - } - Console.WriteLine("====================== ITI ======================"); - X509Certificate2Collection ITIcertificates = GetZIPCertificates("http://acraiz.icpbrasil.gov.br/credenciadas/CertificadosAC-ICP-Brasil/ACcompactado.zip"); - if (ITIcertificates.Count > 0) { - InstallCertificates(ITIcertificates); + return 0; } - - Console.WriteLine("====================== MPF ======================"); - X509Certificate2Collection MPFCertificates = GetP7BCertificates("http://repositorio.acinterna.mpf.mp.br/ejbca/ra/downloads/ACIMPF-cadeia-completa.p7b"); - - if (MPFCertificates.Count > 0) + catch (ArgumentException ex) { - InstallCertificates(MPFCertificates); + Console.WriteLine("Error: {0}", ex.Message); + PrintUsage(); + return -1; } - - Console.WriteLine("================================================="); - Console.WriteLine("Finished!"); - - if (!quiet) + catch (Exception ex) { - Console.WriteLine("To run in quiet mode use -q parameters."); - Console.WriteLine("Press any key to exit."); - Console.ReadKey(true); + Console.WriteLine("Unexpected error: {0}", ex.Message); + return -1; } - } } } diff --git a/WinCertInstaller/Properties/AssemblyInfo.cs b/WinCertInstaller/Properties/AssemblyInfo.cs deleted file mode 100644 index f45b945..0000000 --- a/WinCertInstaller/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - - -[assembly: AssemblyTitle("WinCertInstaller")] -[assembly: AssemblyDescription("Install ITI and MPF Certificates on Windows Trusted Certificate Store")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("WinCertInstaller")] -[assembly: AssemblyCopyright("Copyright © 2018")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - - -[assembly: ComVisible(false)] -[assembly: Guid("c22fa746-778e-4806-bd82-f4e0fce53763")] -[assembly: AssemblyVersion("1.1.0.0")] -[assembly: AssemblyFileVersion("1.1.0.0")] diff --git a/WinCertInstaller/Properties/Settings.Designer.cs b/WinCertInstaller/Properties/Settings.Designer.cs deleted file mode 100644 index b7bc3f6..0000000 --- a/WinCertInstaller/Properties/Settings.Designer.cs +++ /dev/null @@ -1,26 +0,0 @@ -//------------------------------------------------------------------------------ -// -// O código foi gerado por uma ferramenta. -// Versão de Tempo de Execução:4.0.30319.42000 -// -// As alterações ao arquivo poderão causar comportamento incorreto e serão perdidas se -// o código for gerado novamente. -// -//------------------------------------------------------------------------------ - -namespace WinCertInstaller.Properties { - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.7.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default { - get { - return defaultInstance; - } - } - } -} diff --git a/WinCertInstaller/Properties/Settings.settings b/WinCertInstaller/Properties/Settings.settings deleted file mode 100644 index 049245f..0000000 --- a/WinCertInstaller/Properties/Settings.settings +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/WinCertInstaller/Properties/WinCertInstaller.manifest b/WinCertInstaller/Properties/WinCertInstaller.manifest deleted file mode 100644 index 7d36b29..0000000 --- a/WinCertInstaller/Properties/WinCertInstaller.manifest +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/WinCertInstaller/Services/CertificateDownloader.cs b/WinCertInstaller/Services/CertificateDownloader.cs new file mode 100644 index 0000000..cedfc47 --- /dev/null +++ b/WinCertInstaller/Services/CertificateDownloader.cs @@ -0,0 +1,114 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Net.Http; +using System.Security.Cryptography.Pkcs; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; + +namespace WinCertInstaller.Services +{ + public class CertificateDownloader : ICertificateDownloader + { + private static readonly HttpClient HttpClient = new HttpClient + { + Timeout = TimeSpan.FromSeconds(30) + }; + + private async Task DownloadFileAsync(string url, CancellationToken cancellationToken = default, int maxAttempts = 3, TimeSpan? delayBetweenAttempts = null) + { + delayBetweenAttempts ??= TimeSpan.FromSeconds(2); + + for (int attempt = 1; attempt <= maxAttempts; attempt++) + { + try + { + using var response = await HttpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStreamAsync(cancellationToken); + } + catch (OperationCanceledException) + { + Console.WriteLine("Download canceled."); + return null; + } + catch (Exception ex) when (attempt < maxAttempts) + { + if (cancellationToken.IsCancellationRequested) return null; + Console.WriteLine("WARNING: attempt {0} failed for {1}: {2}", attempt, url, ex.Message); + await Task.Delay(delayBetweenAttempts.Value, cancellationToken); + } + catch (Exception ex) + { + Console.WriteLine("ERROR: Unable to download certificates from {0} after {1} attempts.", url, attempt); + Console.WriteLine("ERROR: {0}", ex.Message); + } + } + + return null; + } + + public async Task GetZIPCertificatesAsync(string url, CancellationToken cancellationToken = default) + { + X509Certificate2Collection certCollection = new X509Certificate2Collection(); + Console.WriteLine("Getting certificates from {0} please wait.", url); + + using Stream? stream = await DownloadFileAsync(url, cancellationToken); + + if (stream != null) + { + using var archive = new ZipArchive(stream); + + foreach (ZipArchiveEntry certificate in archive.Entries) + { + if (certificate.Length == 0 || !(certificate.FullName.EndsWith(".cer", StringComparison.OrdinalIgnoreCase) || certificate.FullName.EndsWith(".crt", StringComparison.OrdinalIgnoreCase))) + { + continue; + } + + try + { + using Stream certStream = certificate.Open(); + using MemoryStream ms = new MemoryStream(); + certStream.CopyTo(ms); + + var cert = X509CertificateLoader.LoadCertificate(ms.ToArray()); + certCollection.Add(cert); + } + catch (Exception ex) + { + Console.WriteLine("WARNING: Failed to load certificate from zip entry {0}: {1}", certificate.FullName, ex.Message); + } + } + Console.WriteLine("{0} certificates found.", certCollection.Count); + } + return certCollection; + } + + public async Task GetP7BCertificatesAsync(string url, CancellationToken cancellationToken = default) + { + X509Certificate2Collection certCollection = new X509Certificate2Collection(); + Console.WriteLine("Getting certificates from {0} please wait.", url); + + using Stream? stream = await DownloadFileAsync(url, cancellationToken); + + if (stream != null) + { + using MemoryStream ms = new MemoryStream(); + await stream.CopyToAsync(ms, cancellationToken); + + var signedCms = new SignedCms(); + signedCms.Decode(ms.ToArray()); + + foreach (var cert in signedCms.Certificates) + { + certCollection.Add(cert); + } + + Console.WriteLine("{0} certificates found.", certCollection.Count); + } + return certCollection; + } + } +} diff --git a/WinCertInstaller/Services/CertificateInstaller.cs b/WinCertInstaller/Services/CertificateInstaller.cs new file mode 100644 index 0000000..e80381a --- /dev/null +++ b/WinCertInstaller/Services/CertificateInstaller.cs @@ -0,0 +1,123 @@ +using System; +using System.Security.Cryptography.X509Certificates; + +namespace WinCertInstaller.Services +{ + public class CertificateInstaller : ICertificateInstaller + { + private readonly ICertificateValidator _validator; + + public CertificateInstaller(ICertificateValidator validator) + { + _validator = validator; + } + + private bool IsCertificateInStore(X509Certificate2 certificate, X509Store store) + { + foreach (var existingCert in store.Certificates) + { + if (string.Equals(existingCert.Thumbprint, certificate.Thumbprint, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + + private void Add(X509Certificate2Collection certificates, StoreName storeName, StoreLocation location, bool dryRun) + { + using var store = new X509Store(storeName, location); + store.Open(dryRun ? OpenFlags.ReadOnly : OpenFlags.ReadWrite); + + Console.WriteLine("Installing certificates into {0}.", storeName); + + int added = 0; + int skipped = 0; + int invalid = 0; + + foreach (X509Certificate2 certificate in certificates) + { + if (!_validator.IsCertificateValidForInstall(certificate)) + { + invalid++; + continue; + } + + if (IsCertificateInStore(certificate, store)) + { + skipped++; + Console.WriteLine("Skipped already installed certificate: {0}", certificate.Subject); + continue; + } + + if (dryRun) + { + added++; + continue; + } + + store.Add(certificate); + added++; + } + + Console.WriteLine("Added {0} certificates to {1}.", added, storeName); + if (skipped > 0) Console.WriteLine("Skipped {0} already installed certificate(s).", skipped); + if (invalid > 0) Console.WriteLine("Ignored {0} invalid certificate(s).", invalid); + + store.Close(); + } + + public void InstallCertificates(X509Certificate2Collection certificates, bool dryRun) + { + X509Certificate2Collection caCertificates = new X509Certificate2Collection(); + X509Certificate2Collection caIntermediateCertificates = new X509Certificate2Collection(); + + foreach (X509Certificate2 cert in certificates) + { + bool isCA = _validator.IsCertificateAuthority(cert); + bool isSelfSigned = _validator.IsSelfSigned(cert); + if (isCA) + { + if (isSelfSigned) + { + caCertificates.Add(cert); + } + else + { + caIntermediateCertificates.Add(cert); + } + } + else + { + Console.WriteLine("{0} is not a CA. Ignoring.", cert.Subject); + } + } + + try + { + if (caCertificates.Count > 0) + { + Add(caCertificates, StoreName.Root, StoreLocation.LocalMachine, dryRun); + } + else + { + Console.WriteLine("No CA certificates to import."); + } + + if (caIntermediateCertificates.Count > 0) + { + Add(caIntermediateCertificates, StoreName.CertificateAuthority, StoreLocation.LocalMachine, dryRun); + } + else + { + Console.WriteLine("No Intermediate CA certificates to import."); + } + } + catch (System.Security.Cryptography.CryptographicException ex) + { + Console.WriteLine("Erro ao acessar os repositórios do Windows: {0}", ex.Message); + Console.WriteLine("DICA: Certifique-se de executar este programa como 'Administrador' para instalar certificados na máquina local."); + } + } + } +} diff --git a/WinCertInstaller/Services/CertificateValidator.cs b/WinCertInstaller/Services/CertificateValidator.cs new file mode 100644 index 0000000..5349483 --- /dev/null +++ b/WinCertInstaller/Services/CertificateValidator.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using System.Security.Cryptography.X509Certificates; + +namespace WinCertInstaller.Services +{ + public class CertificateValidator : ICertificateValidator + { + public bool IsCertificateValidForInstall(X509Certificate2 certificate) + { + if (certificate.NotBefore > DateTime.UtcNow) + { + Console.WriteLine("Certificate {0} not active yet. NotBefore={1}", certificate.Subject, certificate.NotBefore); + return false; + } + + if (certificate.NotAfter < DateTime.UtcNow) + { + Console.WriteLine("Certificate {0} expired. NotAfter={1}", certificate.Subject, certificate.NotAfter); + return false; + } + + return true; + } + + public bool IsCertificateAuthority(X509Certificate2 certificate) + { + return certificate.Extensions.OfType().Any(ext => ext.CertificateAuthority); + } + + public bool IsSelfSigned(X509Certificate2 certificate) + { + return certificate.SubjectName.RawData.SequenceEqual(certificate.IssuerName.RawData); + } + } +} diff --git a/WinCertInstaller/Services/ICertificateDownloader.cs b/WinCertInstaller/Services/ICertificateDownloader.cs new file mode 100644 index 0000000..adf62e4 --- /dev/null +++ b/WinCertInstaller/Services/ICertificateDownloader.cs @@ -0,0 +1,12 @@ +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; + +namespace WinCertInstaller.Services +{ + public interface ICertificateDownloader + { + Task GetZIPCertificatesAsync(string url, CancellationToken cancellationToken = default); + Task GetP7BCertificatesAsync(string url, CancellationToken cancellationToken = default); + } +} diff --git a/WinCertInstaller/Services/ICertificateInstaller.cs b/WinCertInstaller/Services/ICertificateInstaller.cs new file mode 100644 index 0000000..5dbd6cb --- /dev/null +++ b/WinCertInstaller/Services/ICertificateInstaller.cs @@ -0,0 +1,9 @@ +using System.Security.Cryptography.X509Certificates; + +namespace WinCertInstaller.Services +{ + public interface ICertificateInstaller + { + void InstallCertificates(X509Certificate2Collection certificates, bool dryRun); + } +} diff --git a/WinCertInstaller/Services/ICertificateValidator.cs b/WinCertInstaller/Services/ICertificateValidator.cs new file mode 100644 index 0000000..ce693e1 --- /dev/null +++ b/WinCertInstaller/Services/ICertificateValidator.cs @@ -0,0 +1,11 @@ +using System.Security.Cryptography.X509Certificates; + +namespace WinCertInstaller.Services +{ + public interface ICertificateValidator + { + bool IsCertificateValidForInstall(X509Certificate2 certificate); + bool IsCertificateAuthority(X509Certificate2 certificate); + bool IsSelfSigned(X509Certificate2 certificate); + } +} diff --git a/WinCertInstaller/WinCertInstaller.csproj b/WinCertInstaller/WinCertInstaller.csproj index 6af74b9..550731b 100644 --- a/WinCertInstaller/WinCertInstaller.csproj +++ b/WinCertInstaller/WinCertInstaller.csproj @@ -1,96 +1,20 @@ - - - + - Debug - AnyCPU - {C22FA746-778E-4806-BD82-F4E0FCE53763} Exe + net10.0-windows + false + false + enable + enable WinCertInstaller WinCertInstaller - v4.5 - 512 - - false - publish\ - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - true + false - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - false - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false - - - WinCertInstaller.Program - - - LocalIntranet - - - false - - - Properties\WinCertInstaller.manifest - - - - - - - - - - - - - True - True - Settings.settings - - - - - - SettingsSingleFileGenerator - Settings.Designer.cs - - + + - - False - .NET Framework 3.5 SP1 - false - + - \ No newline at end of file diff --git a/WinCertInstaller/app.config b/WinCertInstaller/app.config deleted file mode 100644 index 51278a4..0000000 --- a/WinCertInstaller/app.config +++ /dev/null @@ -1,3 +0,0 @@ - - - From bd7fd70f6eb738711a5726e39c96e5a7d76a66e3 Mon Sep 17 00:00:00 2001 From: Fernando Ribeiro Date: Tue, 17 Mar 2026 19:38:32 -0300 Subject: [PATCH 2/3] feat: Implement WinCertInstaller application to download, validate, and install ITI and MPF certificates into the Windows Trusted Root Store. --- README.md | 55 ++++++++++++++++--- .../WinCertInstaller.Tests.csproj | 12 +++- WinCertInstaller/Configuration/AppSettings.cs | 10 ++++ WinCertInstaller/Models/CertSource.cs | 11 ++++ WinCertInstaller/Program.cs | 20 +++++-- .../Services/CertificateDownloader.cs | 35 ++++++++---- .../Services/CertificateInstaller.cs | 3 +- .../Services/CertificateValidator.cs | 2 +- .../Services/ICertificateDownloader.cs | 16 ++++++ .../Services/ICertificateInstaller.cs | 9 +++ .../Services/ICertificateValidator.cs | 14 +++++ WinCertInstaller/WinCertInstaller.csproj | 4 +- WinCertInstaller/app.manifest | 17 ++++++ 13 files changed, 176 insertions(+), 32 deletions(-) create mode 100644 WinCertInstaller/app.manifest diff --git a/README.md b/README.md index 8e091ff..67b1944 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,56 @@ # WinCertInstaller -This repository contains a c# application which install ITI and MPF certificates in Windows Trusted Root Store +[![.NET](https://github.com/fernandoribeiro/WinCertInstaller/actions/workflows/dotnet.yml/badge.svg)](https://github.com/fernandoribeiro/WinCertInstaller/actions/workflows/dotnet.yml) -*Needs Administrator privileges to run.* +This repository contains a C# (.NET 10) application which downloads and installs the official Root and Intermediate Certificates from **ITI (ICP-Brasil)** and **MPF** into the Windows Trusted Root Store. -*Use at your own risk.* +*⚠️ Needs **Administrator privileges** to run, as it writes directly to the `LocalMachine` X509 Store.* -## TODO +## Features -It makes sense to reconstruct a minimally-working original code +* **Automated Downloads**: Fetches the latest `.zip` (ITI) and `.p7b` (MPF) certificate bundles. +* **Smart Validation**: Validates expiration dates, ensures the certificate is active, and separates Self-Signed (Root) from Intermediate CAs. +* **Resilience**: Features automatic retries and cancellation tokens for network requests. +* **Idempotency**: Skips already installed certificates without throwing duplicate errors. +* **Dry-Run Mode**: Allows testing the extraction and validation process without writing anything to the OS registry. -What is missing right now: -* Better exceptions catch +## Architecture & SOLID +The project is structured into clear responsibilities: +* `Models/`: Data structures and Enums (e.g., `CertSource`). +* `Configuration/`: Constant settings and URLs. +* `Services/`: Core business logic broken down into: + * `ICertificateDownloader`: Handles HTTP streams and archive extractions. + * `ICertificateValidator`: Enforces cryptographic rules. + * `ICertificateInstaller`: Interfaces with the Windows `X509Store`. -*Feel free to make pull-requests :)* +## Usage + +You can run the application directly from the command line: + +```console +Usage: WinCertInstaller [options] +Options: + --iti Install certificates from ITI + --mpf Install certificates from MPF + --all Install certificates from ITI and MPF (default) + --dry-run Run without writing certificates to store + -q Quiet mode (no pause at exit) + -h,--help Show this help message +``` + +*Example: Testing ITI extraction without installing* +```powershell +WinCertInstaller.exe --iti --dry-run +``` + +## Building and Testing + +To compile the application and run its unit tests: + +```powershell +dotnet build WinCertInstaller\WinCertInstaller.csproj +dotnet test WinCertInstaller.Tests\WinCertInstaller.Tests.csproj +``` + +*Use at your own risk. Feel free to make pull-requests :)* diff --git a/WinCertInstaller.Tests/WinCertInstaller.Tests.csproj b/WinCertInstaller.Tests/WinCertInstaller.Tests.csproj index 9924ba7..d4e414b 100644 --- a/WinCertInstaller.Tests/WinCertInstaller.Tests.csproj +++ b/WinCertInstaller.Tests/WinCertInstaller.Tests.csproj @@ -8,10 +8,16 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/WinCertInstaller/Configuration/AppSettings.cs b/WinCertInstaller/Configuration/AppSettings.cs index 5944e43..b226bd5 100644 --- a/WinCertInstaller/Configuration/AppSettings.cs +++ b/WinCertInstaller/Configuration/AppSettings.cs @@ -1,8 +1,18 @@ namespace WinCertInstaller.Configuration { + /// + /// Application settings containing default URLs for certificate downloads. + /// public static class AppSettings { + /// + /// The URL to the ZIP file containing ITI (ICP-Brasil) certificates. + /// public const string ITICertUrl = "http://acraiz.icpbrasil.gov.br/credenciadas/CertificadosAC-ICP-Brasil/ACcompactado.zip"; + + /// + /// The URL to the P7B (PKCS #7) file containing MPF certificates. + /// public const string MPFCertUrl = "http://repositorio.acinterna.mpf.mp.br/ejbca/ra/downloads/ACIMPF-cadeia-completa.p7b"; } } diff --git a/WinCertInstaller/Models/CertSource.cs b/WinCertInstaller/Models/CertSource.cs index 6f181d9..9c56979 100644 --- a/WinCertInstaller/Models/CertSource.cs +++ b/WinCertInstaller/Models/CertSource.cs @@ -2,12 +2,23 @@ namespace WinCertInstaller.Models { + /// + /// Represents the sources from which certificates should be installed. + /// This enumeration supports bitwise combinations (Flags). + /// [Flags] public enum CertSource { + /// No source selected. None = 0, + + /// Installs certificates from ITI (Instituto Nacional de Tecnologia da Informação). ITI = 1, + + /// Installs certificates from MPF (Ministério Público Federal). MPF = 2, + + /// Installs certificates from all available sources. All = ITI | MPF } } diff --git a/WinCertInstaller/Program.cs b/WinCertInstaller/Program.cs index b9e1325..122ed60 100644 --- a/WinCertInstaller/Program.cs +++ b/WinCertInstaller/Program.cs @@ -137,12 +137,7 @@ static async Task Main(string[] args) Console.WriteLine("================================================="); Console.WriteLine("Finished!"); - if (!quiet) - { - Console.WriteLine("To run in quiet mode use -q parameter."); - Console.WriteLine("Press any key to exit."); - Console.ReadKey(true); - } + WaitForKeyPress(quiet); return 0; } @@ -150,13 +145,26 @@ static async Task Main(string[] args) { Console.WriteLine("Error: {0}", ex.Message); PrintUsage(); + WaitForKeyPress(quiet: false); return -1; } catch (Exception ex) { Console.WriteLine("Unexpected error: {0}", ex.Message); + WaitForKeyPress(quiet: false); return -1; } } + + static void WaitForKeyPress(bool quiet) + { + if (!quiet) + { + Console.WriteLine(); + Console.WriteLine("To run without this prompt use -q parameter."); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(true); + } + } } } diff --git a/WinCertInstaller/Services/CertificateDownloader.cs b/WinCertInstaller/Services/CertificateDownloader.cs index cedfc47..1237a9d 100644 --- a/WinCertInstaller/Services/CertificateDownloader.cs +++ b/WinCertInstaller/Services/CertificateDownloader.cs @@ -16,7 +16,7 @@ public class CertificateDownloader : ICertificateDownloader Timeout = TimeSpan.FromSeconds(30) }; - private async Task DownloadFileAsync(string url, CancellationToken cancellationToken = default, int maxAttempts = 3, TimeSpan? delayBetweenAttempts = null) + private async Task DownloadFileAsync(string url, CancellationToken cancellationToken = default, int maxAttempts = 3, TimeSpan? delayBetweenAttempts = null) { delayBetweenAttempts ??= TimeSpan.FromSeconds(2); @@ -26,7 +26,11 @@ public class CertificateDownloader : ICertificateDownloader { using var response = await HttpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken); response.EnsureSuccessStatusCode(); - return await response.Content.ReadAsStreamAsync(cancellationToken); + + var memoryStream = new MemoryStream(); + await response.Content.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + return memoryStream; } catch (OperationCanceledException) { @@ -53,8 +57,8 @@ public async Task GetZIPCertificatesAsync(string url { X509Certificate2Collection certCollection = new X509Certificate2Collection(); Console.WriteLine("Getting certificates from {0} please wait.", url); - - using Stream? stream = await DownloadFileAsync(url, cancellationToken); + + using MemoryStream? stream = await DownloadFileAsync(url, cancellationToken); if (stream != null) { @@ -90,16 +94,27 @@ public async Task GetP7BCertificatesAsync(string url { X509Certificate2Collection certCollection = new X509Certificate2Collection(); Console.WriteLine("Getting certificates from {0} please wait.", url); - - using Stream? stream = await DownloadFileAsync(url, cancellationToken); - + + using MemoryStream? stream = await DownloadFileAsync(url, cancellationToken); + if (stream != null) { - using MemoryStream ms = new MemoryStream(); - await stream.CopyToAsync(ms, cancellationToken); + byte[] rawBytes = stream.ToArray(); + string text = System.Text.Encoding.UTF8.GetString(rawBytes); + + // Check if the PKCS#7 is PEM encoded and decode it to raw DER format + if (text.Contains("-----BEGIN PKCS7-----")) + { + string base64 = text.Replace("-----BEGIN PKCS7-----", "") + .Replace("-----END PKCS7-----", "") + .Replace("\r", "") + .Replace("\n", "") + .Trim(); + rawBytes = Convert.FromBase64String(base64); + } var signedCms = new SignedCms(); - signedCms.Decode(ms.ToArray()); + signedCms.Decode(rawBytes); foreach (var cert in signedCms.Certificates) { diff --git a/WinCertInstaller/Services/CertificateInstaller.cs b/WinCertInstaller/Services/CertificateInstaller.cs index e80381a..449f52c 100644 --- a/WinCertInstaller/Services/CertificateInstaller.cs +++ b/WinCertInstaller/Services/CertificateInstaller.cs @@ -46,7 +46,6 @@ private void Add(X509Certificate2Collection certificates, StoreName storeName, S if (IsCertificateInStore(certificate, store)) { skipped++; - Console.WriteLine("Skipped already installed certificate: {0}", certificate.Subject); continue; } @@ -89,7 +88,7 @@ public void InstallCertificates(X509Certificate2Collection certificates, bool dr } else { - Console.WriteLine("{0} is not a CA. Ignoring.", cert.Subject); + // Ignora certificados que não são identificados como Autoridade Certificadora } } diff --git a/WinCertInstaller/Services/CertificateValidator.cs b/WinCertInstaller/Services/CertificateValidator.cs index 5349483..9746dd9 100644 --- a/WinCertInstaller/Services/CertificateValidator.cs +++ b/WinCertInstaller/Services/CertificateValidator.cs @@ -24,7 +24,7 @@ public bool IsCertificateValidForInstall(X509Certificate2 certificate) } public bool IsCertificateAuthority(X509Certificate2 certificate) - { + { return certificate.Extensions.OfType().Any(ext => ext.CertificateAuthority); } diff --git a/WinCertInstaller/Services/ICertificateDownloader.cs b/WinCertInstaller/Services/ICertificateDownloader.cs index adf62e4..7633a7f 100644 --- a/WinCertInstaller/Services/ICertificateDownloader.cs +++ b/WinCertInstaller/Services/ICertificateDownloader.cs @@ -4,9 +4,25 @@ namespace WinCertInstaller.Services { + /// + /// Service responsible for downloading and extracting certificates from remote sources. + /// public interface ICertificateDownloader { + /// + /// Downloads a ZIP archive and extracts all its certificates (.cer or .crt). + /// + /// The URL of the ZIP file. + /// Token to cancel the download operation. + /// A collection of extracted certificates. Task GetZIPCertificatesAsync(string url, CancellationToken cancellationToken = default); + + /// + /// Downloads a PKCS #7 (.p7b) file and extracts its certificates. + /// + /// The URL of the P7B file. + /// Token to cancel the download operation. + /// A collection of extracted certificates. Task GetP7BCertificatesAsync(string url, CancellationToken cancellationToken = default); } } diff --git a/WinCertInstaller/Services/ICertificateInstaller.cs b/WinCertInstaller/Services/ICertificateInstaller.cs index 5dbd6cb..7c73261 100644 --- a/WinCertInstaller/Services/ICertificateInstaller.cs +++ b/WinCertInstaller/Services/ICertificateInstaller.cs @@ -2,8 +2,17 @@ namespace WinCertInstaller.Services { + /// + /// Service that handles writing certificates into the Windows Local Machine X509 Store. + /// public interface ICertificateInstaller { + /// + /// Iterates through a collection and installs certificates into their respective Windows Store + /// (Root for self-signed CAs or CertificateAuthority for Intermediate CAs). + /// + /// The collection of certificates to evaluate and install. + /// If true, validates and logs actions without making actual changes to the machine store. void InstallCertificates(X509Certificate2Collection certificates, bool dryRun); } } diff --git a/WinCertInstaller/Services/ICertificateValidator.cs b/WinCertInstaller/Services/ICertificateValidator.cs index ce693e1..721c2df 100644 --- a/WinCertInstaller/Services/ICertificateValidator.cs +++ b/WinCertInstaller/Services/ICertificateValidator.cs @@ -2,10 +2,24 @@ namespace WinCertInstaller.Services { + /// + /// Validates X509 certificates ensuring they are valid for installation and determines their roles in a PKI. + /// public interface ICertificateValidator { + /// + /// Checks if a certificate is currently active and has not expired based on its NotBefore/NotAfter dates. + /// bool IsCertificateValidForInstall(X509Certificate2 certificate); + + /// + /// Verifies whether the provided certificate is a Certificate Authority (CA) via its Basic Constraints extension. + /// bool IsCertificateAuthority(X509Certificate2 certificate); + + /// + /// Scans if the certificate's Subject matches its Issuer, indicating it is a Self-Signed Root Certificate. + /// bool IsSelfSigned(X509Certificate2 certificate); } } diff --git a/WinCertInstaller/WinCertInstaller.csproj b/WinCertInstaller/WinCertInstaller.csproj index 550731b..4d5edf2 100644 --- a/WinCertInstaller/WinCertInstaller.csproj +++ b/WinCertInstaller/WinCertInstaller.csproj @@ -7,11 +7,11 @@ enable enable WinCertInstaller - WinCertInstaller false + app.manifest - + diff --git a/WinCertInstaller/app.manifest b/WinCertInstaller/app.manifest new file mode 100644 index 0000000..94dc0ec --- /dev/null +++ b/WinCertInstaller/app.manifest @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + From ca4fa7a32796353b0a4eb88e63d842c4fbe9913a Mon Sep 17 00:00:00 2001 From: Fernando Ribeiro Date: Tue, 17 Mar 2026 20:02:23 -0300 Subject: [PATCH 3/3] feat: Implement initial Windows Certificate Installer application with services for downloading, validating, and installing certificates. --- .github/workflows/dotnet.yml | 40 ++++++++ README.md | 91 +++++++++++++------ WinCertInstaller.Tests/ProgramTests.cs | 3 +- .../WinCertInstaller.Tests.csproj | 1 + WinCertInstaller/Configuration/AppSettings.cs | 9 +- .../Logging/CleanConsoleFormatter.cs | 45 +++++++++ WinCertInstaller/Program.cs | 59 ++++++++---- .../Services/CertificateDownloader.cs | 25 +++-- .../Services/CertificateInstaller.cs | 24 +++-- .../Services/CertificateValidator.cs | 11 ++- WinCertInstaller/WinCertInstaller.csproj | 4 + WinCertInstaller/appsettings.json | 13 +++ 12 files changed, 253 insertions(+), 72 deletions(-) create mode 100644 .github/workflows/dotnet.yml create mode 100644 WinCertInstaller/Logging/CleanConsoleFormatter.cs create mode 100644 WinCertInstaller/appsettings.json diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..8f88aa8 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,40 @@ +name: .NET Core Desktop Continuous Integration + +on: + push: + branches: [ "main", "master" ] + pull_request: + branches: [ "main", "master" ] + +jobs: + build: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + + - name: Restore dependencies + run: dotnet restore WinCertInstaller.sln + + - name: Build + run: dotnet build WinCertInstaller.sln --no-restore -c Release + + - name: Test + run: dotnet test WinCertInstaller.sln --no-build -c Release --verbosity normal + + - name: Publish WinCertInstaller + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' + run: dotnet publish WinCertInstaller/WinCertInstaller/WinCertInstaller.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o ./publish + + - name: Upload Artifact + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' + uses: actions/upload-artifact@v4 + with: + name: WinCertInstaller-Win64 + path: ./publish/WinCertInstaller.exe diff --git a/README.md b/README.md index 67b1944..3571b11 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,87 @@ -# WinCertInstaller +# WinCertInstaller 🛡️ [![.NET](https://github.com/fernandoribeiro/WinCertInstaller/actions/workflows/dotnet.yml/badge.svg)](https://github.com/fernandoribeiro/WinCertInstaller/actions/workflows/dotnet.yml) -This repository contains a C# (.NET 10) application which downloads and installs the official Root and Intermediate Certificates from **ITI (ICP-Brasil)** and **MPF** into the Windows Trusted Root Store. +WinCertInstaller is an enterprise-grade C# (.NET 10) utility designed to automate the download and installation of official Root and Intermediate Certificates from **ITI (ICP-Brasil)** and **MPF** into the Windows Certificate Store. -*⚠️ Needs **Administrator privileges** to run, as it writes directly to the `LocalMachine` X509 Store.* +> [!IMPORTANT] +> **Administrator Privileges Required**: This application requires elevated privileges to write certificates to the `LocalMachine` X509 store. It includes an `app.manifest` to automatically request UAC elevation if not already running as Administrator. -## Features +## 🚀 Key Features -* **Automated Downloads**: Fetches the latest `.zip` (ITI) and `.p7b` (MPF) certificate bundles. -* **Smart Validation**: Validates expiration dates, ensures the certificate is active, and separates Self-Signed (Root) from Intermediate CAs. -* **Resilience**: Features automatic retries and cancellation tokens for network requests. -* **Idempotency**: Skips already installed certificates without throwing duplicate errors. -* **Dry-Run Mode**: Allows testing the extraction and validation process without writing anything to the OS registry. +* **Automated Certificate Fetching**: Downloads the latest `.zip` (ITI) and `.p7b` (MPF) bundles directly from official repositories. +* **Robust Decoding**: Handles various formats, including **PEM-encoded PKCS#7** payloads (MPF) and nested ZIP archives (ITI). +* **Intelligent Validation**: + * Filters for Certificate Authorities (CA). + * Distinguishes between Root CAs (installed in `Trusted Root`) and Intermediate CAs (installed in `Intermediate Certification Authorities`). + * Checks for expiration and activation dates. +* **Idempotency**: Detects and skips certificates already present in the store to avoid duplication. +* **Modern CLI Experience**: Features clean, color-coded console output using a custom `ILogger` formatter. +* **Dry-Run Mode**: Validate the entire download and extraction process without modifying the system state. -## Architecture & SOLID +## 🏗️ Architecture (SOLID & Enterprise) -The project is structured into clear responsibilities: -* `Models/`: Data structures and Enums (e.g., `CertSource`). -* `Configuration/`: Constant settings and URLs. -* `Services/`: Core business logic broken down into: - * `ICertificateDownloader`: Handles HTTP streams and archive extractions. - * `ICertificateValidator`: Enforces cryptographic rules. - * `ICertificateInstaller`: Interfaces with the Windows `X509Store`. +The application has been refactored to follow modern .NET best practices: +* **Microsoft.Extensions.Hosting**: Uses the Generic Host pattern for dependency injection, logging, and configuration management. +* **Dynamic Configuration**: All certificate URLs and settings are managed via `appsettings.json`. +* **Structured Logging**: Utilizes `ILogger` for clean, maintainable logging that can be easily redirected to cloud providers or files. +* **Dependency Injection**: Decoupled services for Downloading, Validation, and Installation, making the codebase highly testable and maintainable. -## Usage +## 💻 Compatibility -You can run the application directly from the command line: +* **Runtime**: .NET 10.0+ (Windows-specific payload). +* **Operating Systems**: + * **Windows 10 / 11** (Fully supported, native target). + * **Windows Server 2016 / 2019 / 2022**. + * *Note: Requires administrator access for LocalMachine store operations.* + +## 🛠️ Usage ```console Usage: WinCertInstaller [options] + Options: - --iti Install certificates from ITI - --mpf Install certificates from MPF - --all Install certificates from ITI and MPF (default) - --dry-run Run without writing certificates to store - -q Quiet mode (no pause at exit) + --iti Install ITI certificates + --mpf Install MPF certificates + --all Install both ITI and MPF certificates (default) + --dry-run Simulate installation without writing to the store + -q Quiet mode (suppress exit prompt) -h,--help Show this help message ``` -*Example: Testing ITI extraction without installing* +### Examples + +**Standard Installation (All Sources):** +```powershell +WinCertInstaller.exe +``` + +**Dry-Run of ITI Source:** ```powershell WinCertInstaller.exe --iti --dry-run ``` -## Building and Testing +**Quiet Installation (Script Mode):** +```powershell +WinCertInstaller.exe --all -q +``` + +## 🧪 Development -To compile the application and run its unit tests: +### Configuration +Edit `appsettings.json` to update certificate URLs or tune logging behavior without recompiling. +### Build & Test ```powershell -dotnet build WinCertInstaller\WinCertInstaller.csproj -dotnet test WinCertInstaller.Tests\WinCertInstaller.Tests.csproj +# Restore and build the solution +dotnet build WinCertInstaller.sln + +# Run unit tests +dotnet test WinCertInstaller.sln ``` -*Use at your own risk. Feel free to make pull-requests :)* +### CI/CD +A GitHub Actions workflow is included (`.github/workflows/dotnet.yml`) that automatically builds, tests, and publishes a self-contained, single-file executable on every push to `main`/`master`. + +--- +*Maintained with ❤️ for secure Windows environments.* diff --git a/WinCertInstaller.Tests/ProgramTests.cs b/WinCertInstaller.Tests/ProgramTests.cs index 7baf3d6..633a1d0 100644 --- a/WinCertInstaller.Tests/ProgramTests.cs +++ b/WinCertInstaller.Tests/ProgramTests.cs @@ -4,6 +4,7 @@ using Xunit; using WinCertInstaller.Models; using WinCertInstaller.Services; +using Microsoft.Extensions.Logging.Abstractions; namespace WinCertInstaller.Tests { @@ -55,7 +56,7 @@ public void IsCertificateAuthorityAndSelfSigned_TrueForSelfSignedCA() request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false)); using var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(365)); - var validator = new CertificateValidator(); + var validator = new CertificateValidator(NullLogger.Instance); Assert.True(validator.IsCertificateAuthority(certificate)); Assert.True(validator.IsSelfSigned(certificate)); diff --git a/WinCertInstaller.Tests/WinCertInstaller.Tests.csproj b/WinCertInstaller.Tests/WinCertInstaller.Tests.csproj index d4e414b..dfb5ecd 100644 --- a/WinCertInstaller.Tests/WinCertInstaller.Tests.csproj +++ b/WinCertInstaller.Tests/WinCertInstaller.Tests.csproj @@ -13,6 +13,7 @@ all + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/WinCertInstaller/Configuration/AppSettings.cs b/WinCertInstaller/Configuration/AppSettings.cs index b226bd5..4817c5d 100644 --- a/WinCertInstaller/Configuration/AppSettings.cs +++ b/WinCertInstaller/Configuration/AppSettings.cs @@ -2,17 +2,20 @@ namespace WinCertInstaller.Configuration { /// /// Application settings containing default URLs for certificate downloads. + /// Mapped directly from appsettings.json via IOptions. /// - public static class AppSettings + public class AppSettings { + public const string Position = "CertificateSources"; + /// /// The URL to the ZIP file containing ITI (ICP-Brasil) certificates. /// - public const string ITICertUrl = "http://acraiz.icpbrasil.gov.br/credenciadas/CertificadosAC-ICP-Brasil/ACcompactado.zip"; + public string ITICertUrl { get; set; } = string.Empty; /// /// The URL to the P7B (PKCS #7) file containing MPF certificates. /// - public const string MPFCertUrl = "http://repositorio.acinterna.mpf.mp.br/ejbca/ra/downloads/ACIMPF-cadeia-completa.p7b"; + public string MPFCertUrl { get; set; } = string.Empty; } } diff --git a/WinCertInstaller/Logging/CleanConsoleFormatter.cs b/WinCertInstaller/Logging/CleanConsoleFormatter.cs new file mode 100644 index 0000000..d953341 --- /dev/null +++ b/WinCertInstaller/Logging/CleanConsoleFormatter.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging.Console; + +namespace WinCertInstaller.Logging +{ + public class CleanConsoleFormatter : ConsoleFormatter + { + public CleanConsoleFormatter() : base("CleanLayout") { } + + public override void Write(in LogEntry logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter) + { + string message = logEntry.Formatter(logEntry.State, logEntry.Exception); + + if (string.IsNullOrEmpty(message)) return; + + // Define cores baseadas no nível de log + var color = logEntry.LogLevel switch + { + LogLevel.Error => "\u001b[31m", // Red + LogLevel.Warning => "\u001b[33m", // Yellow + LogLevel.Information => "", // Default + _ => "" + }; + + var reset = "\u001b[0m"; + + if (logEntry.LogLevel >= LogLevel.Warning) + { + textWriter.WriteLine($"{color}{logEntry.LogLevel.ToString().ToUpper()}: {message}{reset}"); + } + else + { + textWriter.WriteLine(message); + } + + if (logEntry.Exception != null) + { + textWriter.WriteLine(logEntry.Exception.ToString()); + } + } + } +} diff --git a/WinCertInstaller/Program.cs b/WinCertInstaller/Program.cs index 122ed60..dbda2d2 100644 --- a/WinCertInstaller/Program.cs +++ b/WinCertInstaller/Program.cs @@ -5,6 +5,12 @@ using WinCertInstaller.Models; using WinCertInstaller.Configuration; using WinCertInstaller.Services; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; +using Microsoft.Extensions.Options; +using WinCertInstaller.Logging; namespace WinCertInstaller { @@ -14,11 +20,11 @@ static void PrintUsage() { Console.WriteLine("Usage: WinCertInstaller [options]"); Console.WriteLine("Options:"); - Console.WriteLine(" --iti Install certificates from ITI"); - Console.WriteLine(" --mpf Install certificates from MPF"); - Console.WriteLine(" --all Install certificates from ITI and MPF (default)"); - Console.WriteLine(" --dry-run Run without writing certificates to store"); - Console.WriteLine(" -q Quiet mode (no pause at exit)"); + Console.WriteLine(" --iti Install ITI certificates"); + Console.WriteLine(" --mpf Install MPF certificates"); + Console.WriteLine(" --all Install both ITI and MPF certificates (default)"); + Console.WriteLine(" --dry-run Simulate installation without writing to the store"); + Console.WriteLine(" -q Quiet mode (suppress exit prompt)"); Console.WriteLine(" -h,--help Show this help message"); Console.WriteLine("Example: WinCertInstaller --iti --dry-run"); } @@ -104,18 +110,35 @@ static async Task Main(string[] args) if (dryRun) { - Console.WriteLine("Dry run mode enabled: no certificates will be added to the store."); + Console.WriteLine("Dry run enabled: No changes will be made to the certificate store."); } - // Dependency Injection Composition Root - ICertificateDownloader downloader = new CertificateDownloader(); - ICertificateValidator validator = new CertificateValidator(); - ICertificateInstaller installer = new CertificateInstaller(validator); + var host = Host.CreateDefaultBuilder(args) + .ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.AddConsole(options => options.FormatterName = "CleanLayout") + .AddConsoleFormatter(); + }) + .ConfigureServices((context, services) => + { + services.Configure(context.Configuration.GetSection(AppSettings.Position)); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + }) + .Build(); + + var downloader = host.Services.GetRequiredService(); + var validator = host.Services.GetRequiredService(); + var installer = host.Services.GetRequiredService(); + var appSettings = host.Services.GetRequiredService>().Value; + var logger = host.Services.GetRequiredService>(); if (selectedSources.HasFlag(CertSource.ITI)) { - Console.WriteLine("====================== ITI ======================"); - X509Certificate2Collection itiCertificates = await downloader.GetZIPCertificatesAsync(AppSettings.ITICertUrl, cts.Token); + logger.LogInformation("====================== ITI ======================"); + X509Certificate2Collection itiCertificates = await downloader.GetZIPCertificatesAsync(appSettings.ITICertUrl, cts.Token); if (itiCertificates.Count > 0) { installer.InstallCertificates(itiCertificates, dryRun); @@ -125,8 +148,8 @@ static async Task Main(string[] args) if (selectedSources.HasFlag(CertSource.MPF)) { - Console.WriteLine("====================== MPF ======================"); - X509Certificate2Collection mpfCertificates = await downloader.GetP7BCertificatesAsync(AppSettings.MPFCertUrl, cts.Token); + logger.LogInformation("====================== MPF ======================"); + X509Certificate2Collection mpfCertificates = await downloader.GetP7BCertificatesAsync(appSettings.MPFCertUrl, cts.Token); if (mpfCertificates.Count > 0) { installer.InstallCertificates(mpfCertificates, dryRun); @@ -134,8 +157,8 @@ static async Task Main(string[] args) } } - Console.WriteLine("================================================="); - Console.WriteLine("Finished!"); + logger.LogInformation("================================================="); + logger.LogInformation("Installation process completed."); WaitForKeyPress(quiet); @@ -161,8 +184,8 @@ static void WaitForKeyPress(bool quiet) if (!quiet) { Console.WriteLine(); - Console.WriteLine("To run without this prompt use -q parameter."); - Console.WriteLine("Press any key to exit."); + Console.WriteLine("Use the -q parameter to run without this prompt."); + Console.WriteLine("Press any key to exit..."); Console.ReadKey(true); } } diff --git a/WinCertInstaller/Services/CertificateDownloader.cs b/WinCertInstaller/Services/CertificateDownloader.cs index 1237a9d..dc0fe85 100644 --- a/WinCertInstaller/Services/CertificateDownloader.cs +++ b/WinCertInstaller/Services/CertificateDownloader.cs @@ -7,15 +7,23 @@ using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + namespace WinCertInstaller.Services { public class CertificateDownloader : ICertificateDownloader { + private readonly ILogger _logger; private static readonly HttpClient HttpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(30) }; + public CertificateDownloader(ILogger logger) + { + _logger = logger; + } + private async Task DownloadFileAsync(string url, CancellationToken cancellationToken = default, int maxAttempts = 3, TimeSpan? delayBetweenAttempts = null) { delayBetweenAttempts ??= TimeSpan.FromSeconds(2); @@ -34,19 +42,18 @@ public class CertificateDownloader : ICertificateDownloader } catch (OperationCanceledException) { - Console.WriteLine("Download canceled."); + _logger.LogInformation("Download canceled."); return null; } catch (Exception ex) when (attempt < maxAttempts) { if (cancellationToken.IsCancellationRequested) return null; - Console.WriteLine("WARNING: attempt {0} failed for {1}: {2}", attempt, url, ex.Message); + _logger.LogWarning(ex, "Attempt {Attempt} failed for {Url}: {Message}", attempt, url, ex.Message); await Task.Delay(delayBetweenAttempts.Value, cancellationToken); } catch (Exception ex) { - Console.WriteLine("ERROR: Unable to download certificates from {0} after {1} attempts.", url, attempt); - Console.WriteLine("ERROR: {0}", ex.Message); + _logger.LogError(ex, "Unable to download certificates from {Url} after {Attempt} attempts. Error: {Message}", url, attempt, ex.Message); } } @@ -56,7 +63,7 @@ public class CertificateDownloader : ICertificateDownloader public async Task GetZIPCertificatesAsync(string url, CancellationToken cancellationToken = default) { X509Certificate2Collection certCollection = new X509Certificate2Collection(); - Console.WriteLine("Getting certificates from {0} please wait.", url); + _logger.LogInformation("Downloading certificates from {Url}. Please wait...", url); using MemoryStream? stream = await DownloadFileAsync(url, cancellationToken); @@ -82,10 +89,10 @@ public async Task GetZIPCertificatesAsync(string url } catch (Exception ex) { - Console.WriteLine("WARNING: Failed to load certificate from zip entry {0}: {1}", certificate.FullName, ex.Message); + _logger.LogWarning(ex, "Failed to load certificate from zip entry {FullName}: {Message}", certificate.FullName, ex.Message); } } - Console.WriteLine("{0} certificates found.", certCollection.Count); + _logger.LogInformation("Found {Count} certificate(s) in ZIP archive.", certCollection.Count); } return certCollection; } @@ -93,7 +100,7 @@ public async Task GetZIPCertificatesAsync(string url public async Task GetP7BCertificatesAsync(string url, CancellationToken cancellationToken = default) { X509Certificate2Collection certCollection = new X509Certificate2Collection(); - Console.WriteLine("Getting certificates from {0} please wait.", url); + _logger.LogInformation("Downloading certificates from {Url}. Please wait...", url); using MemoryStream? stream = await DownloadFileAsync(url, cancellationToken); @@ -121,7 +128,7 @@ public async Task GetP7BCertificatesAsync(string url certCollection.Add(cert); } - Console.WriteLine("{0} certificates found.", certCollection.Count); + _logger.LogInformation("Extracted {Count} certificate(s) from P7B/PKCS7 payload.", certCollection.Count); } return certCollection; } diff --git a/WinCertInstaller/Services/CertificateInstaller.cs b/WinCertInstaller/Services/CertificateInstaller.cs index 449f52c..b037b16 100644 --- a/WinCertInstaller/Services/CertificateInstaller.cs +++ b/WinCertInstaller/Services/CertificateInstaller.cs @@ -1,15 +1,19 @@ using System; using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Logging; + namespace WinCertInstaller.Services { public class CertificateInstaller : ICertificateInstaller { private readonly ICertificateValidator _validator; + private readonly ILogger _logger; - public CertificateInstaller(ICertificateValidator validator) + public CertificateInstaller(ICertificateValidator validator, ILogger logger) { _validator = validator; + _logger = logger; } private bool IsCertificateInStore(X509Certificate2 certificate, X509Store store) @@ -29,7 +33,7 @@ private void Add(X509Certificate2Collection certificates, StoreName storeName, S using var store = new X509Store(storeName, location); store.Open(dryRun ? OpenFlags.ReadOnly : OpenFlags.ReadWrite); - Console.WriteLine("Installing certificates into {0}.", storeName); + _logger.LogInformation("Installing certificates into the {StoreName} store...", storeName); int added = 0; int skipped = 0; @@ -51,17 +55,19 @@ private void Add(X509Certificate2Collection certificates, StoreName storeName, S if (dryRun) { + _logger.LogInformation("Dry-run: Would add {Subject} to {StoreName}", certificate.Subject, storeName); added++; continue; } store.Add(certificate); + _logger.LogInformation("Added: {Subject} to {StoreName}", certificate.Subject, storeName); added++; } - Console.WriteLine("Added {0} certificates to {1}.", added, storeName); - if (skipped > 0) Console.WriteLine("Skipped {0} already installed certificate(s).", skipped); - if (invalid > 0) Console.WriteLine("Ignored {0} invalid certificate(s).", invalid); + _logger.LogInformation("Successfully processed {Added} certificate(s) for {StoreName}.", added, storeName); + if (skipped > 0) _logger.LogInformation("Skipped {Skipped} certificate(s) already present in the store.", skipped); + if (invalid > 0) _logger.LogWarning("Ignored {Invalid} invalid or ineligible certificate(s).", invalid); store.Close(); } @@ -100,7 +106,7 @@ public void InstallCertificates(X509Certificate2Collection certificates, bool dr } else { - Console.WriteLine("No CA certificates to import."); + _logger.LogInformation("No CA certificates to import."); } if (caIntermediateCertificates.Count > 0) @@ -109,13 +115,13 @@ public void InstallCertificates(X509Certificate2Collection certificates, bool dr } else { - Console.WriteLine("No Intermediate CA certificates to import."); + _logger.LogInformation("No Intermediate CA certificates to import."); } } catch (System.Security.Cryptography.CryptographicException ex) { - Console.WriteLine("Erro ao acessar os repositórios do Windows: {0}", ex.Message); - Console.WriteLine("DICA: Certifique-se de executar este programa como 'Administrador' para instalar certificados na máquina local."); + _logger.LogError(ex, "Error accessing Windows certificate stores: {Message}", ex.Message); + _logger.LogWarning("HINT: Ensure you are running this application as 'Administrator' to manage LocalMachine certificates."); } } } diff --git a/WinCertInstaller/Services/CertificateValidator.cs b/WinCertInstaller/Services/CertificateValidator.cs index 9746dd9..aade6c1 100644 --- a/WinCertInstaller/Services/CertificateValidator.cs +++ b/WinCertInstaller/Services/CertificateValidator.cs @@ -1,22 +1,29 @@ using System; using System.Linq; using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Logging; namespace WinCertInstaller.Services { public class CertificateValidator : ICertificateValidator { + private readonly ILogger _logger; + + public CertificateValidator(ILogger logger) + { + _logger = logger; + } public bool IsCertificateValidForInstall(X509Certificate2 certificate) { if (certificate.NotBefore > DateTime.UtcNow) { - Console.WriteLine("Certificate {0} not active yet. NotBefore={1}", certificate.Subject, certificate.NotBefore); + _logger.LogWarning("Certificate {Subject} not active yet. NotBefore={NotBefore}", certificate.Subject, certificate.NotBefore); return false; } if (certificate.NotAfter < DateTime.UtcNow) { - Console.WriteLine("Certificate {0} expired. NotAfter={1}", certificate.Subject, certificate.NotAfter); + _logger.LogWarning("Certificate {Subject} expired. NotAfter={NotAfter}", certificate.Subject, certificate.NotAfter); return false; } diff --git a/WinCertInstaller/WinCertInstaller.csproj b/WinCertInstaller/WinCertInstaller.csproj index 4d5edf2..7f9a51f 100644 --- a/WinCertInstaller/WinCertInstaller.csproj +++ b/WinCertInstaller/WinCertInstaller.csproj @@ -11,10 +11,14 @@ app.manifest + + + PreserveNewest + \ No newline at end of file diff --git a/WinCertInstaller/appsettings.json b/WinCertInstaller/appsettings.json new file mode 100644 index 0000000..522d697 --- /dev/null +++ b/WinCertInstaller/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "CertificateSources": { + "ITICertUrl": "http://acraiz.icpbrasil.gov.br/credenciadas/CertificadosAC-ICP-Brasil/ACcompactado.zip", + "MPFCertUrl": "http://repositorio.acinterna.mpf.mp.br/ejbca/ra/downloads/ACIMPF-cadeia-completa.p7b" + } +}