삽질하다 도저히 문제 파악이 안돼서 opcUaManager로 분리 테스트 중
This commit is contained in:
@@ -1,120 +1,96 @@
|
||||
using Opc.Ua;
|
||||
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 _pfxPath;
|
||||
private readonly string _ownCertPath;
|
||||
private readonly string _baseDataPath;
|
||||
private readonly OpcSessionManager _sessionManager;
|
||||
|
||||
public CertificateGenerator(string baseDataPath)
|
||||
public CertificateGenerator(string baseDataPath, OpcSessionManager sessionManager)
|
||||
{
|
||||
// 경로 설정 (현재 tree 구조 반영)
|
||||
_pfxPath = Path.Combine(baseDataPath, "pki/own/private/OpcTestClient.pfx");
|
||||
_ownCertPath = Path.Combine(baseDataPath, "pki/own/certs/OpcTestClient.der");
|
||||
_baseDataPath = baseDataPath;
|
||||
_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
public async Task<bool> CreateHoneywellCertificateAsync(CertRequestModel model)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. [중요] 기존 인증서 백업
|
||||
BackupExistingCertificate();
|
||||
|
||||
Console.WriteLine("🔐 하니웰 맞춤형 인증서 생성을 시작합니다...");
|
||||
|
||||
// SAN 리스트 구축 (localhost 기본 포함)
|
||||
var subjectAlternativeNames = new List<string> { "localhost", "127.0.0.1" };
|
||||
string appName = "OpcPksClient";
|
||||
// [중요] 호스트네임은 대문자로 처리 (윈도우 기본값 준수)
|
||||
string hostName = Environment.MachineName.ToUpper();
|
||||
string applicationUri = $"urn:{hostName}:{appName}";
|
||||
|
||||
// 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);
|
||||
string derPath = Path.Combine(_baseDataPath, "own", "certs", $"{appName}.der");
|
||||
string pfxPath = Path.Combine(_baseDataPath, "own", "private", $"{appName}.pfx");
|
||||
|
||||
// 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);
|
||||
}
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(derPath)!);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(pfxPath)!);
|
||||
|
||||
// 2. 인증서 생성 (KeySize 2048, 하니웰 규격)
|
||||
// var certificate = CertificateFactory.CreateCertificate(
|
||||
// model.ApplicationName,
|
||||
// model.ApplicationName,
|
||||
// null,
|
||||
// subjectAlternativeNames,
|
||||
// null,
|
||||
// 2048,
|
||||
// DateTime.UtcNow.AddDays(-1),
|
||||
// model.ValidityYears * 12,
|
||||
// null
|
||||
// );
|
||||
using var rsa = RSA.Create(2048);
|
||||
// [교정] 오직 CN만 포함 (Configuration Error 방지를 위해 불필요한 필드 제거)
|
||||
var request = new CertificateRequest(
|
||||
$"CN={appName}",
|
||||
rsa,
|
||||
HashAlgorithmName.SHA256,
|
||||
RSASignaturePadding.Pkcs1);
|
||||
|
||||
// 2. 인증서 생성 (로그에 표시된 15개 인자 서명 순서 엄격 준수)
|
||||
ushort keySize = 2048;
|
||||
ushort lifetimeInMonths = (ushort)(model.ValidityYears * 12);
|
||||
// 1. Subject Key Identifier (필수 지문)
|
||||
request.CertificateExtensions.Add(
|
||||
new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
|
||||
|
||||
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<string>)
|
||||
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)
|
||||
);
|
||||
// 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));
|
||||
|
||||
// 3. 파일 저장
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_pfxPath)!);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_ownCertPath)!);
|
||||
// 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));
|
||||
|
||||
// PFX (개인키 포함) 저장
|
||||
byte[] pfxBytes = certificate.Export(X509ContentType.Pfx, "");
|
||||
await File.WriteAllBytesAsync(_pfxPath, pfxBytes);
|
||||
// 5. Basic Constraints (End Entity 명시)
|
||||
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true));
|
||||
|
||||
// DER (공개키만) 저장 - 하니웰 서버에 수동으로 신뢰 등록할 때 필요할 수 있음
|
||||
byte[] derBytes = certificate.Export(X509ContentType.Cert);
|
||||
await File.WriteAllBytesAsync(_ownCertPath, derBytes);
|
||||
// [교정] 시작 시간을 1시간 전으로 설정하여 하니웰 서버와의 시간차 방어
|
||||
using var certificate = request.CreateSelfSigned(
|
||||
DateTimeOffset.UtcNow.AddHours(-1),
|
||||
DateTimeOffset.UtcNow.AddYears(5));
|
||||
|
||||
Console.WriteLine($"✅ 새 인증서 생성 완료 및 백업 성공.");
|
||||
return true;
|
||||
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}");
|
||||
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)}");
|
||||
Console.WriteLine($"❌ 작업 실패: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user