using Opc.Ua; using System.Security.Cryptography.X509Certificates; using OpcPks.Core.Models; namespace OpcPks.Core.Services { public class CertificateGenerator { private readonly string _pfxPath; private readonly string _ownCertPath; public CertificateGenerator(string baseDataPath) { // 경로 설정 (현재 tree 구조 반영) _pfxPath = Path.Combine(baseDataPath, "pki/own/private/OpcTestClient.pfx"); _ownCertPath = Path.Combine(baseDataPath, "pki/own/certs/OpcTestClient.der"); } public async Task CreateHoneywellCertificateAsync(CertRequestModel model) { try { // 1. [중요] 기존 인증서 백업 BackupExistingCertificate(); Console.WriteLine("🔐 하니웰 맞춤형 인증서 생성을 시작합니다..."); // SAN 리스트 구축 (localhost 기본 포함) var subjectAlternativeNames = new List { "localhost", "127.0.0.1" }; // Primary 정보 (FTE Yellow/Green) if (!string.IsNullOrEmpty(model.PrimaryHostName)) { subjectAlternativeNames.Add(model.PrimaryHostName); subjectAlternativeNames.Add($"{model.PrimaryHostName}A"); } if (!string.IsNullOrEmpty(model.PrimaryIpA)) subjectAlternativeNames.Add(model.PrimaryIpA); if (!string.IsNullOrEmpty(model.PrimaryIpB)) subjectAlternativeNames.Add(model.PrimaryIpB); // Secondary 정보 (Redundant 선택 시) if (model.IsRedundant) { if (!string.IsNullOrEmpty(model.SecondaryHostName)) { subjectAlternativeNames.Add(model.SecondaryHostName); subjectAlternativeNames.Add($"{model.SecondaryHostName}B"); } if (!string.IsNullOrEmpty(model.SecondaryIpA)) subjectAlternativeNames.Add(model.SecondaryIpA); if (!string.IsNullOrEmpty(model.SecondaryIpB)) subjectAlternativeNames.Add(model.SecondaryIpB); } // 2. 인증서 생성 (KeySize 2048, 하니웰 규격) // var certificate = CertificateFactory.CreateCertificate( // model.ApplicationName, // model.ApplicationName, // null, // subjectAlternativeNames, // null, // 2048, // DateTime.UtcNow.AddDays(-1), // model.ValidityYears * 12, // null // ); // 2. 인증서 생성 (로그에 표시된 15개 인자 서명 순서 엄격 준수) ushort keySize = 2048; ushort lifetimeInMonths = (ushort)(model.ValidityYears * 12); var certificate = CertificateFactory.CreateCertificate( "Directory", // 1. storeType (string) Path.GetDirectoryName(_pfxPath), // 2. storePath (string) null, // 3. password (string) $"urn:localhost:{model.ApplicationName}", // 4. applicationUri (string) model.ApplicationName, // 5. applicationName (string) model.ApplicationName, // 6. subjectName (string) subjectAlternativeNames, // 7. domainNames (IList) keySize, // 8. keySize (ushort) DateTime.UtcNow.AddDays(-1), // 9. startTime (DateTime) lifetimeInMonths, // 10. lifetimeInMonths (ushort) (ushort)256, // 11. hashSizeInBits (ushort) false, // 12. isCA (bool) null, // 13. issuer (X509Certificate2) null, // 14. privateKey (byte[]) 0 // 15. hashAlgorithm (int) ); // 3. 파일 저장 Directory.CreateDirectory(Path.GetDirectoryName(_pfxPath)!); Directory.CreateDirectory(Path.GetDirectoryName(_ownCertPath)!); // PFX (개인키 포함) 저장 byte[] pfxBytes = certificate.Export(X509ContentType.Pfx, ""); await File.WriteAllBytesAsync(_pfxPath, pfxBytes); // DER (공개키만) 저장 - 하니웰 서버에 수동으로 신뢰 등록할 때 필요할 수 있음 byte[] derBytes = certificate.Export(X509ContentType.Cert); await File.WriteAllBytesAsync(_ownCertPath, derBytes); Console.WriteLine($"✅ 새 인증서 생성 완료 및 백업 성공."); return true; } catch (Exception ex) { Console.WriteLine($"❌ 인증서 생성 실패: {ex.Message}"); return false; } } private void BackupExistingCertificate() { if (File.Exists(_pfxPath)) { string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); string backupPath = _pfxPath + "." + timestamp + ".bak"; File.Copy(_pfxPath, backupPath, true); Console.WriteLine($"📦 기존 인증서 백업됨: {Path.GetFileName(backupPath)}"); } } } }