Skip to content

Instantly share code, notes, and snippets.

@swaters86
Created February 20, 2026 20:34
Show Gist options
  • Select an option

  • Save swaters86/ab57f79cf4c113d61478b56db9f4fcdf to your computer and use it in GitHub Desktop.

Select an option

Save swaters86/ab57f79cf4c113d61478b56db9f4fcdf to your computer and use it in GitHub Desktop.
// MtlsEnrollmentClient.Net10.cs
// .NET 10 Worker Service helper for enrollment + mTLS API calls
//
// Implements:
// - EnsureClientCertificateInstalledAsync(enrollmentCode)
// - CreateMtlsHttpClient()
// - TryRenewCertificateAsync()
//
// Notes:
// - Looks up cert in LocalMachine\My using a subject marker (e.g. "OU=tenant_demo_001").
// - For production, store and load by thumbprint (more precise) after first enrollment.
// - Do NOT disable server certificate validation in production.
using System.Net.Http;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
public sealed class MtlsEnrollmentClient
{
private readonly Uri _baseApi;
private readonly string _certSubjectContains;
private readonly TimeSpan _timeout;
private readonly JsonSerializerOptions _jsonOptions;
public string? CurrentThumbprint { get; private set; }
/// <param name="baseApiUrl">Base API URL, e.g. https://api.yourdomain.com/</param>
/// <param name="certSubjectContains">Stable subject marker to find cert, e.g. "OU=tenant_demo_001"</param>
/// <param name="timeout">Optional HTTP timeout (default 30s)</param>
public MtlsEnrollmentClient(string baseApiUrl, string certSubjectContains, TimeSpan? timeout = null)
{
if (string.IsNullOrWhiteSpace(baseApiUrl))
throw new ArgumentNullException(nameof(baseApiUrl));
if (string.IsNullOrWhiteSpace(certSubjectContains))
throw new ArgumentNullException(nameof(certSubjectContains));
_baseApi = new Uri(baseApiUrl.TrimEnd('/') + "/");
_certSubjectContains = certSubjectContains;
_timeout = timeout ?? TimeSpan.FromSeconds(30);
_jsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
}
/// <summary>
/// Ensures a client certificate exists in LocalMachine\My.
/// If not found, uses enrollment code to call /enroll and installs returned PFX.
/// </summary>
public async Task EnsureClientCertificateInstalledAsync(string? enrollmentCode, CancellationToken ct = default)
{
var existing = FindExistingClientCert();
if (existing is not null)
{
CurrentThumbprint = existing.Thumbprint;
return;
}
if (string.IsNullOrWhiteSpace(enrollmentCode))
throw new InvalidOperationException("No client certificate installed and enrollmentCode was not provided.");
var request = new
{
enrollmentCode = enrollmentCode.Trim(),
machineName = Environment.MachineName
};
var enrollResponse = await PostJsonAsync<EnrollResponse>(
new Uri(_baseApi, "enroll"),
request,
handler: CreateHandlerNoClientCert(),
ct: ct);
if (enrollResponse is null || string.IsNullOrWhiteSpace(enrollResponse.PfxBase64))
throw new InvalidOperationException("Enrollment failed: empty or invalid response.");
byte[] pfxBytes = Convert.FromBase64String(enrollResponse.PfxBase64);
InstallPfxToLocalMachineMy(pfxBytes, enrollResponse.PfxPassword ?? string.Empty);
var installed = FindExistingClientCert();
if (installed is null)
throw new InvalidOperationException(@"Enrollment succeeded but certificate was not found in LocalMachine\My.");
CurrentThumbprint = installed.Thumbprint;
}
/// <summary>
/// Creates an HttpClient that presents the installed client certificate (mTLS).
/// Caller owns disposal of the returned HttpClient.
/// </summary>
public HttpClient CreateMtlsHttpClient()
{
var cert = FindExistingClientCert()
?? throw new InvalidOperationException("Client certificate not installed. Call EnsureClientCertificateInstalledAsync first.");
CurrentThumbprint = cert.Thumbprint;
var handler = new HttpClientHandler
{
SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13,
ClientCertificateOptions = ClientCertificateOption.Manual
};
handler.ClientCertificates.Add(cert);
// DO NOT do this in production:
// handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
var client = new HttpClient(handler)
{
BaseAddress = _baseApi,
Timeout = _timeout
};
client.DefaultRequestHeaders.UserAgent.ParseAdd("YourWorkerService/1.0");
return client;
}
/// <summary>
/// Renews the client certificate via POST /cert/renew when close to expiry.
/// Returns true if cert is valid/renewed, false if renewal failed.
/// </summary>
/// <param name="renewWhenWithinDays">Renew if cert expires within this many days (default 14)</param>
public async Task<bool> TryRenewCertificateAsync(int renewWhenWithinDays = 14, CancellationToken ct = default)
{
var cert = FindExistingClientCert();
if (cert is null)
return false;
if (cert.NotAfter.ToUniversalTime() > DateTime.UtcNow.AddDays(renewWhenWithinDays))
{
CurrentThumbprint = cert.Thumbprint;
return true; // not due yet
}
using var http = CreateMtlsHttpClient();
using var content = new StringContent("{}", Encoding.UTF8, "application/json");
using var response = await http.PostAsync("cert/renew", content, ct);
if (!response.IsSuccessStatusCode)
return false;
var responseText = await response.Content.ReadAsStringAsync(ct);
var renewResponse = JsonSerializer.Deserialize<EnrollResponse>(responseText, _jsonOptions);
if (renewResponse is null || string.IsNullOrWhiteSpace(renewResponse.PfxBase64))
return false;
byte[] pfxBytes = Convert.FromBase64String(renewResponse.PfxBase64);
InstallPfxToLocalMachineMy(pfxBytes, renewResponse.PfxPassword ?? string.Empty);
var newCert = FindExistingClientCert();
if (newCert is not null)
CurrentThumbprint = newCert.Thumbprint;
return true;
}
// ---------------- Internals ----------------
private HttpClientHandler CreateHandlerNoClientCert()
{
return new HttpClientHandler
{
SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13
};
}
/// <summary>
/// Finds the newest valid certificate in LocalMachine\My matching the configured subject marker.
/// </summary>
private X509Certificate2? FindExistingClientCert()
{
using var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
return store.Certificates
.OfType<X509Certificate2>()
.Where(c =>
c.HasPrivateKey &&
!string.IsNullOrWhiteSpace(c.Subject) &&
c.Subject.Contains(_certSubjectContains, StringComparison.OrdinalIgnoreCase) &&
c.NotAfter.ToUniversalTime() > DateTime.UtcNow.AddMinutes(5))
.OrderByDescending(c => c.NotAfter)
.FirstOrDefault();
}
/// <summary>
/// Installs the returned PFX into LocalMachine\My. Private key persists on machine.
/// </summary>
private static void InstallPfxToLocalMachineMy(byte[] pfxBytes, string password)
{
// NOTE:
// - MachineKeySet: store private key in machine context
// - PersistKeySet: key remains after X509Certificate2 object disposal
// - EphemeralKeySet is NOT wanted here because we need the cert for future calls
//
// If the API issues a cert with an exportable private key, Windows may still permit export.
// For stronger controls, use CSR-based enrollment so the private key is generated locally.
using var cert = new X509Certificate2(
pfxBytes,
password,
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);
using var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadWrite);
store.Add(cert);
}
private async Task<T?> PostJsonAsync<T>(Uri url, object body, HttpClientHandler handler, CancellationToken ct) where T : class
{
using var client = new HttpClient(handler) { Timeout = _timeout };
string json = JsonSerializer.Serialize(body);
using var content = new StringContent(json, Encoding.UTF8, "application/json");
using var response = await client.PostAsync(url, content, ct);
string responseText = await response.Content.ReadAsStringAsync(ct);
if (!response.IsSuccessStatusCode)
throw new InvalidOperationException($"HTTP {(int)response.StatusCode} {response.ReasonPhrase} :: {responseText}");
return JsonSerializer.Deserialize<T>(responseText, _jsonOptions);
}
// ---------------- DTOs (match API JSON camelCase) ----------------
private sealed class EnrollResponse
{
public string? TenantId { get; set; }
public string? Thumbprint { get; set; }
public DateTime? NotAfterUtc { get; set; }
public string? PfxBase64 { get; set; }
public string? PfxPassword { get; set; }
}
}
/*
========================================
EXAMPLE USAGE IN A .NET 10 WORKER SERVICE
========================================
1) appsettings.json (example)
{
"ApiSettings": {
"BaseUrl": "https://api.yourdomain.com/",
"EnrollmentCode": "ABC-123-ENROLL",
"CertSubjectContains": "OU=tenant_demo_001"
}
}
2) Worker.cs (BackgroundService) usage example:
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public sealed class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly IConfiguration _config;
private MtlsEnrollmentClient? _mtls;
public Worker(ILogger<Worker> logger, IConfiguration config)
{
_logger = logger;
_config = config;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
string baseUrl = _config["ApiSettings:BaseUrl"]!;
string? enrollmentCode = _config["ApiSettings:EnrollmentCode"]; // can be removed after first install
string subjectMarker = _config["ApiSettings:CertSubjectContains"]!;
_mtls = new MtlsEnrollmentClient(baseUrl, subjectMarker);
// Ensure cert exists (first run enrolls; later runs reuse installed cert)
await _mtls.EnsureClientCertificateInstalledAsync(enrollmentCode, stoppingToken);
_logger.LogInformation("mTLS cert ready. Thumbprint: {Thumbprint}", _mtls.CurrentThumbprint);
while (!stoppingToken.IsCancellationRequested)
{
try
{
// Periodic renewal check (no-op if not near expiry)
bool renewOk = await _mtls.TryRenewCertificateAsync(ct: stoppingToken);
if (!renewOk)
{
_logger.LogWarning("Certificate renewal check failed.");
}
// Example protected API call
using var http = _mtls.CreateMtlsHttpClient();
using var resp = await http.GetAsync("phi/ping", stoppingToken);
string body = await resp.Content.ReadAsStringAsync(stoppingToken);
_logger.LogInformation("GET /phi/ping => {Status} {Body}", (int)resp.StatusCode, body);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during mTLS API call.");
}
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
}
3) Program.cs (Worker) typical registration:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
Host.CreateDefaultBuilder(args)
.UseWindowsService() // optional if running as Windows Service
.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
})
.Build()
.Run();
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment