Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

Save swaters86/8a3f5e01cb909acc0b09305728221efa to your computer and use it in GitHub Desktop.
// MtlsEnrollmentClient.cs - .NET Framework 4.8 / C# 7.3
// Drop into your Windows Service project.
// Implements:
// - EnsureClientCertificateInstalledAsync(enrollmentCode) -> installs client cert if missing
// - CreateMtlsHttpClient() -> HttpClient that automatically presents the client cert (mTLS)
// - TryRenewCertificateAsync() -> rotate cert using /cert/renew
//
// Storage:
// - Certificate is stored in LocalMachine\My and private key is marked non-exportable when imported.
// - You should run your service under a dedicated service account and lock down private key ACLs.
//
// SECURITY NOTE:
// - This example enrolls by downloading a PFX from your API over TLS.
// - For maximum security, replace enrollment with CSR-based issuance (private key generated locally).
//
// Requires references: System.Net.Http, System.Security, System.Runtime.Serialization (for DataContractJsonSerializer)
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
public sealed class MtlsEnrollmentClient
{
private readonly Uri _baseApi;
private readonly string _certSubjectPrefix; // helps find our cert if thumbprint not known
private readonly string _tenantLabel; // optional label for debugging
private readonly TimeSpan _timeout = TimeSpan.FromSeconds(30);
// You can store thumbprint in Registry/Config after first enrollment.
// For simplicity, this helper can locate cert by subject prefix as a fallback.
public string CurrentThumbprint { get; private set; }
public MtlsEnrollmentClient(string baseApiUrl, string certSubjectPrefix, string tenantLabel = null)
{
if (string.IsNullOrWhiteSpace(baseApiUrl)) throw new ArgumentNullException("baseApiUrl");
if (string.IsNullOrWhiteSpace(certSubjectPrefix)) throw new ArgumentNullException("certSubjectPrefix");
_baseApi = new Uri(baseApiUrl.TrimEnd('/') + "/");
_certSubjectPrefix = certSubjectPrefix; // example: "CN=client-tenant_demo_001"
_tenantLabel = tenantLabel ?? "unknown";
}
// -------------------- Public API --------------------
// Call this once at startup (OnStart) to ensure cert exists.
public async Task EnsureClientCertificateInstalledAsync(string enrollmentCode, CancellationToken ct = default(CancellationToken))
{
// Ensure TLS 1.2 on older Windows/.NET settings
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
// 1) Try to locate existing cert
var existing = FindExistingClientCert();
if (existing != null)
{
CurrentThumbprint = existing.Thumbprint;
return;
}
if (string.IsNullOrWhiteSpace(enrollmentCode))
throw new InvalidOperationException("No client certificate installed and enrollmentCode was not provided.");
// 2) Enroll: request PFX
var enrollReq = new EnrollRequest
{
EnrollmentCode = enrollmentCode.Trim(),
MachineName = Environment.MachineName
};
var enrollResp = await PostJsonAsync<EnrollRequest, EnrollResponse>(
new Uri(_baseApi, "enroll"),
enrollReq,
handler: CreateDefaultHandlerNoClientCert(),
ct: ct).ConfigureAwait(false);
if (enrollResp == null || string.IsNullOrWhiteSpace(enrollResp.PfxBase64))
throw new InvalidOperationException("Enrollment failed: empty response.");
// 3) Install cert to LocalMachine\My with non-exportable private key
var pfxBytes = Convert.FromBase64String(enrollResp.PfxBase64);
var pfxPassword = enrollResp.PfxPassword ?? "";
InstallPfxToLocalMachineMy(pfxBytes, pfxPassword);
// 4) Confirm installed
var installed = FindExistingClientCert();
if (installed == null)
throw new InvalidOperationException("Enrollment succeeded but certificate was not found in LocalMachine\\My.");
CurrentThumbprint = installed.Thumbprint;
}
// Use this for all service->API calls after enrollment
public HttpClient CreateMtlsHttpClient()
{
var cert = FindExistingClientCert();
if (cert == null)
throw new InvalidOperationException("Client certificate not installed. Call EnsureClientCertificateInstalledAsync first.");
CurrentThumbprint = cert.Thumbprint;
var handler = new HttpClientHandler
{
SslProtocols = SslProtocols.Tls12,
ClientCertificateOptions = ClientCertificateOption.Manual
};
handler.ClientCertificates.Add(cert);
// IMPORTANT: Do NOT disable server certificate validation in production.
// handler.ServerCertificateCustomValidationCallback = (m, c, ch, e) => true; // <-- DON’T DO THIS.
var client = new HttpClient(handler)
{
BaseAddress = _baseApi,
Timeout = _timeout
};
// Example default headers (optional)
client.DefaultRequestHeaders.UserAgent.ParseAdd("YourService/1.0");
return client;
}
// Call periodically (e.g., daily) or when cert is near expiry.
public async Task<bool> TryRenewCertificateAsync(CancellationToken ct = default(CancellationToken))
{
var cert = FindExistingClientCert();
if (cert == null) return false;
// If not close to expiry, you can skip renewal (example threshold: 14 days)
if (cert.NotAfter.ToUniversalTime() > DateTime.UtcNow.AddDays(14))
return true;
using (var mtlsClient = CreateMtlsHttpClient())
{
var resp = await mtlsClient.PostAsync("cert/renew", new StringContent("{}", Encoding.UTF8, "application/json"), ct)
.ConfigureAwait(false);
if (!resp.IsSuccessStatusCode)
return false;
var enrollResp = DeserializeJson<EnrollResponse>(await resp.Content.ReadAsStreamAsync().ConfigureAwait(false));
if (enrollResp == null || string.IsNullOrWhiteSpace(enrollResp.PfxBase64))
return false;
InstallPfxToLocalMachineMy(Convert.FromBase64String(enrollResp.PfxBase64), enrollResp.PfxPassword ?? "");
// Optional: you may also remove the old cert if you track its thumbprint.
// In many ops setups, leaving old certs until cleanup is acceptable.
// Update thumbprint
var newCert = FindExistingClientCert();
if (newCert != null) CurrentThumbprint = newCert.Thumbprint;
return true;
}
}
// -------------------- Internals --------------------
private HttpClientHandler CreateDefaultHandlerNoClientCert()
{
return new HttpClientHandler
{
SslProtocols = SslProtocols.Tls12
};
}
private X509Certificate2 FindExistingClientCert()
{
// Search LocalMachine\My for a cert whose subject starts with our prefix and has a private key
using (var store = new X509Store(StoreName.My, StoreLocation.LocalMachine))
{
store.Open(OpenFlags.ReadOnly);
var candidates = store.Certificates
.OfType<X509Certificate2>()
.Where(c =>
c.HasPrivateKey &&
c.Subject != null &&
c.Subject.IndexOf(_certSubjectPrefix, StringComparison.OrdinalIgnoreCase) >= 0 &&
c.NotAfter.ToUniversalTime() > DateTime.UtcNow.AddMinutes(5))
.OrderByDescending(c => c.NotAfter)
.ToList();
return candidates.FirstOrDefault();
}
}
private void InstallPfxToLocalMachineMy(byte[] pfxBytes, string password)
{
// MachineKeySet: store private key for LocalMachine
// PersistKeySet: keep it after object disposed
// (Do NOT use Exportable unless you explicitly need it.)
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<TResp> PostJsonAsync<TReq, TResp>(Uri url, TReq body, HttpClientHandler handler, CancellationToken ct)
where TReq : class
where TResp : class
{
using (var client = new HttpClient(handler) { Timeout = _timeout })
{
var json = SerializeJson(body);
using (var content = new StringContent(json, Encoding.UTF8, "application/json"))
using (var resp = await client.PostAsync(url, content, ct).ConfigureAwait(false))
{
if (!resp.IsSuccessStatusCode)
{
var msg = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new InvalidOperationException("HTTP " + (int)resp.StatusCode + " " + resp.ReasonPhrase + " :: " + msg);
}
var stream = await resp.Content.ReadAsStreamAsync().ConfigureAwait(false);
return DeserializeJson<TResp>(stream);
}
}
}
private static string SerializeJson<T>(T obj)
{
var ser = new DataContractJsonSerializer(typeof(T));
using (var ms = new MemoryStream())
{
ser.WriteObject(ms, obj);
return Encoding.UTF8.GetString(ms.ToArray());
}
}
private static T DeserializeJson<T>(Stream stream) where T : class
{
var ser = new DataContractJsonSerializer(typeof(T));
return ser.ReadObject(stream) as T;
}
// -------------------- DTOs --------------------
[DataContract]
private sealed class EnrollRequest
{
[DataMember(Name = "enrollmentCode", IsRequired = true)]
public string EnrollmentCode { get; set; }
[DataMember(Name = "machineName", IsRequired = false)]
public string MachineName { get; set; }
}
[DataContract]
private sealed class EnrollResponse
{
[DataMember(Name = "tenantId", IsRequired = false)]
public string TenantId { get; set; }
[DataMember(Name = "thumbprint", IsRequired = false)]
public string Thumbprint { get; set; }
[DataMember(Name = "notAfterUtc", IsRequired = false)]
public DateTime NotAfterUtc { get; set; }
[DataMember(Name = "pfxBase64", IsRequired = true)]
public string PfxBase64 { get; set; }
[DataMember(Name = "pfxPassword", IsRequired = true)]
public string PfxPassword { get; set; }
}
}
// -------------------- Example usage in a Windows Service --------------------
// In your service:
// 1) Call EnsureClientCertificateInstalledAsync(enrollmentCode) at startup
// 2) Use CreateMtlsHttpClient() for all API calls
//
// Example (pseudo in OnStart):
//
// var mtls = new MtlsEnrollmentClient(
// baseApiUrl: "https://api.yourdomain.com/",
// certSubjectPrefix: "CN=client-tenant_demo_001" // match whatever your API uses for subject
// );
//
// await mtls.EnsureClientCertificateInstalledAsync(enrollmentCodeFromInstaller);
// using (var http = mtls.CreateMtlsHttpClient())
// {
// var r = await http.GetAsync("phi/ping");
// var body = await r.Content.ReadAsStringAsync();
// // log body
// }
//
// Optional daily renewal:
// await mtls.TryRenewCertificateAsync();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment