Codemaru 1.0.0.0
QR 코드, 모바일 랜딩 페이지, vCard 연락처 저장과 명함 디자인을 한 화면에서 제공하는 디지털 명함 서비스입니다.
로딩중...
검색중...
일치하는것 없음
X509CertificateMonitorService.cs
이 파일의 문서화 페이지로 가기
3using System;
4using System.Collections.Generic;
5using System.IO;
6using System.Linq;
7using System.Security.Cryptography;
8using System.Security.Cryptography.X509Certificates;
9
11
21{
54 public Task<CertificateStatusInfo> GetStatusAsync(
56 CancellationToken cancellationToken)
57 {
58 ArgumentNullException.ThrowIfNull(options);
59 cancellationToken.ThrowIfCancellationRequested();
60
61 if (string.IsNullOrWhiteSpace(options.CertificateDirectory))
62 {
63 return Task.FromResult(Failure(options.CertificateDirectory, "Certificate directory is empty."));
64 }
65
66 if (!Directory.Exists(options.CertificateDirectory))
67 {
68 return Task.FromResult(Failure(
70 $"Certificate directory not found: {options.CertificateDirectory}"));
71 }
72
73 try
74 {
75 string[] files = FindCertificateFiles(options).ToArray();
76
77 if (files.Length == 0)
78 {
79 return Task.FromResult(Failure(
81 $"No certificate files were found. Directory: {options.CertificateDirectory}"));
82 }
83
84 List<string> readErrors = [];
85 List<CertificateStatusInfo> candidates = [];
86
87 foreach (string file in files)
88 {
89 cancellationToken.ThrowIfCancellationRequested();
90
91 CertificateStatusInfo? status = TryReadCertificate(file, options, readErrors);
92 if (status is not null)
93 {
94 candidates.Add(status);
95 }
96 }
97
98 if (candidates.Count == 0)
99 {
100 string errorText = readErrors.Count == 0
101 ? "No detailed read errors were captured."
102 : string.Join(Environment.NewLine, readErrors);
103
104 return Task.FromResult(Failure(
105 options.CertificateDirectory,
106 $"Certificate files were found, but none could be loaded.{Environment.NewLine}{errorText}"));
107 }
108
109 CertificateStatusInfo selected = SelectBestCertificate(candidates);
110
111 return Task.FromResult(selected);
112 }
113 catch (OperationCanceledException)
114 {
115 throw;
116 }
117 catch (Exception ex)
118 {
119 return Task.FromResult(Failure(options.CertificateDirectory, ex.Message));
120 }
121 }
122
147 private static IEnumerable<string> FindCertificateFiles(CertificateMonitorOptions options)
148 {
149 string[] patterns = options.CertificateFilePatterns is { Length: > 0 }
150 ? options.CertificateFilePatterns
151 : ["*.pem", "*.cer", "*.crt", "*.pfx", "*.p12"];
152
153 IEnumerable<string> files = patterns
154 .SelectMany(pattern =>
155 Directory.EnumerateFiles(
156 options.CertificateDirectory,
157 pattern,
158 SearchOption.AllDirectories));
159
160 return files
162 .OrderByDescending(GetCertificatePriority)
163 .ThenByDescending(File.GetLastWriteTimeUtc);
164 }
165
190 private static bool IsCertificateCandidate(string path)
191 {
192 string fileName = Path.GetFileName(path).ToLowerInvariant();
193 string extension = Path.GetExtension(path).ToLowerInvariant();
194
195 if (fileName.Contains("key", StringComparison.OrdinalIgnoreCase))
196 {
197 return false;
198 }
199
200 if (fileName.Contains("privkey", StringComparison.OrdinalIgnoreCase))
201 {
202 return false;
203 }
204
205 if (fileName.Contains("chain-only", StringComparison.OrdinalIgnoreCase))
206 {
207 return false;
208 }
209
210 return extension is ".pem" or ".crt" or ".cer" or ".pfx" or ".p12";
211 }
212
237 private static int GetCertificatePriority(string path)
238 {
239 string fileName = Path.GetFileName(path).ToLowerInvariant();
240
241 if (fileName.Contains("-crt.pem", StringComparison.OrdinalIgnoreCase))
242 {
243 return 700;
244 }
245
246 if (fileName.Contains("fullchain", StringComparison.OrdinalIgnoreCase))
247 {
248 return 600;
249 }
250
251 if (fileName.EndsWith(".crt", StringComparison.OrdinalIgnoreCase))
252 {
253 return 500;
254 }
255
256 if (fileName.EndsWith(".cer", StringComparison.OrdinalIgnoreCase))
257 {
258 return 400;
259 }
260
261 if (fileName.EndsWith(".pfx", StringComparison.OrdinalIgnoreCase) ||
262 fileName.EndsWith(".p12", StringComparison.OrdinalIgnoreCase))
263 {
264 return 300;
265 }
266
267 if (fileName.EndsWith(".pem", StringComparison.OrdinalIgnoreCase))
268 {
269 return 100;
270 }
271
272 return 0;
273 }
274
316 string path,
318 List<string> readErrors)
319 {
320 try
321 {
322 using X509Certificate2 certificate = LoadCertificate(path, options);
323
324 if (string.IsNullOrWhiteSpace(certificate.Subject))
325 {
326 readErrors.Add($"{Path.GetFileName(path)}: Certificate subject is empty.");
327 return null;
328 }
329
330 DateTimeOffset notAfter = new(certificate.NotAfter);
331 DateTimeOffset notBefore = new(certificate.NotBefore);
332 int remainingDays = (int)Math.Floor((notAfter - DateTimeOffset.Now).TotalDays);
333 CertificateHealthState state = ResolveState(remainingDays, options);
334
335 return new CertificateStatusInfo
336 {
337 IsSuccess = true,
338 CertificateDirectory = options.CertificateDirectory,
339 CertificatePath = path,
340 Subject = certificate.Subject,
341 Issuer = certificate.Issuer,
342 NotBefore = notBefore,
343 NotAfter = notAfter,
344 RemainingDays = remainingDays,
345 State = state,
346 Message = BuildMessage(state, remainingDays, path)
347 };
348 }
349 catch (Exception ex)
350 {
351 readErrors.Add($"{Path.GetFileName(path)}: {ex.GetType().Name} - {ex.Message}");
352 return null;
353 }
354 }
355
388 private static X509Certificate2 LoadCertificate(
389 string path,
391 {
392 string extension = Path.GetExtension(path).ToLowerInvariant();
393
394 if (extension is ".pfx" or ".p12")
395 {
396 return string.IsNullOrEmpty(options.PfxPassword)
397 ? new X509Certificate2(path)
398 : new X509Certificate2(path, options.PfxPassword);
399 }
400
401 if (extension == ".pem")
402 {
403 return LoadCertificateFromPem(path);
404 }
405
406 return new X509Certificate2(path);
407 }
408
441 private static X509Certificate2 LoadCertificateFromPem(string path)
442 {
443 string pem = File.ReadAllText(path);
444
445 X509Certificate2Collection collection = new();
446 collection.ImportFromPem(pem);
447
448 if (collection.Count == 0)
449 {
450 throw new InvalidOperationException("No certificate block was found in the PEM file.");
451 }
452
453 X509Certificate2? serverCertificate = collection
454 .OfType<X509Certificate2>()
455 .Where(IsServerCertificate)
456 .OrderByDescending(item => item.NotAfter)
457 .FirstOrDefault();
458
459 if (serverCertificate is not null)
460 {
461 return new X509Certificate2(serverCertificate.RawData);
462 }
463
464 X509Certificate2 first = collection
465 .OfType<X509Certificate2>()
466 .OrderByDescending(item => item.NotAfter)
467 .First();
468
469 return new X509Certificate2(first.RawData);
470 }
471
496 private static bool IsServerCertificate(X509Certificate2 certificate)
497 {
498 foreach (X509Extension extension in certificate.Extensions)
499 {
500 if (extension is X509EnhancedKeyUsageExtension eku)
501 {
502 foreach (Oid oid in eku.EnhancedKeyUsages)
503 {
504 if (oid.Value == "1.3.6.1.5.5.7.3.1")
505 {
506 return true;
507 }
508 }
509 }
510 }
511
512 return certificate.Subject.Contains("CN=", StringComparison.OrdinalIgnoreCase);
513 }
514
540 IReadOnlyCollection<CertificateStatusInfo> candidates)
541 {
542 DateTimeOffset now = DateTimeOffset.Now;
543
544 CertificateStatusInfo? validCertificate = candidates
545 .Where(item => item.NotAfter is not null && item.NotAfter.Value > now)
546 .OrderBy(item => GetSelectedFilePriority(item.CertificatePath))
547 .ThenBy(item => item.NotAfter)
548 .FirstOrDefault();
549
550 if (validCertificate is not null)
551 {
552 return validCertificate;
553 }
554
555 return candidates
556 .OrderByDescending(item => item.NotAfter ?? DateTimeOffset.MinValue)
557 .First();
558 }
559
584 private static int GetSelectedFilePriority(string? path)
585 {
586 if (string.IsNullOrWhiteSpace(path))
587 {
588 return int.MaxValue;
589 }
590
591 string fileName = Path.GetFileName(path).ToLowerInvariant();
592
593 if (fileName.Contains("-crt.pem", StringComparison.OrdinalIgnoreCase))
594 {
595 return 0;
596 }
597
598 if (fileName.Contains("fullchain", StringComparison.OrdinalIgnoreCase))
599 {
600 return 1;
601 }
602
603 if (fileName.EndsWith(".crt", StringComparison.OrdinalIgnoreCase))
604 {
605 return 2;
606 }
607
608 if (fileName.EndsWith(".cer", StringComparison.OrdinalIgnoreCase))
609 {
610 return 3;
611 }
612
613 if (fileName.EndsWith(".pfx", StringComparison.OrdinalIgnoreCase) ||
614 fileName.EndsWith(".p12", StringComparison.OrdinalIgnoreCase))
615 {
616 return 4;
617 }
618
619 return 10;
620 }
621
655 int remainingDays,
657 {
658 if (remainingDays < 0)
659 {
660 return CertificateHealthState.Expired;
661 }
662
663 if (remainingDays < options.CriticalDays)
664 {
665 return CertificateHealthState.Critical;
666 }
667
668 if (remainingDays < options.WarningDays)
669 {
670 return CertificateHealthState.Warning;
671 }
672
673 return CertificateHealthState.Ok;
674 }
675
716 private static string BuildMessage(
718 int remainingDays,
719 string certificatePath)
720 {
721 string fileName = Path.GetFileName(certificatePath);
722
723 return state switch
724 {
726 $"Certificate is valid. Remaining days: {remainingDays}. File: {fileName}",
727
729 $"Certificate renewal should be checked. Remaining days: {remainingDays}. File: {fileName}",
730
732 $"Certificate expires soon. Remaining days: {remainingDays}. File: {fileName}",
733
735 $"Certificate expired. Expired days: {Math.Abs(remainingDays)}. File: {fileName}",
736
737 _ =>
738 $"Certificate state is unknown. Remaining days: {remainingDays}. File: {fileName}"
739 };
740 }
741
774 private static CertificateStatusInfo Failure(string? directory, string message)
775 {
776 return new CertificateStatusInfo
777 {
778 IsSuccess = false,
779 CertificateDirectory = directory ?? string.Empty,
780 State = CertificateHealthState.Error,
781 Message = message
782 };
783 }
784}
Task< CertificateStatusInfo > GetStatusAsync(CertificateMonitorOptions options, CancellationToken cancellationToken)
static string BuildMessage(CertificateHealthState state, int remainingDays, string certificatePath)
static CertificateStatusInfo SelectBestCertificate(IReadOnlyCollection< CertificateStatusInfo > candidates)
static IEnumerable< string > FindCertificateFiles(CertificateMonitorOptions options)
static CertificateHealthState ResolveState(int remainingDays, CertificateMonitorOptions options)
static X509Certificate2 LoadCertificate(string path, CertificateMonitorOptions options)
static CertificateStatusInfo Failure(string? directory, string message)
static ? CertificateStatusInfo TryReadCertificate(string path, CertificateMonitorOptions options, List< string > readErrors)