Files
hcs/Hcs.Client/ClientApi/HcsX509Tools.cs
HOME-LAPTOP\kshkulev 33ab055b43 Add project
Basic formatting applied. Unnecessary comments have been removed. Suspicious code is covered by TODO.
2025-08-12 11:21:10 +09:00

230 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Org.BouncyCastle.Asn1;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text;
namespace Hcs.ClientApi
{
/// <summary>
/// Методы работы с сертификатами X509, которых нет в системе
/// </summary>
public class HcsX509Tools
{
public static bool IsValidCertificate(X509Certificate2 cert)
{
var now = DateTime.Now;
return (now >= cert.NotBefore && now <= GetNotAfterDate(cert));
}
public static IEnumerable<X509Certificate2> EnumerateCertificates(bool includeInvalid = false)
{
X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
try
{
var now = DateTime.Now;
foreach (var x in store.Certificates)
{
if (includeInvalid) yield return x;
else if (IsValidCertificate(x)) yield return x;
}
}
finally
{
store.Close();
}
}
public static X509Certificate2 FindCertificate(Func<X509Certificate2, bool> predicate)
{
if (predicate == null) throw new ArgumentException("Null subject predicate");
return EnumerateCertificates(true).FirstOrDefault(x => predicate(x));
}
public static X509Certificate2 FindValidCertificate(Func<X509Certificate2, bool> predicate)
{
if (predicate == null) throw new ArgumentException("Null subject predicate");
return EnumerateCertificates(false).FirstOrDefault(x => predicate(x));
}
/// <summary>
/// Возвращает Common Name сертификата
/// </summary>
public static string GetCommonName(X509Certificate2 x509cert)
{
return x509cert.GetNameInfo(X509NameType.SimpleName, false);
}
/// <summary>
/// Возвращает дату окончания действия сертификата
/// </summary>
public static DateTime GetNotAfterDate(X509Certificate2 x509cert)
{
// Сначала пытаемся определить срок первичного ключа, а затем уже самого сертификата
DateTime? датаОкончания = GetPrivateKeyUsageEndDate(x509cert);
if (датаОкончания != null) return (DateTime)датаОкончания;
return x509cert.NotAfter;
}
/// <summary>
/// Известные номера расширений сертификата
/// </summary>
private class KnownOids
{
public const string PrivateKeyUsagePeriod = "2.5.29.16";
}
public static DateTime? GetPrivateKeyUsageEndDate(X509Certificate2 x509cert)
{
foreach (var ext in x509cert.Extensions)
{
if (ext.Oid.Value == KnownOids.PrivateKeyUsagePeriod)
{
// Дата начала с индексом 0, дата окончания с индексом 1
return ParseAsn1Datetime(ext, 1);
}
}
return null;
}
/// <summary>
/// Разбирает значение типа дата из серии значений ASN1 присоединенных к расширению
/// </summary>
private static DateTime? ParseAsn1Datetime(X509Extension ext, int valueIndex)
{
try
{
Asn1Object asnObject = (new Asn1InputStream(ext.RawData)).ReadObject();
if (asnObject == null) return null;
var asnSequence = Asn1Sequence.GetInstance(asnObject);
if (asnSequence.Count <= valueIndex) return null;
var asn = (Asn1TaggedObject)asnSequence[valueIndex];
var asnStr = Asn1OctetString.GetInstance(asn, false);
string s = Encoding.UTF8.GetString(asnStr.GetOctets());
int year = int.Parse(s.Substring(0, 4));
int month = int.Parse(s.Substring(4, 2));
int day = int.Parse(s.Substring(6, 2));
int hour = int.Parse(s.Substring(8, 2));
int minute = int.Parse(s.Substring(10, 2));
int second = int.Parse(s.Substring(12, 2));
// Последний символ - буква 'Z'
return new DateTime(year, month, day, hour, minute, second);
}
catch (Exception)
{
return null;
}
}
public static string ДатьСтрокуФИОСертификатаСДатойОкончания(X509Certificate2 x509cert)
{
var фио = ДатьФИОСертификата(x509cert);
return фио.Фамилия + " " + фио.Имя + " " + фио.Отчество +
" до " + GetNotAfterDate(x509cert).ToString("dd.MM.yyyy");
}
public static string ДатьСтрокуФИОСертификата(X509Certificate2 x509cert)
{
var фио = ДатьФИОСертификата(x509cert);
return фио.Фамилия + " " + фио.Имя + " " + фио.Отчество;
}
/// <summary>
/// Возвращает массив из трех строк, содержащих соответственно Фамилию, Имя и Отчество
/// полученных из данных сертификата. Если сертификат не содержит ФИО возвращается массив
/// из трех пустых строк. Это не точный метод определять имя, он предполагает что
/// поля SN, G, CN содержат ФИО в определенном порядке, что правдоподобно но не обязательно.
/// </summary>
public static (string Фамилия, string Имя, string Отчество) ДатьФИОСертификата(X509Certificate2 x509cert)
{
string фам = "", имя = "", отч = "";
// Сначала ищем поля surname (SN) и given-name (G)
string sn = DecodeSubjectField(x509cert, "SN");
string g = DecodeSubjectField(x509cert, "G");
if (!string.IsNullOrEmpty(sn) && !string.IsNullOrEmpty(g))
{
фам = sn;
string[] gParts = g.Split(' ');
if (gParts != null && gParts.Length >= 1) имя = gParts[0];
if (gParts != null && gParts.Length >= 2) отч = gParts[1];
}
else
{
// Иначе берем три первых слова из common name (CN), игнорируя кавычки
string cn = DecodeSubjectField(x509cert, "CN");
if (!string.IsNullOrEmpty(cn))
{
cn = new StringBuilder(cn).Replace("\"", "").ToString();
char[] separators = { ' ', ';' };
string[] cnParts = cn.Split(separators);
if (cnParts != null && cnParts.Length >= 1) фам = cnParts[0];
if (cnParts != null && cnParts.Length >= 2) имя = cnParts[1];
if (cnParts != null && cnParts.Length >= 3) отч = cnParts[2];
}
}
return (фам, имя, отч);
}
/// <summary>
/// Возвращает строку ИНН владельца сертификата
/// </summary>
public static string ДатьИННСертификата(X509Certificate2 x509cert)
{
return DecodeSubjectField(x509cert, "ИНН");
}
/// <summary>
/// Возвращает значение поля с именем @subName включенного в различимое имя Subject
/// </summary>
private static string DecodeSubjectField(X509Certificate2 x509cert, string subName)
{
// Чтобы посмотреть все поля сертификата
//System.Diagnostics.Trace.WriteLine("x509decode=" + x509cert.SubjectName.Decode(
//X500DistinguishedNameFlags.UseNewLines));
// Декодируем различимое имя на отдельные строки через переводы строк для надежности разбора
string decoded = x509cert.SubjectName.Decode(X500DistinguishedNameFlags.UseNewLines);
char[] separators = { '\n', '\r' };
string[] parts = decoded.Split(separators, StringSplitOptions.RemoveEmptyEntries);
if (parts == null) return null;
// Каждая часть начинается с имени и отделяется от значения символом равно
foreach (string part in parts)
{
if (part.Length <= subName.Length + 1) continue;
if (part.StartsWith(subName) && part[subName.Length] == '=')
{
return part.Substring(subName.Length + 1);
}
}
return null;
}
public static int Compare(X509Certificate2 x, X509Certificate2 y)
{
if (x == null && y != null) return -1;
if (x != null && y == null) return 1;
if (x == null && y == null) return 0;
// Сначала сравниваем ФИО
int sign = string.Compare(ДатьСтрокуФИОСертификата(x), ДатьСтрокуФИОСертификата(y), true);
if (sign != 0) return sign;
// Затем дату окончания действия
return GetNotAfterDate(x).CompareTo(GetNotAfterDate(y));
}
public class CertificateComparer : IComparer<X509Certificate2>
{
public int Compare(X509Certificate2 x, X509Certificate2 y) => HcsX509Tools.Compare(x, y);
}
}
}