삽질하다 도저히 문제 파악이 안돼서 opcUaManager로 분리 테스트 중

This commit is contained in:
2026-02-25 08:52:03 +09:00
parent 4ea351946a
commit e88ab87771
138 changed files with 1051971 additions and 351 deletions

View File

@@ -0,0 +1,221 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Net;
using System.Net.NetworkInformation;
using OpcPks.Core.Models;
using System.Diagnostics;
using System.Text;
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<bool> CreateHoneywellCertificateAsync(CertRequestModel model)
{
try
{
string appName = "OpcPksClient";
// 초기 성공 모델과 동일한 URI 패턴 유지
string applicationUri = $"urn:{Environment.MachineName}:OpcPksClient";
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)!);
// 1. RSA 키 생성 (2048-bit)
using var rsa = RSA.Create(2048);
// 2. 인증서 요청 생성
var request = new CertificateRequest(
$"CN={appName}",
rsa,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
// 3. [Critical] 하니웰이 요구하는 '신분증 지문' 및 '도장' 추가
// A. Subject Key Identifier (이게 없으면 하니웰 엔진이 인증서를 무시합니다)
request.CertificateExtensions.Add(
new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
// B. Subject Alternative Name (SAN)
var sanBuilder = new SubjectAlternativeNameBuilder();
sanBuilder.AddUri(new Uri(applicationUri));
sanBuilder.AddDnsName(Environment.MachineName);
sanBuilder.AddDnsName("localhost");
foreach (var ip in GetLocalIpAddresses()) sanBuilder.AddIpAddress(ip);
request.CertificateExtensions.Add(sanBuilder.Build());
// C. Key Usage (하니웰 보안 4종 세트)
request.CertificateExtensions.Add(new X509KeyUsageExtension(
X509KeyUsageFlags.DigitalSignature |
X509KeyUsageFlags.KeyEncipherment |
X509KeyUsageFlags.NonRepudiation |
X509KeyUsageFlags.DataEncipherment,
true));
// D. Enhanced Key Usage (EKU)
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));
// E. Basic Constraints (End Entity 명시)
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true));
// 4. 인증서 생성 (초기 코드처럼 넉넉하게 10년)
using var certificate = request.CreateSelfSigned(
DateTimeOffset.UtcNow.AddDays(-1),
DateTimeOffset.UtcNow.AddYears(10));
// 5. 파일 저장
// PFX는 암호 없이 추출 (호환성)
await File.WriteAllBytesAsync(pfxPath, certificate.Export(X509ContentType.Pfx));
// DER은 하니웰 서버 전송용
await File.WriteAllBytesAsync(derPath, certificate.Export(X509ContentType.Cert));
Console.WriteLine($"✅ [Step 1] 하니웰 지문(SKI) 포함 인증서 제작 완료: {derPath}");
// 6. 전송 및 검증
string primaryIp = model.PrimaryIpA?.Trim() ?? "";
if (string.IsNullOrEmpty(primaryIp)) throw new Exception("Primary IP 주소가 비어있습니다.");
await TransferAndVerifyWithRetryAsync(model, primaryIp, $"{appName}.der", derPath);
return true;
}
catch (Exception ex)
{
Console.WriteLine($"❌ 작업 최종 실패: {ex.Message}");
throw;
}
}
private async Task TransferAndVerifyWithRetryAsync(CertRequestModel model, string ip, string fileName, string localPath)
{
string winTrustedDir = @"ProgramData\Honeywell\Experion PKS\Server\data\CertStore\opcuaserver\pki\DefaultApplicationGroup\trusted\certs";
string winRejectedDir = @"ProgramData\Honeywell\Experion PKS\Server\data\CertStore\opcuaserver\pki\DefaultApplicationGroup\rejected\certs";
string auth = $"{model.AdminId}%{model.AdminPassword}";
int maxRetries = 3;
bool isAuthorized = false;
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
Console.WriteLine($"\n🔄 [{ip}] {attempt}회차 시퀀스 시작...");
string trustedFile = $"{winTrustedDir}\\{fileName}";
string rejectedFile = $"{winRejectedDir}\\{fileName}";
// 1. 기존 파일 삭제 및 새 인증서 전송
await DeleteFileFromHoneywell(ip, trustedFile, auth);
await PutFileToHoneywell(ip, localPath, trustedFile, auth);
await Task.Delay(3000);
// 2. 접속 시도 (하니웰 반응 유도)
isAuthorized = await _sessionManager.SayHelloAsync(ip);
if (isAuthorized)
{
Console.WriteLine($"🎯 [{ip}] 인증 성공!");
return;
}
Console.WriteLine($"⚠️ [{ip}] 거절됨(0x80830000). Rejected 폴더 확인 중...");
await Task.Delay(2000);
// 3. Rejected 유배지로 갔다면 강제 이동 (하니웰의 '불신' 극복)
if (await CheckFileExists(ip, rejectedFile, auth))
{
Console.WriteLine($"🚩 [{ip}] Rejected 유배 확인! Trusted로 강제 이동합니다.");
string moveArgs = $"//{ip}/C$ -U \"{auth}\" -c \"rename \\\"{rejectedFile}\\\" \\\"{trustedFile}\\\"\"";
await ExecuteSmbCommandAsync(moveArgs);
await Task.Delay(2000);
if (await _sessionManager.SayHelloAsync(ip)) return;
}
}
if (!isAuthorized) throw new Exception($"❌ [{ip}] 지문 인식 실패 또는 Extensions 불일치. 수동 Trust 필요.");
}
private async Task PutFileToHoneywell(string ip, string localPath, string remotePath, string auth)
{
string args = $"//{ip}/C$ -U \"{auth}\" -c \"put \\\"{localPath}\\\" \\\"{remotePath}\\\"\"";
await ExecuteSmbCommandAsync(args);
}
private async Task DeleteFileFromHoneywell(string ip, string remotePath, string auth)
{
try {
string args = $"//{ip}/C$ -U \"{auth}\" -c \"del \\\"{remotePath}\\\"\"";
await ExecuteSmbCommandAsync(args);
} catch { }
}
private async Task<bool> CheckFileExists(string ip, string remotePath, string auth)
{
try {
string args = $"//{ip}/C$ -U \"{auth}\" -c \"allinfo \\\"{remotePath}\\\"\"";
string result = await ExecuteSmbCommandWithOutputAsync(args);
return !result.Contains("NT_STATUS_OBJECT_NAME_NOT_FOUND");
} catch { return false; }
}
private async Task ExecuteSmbCommandAsync(string args)
{
using var process = new Process {
StartInfo = new ProcessStartInfo {
FileName = "smbclient",
Arguments = args,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
string error = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
if (process.ExitCode != 0 && !error.Contains("NT_STATUS_OBJECT_NAME_NOT_FOUND"))
throw new Exception($"SMB 실패: {error.Trim()}");
}
private async Task<string> ExecuteSmbCommandWithOutputAsync(string args)
{
using var process = new Process {
StartInfo = new ProcessStartInfo {
FileName = "smbclient",
Arguments = args,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
string output = await process.StandardOutput.ReadToEndAsync();
string error = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
return output + error;
}
private List<IPAddress> GetLocalIpAddresses()
{
return NetworkInterface.GetAllNetworkInterfaces()
.Where(i => i.OperationalStatus == OperationalStatus.Up)
.SelectMany(i => i.GetIPProperties().UnicastAddresses)
.Where(a => a.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
.Select(a => a.Address).ToList();
}
}
}

Binary file not shown.

View File

@@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIIEAzCCAuugAwIBAgIUdxkuyO0kUlLnKYoSV4bt7FJZ7+4wDQYJKoZIhvcNAQEL
BQAwgZAxCzAJBgNVBAYTAktPMQ4wDAYDVQQIDAVTZW91bDEOMAwGA1UEBwwFU2Vv
dWwxFjAUBgNVBAoMDWhhbm1vIGNvbnRyb2wxEDAOBgNVBAsMB2NvbnRyb2wxEDAO
BgNVBAMMB25ldHdvcmsxJTAjBgkqhkiG9w0BCQEWFmNvbnRhY3RAaGFubW9jbm4u
Y28ua3IwHhcNMjYwMjIyMDUzNDExWhcNMzYwMjIwMDUzNDExWjCBkDELMAkGA1UE
BhMCS08xDjAMBgNVBAgMBVNlb3VsMQ4wDAYDVQQHDAVTZW91bDEWMBQGA1UECgwN
aGFubW8gY29udHJvbDEQMA4GA1UECwwHY29udHJvbDEQMA4GA1UEAwwHbmV0d29y
azElMCMGCSqGSIb3DQEJARYWY29udGFjdEBoYW5tb2Nubi5jby5rcjCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBANV459zeHqHfvkfVfPeXu3lUOQfHvX9T
ca2roF8G1e3NKUQJeuCPCCwK1AM+++lyw70iGmIoULkSwMtIlAloKtyrf5FG/LWB
bUdjirF157CrkpITXmNuDDxTbVn6YXLZHC5R+OhZXH32iUj1aTq3tx37lOmTh3R/
IZOGxsLkCGDni0GirE54Z0ufdl6CFr8FfcakVO+9Y6SEUEsZp4uybrA5LfprZlj/
2uXu9svMoePTOb8D7MMg6WfZrsOb4hMg483P9t1rHqMg2AESbOT4N9H3HFDDrA5l
w7KozmIwCeskYeLQPGX/DQ+GxYGoezfsWSM1fXKM/0XNskXqZGmi59UCAwEAAaNT
MFEwHQYDVR0OBBYEFI3WwCA/ZNQgUkTaZtQttffbcnpeMB8GA1UdIwQYMBaAFI3W
wCA/ZNQgUkTaZtQttffbcnpeMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
BQADggEBAEHVsKOCTJSY7Hk0rLXb5yU7qbiS3uHq8elm+2JGaodJZ7TPnWuyqkEF
9/7713Y7MAXHcv2qEYeIE+qS4ZrlT9Xz+xf452zd+xRpUq6j4PBlKwEM77uXHnNl
yO0g1GgIdBRz5F61ESWfvwfR1GJ2Q3j0ry5qZPlVD1KjJyGYL8DJdVBsg/yJ62+M
xYFSk2JrRxU/JiV/Dq7Mxy3hvGupVGjdcQdnW/1xZK+xmWDG5ky+u/3qcH9nNlce
UiblRoYjlrdHnrVi3VAJFaquJZNwDr/Ar+LSxDHjEzDtpby9Q6VbtDnA77GcG7vn
TsBE6iGyly9WG9Q0dgNKEw59ciRGi6M=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDVeOfc3h6h375H
1Xz3l7t5VDkHx71/U3Gtq6BfBtXtzSlECXrgjwgsCtQDPvvpcsO9IhpiKFC5EsDL
SJQJaCrcq3+RRvy1gW1HY4qxdeewq5KSE15jbgw8U21Z+mFy2RwuUfjoWVx99olI
9Wk6t7cd+5Tpk4d0fyGThsbC5Ahg54tBoqxOeGdLn3Zegha/BX3GpFTvvWOkhFBL
GaeLsm6wOS36a2ZY/9rl7vbLzKHj0zm/A+zDIOln2a7Dm+ITIOPNz/bdax6jINgB
Emzk+DfR9xxQw6wOZcOyqM5iMAnrJGHi0Dxl/w0PhsWBqHs37FkjNX1yjP9FzbJF
6mRpoufVAgMBAAECggEABhcvglk3WttP31Z40iBFoeC5ydOJ6rk+QTwS1MiUpPbE
Cnlnb0MVcsWSUU9kOj+4fvZwoJlRiEjqhZeaBaoFAwvE6tJcK8TivHUww/8UWlw4
JSvL9x6bgcMdl8IcMqh9dILejP6JCYerA2ZhF2LzegsEr3oH6ivQZkL4+8mBcFui
1w2vuIMGlJbFbBjiWWvuWe04dRtKQ172PcH1980IGjT3zjwBzn3nbR69LBC+5tDj
ORPs/uORUb9SKAtrxlhggRQIehBuTqbmgAiEwEZXH0OaNLUZzUM6b8XfrCLYXEM9
Cexh5apqpTR0jDV7WVrCTOSvQ6VcfO5+ibs4DkeqUQKBgQDtDuH347CXDaaWuQqK
yOAugpWohFM0LArZySV6FXjX/fqOfpWwGRY6SAftF+GmR6MiPphjuI4yW9DsZG1h
8FWLwZrwkazVbk2cHwU4CZhoQVuNHhLYhF3G2QcyNqlL8uHb49z4iyvTYr3XFMgn
p7qULd0BFfzb04butWylgVy5GQKBgQDmh5Cz7wCeZBOR1KjAJqo8O2BhHp6j2GsA
5OmaxZw++O0uqh0JZnJpjXkyEpR7kUOM8+3uvuAti68kQis3chFNlITzNNLLZaDO
oy8qwPvPzTSbUVzDD2acfNrM7oHnsJ6IHmHMnzQGLg9zjGJt31wNtJyfdOp7Kn33
Q06QRD9wHQKBgQClj65b3YZ4iM0fGQ72zMJdWVBCiGA/4L6XSfdFo3dpinUSTfAn
M+4lOCdo/DPZWNDjWso9YyjUnPF2F9GZBCwK1mVqvKLz0PydG8EeWP07WuIg1a8d
zpxcAzkWZbypUXFSjHrIjxJFqQGjFF2R7H/Pe5SNbJjTwpDLaKP/lzB2CQKBgDe8
TQMD5O1mmsimVspmTsBTRsEUaxyIBY7oyYYPAvDCtG2U2YJdT4ovlz7A+T9K5r8c
dslDQuYgII8upE46eO592wsGGXTttExhbdTzZa5fGbn3mOrcPV3WXfwwKh4/OIUG
e3TChQx9dGTmayHPX+08XqW62bo/kscGcec1aPUNAoGAUq++tYrNfzWvu8GXTpTI
KWhHxRNd3zOOjdG62ss/dFCuZ+hr1gEKZxsHgvEO56wbzNTPsZdY+oq6p/hrOS7P
IsNvSmZO0QJeOxnU/NZ1Hyd7sHCYp7guyJOhhPAuFMa7w/8yrYzBVe3PwTp/cnEk
lx20RyQMp2eQSQF0gs6hr1U=
-----END PRIVATE KEY-----

Binary file not shown.

Binary file not shown.

Binary file not shown.