using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Net; using System.Net.NetworkInformation; using OpcPks.Core.Models; using System.Diagnostics; namespace OpcPks.Core.Services { public class CertificateGenerator { private readonly string _baseDataPath; private readonly OpcSessionManager _sessionManager; public CertificateGenerator(string baseDataPath, OpcSessionManager sessionManager) { _baseDataPath = baseDataPath; _sessionManager = sessionManager; } public async Task CreateHoneywellCertificateAsync(CertRequestModel model) { try { string appName = "OpcPksClient"; // [중요] 호스트네임은 대문자로 처리 (윈도우 기본값 준수) string hostName = Environment.MachineName.ToUpper(); string applicationUri = $"urn:{hostName}:{appName}"; string derPath = Path.Combine(_baseDataPath, "own", "certs", $"{appName}.der"); string pfxPath = Path.Combine(_baseDataPath, "own", "private", $"{appName}.pfx"); Directory.CreateDirectory(Path.GetDirectoryName(derPath)!); Directory.CreateDirectory(Path.GetDirectoryName(pfxPath)!); using var rsa = RSA.Create(2048); // [교정] 오직 CN만 포함 (Configuration Error 방지를 위해 불필요한 필드 제거) var request = new CertificateRequest( $"CN={appName}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); // 1. Subject Key Identifier (필수 지문) request.CertificateExtensions.Add( new X509SubjectKeyIdentifierExtension(request.PublicKey, false)); // 2. SAN (URI와 DNS 단 하나씩만 - 하니웰의 해석 오류 방지) var sanBuilder = new SubjectAlternativeNameBuilder(); sanBuilder.AddUri(new Uri(applicationUri)); sanBuilder.AddDnsName(hostName); request.CertificateExtensions.Add(sanBuilder.Build()); // 3. Key Usage (가장 표준적인 3종으로 축소) // NonRepudiation 플래그가 가끔 BadConfiguration을 유발하므로 뺍니다. request.CertificateExtensions.Add(new X509KeyUsageExtension( X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DataEncipherment, true)); // 4. Enhanced Key Usage request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension( new OidCollection { new Oid("1.3.6.1.5.5.7.3.1"), // Server Auth new Oid("1.3.6.1.5.5.7.3.2") // Client Auth }, false)); // 5. Basic Constraints (End Entity 명시) request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true)); // [교정] 시작 시간을 1시간 전으로 설정하여 하니웰 서버와의 시간차 방어 using var certificate = request.CreateSelfSigned( DateTimeOffset.UtcNow.AddHours(-1), DateTimeOffset.UtcNow.AddYears(5)); await File.WriteAllBytesAsync(pfxPath, certificate.Export(X509ContentType.Pfx)); await File.WriteAllBytesAsync(derPath, certificate.Export(X509ContentType.Cert)); Console.WriteLine($"\n--- [수동 작업 지시: mngr 계정 모드] ---"); Console.WriteLine($"1. 파일 생성 완료: {derPath}"); Console.WriteLine($"2. 하니웰 서버의 Trusted/certs 폴더에 기존 파일을 지우고 새 파일을 넣으세요."); Console.WriteLine($"3. 'mngr' 계정의 권한으로 복사가 완료되면 엔터를 치세요."); Console.ReadLine(); string primaryIp = model.PrimaryIpA?.Trim() ?? ""; // 바로 접속 테스트 진행 return await _sessionManager.SayHelloAsync(primaryIp); } catch (Exception ex) { Console.WriteLine($"❌ 작업 실패: {ex.Message}"); throw; } } } }