From dee65c9ee4ce4d91c06809987aa3813f65432d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szak=C3=A1ts=20Alp=C3=A1r=20Zsolt?= Date: Sun, 17 Aug 2025 16:13:39 +0200 Subject: [PATCH] Certificate Manager? --- Source/Automatic Parking.sln | 6 +++ .../CertificateManager.csproj | 24 ++++++++++ Source/CertificateManager/Dockerfile | 21 +++++++++ .../Models/NameComCredentials.cs | 3 ++ Source/CertificateManager/Program.cs | 10 +++++ .../Properties/launchSettings.json | 12 +++++ Source/CertificateManager/Worker.cs | 45 +++++++++++++++++++ .../appsettings.Development.json | 8 ++++ Source/CertificateManager/appsettings.json | 8 ++++ 9 files changed, 137 insertions(+) create mode 100644 Source/CertificateManager/CertificateManager.csproj create mode 100644 Source/CertificateManager/Dockerfile create mode 100644 Source/CertificateManager/Models/NameComCredentials.cs create mode 100644 Source/CertificateManager/Program.cs create mode 100644 Source/CertificateManager/Properties/launchSettings.json create mode 100644 Source/CertificateManager/Worker.cs create mode 100644 Source/CertificateManager/appsettings.Development.json create mode 100644 Source/CertificateManager/appsettings.json diff --git a/Source/Automatic Parking.sln b/Source/Automatic Parking.sln index 9e928dc..3939122 100644 --- a/Source/Automatic Parking.sln +++ b/Source/Automatic Parking.sln @@ -7,6 +7,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution compose.yaml = compose.yaml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CertificateManager", "CertificateManager\CertificateManager.csproj", "{8118AB3F-CF86-4B17-9C0B-E27C37FCE638}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -17,5 +19,9 @@ Global {93F01B86-2434-42E2-AE67-774BA61CFF7B}.Debug|Any CPU.Build.0 = Debug|Any CPU {93F01B86-2434-42E2-AE67-774BA61CFF7B}.Release|Any CPU.ActiveCfg = Release|Any CPU {93F01B86-2434-42E2-AE67-774BA61CFF7B}.Release|Any CPU.Build.0 = Release|Any CPU + {8118AB3F-CF86-4B17-9C0B-E27C37FCE638}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8118AB3F-CF86-4B17-9C0B-E27C37FCE638}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8118AB3F-CF86-4B17-9C0B-E27C37FCE638}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8118AB3F-CF86-4B17-9C0B-E27C37FCE638}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Source/CertificateManager/CertificateManager.csproj b/Source/CertificateManager/CertificateManager.csproj new file mode 100644 index 0000000..dc04320 --- /dev/null +++ b/Source/CertificateManager/CertificateManager.csproj @@ -0,0 +1,24 @@ + + + + net10.0 + enable + enable + true + true + dotnet-CertificateManager-9dd1279b-a435-420b-b714-c1f017b1b0df + Linux + + + + + + + + + + + .dockerignore + + + diff --git a/Source/CertificateManager/Dockerfile b/Source/CertificateManager/Dockerfile new file mode 100644 index 0000000..f25d5a8 --- /dev/null +++ b/Source/CertificateManager/Dockerfile @@ -0,0 +1,21 @@ +FROM mcr.microsoft.com/dotnet/runtime:10.0 AS base +USER $APP_UID +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["CertificateManager/CertificateManager.csproj", "CertificateManager/"] +RUN dotnet restore "CertificateManager/CertificateManager.csproj" +COPY . . +WORKDIR "/src/CertificateManager" +RUN dotnet build "./CertificateManager.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./CertificateManager.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "CertificateManager.dll"] diff --git a/Source/CertificateManager/Models/NameComCredentials.cs b/Source/CertificateManager/Models/NameComCredentials.cs new file mode 100644 index 0000000..106fe56 --- /dev/null +++ b/Source/CertificateManager/Models/NameComCredentials.cs @@ -0,0 +1,3 @@ +namespace CertificateManager.Models; + +public record NameComCredentials(String Username, String APIToken, string Server = "https://api.name.com/v4"); \ No newline at end of file diff --git a/Source/CertificateManager/Program.cs b/Source/CertificateManager/Program.cs new file mode 100644 index 0000000..8e501f2 --- /dev/null +++ b/Source/CertificateManager/Program.cs @@ -0,0 +1,10 @@ +using CertificateManager; + +var builder = Host.CreateApplicationBuilder(args); +builder.Services.AddHostedService(); + +builder.Logging.ClearProviders(); +builder.Logging.AddConsole(); + +var host = builder.Build(); +host.Run(); \ No newline at end of file diff --git a/Source/CertificateManager/Properties/launchSettings.json b/Source/CertificateManager/Properties/launchSettings.json new file mode 100644 index 0000000..cf4a5e2 --- /dev/null +++ b/Source/CertificateManager/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "CertificateManager": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Source/CertificateManager/Worker.cs b/Source/CertificateManager/Worker.cs new file mode 100644 index 0000000..5ef3f10 --- /dev/null +++ b/Source/CertificateManager/Worker.cs @@ -0,0 +1,45 @@ +using System.Diagnostics.CodeAnalysis; +using Certes; +using Certes.Acme; +using CertificateManager.Models; + +namespace CertificateManager; + +public class Worker(ILogger logger, TimeProvider timeProvider, IConfiguration configuration) : BackgroundService +{ + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + logger.LogTrace("Certificate manager started"); + + // Local keys and CA + string localDomain = configuration.GetValue("localDomain", "local"); + string keysPath = configuration.GetValue("keys_path", "/Certificates/Keys"); + string caPath = configuration.GetValue("ca_path", "/Certificates/CA"); + string localPath = configuration.GetValue("local_wildcard_path", "/Certificates/Local"); + + // Real CA, real domain + string domain = configuration.GetValue("domain", "automatic-parking.app"); + string wildcardPath = configuration.GetValue("wildcard_path", "/Certificates/Wildcard"); + + string acmeEmail = configuration.GetValue("acmeEmail", ""); + logger.LogTrace("Acme email provided: {acmeEmail}", acmeEmail); + + string nameComUsername = configuration.GetValue("nameComUsername", ""); + string nameComToken = configuration.GetValue("nameComAPIToken", ""); + string nameComServer = configuration.GetValue("nameComServer", "https://api.name.com/v4"); + NameComCredentials nameComCredentials = new NameComCredentials(nameComUsername, nameComToken, nameComServer); + logger.LogTrace("Name.com credentials provided: {nameComUsername} (with token of {nameComTokenLength} characters)", nameComUsername, nameComToken.Length); + + // Generate keys, CA and certificates + var keys = GenerateKeys(keysPath, "private.pem", "public.pem", "chain.pem"); + var ca = CreateRootCA(keys, caPath, "private.pem", "public.pem"); + var local = CreateWildcardCertificate(ca, localDomain, localPath, "private.pem", "public.pem", "chain.pem"); + + var external = AcquireWildcardCertificate(domain, nameComCredentials, wildcardPath, "private.pem", "public.pem", "chain.pem"); + DateTimeOffset expiry = external.Expires; + logger.LogTrace("Wildcard certificate will expire on {expiry}", expiry); + + await Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Source/CertificateManager/appsettings.Development.json b/Source/CertificateManager/appsettings.Development.json new file mode 100644 index 0000000..b2dcdb6 --- /dev/null +++ b/Source/CertificateManager/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/Source/CertificateManager/appsettings.json b/Source/CertificateManager/appsettings.json new file mode 100644 index 0000000..b2dcdb6 --- /dev/null +++ b/Source/CertificateManager/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +}