Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
@@ -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
88 changes: 79 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,87 @@
# WinCertInstaller
# 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.*
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.

*Use at your own risk.*
> [!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.

## TODO
## 🚀 Key Features

It makes sense to reconstruct a minimally-working original 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.

What is missing right now:
* Better exceptions catch
## 🏗️ 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<T>` 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.

*Feel free to make pull-requests :)*
## 💻 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.*

## 🛠️ 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
```

### Examples

**Standard Installation (All Sources):**
```powershell
WinCertInstaller.exe
```

**Dry-Run of ITI Source:**
```powershell
WinCertInstaller.exe --iti --dry-run
```

**Quiet Installation (Script Mode):**
```powershell
WinCertInstaller.exe --all -q
```

## 🧪 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
```

### 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.*
65 changes: 65 additions & 0 deletions WinCertInstaller.Tests/ProgramTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Xunit;
using WinCertInstaller.Models;
using WinCertInstaller.Services;
using Microsoft.Extensions.Logging.Abstractions;

namespace WinCertInstaller.Tests
{
public class ProgramTests
{
[Fact]
public void ParseArguments_DefaultsToAll()
{
var result = WinCertInstaller.Program.ParseArguments(Array.Empty<string>());

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<ArgumentException>(() => 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(NullLogger<CertificateValidator>.Instance);

Assert.True(validator.IsCertificateAuthority(certificate));
Assert.True(validator.IsSelfSigned(certificate));
}
}
}
32 changes: 32 additions & 0 deletions WinCertInstaller.Tests/WinCertInstaller.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0-windows7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="8.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\WinCertInstaller\WinCertInstaller.csproj" />
</ItemGroup>

</Project>
26 changes: 26 additions & 0 deletions WinCertInstaller.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions WinCertInstaller/Configuration/AppSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace WinCertInstaller.Configuration
{
/// <summary>
/// Application settings containing default URLs for certificate downloads.
/// Mapped directly from appsettings.json via IOptions.
/// </summary>
public class AppSettings
{
public const string Position = "CertificateSources";

/// <summary>
/// The URL to the ZIP file containing ITI (ICP-Brasil) certificates.
/// </summary>
public string ITICertUrl { get; set; } = string.Empty;

/// <summary>
/// The URL to the P7B (PKCS #7) file containing MPF certificates.
/// </summary>
public string MPFCertUrl { get; set; } = string.Empty;
}
}
45 changes: 45 additions & 0 deletions WinCertInstaller/Logging/CleanConsoleFormatter.cs
Original file line number Diff line number Diff line change
@@ -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<TState>(in LogEntry<TState> 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());
}
}
}
}
24 changes: 24 additions & 0 deletions WinCertInstaller/Models/CertSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;

namespace WinCertInstaller.Models
{
/// <summary>
/// Represents the sources from which certificates should be installed.
/// This enumeration supports bitwise combinations (Flags).
/// </summary>
[Flags]
public enum CertSource
{
/// <summary>No source selected.</summary>
None = 0,

/// <summary>Installs certificates from ITI (Instituto Nacional de Tecnologia da Informação).</summary>
ITI = 1,

/// <summary>Installs certificates from MPF (Ministério Público Federal).</summary>
MPF = 2,

/// <summary>Installs certificates from all available sources.</summary>
All = ITI | MPF
}
}
Loading
Loading