using Org.BouncyCastle.Asn1; using System; using System.Security.Cryptography.X509Certificates; using System.Text; namespace Hcs.Client.Api.Request { internal class X509Tools { private const string PRIVATE_KEY_USAGE_PERIOD = "2.5.29.16"; public static string GetFullnameWithExpirationDateStr(X509Certificate2 x509cert) { var (фамилия, имя, отчество) = GetFullname(x509cert); return (string.IsNullOrEmpty(фамилия) ? "" : $"{фамилия} ") + (string.IsNullOrEmpty(имя) ? "" : $"{имя} ") + (string.IsNullOrEmpty(отчество) ? "" : $"{отчество} ") + "until " + GetNotAfterDate(x509cert).ToString("dd.MM.yyyy"); } /// /// Возвращает массив из трех строк, содержащих соответственно Фамилию, Имя и Отчество /// полученных из данных сертификата. Если сертификат не содержит ФИО, то возвращается массив /// из трех пустых строк. Это не точный метод определять имя, он предполагает, что /// поля SN, G, CN содержат ФИО в определенном порядке, что правдоподобно но не обязательно. /// private static (string Фамилия, string Имя, string Отчество) GetFullname(X509Certificate2 x509cert) { string фам = "", имя = "", отч = ""; // Сначала ищем поля surname (SN) и given-name (G) var sn = DecodeSubjectField(x509cert, "SN"); var g = DecodeSubjectField(x509cert, "G"); if (!string.IsNullOrEmpty(sn) && !string.IsNullOrEmpty(g)) { фам = sn; var gParts = g.Split(' '); if (gParts != null && gParts.Length >= 1) { имя = gParts[0]; } if (gParts != null && gParts.Length >= 2) { отч = gParts[1]; } } else { // Иначе берем три первых слова из common name (CN), игнорируя кавычки var cn = DecodeSubjectField(x509cert, "CN"); if (!string.IsNullOrEmpty(cn)) { cn = new StringBuilder(cn).Replace("\"", "").ToString(); char[] separators = [' ', ';']; var 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 (фам, имя, отч); } /// /// Возвращает значение поля с именем @subName включенного в различимое имя Subject /// private static string DecodeSubjectField(X509Certificate2 x509cert, string subName) { // Чтобы посмотреть все поля сертификата //System.Diagnostics.Trace.WriteLine("x509decode = " + x509cert.SubjectName.Decode( //X500DistinguishedNameFlags.UseNewLines)); // Декодируем различимое имя на отдельные строки через переводы строк для надежности разбора var decoded = x509cert.SubjectName.Decode(X500DistinguishedNameFlags.UseNewLines); char[] separators = ['\n', '\r']; var parts = decoded.Split(separators, StringSplitOptions.RemoveEmptyEntries); if (parts == null) { return null; } // Каждая часть начинается с имени и отделяется от значения символом равно foreach (var 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; } /// /// Возвращает дату окончания действия сертификата /// private static DateTime GetNotAfterDate(X509Certificate2 x509cert) { // Сначала пытаемся определить срок первичного ключа, а затем уже самого сертификата var датаОкончания = GetPrivateKeyUsageEndDate(x509cert); if (датаОкончания != null) { return (DateTime)датаОкончания; } return x509cert.NotAfter; } private static DateTime? GetPrivateKeyUsageEndDate(X509Certificate2 x509cert) { foreach (var ext in x509cert.Extensions) { if (ext.Oid.Value == PRIVATE_KEY_USAGE_PERIOD) { // Дата начала с индексом 0, дата окончания с индексом 1 return ParseAsn1Datetime(ext, 1); } } return null; } /// /// Разбирает значение типа дата из серии значений ASN1 присоединенных к расширению /// private static DateTime? ParseAsn1Datetime(X509Extension ext, int valueIndex) { try { var 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); var s = Encoding.UTF8.GetString(asnStr.GetOctets()); var year = int.Parse(s.Substring(0, 4)); var month = int.Parse(s.Substring(4, 2)); var day = int.Parse(s.Substring(6, 2)); var hour = int.Parse(s.Substring(8, 2)); var minute = int.Parse(s.Substring(10, 2)); var second = int.Parse(s.Substring(12, 2)); return new DateTime(year, month, day, hour, minute, second); } catch (System.Exception) { return null; } } } }