삽질하다 도저히 문제 파악이 안돼서 opcUaManager로 분리 테스트 중
This commit is contained in:
221
OpcPksPlatform/Documents/CertificateGenerator.cs
Normal file
221
OpcPksPlatform/Documents/CertificateGenerator.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
OpcPksPlatform/Documents/application_rsa_sha256.der
Normal file
BIN
OpcPksPlatform/Documents/application_rsa_sha256.der
Normal file
Binary file not shown.
24
OpcPksPlatform/Documents/pki/own/certs/8845hs.crt
Executable file
24
OpcPksPlatform/Documents/pki/own/certs/8845hs.crt
Executable 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-----
|
||||
28
OpcPksPlatform/Documents/pki/own/private/8845hs.key
Executable file
28
OpcPksPlatform/Documents/pki/own/private/8845hs.key
Executable 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-----
|
||||
BIN
OpcPksPlatform/Documents/pki/own/private/OpcTestClient.pfx
Executable file
BIN
OpcPksPlatform/Documents/pki/own/private/OpcTestClient.pfx
Executable file
Binary file not shown.
BIN
OpcPksPlatform/Documents/pki/trusted/certs/application_rsa_sha256.der
Executable file
BIN
OpcPksPlatform/Documents/pki/trusted/certs/application_rsa_sha256.der
Executable file
Binary file not shown.
BIN
OpcPksPlatform/Documents/pki/trusted/certs/rootCA.der
Executable file
BIN
OpcPksPlatform/Documents/pki/trusted/certs/rootCA.der
Executable file
Binary file not shown.
BIN
OpcPksPlatform/Documents/rootCA.der
Executable file
BIN
OpcPksPlatform/Documents/rootCA.der
Executable file
Binary file not shown.
Reference in New Issue
Block a user