using System.Net.Http.Json; using System.Security.Cryptography; using System.Text; using Microsoft.Extensions.Logging; using Microsoft.Win32; using PatchProbe.Cli.Auth; using PatchProbe.Engine.Contracts.ApiModels; namespace PatchProbe.Cli.Services; internal sealed class EnrollmentService( IHttpClientFactory httpClientFactory, IDeviceCredentialStore credentialStore, ILogger logger) { public async Task EnrollAsync(string serverUrl, string enrollmentKey, CancellationToken ct = default) { using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); var publicKeySpki = Convert.ToBase64String(ecdsa.ExportSubjectPublicKeyInfo()); var privateKeyPkcs8 = Convert.ToBase64String(ecdsa.ExportPkcs8PrivateKey()); var requestBody = new EnrollmentRequest( EnrollmentKey: enrollmentKey, MachineName: Environment.MachineName, DeviceFingerprint: GetMachineFingerprint(), PublicKeySpki: publicKeySpki); var http = httpClientFactory.CreateClient(); var url = $"{serverUrl.TrimEnd('/')}/api/enrollments"; logger.LogInformation("Enrolling device with server at {Url}", url); var response = await http.PostAsJsonAsync(url, requestBody, ct); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync(cancellationToken: ct) ?? throw new InvalidOperationException("Server returned an empty enrollment response."); credentialStore.Save(new DeviceCredentials( DeviceId: result.DeviceId, PrivateKeyPkcs8: privateKeyPkcs8, ServerUrl: serverUrl)); logger.LogInformation("Enrollment complete — Device ID: {DeviceId}", result.DeviceId); } private static string GetMachineFingerprint() { // MachineGuid is a stable, per-install GUID set by Windows Setup. using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography"); var guid = key?.GetValue("MachineGuid")?.ToString() ?? Environment.MachineName; return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(guid))).ToLowerInvariant(); } }