173 lines
7.3 KiB
C#
173 lines
7.3 KiB
C#
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");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Возвращает массив из трех строк, содержащих соответственно Фамилию, Имя и Отчество
|
||
/// полученных из данных сертификата. Если сертификат не содержит ФИО, то возвращается массив
|
||
/// из трех пустых строк. Это не точный метод определять имя, он предполагает, что
|
||
/// поля SN, G, CN содержат ФИО в определенном порядке, что правдоподобно но не обязательно.
|
||
/// </summary>
|
||
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 (фам, имя, отч);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Возвращает значение поля с именем @subName включенного в различимое имя Subject
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Возвращает дату окончания действия сертификата
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Разбирает значение типа дата из серии значений ASN1 присоединенных к расширению
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
}
|