삽질하다 도저히 문제 파악이 안돼서 opcUaManager로 분리 테스트 중
This commit is contained in:
158
opcUaManager/Services/CertService.cs
Normal file
158
opcUaManager/Services/CertService.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using OpcUaManager.Models;
|
||||
|
||||
namespace OpcUaManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// OPC UA 클라이언트용 X.509 인증서를 생성하고 PFX로 저장합니다.
|
||||
/// 원본 Program.cs 의 인증서 체계(pki/ 폴더 구조, X509KeyStorageFlags)를 유지합니다.
|
||||
/// </summary>
|
||||
public class CertService
|
||||
{
|
||||
private readonly ILogger<CertService> _logger;
|
||||
private readonly string _pkiRoot;
|
||||
|
||||
public CertService(ILogger<CertService> logger, IWebHostEnvironment env)
|
||||
{
|
||||
_logger = logger;
|
||||
_pkiRoot = Path.Combine(env.ContentRootPath, "pki");
|
||||
}
|
||||
|
||||
private void EnsurePkiDirectories()
|
||||
{
|
||||
Directory.CreateDirectory(Path.Combine(_pkiRoot, "own", "certs"));
|
||||
Directory.CreateDirectory(Path.Combine(_pkiRoot, "trusted", "certs"));
|
||||
Directory.CreateDirectory(Path.Combine(_pkiRoot, "issuers", "certs"));
|
||||
Directory.CreateDirectory(Path.Combine(_pkiRoot, "rejected", "certs"));
|
||||
}
|
||||
|
||||
public Task<CertCreateResult> GenerateAsync(CertCreateRequest req)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsurePkiDirectories();
|
||||
|
||||
string appUri = $"urn:{req.ClientHostName}:{req.ApplicationName}";
|
||||
|
||||
// ── 1. RSA 키 생성 ────────────────────────────────────────
|
||||
using var rsa = RSA.Create(2048);
|
||||
|
||||
// ── 2. Distinguished Name ──────────────────────────────────
|
||||
var dn = new X500DistinguishedName(
|
||||
$"CN={req.ApplicationName}, O={req.ClientHostName}, C=KR");
|
||||
|
||||
// ── 3. Certificate Request ─────────────────────────────────
|
||||
var certReq = new CertificateRequest(
|
||||
dn, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
|
||||
// ── 4. Extensions ──────────────────────────────────────────
|
||||
certReq.CertificateExtensions.Add(
|
||||
new X509BasicConstraintsExtension(false, false, 0, true));
|
||||
|
||||
certReq.CertificateExtensions.Add(
|
||||
new X509KeyUsageExtension(
|
||||
X509KeyUsageFlags.DigitalSignature |
|
||||
X509KeyUsageFlags.NonRepudiation |
|
||||
X509KeyUsageFlags.KeyEncipherment |
|
||||
X509KeyUsageFlags.DataEncipherment,
|
||||
critical: true));
|
||||
|
||||
certReq.CertificateExtensions.Add(
|
||||
new X509EnhancedKeyUsageExtension(
|
||||
new OidCollection
|
||||
{
|
||||
new Oid("1.3.6.1.5.5.7.3.2"), // clientAuth
|
||||
new Oid("1.3.6.1.5.5.7.3.1") // serverAuth
|
||||
}, false));
|
||||
|
||||
certReq.CertificateExtensions.Add(
|
||||
new X509SubjectKeyIdentifierExtension(certReq.PublicKey, false));
|
||||
|
||||
// ── 5. Subject Alternative Names ──────────────────────────
|
||||
var sanBuilder = new SubjectAlternativeNameBuilder();
|
||||
sanBuilder.AddUri(new Uri(appUri));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(req.ServerIp) &&
|
||||
System.Net.IPAddress.TryParse(req.ServerIp, out var ip))
|
||||
sanBuilder.AddIpAddress(ip);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(req.ServerHostName))
|
||||
sanBuilder.AddDnsName(req.ServerHostName);
|
||||
|
||||
sanBuilder.AddDnsName(req.ClientHostName);
|
||||
sanBuilder.AddDnsName("localhost");
|
||||
|
||||
certReq.CertificateExtensions.Add(sanBuilder.Build());
|
||||
|
||||
// ── 6. 자체 서명 인증서 생성 ──────────────────────────────
|
||||
var notBefore = DateTimeOffset.UtcNow.AddMinutes(-5);
|
||||
var notAfter = notBefore.AddDays(req.ValidDays);
|
||||
|
||||
// FIX: CreateSelfSigned 는 이미 RSA 키가 연결된 X509Certificate2 를 반환함
|
||||
// → CopyWithPrivateKey() 를 추가로 호출하면
|
||||
// "The certificate already has an associated private key" 에러 발생
|
||||
// → CreateSelfSigned 결과를 직접 Export(Pfx) 하면 개인키 포함됨
|
||||
using var certWithKey = certReq.CreateSelfSigned(notBefore, notAfter);
|
||||
|
||||
// ── 7. PFX 내보내기 ───────────────────────────────────────
|
||||
string pfxPassword = string.IsNullOrEmpty(req.PfxPassword) ? "" : req.PfxPassword;
|
||||
|
||||
// .NET 9 이상: X509CertificateLoader 권장이지만 net8 에서는 Export 사용
|
||||
byte[] pfxBytes = certWithKey.Export(X509ContentType.Pfx, pfxPassword);
|
||||
|
||||
string pfxFileName = $"{req.ApplicationName}.pfx";
|
||||
string pfxPath = Path.Combine(_pkiRoot, "own", "certs", pfxFileName);
|
||||
File.WriteAllBytes(pfxPath, pfxBytes);
|
||||
|
||||
// ── 8. Trusted 폴더에 DER(공개키) 복사 ───────────────────
|
||||
byte[] derBytes = certWithKey.Export(X509ContentType.Cert);
|
||||
string derPath = Path.Combine(_pkiRoot, "trusted", "certs",
|
||||
$"{req.ApplicationName}[{certWithKey.Thumbprint}].der");
|
||||
File.WriteAllBytes(derPath, derBytes);
|
||||
|
||||
_logger.LogInformation(
|
||||
"인증서 생성 완료 → {PfxPath} | Thumbprint: {Thumbprint}",
|
||||
pfxPath, certWithKey.Thumbprint);
|
||||
|
||||
return Task.FromResult(new CertCreateResult
|
||||
{
|
||||
Success = true,
|
||||
Message = $"인증서 생성 완료: {pfxFileName}",
|
||||
PfxPath = pfxPath,
|
||||
ApplicationUri = appUri,
|
||||
Thumbprint = certWithKey.Thumbprint ?? string.Empty,
|
||||
SerialNumber = certWithKey.SerialNumber ?? string.Empty,
|
||||
NotBefore = notBefore.ToString("yyyy-MM-dd HH:mm:ss UTC"),
|
||||
NotAfter = notAfter.ToString("yyyy-MM-dd HH:mm:ss UTC"),
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "인증서 생성 실패");
|
||||
return Task.FromResult(new CertCreateResult
|
||||
{
|
||||
Success = false,
|
||||
Message = $"인증서 생성 실패: {ex.Message}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기존 PFX를 로드합니다 (원본 코드와 동일한 플래그).
|
||||
/// </summary>
|
||||
public X509Certificate2 LoadPfx(string pfxPath, string pfxPassword)
|
||||
{
|
||||
return new X509Certificate2(
|
||||
pfxPath,
|
||||
pfxPassword,
|
||||
X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet);
|
||||
}
|
||||
|
||||
public IEnumerable<string> ListCertificates()
|
||||
{
|
||||
string dir = Path.Combine(_pkiRoot, "own", "certs");
|
||||
if (!Directory.Exists(dir)) return [];
|
||||
return Directory.GetFiles(dir, "*.pfx").Select(Path.GetFileName)!;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user