Created
February 20, 2026 21:05
-
-
Save swaters86/63aab11f750489059aecc69332995323 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // MtlsEnrollmentClient.Csr.Production.Net10.cs | |
| // .NET 10 Worker Service helper (CSR-based enrollment, production-upgraded) | |
| // | |
| // Features: | |
| // - Generates private key locally and creates CSR (PKCS#10) | |
| // - Sends CSR + enrollment code to API /enroll/csr | |
| // - Installs returned signed cert and associates with local private key | |
| // - mTLS HttpClient creation | |
| // - Renewal via /cert/renew/csr using mTLS + fresh CSR | |
| // - Thumbprint + DeviceId persisted in HKLM registry using DPAPI | |
| // - Retry/backoff for enroll/renew | |
| // - Best-effort private key ACL hardening for service account | |
| // | |
| // Target: .NET 10 Worker Service on Windows (net10.0-windows recommended) | |
| // | |
| // appsettings example: | |
| // { | |
| // "ApiSettings": { | |
| // "BaseUrl": "https://api.yourdomain.com/", | |
| // "EnrollmentCode": "ABC-123-ENROLL", | |
| // "TenantSubjectMarker": "OU=tenant_demo_001", | |
| // "RegistryPath": "Software\\YourCompany\\IntegrationAgent\\Security", | |
| // "ServiceAccountIdentity": "NT SERVICE\\YourWorkerService" | |
| // } | |
| // } | |
| using Microsoft.Win32; | |
| using System.Net.Http; | |
| using System.Security.AccessControl; | |
| using System.Security.Authentication; | |
| using System.Security.Claims; | |
| using System.Security.Cryptography; | |
| using System.Security.Cryptography.X509Certificates; | |
| using System.Security.Principal; | |
| using System.Text; | |
| using System.Text.Json; | |
| public sealed class MtlsEnrollmentClient | |
| { | |
| private readonly Uri _baseApi; | |
| private readonly string _tenantSubjectMarkerFallback; | |
| private readonly string _registryPath; | |
| private readonly string? _serviceAccountIdentity; | |
| private readonly TimeSpan _timeout; | |
| private readonly JsonSerializerOptions _jsonOptions; | |
| private readonly byte[] _dpapiEntropy; | |
| public string? CurrentThumbprint { get; private set; } | |
| public string? CurrentDeviceId { get; private set; } | |
| public MtlsEnrollmentClient( | |
| string baseApiUrl, | |
| string tenantSubjectMarkerFallback, | |
| string registryPath = @"Software\YourCompany\IntegrationAgent\Security", | |
| string? serviceAccountIdentity = null, | |
| TimeSpan? timeout = null) | |
| { | |
| if (string.IsNullOrWhiteSpace(baseApiUrl)) throw new ArgumentNullException(nameof(baseApiUrl)); | |
| if (string.IsNullOrWhiteSpace(tenantSubjectMarkerFallback)) throw new ArgumentNullException(nameof(tenantSubjectMarkerFallback)); | |
| if (string.IsNullOrWhiteSpace(registryPath)) throw new ArgumentNullException(nameof(registryPath)); | |
| _baseApi = new Uri(baseApiUrl.TrimEnd('/') + "/"); | |
| _tenantSubjectMarkerFallback = tenantSubjectMarkerFallback; | |
| _registryPath = registryPath; | |
| _serviceAccountIdentity = serviceAccountIdentity; | |
| _timeout = timeout ?? TimeSpan.FromSeconds(30); | |
| _dpapiEntropy = Encoding.UTF8.GetBytes("YourCompany|IntegrationAgent|mTLS-CSR|v1"); | |
| _jsonOptions = new JsonSerializerOptions | |
| { | |
| PropertyNameCaseInsensitive = true | |
| }; | |
| } | |
| // ============================================================ | |
| // Public API | |
| // ============================================================ | |
| /// <summary> | |
| /// Ensures client cert exists. If not, performs CSR enrollment. | |
| /// </summary> | |
| public async Task EnsureClientCertificateInstalledAsync(string? enrollmentCode, CancellationToken ct = default) | |
| { | |
| // 1) Exact thumbprint lookup from registry | |
| var cert = FindCertByPersistedThumbprint(); | |
| if (cert is not null) | |
| { | |
| CurrentThumbprint = cert.Thumbprint; | |
| CurrentDeviceId = LoadPersistedDeviceId(); | |
| return; | |
| } | |
| // 2) Fallback subject search (legacy/migration safety) | |
| cert = FindNewestValidCertBySubjectMarker(); | |
| if (cert is not null) | |
| { | |
| CurrentThumbprint = cert.Thumbprint; | |
| PersistThumbprint(cert.Thumbprint!); | |
| CurrentDeviceId = LoadPersistedDeviceId(); | |
| return; | |
| } | |
| // 3) Need enrollment code for first enrollment | |
| if (string.IsNullOrWhiteSpace(enrollmentCode)) | |
| throw new InvalidOperationException("No client certificate installed and enrollmentCode was not provided."); | |
| // Generate local key + CSR (private key never leaves machine) | |
| using var rsa = RSA.Create(3072); | |
| // Subject used in CSR; API can override/normalize if it wants. | |
| // Include machine name for auditability. | |
| string subject = $"CN=client-{Sanitize(Environment.MachineName)}, O=YourCompany"; | |
| byte[] csrDer = CreateClientCsrDer(rsa, subject); | |
| var request = new CsrEnrollRequest | |
| { | |
| EnrollmentCode = enrollmentCode.Trim(), | |
| MachineName = Environment.MachineName, | |
| CsrBase64 = Convert.ToBase64String(csrDer) | |
| }; | |
| var enrollResponse = await ExecuteWithRetryAsync(async () => | |
| { | |
| return await PostJsonAsync<CsrEnrollResponse>( | |
| new Uri(_baseApi, "enroll/csr"), | |
| request, | |
| CreateHandlerNoClientCert(), | |
| ct).ConfigureAwait(false); | |
| }, ct).ConfigureAwait(false); | |
| if (enrollResponse is null || string.IsNullOrWhiteSpace(enrollResponse.CertBase64)) | |
| throw new InvalidOperationException("CSR enrollment failed: empty certificate response."); | |
| // Install cert by attaching returned cert to local private key | |
| byte[] certDer = Convert.FromBase64String(enrollResponse.CertBase64); | |
| using var issuedCertNoKey = new X509Certificate2(certDer); | |
| // Attach local private key to the issued cert | |
| using var certWithKeyTemp = issuedCertNoKey.CopyWithPrivateKey(rsa); | |
| // Persist to LocalMachine\My | |
| var installed = PersistCertificateWithPrivateKeyToLocalMachine(certWithKeyTemp); | |
| // Best-effort ACL hardening for service account | |
| TryHardenPrivateKeyAcl(installed, _serviceAccountIdentity); | |
| CurrentThumbprint = installed.Thumbprint; | |
| CurrentDeviceId = enrollResponse.DeviceId; | |
| PersistThumbprint(installed.Thumbprint!); | |
| if (!string.IsNullOrWhiteSpace(enrollResponse.DeviceId)) | |
| PersistDeviceId(enrollResponse.DeviceId!); | |
| } | |
| /// <summary> | |
| /// Creates an HttpClient using the installed client certificate (mTLS). | |
| /// Caller is responsible for disposing it. | |
| /// </summary> | |
| public HttpClient CreateMtlsHttpClient() | |
| { | |
| var cert = FindCertByPersistedThumbprint() ?? FindNewestValidCertBySubjectMarker(); | |
| if (cert is null) | |
| 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 disable server cert validation in production. | |
| // handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; | |
| var client = new HttpClient(handler) | |
| { | |
| BaseAddress = _baseApi, | |
| Timeout = _timeout | |
| }; | |
| client.DefaultRequestHeaders.UserAgent.ParseAdd("YourWorkerService/1.0"); | |
| var deviceId = LoadPersistedDeviceId(); | |
| if (!string.IsNullOrWhiteSpace(deviceId)) | |
| client.DefaultRequestHeaders.TryAddWithoutValidation("X-Device-Id", deviceId); // diagnostics only (server should not trust for auth) | |
| return client; | |
| } | |
| /// <summary> | |
| /// Renews cert when close to expiry by generating a NEW local key + CSR and sending it over mTLS. | |
| /// </summary> | |
| public async Task<bool> TryRenewCertificateAsync(int renewWhenWithinDays = 14, CancellationToken ct = default) | |
| { | |
| var existing = FindCertByPersistedThumbprint() ?? FindNewestValidCertBySubjectMarker(); | |
| if (existing is null) return false; | |
| CurrentThumbprint = existing.Thumbprint; | |
| if (existing.NotAfter.ToUniversalTime() > DateTime.UtcNow.AddDays(renewWhenWithinDays)) | |
| return true; // not due yet | |
| try | |
| { | |
| // Generate new local private key + CSR for rotation | |
| using var rsa = RSA.Create(3072); | |
| string subject = $"CN=client-{Sanitize(Environment.MachineName)}, O=YourCompany"; | |
| byte[] csrDer = CreateClientCsrDer(rsa, subject); | |
| var req = new CsrRenewRequest | |
| { | |
| CsrBase64 = Convert.ToBase64String(csrDer) | |
| }; | |
| var renewResponse = await ExecuteWithRetryAsync(async () => | |
| { | |
| using var http = CreateMtlsHttpClient(); | |
| using var content = new StringContent(JsonSerializer.Serialize(req), Encoding.UTF8, "application/json"); | |
| using var resp = await http.PostAsync("cert/renew/csr", content, ct).ConfigureAwait(false); | |
| var respText = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); | |
| if (!resp.IsSuccessStatusCode) | |
| throw new InvalidOperationException($"Renew CSR failed HTTP {(int)resp.StatusCode} {resp.ReasonPhrase}: {respText}"); | |
| return JsonSerializer.Deserialize<CsrEnrollResponse>(respText, _jsonOptions); | |
| }, ct).ConfigureAwait(false); | |
| if (renewResponse is null || string.IsNullOrWhiteSpace(renewResponse.CertBase64)) | |
| return false; | |
| byte[] certDer = Convert.FromBase64String(renewResponse.CertBase64); | |
| using var issuedCertNoKey = new X509Certificate2(certDer); | |
| using var certWithKeyTemp = issuedCertNoKey.CopyWithPrivateKey(rsa); | |
| var installed = PersistCertificateWithPrivateKeyToLocalMachine(certWithKeyTemp); | |
| TryHardenPrivateKeyAcl(installed, _serviceAccountIdentity); | |
| CurrentThumbprint = installed.Thumbprint; | |
| PersistThumbprint(installed.Thumbprint!); | |
| if (!string.IsNullOrWhiteSpace(renewResponse.DeviceId)) | |
| { | |
| CurrentDeviceId = renewResponse.DeviceId; | |
| PersistDeviceId(renewResponse.DeviceId!); | |
| } | |
| // Optional cleanup: remove old cert after successful rotation | |
| TryRemoveCertificate(existing); | |
| return true; | |
| } | |
| catch | |
| { | |
| return false; | |
| } | |
| } | |
| // ============================================================ | |
| // CSR generation | |
| // ============================================================ | |
| /// <summary> | |
| /// Creates a PKCS#10 CSR (DER bytes) for client auth. | |
| /// </summary> | |
| private static byte[] CreateClientCsrDer(RSA rsa, string subjectName) | |
| { | |
| var dn = new X500DistinguishedName(subjectName); | |
| var req = new CertificateRequest(dn, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); | |
| // Client Authentication EKU | |
| var eku = new OidCollection { new Oid("1.3.6.1.5.5.7.3.2") }; | |
| req.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(eku, critical: true)); | |
| // Digital signature + key encipherment for TLS client auth | |
| req.CertificateExtensions.Add(new X509KeyUsageExtension( | |
| X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, | |
| critical: true)); | |
| // Subject Key Identifier helps some PKI toolchains | |
| req.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(req.PublicKey, critical: false)); | |
| // DER-encoded PKCS#10 | |
| return req.CreateSigningRequest(); | |
| } | |
| // ============================================================ | |
| // HTTP + retry | |
| // ============================================================ | |
| private HttpClientHandler CreateHandlerNoClientCert() | |
| { | |
| return new HttpClientHandler | |
| { | |
| SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13 | |
| }; | |
| } | |
| 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 }; | |
| using var content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"); | |
| using var response = await client.PostAsync(url, content, ct).ConfigureAwait(false); | |
| string responseText = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false); | |
| if (!response.IsSuccessStatusCode) | |
| throw new InvalidOperationException($"HTTP {(int)response.StatusCode} {response.ReasonPhrase} :: {responseText}"); | |
| return JsonSerializer.Deserialize<T>(responseText, _jsonOptions); | |
| } | |
| private static async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> action, CancellationToken ct, int maxAttempts = 4) | |
| { | |
| Exception? last = null; | |
| for (int attempt = 1; attempt <= maxAttempts; attempt++) | |
| { | |
| ct.ThrowIfCancellationRequested(); | |
| try | |
| { | |
| return await action().ConfigureAwait(false); | |
| } | |
| catch (Exception ex) when (attempt < maxAttempts && IsTransient(ex)) | |
| { | |
| last = ex; | |
| int backoffMs = (int)Math.Pow(2, attempt - 1) * 1000; | |
| int jitterMs = Random.Shared.Next(100, 400); | |
| await Task.Delay(backoffMs + jitterMs, ct).ConfigureAwait(false); | |
| } | |
| catch (Exception ex) | |
| { | |
| last = ex; | |
| break; | |
| } | |
| } | |
| throw last ?? new InvalidOperationException("Retry operation failed."); | |
| } | |
| private static bool IsTransient(Exception ex) | |
| { | |
| return ex is HttpRequestException || ex is IOException || ex is TaskCanceledException; | |
| } | |
| // ============================================================ | |
| // Certificate persistence / lookup | |
| // ============================================================ | |
| private static X509Certificate2 PersistCertificateWithPrivateKeyToLocalMachine(X509Certificate2 certWithKey) | |
| { | |
| // Export + re-import to ensure machine-context persisted private key | |
| // (private key generated locally and remains local) | |
| byte[] pfx = certWithKey.Export(X509ContentType.Pfx, string.Empty); | |
| using var persisted = new X509Certificate2( | |
| pfx, | |
| string.Empty, | |
| X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet); | |
| using var store = new X509Store(StoreName.My, StoreLocation.LocalMachine); | |
| store.Open(OpenFlags.ReadWrite); | |
| store.Add(persisted); | |
| string thumb = NormalizeThumbprint(persisted.Thumbprint ?? string.Empty); | |
| var found = store.Certificates | |
| .OfType<X509Certificate2>() | |
| .Where(c => string.Equals(NormalizeThumbprint(c.Thumbprint ?? string.Empty), thumb, StringComparison.OrdinalIgnoreCase)) | |
| .OrderByDescending(c => c.NotAfter) | |
| .FirstOrDefault(); | |
| return found ?? persisted; | |
| } | |
| private X509Certificate2? FindCertByPersistedThumbprint() | |
| { | |
| var thumb = LoadPersistedThumbprint(); | |
| if (string.IsNullOrWhiteSpace(thumb)) return null; | |
| thumb = NormalizeThumbprint(thumb); | |
| using var store = new X509Store(StoreName.My, StoreLocation.LocalMachine); | |
| store.Open(OpenFlags.ReadOnly); | |
| var cert = store.Certificates | |
| .OfType<X509Certificate2>() | |
| .FirstOrDefault(c => | |
| c.HasPrivateKey && | |
| !string.IsNullOrWhiteSpace(c.Thumbprint) && | |
| string.Equals(NormalizeThumbprint(c.Thumbprint), thumb, StringComparison.OrdinalIgnoreCase) && | |
| c.NotAfter.ToUniversalTime() > DateTime.UtcNow.AddMinutes(5)); | |
| if (cert is null) | |
| DeletePersistedThumbprint(); | |
| return cert; | |
| } | |
| private X509Certificate2? FindNewestValidCertBySubjectMarker() | |
| { | |
| 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(_tenantSubjectMarkerFallback, StringComparison.OrdinalIgnoreCase) && | |
| c.NotAfter.ToUniversalTime() > DateTime.UtcNow.AddMinutes(5)) | |
| .OrderByDescending(c => c.NotAfter) | |
| .FirstOrDefault(); | |
| } | |
| private static void TryRemoveCertificate(X509Certificate2 cert) | |
| { | |
| try | |
| { | |
| using var store = new X509Store(StoreName.My, StoreLocation.LocalMachine); | |
| store.Open(OpenFlags.ReadWrite); | |
| store.Remove(cert); | |
| } | |
| catch | |
| { | |
| // Best-effort cleanup only | |
| } | |
| } | |
| /// <summary> | |
| /// Best-effort grant GenericRead to service account on private key (CNG keys). | |
| /// </summary> | |
| private static void TryHardenPrivateKeyAcl(X509Certificate2 cert, string? serviceAccountIdentity) | |
| { | |
| if (cert is null || !cert.HasPrivateKey || string.IsNullOrWhiteSpace(serviceAccountIdentity)) | |
| return; | |
| try | |
| { | |
| using RSA? rsa = cert.GetRSAPrivateKey(); | |
| if (rsa is RSACng rsaCng) | |
| { | |
| var key = rsaCng.Key; | |
| var sec = key.GetAccessControl(); | |
| sec.AddAccessRule(new CngAccessRule( | |
| new NTAccount(serviceAccountIdentity), | |
| CngKeyRights.GenericRead, | |
| AccessControlType.Allow)); | |
| key.SetAccessControl(sec); | |
| return; | |
| } | |
| using ECDsa? ecdsa = cert.GetECDsaPrivateKey(); | |
| if (ecdsa is ECDsaCng ecdsaCng) | |
| { | |
| var key = ecdsaCng.Key; | |
| var sec = key.GetAccessControl(); | |
| sec.AddAccessRule(new CngAccessRule( | |
| new NTAccount(serviceAccountIdentity), | |
| CngKeyRights.GenericRead, | |
| AccessControlType.Allow)); | |
| key.SetAccessControl(sec); | |
| return; | |
| } | |
| } | |
| catch | |
| { | |
| // Best effort only (log in your worker if desired) | |
| } | |
| } | |
| // ============================================================ | |
| // Registry + DPAPI persistence | |
| // ============================================================ | |
| private void PersistThumbprint(string thumbprint) | |
| => WriteProtectedRegistryString("ClientCertThumbprint", NormalizeThumbprint(thumbprint)); | |
| private string? LoadPersistedThumbprint() | |
| => ReadProtectedRegistryString("ClientCertThumbprint"); | |
| private void DeletePersistedThumbprint() | |
| { | |
| using var key = Registry.LocalMachine.CreateSubKey(_registryPath, writable: true); | |
| key?.DeleteValue("ClientCertThumbprint", throwOnMissingValue: false); | |
| } | |
| private void PersistDeviceId(string deviceId) | |
| => WriteProtectedRegistryString("DeviceId", deviceId); | |
| public string? LoadPersistedDeviceId() | |
| { | |
| var v = ReadProtectedRegistryString("DeviceId"); | |
| CurrentDeviceId = v; | |
| return v; | |
| } | |
| private void WriteProtectedRegistryString(string valueName, string plaintext) | |
| { | |
| byte[] plain = Encoding.UTF8.GetBytes(plaintext); | |
| byte[] protectedBytes = ProtectedData.Protect(plain, _dpapiEntropy, DataProtectionScope.LocalMachine); | |
| string b64 = Convert.ToBase64String(protectedBytes); | |
| using var key = Registry.LocalMachine.CreateSubKey(_registryPath, writable: true) | |
| ?? throw new InvalidOperationException($"Unable to open/create HKLM\\{_registryPath}"); | |
| key.SetValue(valueName, b64, RegistryValueKind.String); | |
| } | |
| private string? ReadProtectedRegistryString(string valueName) | |
| { | |
| using var key = Registry.LocalMachine.OpenSubKey(_registryPath, writable: false); | |
| var b64 = key?.GetValue(valueName) as string; | |
| if (string.IsNullOrWhiteSpace(b64)) return null; | |
| try | |
| { | |
| byte[] protectedBytes = Convert.FromBase64String(b64); | |
| byte[] plain = ProtectedData.Unprotect(protectedBytes, _dpapiEntropy, DataProtectionScope.LocalMachine); | |
| return Encoding.UTF8.GetString(plain); | |
| } | |
| catch | |
| { | |
| return null; | |
| } | |
| } | |
| // ============================================================ | |
| // Helpers + DTOs | |
| // ============================================================ | |
| private static string NormalizeThumbprint(string thumb) | |
| => thumb.Replace(" ", "", StringComparison.Ordinal).Trim().ToUpperInvariant(); | |
| private static string Sanitize(string? value) | |
| { | |
| if (string.IsNullOrWhiteSpace(value)) return "unknown"; | |
| var chars = value.Where(c => char.IsLetterOrDigit(c) || c == '-' || c == '_').ToArray(); | |
| return new string(chars); | |
| } | |
| private sealed class CsrEnrollRequest | |
| { | |
| public string EnrollmentCode { get; set; } = ""; | |
| public string? MachineName { get; set; } | |
| public string CsrBase64 { get; set; } = ""; | |
| } | |
| private sealed class CsrRenewRequest | |
| { | |
| public string CsrBase64 { get; set; } = ""; | |
| } | |
| private sealed class CsrEnrollResponse | |
| { | |
| public string? TenantId { get; set; } | |
| public string? DeviceId { get; set; } | |
| public string? Thumbprint { get; set; } | |
| public DateTime? NotAfterUtc { get; set; } | |
| public string? CertBase64 { get; set; } // DER/PEM-less cert bytes, base64 encoded | |
| } | |
| } | |
| /* | |
| ======================================== | |
| EXAMPLE USAGE IN .NET 10 WORKER SERVICE | |
| ======================================== | |
| Worker.cs (snippet) | |
| 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) | |
| { | |
| _mtls = new MtlsEnrollmentClient( | |
| baseApiUrl: _config["ApiSettings:BaseUrl"]!, | |
| tenantSubjectMarkerFallback: _config["ApiSettings:TenantSubjectMarker"]!, // e.g. OU=tenant_demo_001 | |
| registryPath: _config["ApiSettings:RegistryPath"] ?? @"Software\YourCompany\IntegrationAgent\Security", | |
| serviceAccountIdentity: _config["ApiSettings:ServiceAccountIdentity"]); // optional but recommended | |
| await _mtls.EnsureClientCertificateInstalledAsync(_config["ApiSettings:EnrollmentCode"], stoppingToken); | |
| _logger.LogInformation("mTLS ready. Thumbprint={Thumbprint}, DeviceId={DeviceId}", | |
| _mtls.CurrentThumbprint, _mtls.LoadPersistedDeviceId()); | |
| while (!stoppingToken.IsCancellationRequested) | |
| { | |
| try | |
| { | |
| bool renewOk = await _mtls.TryRenewCertificateAsync(ct: stoppingToken); | |
| if (!renewOk) | |
| _logger.LogWarning("Renewal check failed."); | |
| using var http = _mtls.CreateMtlsHttpClient(); | |
| using var resp = await http.GetAsync("phi/ping", stoppingToken); | |
| var body = await resp.Content.ReadAsStringAsync(stoppingToken); | |
| _logger.LogInformation("phi/ping => {Status} {Body}", (int)resp.StatusCode, body); | |
| } | |
| catch (Exception ex) | |
| { | |
| _logger.LogError(ex, "mTLS call failed"); | |
| } | |
| await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); | |
| } | |
| } | |
| } | |
| */ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment