diff --git a/.gitattributes b/.gitattributes index 1ff0c42..9efc26c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,63 +1,18 @@ -############################################################################### -# Set default behavior to automatically normalize line endings. -############################################################################### +# Auto-normalize line endings * text=auto -############################################################################### -# Set default behavior for command prompt diff. -# -# This is need for earlier builds of msysgit that does not have it on by -# default for csharp files. -# Note: This is only used by command line -############################################################################### -#*.cs diff=csharp +# Ensure PowerShell scripts use CRLF (standard for Windows) +*.ps1 text eol=crlf +*.psm1 text eol=crlf +*.psd1 text eol=crlf -############################################################################### -# Set the merge driver for project and solution files -# -# Merging from the command prompt will add diff markers to the files if there -# are conflicts (Merging from VS is not affected by the settings below, in VS -# the diff markers are never inserted). Diff markers may cause the following -# file extensions to fail to load in VS. An alternative would be to treat -# these files as binary and thus will always conflict and require user -# intervention with every merge. To do so, just uncomment the entries below -############################################################################### -#*.sln merge=binary -#*.csproj merge=binary -#*.vbproj merge=binary -#*.vcxproj merge=binary -#*.vcproj merge=binary -#*.dbproj merge=binary -#*.fsproj merge=binary -#*.lsproj merge=binary -#*.wixproj merge=binary -#*.modelproj merge=binary -#*.sqlproj merge=binary -#*.wwaproj merge=binary +# Common text formats +*.md text +*.yml text +*.json text -############################################################################### -# behavior for image files -# -# image files are treated as binary by default. -############################################################################### -#*.jpg binary -#*.png binary -#*.gif binary - -############################################################################### -# diff behavior for common document formats -# -# Convert binary document formats to text before diffing them. This feature -# is only available from the command line. Turn it on by uncommenting the -# entries below. -############################################################################### -#*.doc diff=astextplain -#*.DOC diff=astextplain -#*.docx diff=astextplain -#*.DOCX diff=astextplain -#*.dot diff=astextplain -#*.DOT diff=astextplain -#*.pdf diff=astextplain -#*.PDF diff=astextplain -#*.rtf diff=astextplain -#*.RTF diff=astextplain +# Mark binary files +*.zip binary +*.p7b binary +*.cer binary +*.crt binary diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml deleted file mode 100644 index 3ade5de..0000000 --- a/.github/workflows/dotnet.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: .NET Core Desktop Continuous Integration - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -on: - push: - branches: [ "main", "master" ] - tags: [ "v*.*.*" ] - pull_request: - branches: [ "main", "master" ] - -jobs: - build: - - runs-on: windows-latest - - steps: - - uses: actions/checkout@v5 - - - name: Setup .NET - uses: actions/setup-dotnet@v5 - 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' || startsWith(github.ref, 'refs/tags/') - run: dotnet publish WinCertInstaller/WinCertInstaller.csproj -c Release -r win-x64 -o ./publish - - - name: Upload Artifact - if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' - uses: actions/upload-artifact@v6 - with: - name: WinCertInstaller-Win64 - path: ./publish/ - - - name: Create GitHub Release - if: startsWith(github.ref, 'refs/tags/') - shell: pwsh - run: | - $tag = $env:GITHUB_REF -replace 'refs/tags/', '' - gh release create $tag ./publish/WinCertInstaller.exe --title "Release $tag" --notes "Automated release for version $tag" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/powershell.yml b/.github/workflows/powershell.yml new file mode 100644 index 0000000..c61f526 --- /dev/null +++ b/.github/workflows/powershell.yml @@ -0,0 +1,29 @@ +name: PowerShell CI + +on: + push: + branches: [ "main", "master" ] + pull_request: + branches: [ "main", "master" ] + +jobs: + lint: + name: Lint PowerShell Script + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run PSScriptAnalyzer + uses: microsoft/psscriptanalyzer-action@v1.1 + with: + path: .\wincertinstall.ps1 + recurse: true + + test: + name: Test (Dry-Run) + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - name: Run Script Dry-Run + shell: powershell + run: | + .\wincertinstall.ps1 -DryRun diff --git a/.gitignore b/.gitignore index 3c4efe2..c4ce83b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,261 +1,27 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -project.fragment.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -#*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config -# NuGet v3's project.json files produces more ignoreable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc \ No newline at end of file +# Windows temporary files +[Tt]humbs.db +desktop.ini + +# PowerShell temporary/output files +*.zip +*.p7b +ITI_Certs/ +ACIMPF_Certs/ + +# IDE files +.vs/ +.vscode/ +.idea/ +*.suo +*.user +*.swp + +# Logs +*.log + +# Project artifacts (if any remained from Go/C#) +WinCertInstaller.exe +debug/ +release/ +bin/ +obj/ \ No newline at end of file diff --git a/README.md b/README.md index 3571b11..1c81007 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,66 @@ # WinCertInstaller 🛡️ -[![.NET](https://github.com/fernandoribeiro/WinCertInstaller/actions/workflows/dotnet.yml/badge.svg)](https://github.com/fernandoribeiro/WinCertInstaller/actions/workflows/dotnet.yml) +WinCertInstaller is a native PowerShell 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. -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. +It was developed to be lightweight (only **~6 KB**) and requires no compilation or external dependencies, leveraging native Windows APIs and the .NET framework already built into PowerShell. > [!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. +> **Administrator Privileges Required**: This script requires elevated privileges to write certificates to the `LocalMachine` X509 store. Run your PowerShell terminal as Administrator. ## 🚀 Key Features +* **Ultra-Lightweight**: Only a few KB of native PowerShell code. * **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 & Enterprise) - -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. - -## 💻 Compatibility - -* **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.* +* **Data Integrity (SHA512)**: Verifies both ITI and MPF certificate bundles against their official SHA512 hash files before processing. +* **Enterprise-Ready Robustness**: + * **Audit Logging**: Automatically records all operations (with timestamps and severity) to `%ProgramData%\WinCertInstaller\install.log` for post-deployment auditing. + * **Security**: Forces **TLS 1.2+** for all downloads. + * **Pre-emptive Admin Check**: Validates permissions before starting long operations. + * **Resource Management**: Uses `.Dispose()` in `finally` blocks to ensure system resources are freed. + * **Performance**: Optimized memory usage using captured loops instead of array re-allocation. + * **Error Resilience**: Comprehensive `try-catch` blocks for network and extraction failures. +* **Highly Configurable**: Official repository URLs are available as parameters, making the script future-proof. +* **Intelligent Store Detection**: Automatically distinguishes between Root CAs and Intermediate CAs. +* **Idempotency & Reinstallation**: Detects if a certificate is missing from its correct store and reinstalls it even if it exists in another store. +* **Force Install**: Option to force reinstallation of all certificates regardless of their current status. ## 🛠️ Usage -```console -Usage: WinCertInstaller [options] - -Options: - --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 +```powershell +.\wincertinstall.ps1 [options] ``` +### Options +* `-Iti`: Install ITI certificates. +* `-Mpf`: Install MPF certificates. +* `-All`: Install both ITI and MPF certificates (default). +* `-DryRun`: Simulate installation without modifying the store. +* `-ForceInstall`: Force installation of all certificates, ignoring existing ones. + ### Examples **Standard Installation (All Sources):** ```powershell -WinCertInstaller.exe +.\wincertinstall.ps1 ``` -**Dry-Run of ITI Source:** +**Force Reinstallation of everything:** ```powershell -WinCertInstaller.exe --iti --dry-run +.\wincertinstall.ps1 -All -ForceInstall ``` -**Quiet Installation (Script Mode):** +**Dry-Run (Simulation) of ITI Source:** ```powershell -WinCertInstaller.exe --all -q +.\wincertinstall.ps1 -Iti -DryRun ``` -## 🧪 Development - -### Configuration -Edit `appsettings.json` to update certificate URLs or tune logging behavior without recompiling. - -### Build & Test -```powershell -# Restore and build the solution -dotnet build WinCertInstaller.sln - -# Run unit tests -dotnet test WinCertInstaller.sln -``` +## 💻 Compatibility -### 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`. +* **PowerShell Versions**: Optimized for **PowerShell 5.1** (Windows default) and **PowerShell Core (6+)**. +* **Operating Systems**: + * Windows 10 / 11. + * Windows Server 2016 / 2019 / 2022. + * *Note: Requires administrator access for LocalMachine store operations.* --- *Maintained with ❤️ for secure Windows environments.* diff --git a/WinCertInstaller.Tests/ProgramTests.cs b/WinCertInstaller.Tests/ProgramTests.cs deleted file mode 100644 index eade8aa..0000000 --- a/WinCertInstaller.Tests/ProgramTests.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using Xunit; -using WinCertInstaller.Models; -using WinCertInstaller.Services; -using WinCertInstaller.Logging; - -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(new SimpleLogger()); - - Assert.True(validator.IsCertificateAuthority(certificate)); - Assert.True(validator.IsSelfSigned(certificate)); - } - } -} diff --git a/WinCertInstaller.Tests/WinCertInstaller.Tests.csproj b/WinCertInstaller.Tests/WinCertInstaller.Tests.csproj deleted file mode 100644 index dfb5ecd..0000000 --- a/WinCertInstaller.Tests/WinCertInstaller.Tests.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - - net10.0-windows7.0 - enable - enable - false - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - \ No newline at end of file diff --git a/WinCertInstaller.sln b/WinCertInstaller.sln deleted file mode 100644 index bc2cd24..0000000 --- a/WinCertInstaller.sln +++ /dev/null @@ -1,56 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27703.2018 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinCertInstaller", "WinCertInstaller\WinCertInstaller.csproj", "{C22FA746-778E-4806-BD82-F4E0FCE53763}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F9C2C0C5-A048-4FFC-BB36-EEADC2EF47B3}" - ProjectSection(SolutionItems) = preProject - 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 - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {91DC4A30-06F1-436C-A7FF-FE25C6FDE50E} - EndGlobalSection -EndGlobal diff --git a/WinCertInstaller/Configuration/AppSettings.cs b/WinCertInstaller/Configuration/AppSettings.cs deleted file mode 100644 index a2c453f..0000000 --- a/WinCertInstaller/Configuration/AppSettings.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace WinCertInstaller.Configuration -{ - /// - /// Application settings containing default URLs for certificate downloads. - /// Mapped directly from appsettings.json via IOptions. - /// - using System.Diagnostics.CodeAnalysis; - - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] - public class AppSettings - { - public const string Position = "CertificateSources"; - - /// - /// The URL to the ZIP file containing ITI (ICP-Brasil) certificates. - /// - public string ITICertUrl { get; set; } = "http://acraiz.icpbrasil.gov.br/credenciadas/CertificadosAC-ICP-Brasil/ACcompactado.zip"; - - /// - /// The URL to the P7B (PKCS #7) file containing MPF certificates. - /// - public string MPFCertUrl { get; set; } = "http://repositorio.acinterna.mpf.mp.br/ejbca/ra/downloads/ACIMPF-cadeia-completa.p7b"; - } -} diff --git a/WinCertInstaller/Configuration/AppSettingsJsonContext.cs b/WinCertInstaller/Configuration/AppSettingsJsonContext.cs deleted file mode 100644 index f298d7b..0000000 --- a/WinCertInstaller/Configuration/AppSettingsJsonContext.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; -using WinCertInstaller.Configuration; - -namespace WinCertInstaller.Configuration -{ - [JsonSerializable(typeof(AppSettings))] - internal partial class AppSettingsJsonContext : JsonSerializerContext - { - } -} diff --git a/WinCertInstaller/Logging/SimpleLogger.cs b/WinCertInstaller/Logging/SimpleLogger.cs deleted file mode 100644 index 446643b..0000000 --- a/WinCertInstaller/Logging/SimpleLogger.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; - -namespace WinCertInstaller.Logging -{ - /// - /// A lightweight logger that mimics the basic functionality of ILogger - /// without the overhead of the Microsoft.Extensions.Logging infrastructure. - /// - public class SimpleLogger - { - private readonly string _categoryName; - - public SimpleLogger() - { - _categoryName = typeof(T).Name; - } - - public void LogInformation(string message, params object?[] args) - { - WriteLog("INFO", message, args); - } - - public void LogWarning(string message, params object?[] args) - { - Console.ForegroundColor = ConsoleColor.Yellow; - WriteLog("WARN", message, args); - Console.ResetColor(); - } - - public void LogWarning(Exception ex, string message, params object?[] args) - { - LogWarning(message + " Error: " + ex.Message, args); - } - - public void LogError(string message, params object?[] args) - { - Console.ForegroundColor = ConsoleColor.Red; - WriteLog("ERROR", message, args); - Console.ResetColor(); - } - - public void LogError(Exception ex, string message, params object?[] args) - { - LogError(message + " Error: " + ex.Message, args); - if (ex.StackTrace != null) - { - Console.WriteLine(ex.StackTrace); - } - } - - private void WriteLog(string level, string message, object?[] args) - { - string formattedMessage = args.Length > 0 ? string.Format(message.Replace("{", "{{").Replace("}", "}}"), args) : message; - - // Simple approach to handle .NET style curly braces in ILogger - // Since we want to be fast and light, we just do a basic replace for the common cases - for (int i = 0; i < args.Length; i++) - { - string placeholder = "{" + i + "}"; - if (message.Contains(placeholder)) continue; // Already formatted by string.Format logic above? - // The original code used named placeholders like {Url}. We'll try a regex-free approach. - } - - // For simplicity in this lightweight version, we'll just print the message as is if it matches - // or use string.Format if indices are used. - // Most of our calls used log.LogInformation("Message {Arg}", arg). - - string output = formattedMessage; - - // Re-implementing a very basic named placeholder replacement - if (args != null && args.Length > 0) - { - int argIndex = 0; - while (output.Contains("{") && output.Contains("}") && argIndex < args.Length) - { - int start = output.IndexOf('{'); - int end = output.IndexOf('}'); - if (end > start) - { - string before = output.Substring(0, start); - string after = output.Substring(end + 1); - output = before + (args[argIndex]?.ToString() ?? "null") + after; - argIndex++; - } - else break; - } - } - - if (level == "INFO") - { - Console.WriteLine(output); - } - else - { - Console.WriteLine($"{level}: {output}"); - } - } - } -} diff --git a/WinCertInstaller/Models/CertSource.cs b/WinCertInstaller/Models/CertSource.cs deleted file mode 100644 index 9c56979..0000000 --- a/WinCertInstaller/Models/CertSource.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -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 deleted file mode 100644 index 3593ddb..0000000 --- a/WinCertInstaller/Program.cs +++ /dev/null @@ -1,229 +0,0 @@ -using System; -using System.IO; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using System.Security.Cryptography.X509Certificates; -using WinCertInstaller.Models; -using WinCertInstaller.Configuration; -using WinCertInstaller.Services; -using WinCertInstaller.Logging; - -namespace WinCertInstaller -{ - public class Program - { - static void PrintUsage() - { - Console.WriteLine("Usage: WinCertInstaller [options]"); - Console.WriteLine("Options:"); - 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"); - } - - public static (CertSource source, bool dryRun, bool quiet, bool showHelp) ParseArguments(string[] args) - { - bool quiet = false; - bool dryRun = false; - CertSource selectedSources = CertSource.None; - bool showHelp = false; - - if (args.Length == 0) - { - selectedSources = CertSource.All; - } - else - { - foreach (string arg in args) - { - 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}"); - } - } - } - - if (selectedSources == CertSource.None && !showHelp) - { - throw new ArgumentException("No certificate source selected."); - } - - return (selectedSources, dryRun, quiet, showHelp); - } - - static void DisposeCertificates(X509Certificate2Collection collection) - { - if (collection == null) return; - foreach (var cert in collection) - { - cert.Dispose(); - } - } - - static async Task Main(string[] args) - { - using var cts = new CancellationTokenSource(); - Console.CancelKeyPress += (s, e) => - { - Console.WriteLine("\nCanceling..."); - e.Cancel = true; - cts.Cancel(); - }; - - try - { - var (selectedSources, dryRun, quiet, showHelp) = ParseArguments(args); - - if (showHelp) - { - PrintUsage(); - return 0; - } - - if (dryRun) - { - Console.WriteLine("Dry run enabled: No changes will be made to the certificate store."); - } - - // --- Lightweight Initialization --- - - // 1. Load Configuration - AppSettings settings = LoadSettings(); - - // 2. Initialize Loggers - var programLogger = new SimpleLogger(); - var downloaderLogger = new SimpleLogger(); - var validatorLogger = new SimpleLogger(); - var installerLogger = new SimpleLogger(); - - // 3. Initialize Services - var validator = new CertificateValidator(validatorLogger); - var downloader = new CertificateDownloader(downloaderLogger); - var installer = new CertificateInstaller(validator, installerLogger); - - if (selectedSources.HasFlag(CertSource.ITI)) - { - programLogger.LogInformation("====================== ITI ======================"); - if (string.IsNullOrWhiteSpace(settings.ITICertUrl)) - { - programLogger.LogError("Configuration Error: ITICertUrl is empty or missing from appsettings.json."); - } - else - { - X509Certificate2Collection itiCertificates = await downloader.GetZIPCertificatesAsync(settings.ITICertUrl, cts.Token); - if (itiCertificates.Count > 0) - { - installer.InstallCertificates(itiCertificates, dryRun); - DisposeCertificates(itiCertificates); - } - } - } - - if (selectedSources.HasFlag(CertSource.MPF)) - { - programLogger.LogInformation("====================== MPF ======================"); - if (string.IsNullOrWhiteSpace(settings.MPFCertUrl)) - { - programLogger.LogError("Configuration Error: MPFCertUrl is empty or missing from appsettings.json."); - } - else - { - X509Certificate2Collection mpfCertificates = await downloader.GetP7BCertificatesAsync(settings.MPFCertUrl, cts.Token); - if (mpfCertificates.Count > 0) - { - installer.InstallCertificates(mpfCertificates, dryRun); - DisposeCertificates(mpfCertificates); - } - } - } - - programLogger.LogInformation("================================================="); - programLogger.LogInformation("Installation process completed."); - - WaitForKeyPress(quiet); - - return 0; - } - catch (ArgumentException ex) - { - 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; - } - } - - private static AppSettings LoadSettings() - { - string configPath = Path.Combine(AppContext.BaseDirectory, "appsettings.json"); - if (!File.Exists(configPath)) - { - return new AppSettings(); - } - - try - { - string json = File.ReadAllText(configPath); - if (string.IsNullOrWhiteSpace(json)) return new AppSettings(); - - using var doc = JsonDocument.Parse(json); - var root = doc.RootElement; - - // Try to get the "CertificateSources" property if it exists (nested structure support) - if (root.TryGetProperty("CertificateSources", out var sources)) - { - return JsonSerializer.Deserialize(sources.GetRawText(), AppSettingsJsonContext.Default.AppSettings) ?? new AppSettings(); - } - - // Try to deserialize from the root (flat structure support) - return JsonSerializer.Deserialize(json, AppSettingsJsonContext.Default.AppSettings) ?? new AppSettings(); - } - catch - { - return new AppSettings(); - } - } - - static void WaitForKeyPress(bool quiet) - { - if (!quiet) - { - Console.WriteLine(); - 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 deleted file mode 100644 index ecd99b9..0000000 --- a/WinCertInstaller/Services/CertificateDownloader.cs +++ /dev/null @@ -1,132 +0,0 @@ -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; -using WinCertInstaller.Logging; - -namespace WinCertInstaller.Services -{ - public class CertificateDownloader : ICertificateDownloader - { - private readonly SimpleLogger _logger; - private static readonly HttpClient HttpClient = new HttpClient(); - - public CertificateDownloader(SimpleLogger logger) - { - _logger = logger; - } - - 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(); - - var memoryStream = new MemoryStream(); - await response.Content.CopyToAsync(memoryStream, cancellationToken); - memoryStream.Position = 0; - return memoryStream; - } - catch (OperationCanceledException) - { - _logger.LogInformation("Download canceled."); - return null; - } - catch (Exception ex) when (attempt < maxAttempts) - { - if (cancellationToken.IsCancellationRequested) return null; - _logger.LogWarning(ex, "Attempt {Attempt} failed for {Url}: {Message}", attempt, url, ex.Message); - await Task.Delay(delayBetweenAttempts.Value, cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Unable to download certificates from {Url} after {Attempt} attempts. Error: {Message}", url, attempt, ex.Message); - } - } - - return null; - } - - public async Task GetZIPCertificatesAsync(string url, CancellationToken cancellationToken = default) - { - X509Certificate2Collection certCollection = new X509Certificate2Collection(); - _logger.LogInformation("Downloading certificates from {Url}. Please wait...", url); - - using MemoryStream? 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) - { - _logger.LogWarning(ex, "Failed to load certificate from zip entry {FullName}: {Message}", certificate.FullName, ex.Message); - } - } - _logger.LogInformation("Found {Count} certificate(s) in ZIP archive.", certCollection.Count); - } - return certCollection; - } - - public async Task GetP7BCertificatesAsync(string url, CancellationToken cancellationToken = default) - { - X509Certificate2Collection certCollection = new X509Certificate2Collection(); - _logger.LogInformation("Downloading certificates from {Url}. Please wait...", url); - - using MemoryStream? stream = await DownloadFileAsync(url, cancellationToken); - - if (stream != null) - { - 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(rawBytes); - - foreach (var cert in signedCms.Certificates) - { - certCollection.Add(cert); - } - - _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 deleted file mode 100644 index b2495cd..0000000 --- a/WinCertInstaller/Services/CertificateInstaller.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.Security.Cryptography.X509Certificates; -using WinCertInstaller.Logging; - -namespace WinCertInstaller.Services -{ - public class CertificateInstaller : ICertificateInstaller - { - private readonly ICertificateValidator _validator; - private readonly SimpleLogger _logger; - - public CertificateInstaller(ICertificateValidator validator, SimpleLogger logger) - { - _validator = validator; - _logger = logger; - } - - 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); - - _logger.LogInformation("Installing certificates into the {StoreName} store...", 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++; - continue; - } - - 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++; - } - - _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(); - } - - 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 - { - // Ignora certificados que não são identificados como Autoridade Certificadora - } - } - - try - { - if (caCertificates.Count > 0) - { - Add(caCertificates, StoreName.Root, StoreLocation.LocalMachine, dryRun); - } - else - { - _logger.LogInformation("No CA certificates to import."); - } - - if (caIntermediateCertificates.Count > 0) - { - Add(caIntermediateCertificates, StoreName.CertificateAuthority, StoreLocation.LocalMachine, dryRun); - } - else - { - _logger.LogInformation("No Intermediate CA certificates to import."); - } - } - catch (System.Security.Cryptography.CryptographicException ex) - { - _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 deleted file mode 100644 index 1f84cec..0000000 --- a/WinCertInstaller/Services/CertificateValidator.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Linq; -using System.Security.Cryptography.X509Certificates; -using WinCertInstaller.Logging; - -namespace WinCertInstaller.Services -{ - public class CertificateValidator : ICertificateValidator - { - private readonly SimpleLogger _logger; - - public CertificateValidator(SimpleLogger logger) - { - _logger = logger; - } - public bool IsCertificateValidForInstall(X509Certificate2 certificate) - { - if (certificate.NotBefore > DateTime.UtcNow) - { - _logger.LogWarning("Certificate {Subject} not active yet. NotBefore={NotBefore}", certificate.Subject, certificate.NotBefore); - return false; - } - - if (certificate.NotAfter < DateTime.UtcNow) - { - _logger.LogWarning("Certificate {Subject} expired. NotAfter={NotAfter}", 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 deleted file mode 100644 index 7633a7f..0000000 --- a/WinCertInstaller/Services/ICertificateDownloader.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Security.Cryptography.X509Certificates; -using System.Threading; -using System.Threading.Tasks; - -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 deleted file mode 100644 index 7c73261..0000000 --- a/WinCertInstaller/Services/ICertificateInstaller.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Security.Cryptography.X509Certificates; - -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 deleted file mode 100644 index 721c2df..0000000 --- a/WinCertInstaller/Services/ICertificateValidator.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Security.Cryptography.X509Certificates; - -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 deleted file mode 100644 index e2d1ce1..0000000 --- a/WinCertInstaller/WinCertInstaller.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - Exe - net10.0-windows - false - false - enable - enable - WinCertInstaller - false - app.manifest - true - true - false - true - Size - - - - - - - - - \ No newline at end of file diff --git a/WinCertInstaller/app.manifest b/WinCertInstaller/app.manifest deleted file mode 100644 index 94dc0ec..0000000 --- a/WinCertInstaller/app.manifest +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/wincertinstall.ps1 b/wincertinstall.ps1 new file mode 100644 index 0000000..f9a9d12 --- /dev/null +++ b/wincertinstall.ps1 @@ -0,0 +1,217 @@ +<# +.SYNOPSIS + Script to install ITI and MPF certificates on Windows. +.EXAMPLE + .\WinCertInstaller.ps1 -All + .\WinCertInstaller.ps1 -All -ForceInstall +#> + +param ( + [switch]$Iti, + [switch]$Mpf, + [switch]$All, + [switch]$DryRun, + [switch]$ForceInstall, + [string]$ItiUrl = "http://acraiz.icpbrasil.gov.br/credenciadas/CertificadosAC-ICP-Brasil/ACcompactado.zip", + [string]$MpfUrl = "http://repositorio.acinterna.mpf.mp.br/ejbca/ra/downloads/ACIMPF-cadeia-completa.p7b", + [string]$ItiHashUrl = "https://acraiz.icpbrasil.gov.br/credenciadas/CertificadosAC-ICP-Brasil/hashsha512.txt", + [string]$MpfHashUrl = "http://repositorio.acinterna.mpf.mp.br/ejbca/ra/downloads/ACIMPF-cadeia-completa.sha512sum", + [string]$LogPath = "$env:ProgramData\WinCertInstaller\install.log" +) + +# 1. Force TLS 1.2+ for secure downloads +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +# 2. Robust UTF-8 configuration for PowerShell 5.1 +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::InputEncoding = [System.Text.Encoding]::UTF8 + +# Logging Setup +$logDir = [System.IO.Path]::GetDirectoryName($LogPath) +if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } + +function Write-Log { + param ( + [Parameter(Mandatory=$true)] + [string]$Message, + [ValidateSet("INFO", "WARN", "ERROR", "SUCCESS", "DRYRUN")] + [string]$Level = "INFO", + [ConsoleColor]$Color = "White" + ) + $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + $logLine = "$timestamp [$Level] $Message" + + # Console Output (Pretty) + switch ($Level) { + "WARN" { Write-Warning $Message } + "ERROR" { Write-Error $Message -ErrorAction SilentlyContinue } + "SUCCESS" { Write-Host $Message -ForegroundColor Green } + "DRYRUN" { Write-Host $Message -ForegroundColor Cyan } + default { Write-Host $Message -ForegroundColor $Color } + } + + # File Persistence (Audit) + try { + $logLine | Out-File -FilePath $LogPath -Append -Encoding UTF8 + } catch { + Write-Warning "Failed to write to log file: $($_.Exception.Message)" + } +} + +# 3. Check for Administrator privileges upfront (unless DryRun) +if (-not $DryRun -and -not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + Write-Log "This script must be executed as Administrator for certificate store operations." -Level "ERROR" + return +} + +if (-not $Iti -and -not $Mpf -and -not $All) { $All = $true } +if ($All) { $Iti = $true; $Mpf = $true } + +function Install-Certs { + param ([System.Security.Cryptography.X509Certificates.X509Certificate2[]]$Certificates) + + $now = Get-Date + + # Store Names (Enum based for robustness) + $storeRoot = [System.Security.Cryptography.X509Certificates.X509Store]::new([System.Security.Cryptography.X509Certificates.StoreName]::Root, [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine) + $storeCA = [System.Security.Cryptography.X509Certificates.X509Store]::new([System.Security.Cryptography.X509Certificates.StoreName]::CertificateAuthority, [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine) + + try { + $openMode = if ($DryRun) { "ReadOnly" } else { "ReadWrite" } + $storeRoot.Open($openMode) + $storeCA.Open($openMode) + + foreach ($cert in $Certificates) { + $cn = $cert.GetNameInfo([System.Security.Cryptography.X509Certificates.X509NameType]::SimpleName, $false) + + if ($now -lt $cert.NotBefore) { + Write-Log "Skipped: Certificate '$cn' is not yet active." -Level "WARN" + continue + } + if ($now -gt $cert.NotAfter) { + Write-Log "Skipped: Certificate '$cn' is expired." -Level "WARN" + continue + } + + $isRoot = ($cert.Subject -eq $cert.Issuer) + $storeName = if ($isRoot) { "Root (Root)" } else { "CA (Intermediate)" } + $targetStore = if ($isRoot) { $storeRoot } else { $storeCA } + + if (-not $ForceInstall) { + $existing = $targetStore.Certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $cert.Thumbprint, $false) + if ($existing.Count -gt 0) { + Write-Log "Already Inst.: '$cn' in $storeName." -Color DarkGray + continue + } + } + + if ($DryRun) { + Write-Log "Dry-run: Would add '$cn' to $storeName." -Level "DRYRUN" + } + else { + $targetStore.Add($cert) + Write-Log "$(if($ForceInstall){'Forced'}else{'Installed'}): '$cn' added to $storeName." -Level "SUCCESS" + } + } + } + finally { + # 4. Ensure resources are disposed even on error + $storeRoot.Dispose() + $storeCA.Dispose() + } +} + +Write-Log "Starting WinCertInstaller process..." + +# --- ITI Installation --- +if ($Iti) { + Write-Host "====================== ITI ======================" -ForegroundColor Yellow + Write-Log "Processing ITI certificates..." + $zipPath = "$env:TEMP\ACcompactado.zip" + $extractPath = "$env:TEMP\ITI_Certs" + + try { + # 1. Download ITI ZIP and its corresponding SHA512 hash + Invoke-WebRequest -Uri $ItiUrl -OutFile $zipPath -UseBasicParsing -ErrorAction Stop + + $hashPath = "$env:TEMP\iti_hash.txt" + Invoke-WebRequest -Uri $ItiHashUrl -OutFile $hashPath -UseBasicParsing -ErrorAction Stop + + # 2. Extract expected hash (first word/128 chars) and compare + $expectedHash = (Get-Content $hashPath).Substring(0, 128).Trim() + $actualHash = (Get-FileHash -Path $zipPath -Algorithm SHA512).Hash + + if ($expectedHash -ne $actualHash) { + Write-Log "Hash mismatch! Expected: $expectedHash, Actual: $actualHash" -Level "ERROR" + return + } + Write-Log "SHA512 Verification Successful." -Level "SUCCESS" + + if (Test-Path $extractPath) { Remove-Item -Path $extractPath -Recurse -Force } + Expand-Archive -Path $zipPath -DestinationPath $extractPath -Force + + $certFiles = Get-ChildItem -Path $extractPath -Include "*.cer", "*.crt" -Recurse + + # 5. Optimize memory: capture loop output + $certs = foreach ($file in $certFiles) { + [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($file.FullName) + } + + if ($certs) { + Install-Certs -Certificates $certs + } + } + catch { + Write-Log "Failed to process ITI certificates: $($_.Exception.Message)" -Level "ERROR" + } + finally { + if (Test-Path $zipPath) { Remove-Item $zipPath -Force } + if (Test-Path $extractPath) { Remove-Item $extractPath -Recurse -Force } + } +} + +# --- MPF Installation --- +if ($Mpf) { + Write-Host "====================== MPF ======================" -ForegroundColor Yellow + Write-Log "Processing MPF certificates..." + $p7bPath = "$env:TEMP\ACIMPF.p7b" + + try { + # 1. Download MPF P7B and its corresponding SHA512 hash + Invoke-WebRequest -Uri $MpfUrl -OutFile $p7bPath -UseBasicParsing -ErrorAction Stop + + $hashPath = "$env:TEMP\mpf_hash.txt" + Invoke-WebRequest -Uri $MpfHashUrl -OutFile $hashPath -UseBasicParsing -ErrorAction Stop + + # 2. Extract expected hash (first word/128 chars) and compare + $expectedHash = (Get-Content $hashPath).Substring(0, 128).Trim() + $actualHash = (Get-FileHash -Path $p7bPath -Algorithm SHA512).Hash + + if ($expectedHash -ne $actualHash) { + Write-Log "Hash mismatch for MPF certificates! Expected: $expectedHash, Actual: $actualHash" -Level "ERROR" + return + } + Write-Log "SHA512 Verification Successful (MPF)." -Level "SUCCESS" + + $certCollection = [System.Security.Cryptography.X509Certificates.X509Certificate2Collection]::new() + $certCollection.Import($p7bPath) + + $certs = foreach ($cert in $certCollection) { + $cert + } + + if ($certs) { + Install-Certs -Certificates $certs + } + } + catch { + Write-Log "Failed to process MPF certificates: $($_.Exception.Message)" -Level "ERROR" + } + finally { + if (Test-Path $p7bPath) { Remove-Item $p7bPath -Force } + } +} + +Write-Host "=================================================" -ForegroundColor Yellow +Write-Log "Process completed." \ No newline at end of file