Add project

Basic formatting applied. Unnecessary comments have been removed. Suspicious code is covered by TODO.
This commit is contained in:
2025-08-12 11:21:10 +09:00
parent bbcbe841a7
commit 33ab055b43
546 changed files with 176950 additions and 0 deletions

View File

@ -0,0 +1,18 @@
using Microsoft.Xades;
using System.Security.Cryptography.X509Certificates;
namespace Hcs.GostXades.Abstractions
{
/// <summary>
/// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> XML <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// </summary>
public interface ICertificateMatcher
{
/// <summary>
/// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// </summary>
/// <param name="signedXml"><3E><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> xml <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD></param>
/// <returns></returns>
X509Certificate2 GetSignatureCertificate(XadesSignedXml signedXml);
}
}

View File

@ -0,0 +1,58 @@
using Microsoft.Xades;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;
namespace Hcs.GostXades.Abstractions
{
public interface ICryptoProvider
{
/// <summary>
/// URI метода подписи
/// </summary>
string SignatureMethod { get; }
/// <summary>
/// URI метода хеширования
/// </summary>
string DigestMethod { get; }
/// <summary>
/// Получение реализации асимметричного алгоритма
/// </summary>
/// <param name="certificate">Сертификат, исользуемый для подписания</param>
/// <param name="privateKeyPassword">Пароль от контейнера закрытого ключа</param>
/// <returns></returns>
AsymmetricAlgorithm GetAsymmetricAlgorithm(X509Certificate2 certificate, string privateKeyPassword);
/// <summary>
/// Получение Reference-элемента для XML-документа
/// </summary>
/// <param name="signedElementId">Идентификатор подписываемого узла XML-документа</param>
/// <param name="signatureId">Идентификатор подписи</param>
/// <returns></returns>
Reference GetReference(string signedElementId, string signatureId);
/// <summary>
/// Получение форматтера, с помощью которого будет производиться подпись
/// </summary>
/// <param name="certificate">Сертификат, с помощью которого будет производиться подпись</param>
/// <returns></returns>
AsymmetricSignatureFormatter GetSignatureFormatter(X509Certificate2 certificate);
/// <summary>
/// Получение реализации алгоритма хеширования по URI
/// </summary>
/// <param name="algorithm">URI метода хеширования</param>
/// <returns></returns>
HashAlgorithm GetHashAlgorithm(string algorithm);
/// <summary>
/// Получение объекта XadesObject
/// </summary>
/// <param name="xadesInfo">Информация о XAdES-подписи</param>
/// <param name="signatureId">Идентификатор подписи</param>
/// <returns></returns>
XadesObject GetXadesObject(XadesInfo xadesInfo, string signatureId);
}
}

View File

@ -0,0 +1,16 @@
namespace Hcs.GostXades.Abstractions
{
/// <summary>
/// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// </summary>
public interface IIssuerComparer
{
/// <summary>
/// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (X509IssuerName)
/// </summary>
/// <param name="first"><3E><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD></param>
/// <param name="second"><3E><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD></param>
/// <returns>true, <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, false <20><><EFBFBD><EFBFBD><EFBFBD></returns>
bool AreSameIssuer(string first, string second);
}
}

View File

@ -0,0 +1,23 @@
namespace Hcs.GostXades.Abstractions
{
public interface IXadesService
{
/// <summary>
/// Валидация XAdES подписи.
/// Бросает исключение, если подпись не верна.
/// </summary>
/// <param name="xmlData"></param>
/// <param name="elementId"></param>
void ValidateSignature(string xmlData, string elementId);
/// <summary>
/// Подпись XML-документа с помощью XAdES подписи
/// </summary>
/// <param name="xmlData">XML-документ, который необходимо подписать</param>
/// <param name="elementId">Значение атрибута Id узла XML-документа, который необходимо подписать</param>
/// <param name="certificateThumbprint">Отпечаток сертификата, который необходимо использовать для подписи</param>
/// <param name="certificatePassword">Пароль от контейнера закрытого ключа используемого сертификата</param>
/// <returns>Подписанный XML-документ</returns>
string Sign(string xmlData, string elementId, string certificateThumbprint, string certificatePassword);
}
}

View File

@ -0,0 +1,129 @@
using Hcs.GostXades.Abstractions;
using Hcs.GostXades.Helpers;
using Microsoft.Xades;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;
namespace Hcs.GostXades
{
public class CertificateMatcher : ICertificateMatcher
{
public IIssuerComparer IssuerComparer { get; set; } = new IssuerComparer();
public CertificateMatcher(ICryptoProvider cryptoProvider)
{
if (cryptoProvider == null)
{
throw new ArgumentNullException(nameof(cryptoProvider));
}
_cryptoProvider = cryptoProvider;
}
public X509Certificate2 GetSignatureCertificate(XadesSignedXml signedXml)
{
var candidates = GetCandidateCertificates(signedXml).ToArray();
if (!candidates.Any())
{
throw new InvalidOperationException("Не найдено ни одного сертификата, обозначенного в xades:SigningCertificate");
}
return FindMatchedCertificate(signedXml, candidates);
}
private IEnumerable<X509Certificate2> GetCandidateCertificates(XadesSignedXml signedXml)
{
var certificates = ExtractCertificates(signedXml).ToArray();
if (certificates.Length == 0)
{
throw new InvalidOperationException("Элемент KeyInfo.X509Data.X509Certificate не заполнен или отсутствует");
}
var certInfosCollection = signedXml.SignedSignatureProperties.SigningCertificate.CertCollection;
if (certInfosCollection == null)
{
throw new InvalidOperationException("Элемент xades:SigningCertificate не заполнен или отсутствует");
}
var certInfos = certInfosCollection.OfType<Cert>().ToArray();
foreach (var certificate in certificates)
{
// TODO: Проверить комментарий
var isCertificateMatch = true; //certInfos.Any(certInfo => IsCertificateMatchCertInfo(certificate, certInfo));
if (isCertificateMatch)
{
yield return certificate;
}
}
}
private IEnumerable<X509Certificate2> ExtractCertificates(XadesSignedXml signedXml)
{
var keyInfo = signedXml.KeyInfo;
if (keyInfo == null || keyInfo.Count == 0)
{
throw new InvalidOperationException("Элемент KeyInfo не заполнен или отсутствует");
}
if (keyInfo.Count > 1)
{
throw new InvalidOperationException("Найдено более одного элемента KeyInfo");
}
var x509Data = keyInfo.OfType<KeyInfoX509Data>().FirstOrDefault();
if (x509Data == null)
{
throw new InvalidOperationException("Элемент X509Data не заполнен или отсутствует");
}
return x509Data.Certificates.OfType<X509Certificate2>();
}
private readonly ICryptoProvider _cryptoProvider;
#region Certificate match
private bool IsCertificateMatchCertInfo(X509Certificate2 certificate, Cert certInfo)
{
var isSerialMatch = IsSerialMatch(certificate, certInfo);
var issuerNameMatch = IsNameMatch(certificate, certInfo);
var isCertHashMatch = IsCertHashMatch(certificate, certInfo);
return isSerialMatch && issuerNameMatch && isCertHashMatch;
}
private bool IsSerialMatch(X509Certificate2 certificate, Cert certInfo)
{
var issuerSerial = certInfo.IssuerSerial.X509SerialNumber;
string certInfoSerialNumberHex;
try
{
certInfoSerialNumberHex = ConvertHelper.BigIntegerToHex(issuerSerial);
}
catch (FormatException)
{
certInfoSerialNumberHex = issuerSerial;
}
return certificate.SerialNumber == certInfoSerialNumberHex;
}
private bool IsCertHashMatch(X509Certificate2 certificate, Cert certInfo)
{
var certDigest = certInfo.CertDigest;
var pkHash = _cryptoProvider.GetHashAlgorithm(certDigest.DigestMethod.Algorithm);
if (pkHash == null)
{
throw new XadesBesValidationException($"Алгоритм {certDigest.DigestMethod.Algorithm} не поддерживается");
}
var hashValue = pkHash.ComputeHash(certificate.RawData);
return ArrayHelper.AreEquals(hashValue, certDigest.DigestValue);
}
private bool IsNameMatch(X509Certificate2 certificate, Cert certInfo)
{
return IssuerComparer.AreSameIssuer(certificate.Issuer, certInfo.IssuerSerial.X509IssuerName);
}
private X509Certificate2 FindMatchedCertificate(SignedXml signedXml, IEnumerable<X509Certificate2> candidates)
{
return candidates.FirstOrDefault(candidate => signedXml.CheckSignature(candidate, true));
}
#endregion
}
}

View File

@ -0,0 +1,13 @@
namespace Hcs.GostXades
{
/// <summary>
/// Типы криптопровайдеров
/// </summary>
public enum CryptoProviderTypeEnum
{
CryptoPRO = GostCryptography.Base.ProviderType.CryptoPro,
CryptoPro_2012_512 = GostCryptography.Base.ProviderType.CryptoPro_2012_512,
CryptoPro_2012_1024 = GostCryptography.Base.ProviderType.CryptoPro_2012_1024,
VipNET = GostCryptography.Base.ProviderType.VipNet
}
}

View File

@ -0,0 +1,130 @@
using GostCryptography.Config;
using GostCryptography.Gost_R3410;
using GostCryptography.Gost_R3411;
using Hcs.GostXades.Abstractions;
using Hcs.GostXades.Helpers;
using Microsoft.Xades;
using Org.BouncyCastle.X509;
using System;
using System.Collections.Generic;
using System.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;
namespace Hcs.GostXades
{
public class GostCryptoProvider : ICryptoProvider
{
public GostCryptoProvider(CryptoProviderTypeEnum cryptoProviderType)
{
GostCryptoConfig.ProviderType = (GostCryptography.Base.ProviderType)cryptoProviderType;
}
private Dictionary<string, string> HashAlgorithmMap { get; set; } = new Dictionary<string, string>
{
["http://www.w3.org/2001/04/xmldsig-more#gostr3411"] = "GOST3411"
};
public HashAlgorithm GetHashAlgorithm(string algorithm)
{
string algorithmName;
var algorithmUrl = algorithm;
if (!HashAlgorithmMap.TryGetValue(algorithmUrl, out algorithmName))
{
return null;
}
var pkHash = HashAlgorithm.Create(algorithmName);
return pkHash;
}
private int _referenceIndex;
public string SignatureMethod => Gost_R3410_2012_256_AsymmetricAlgorithm.SignatureAlgorithmValue;
public string DigestMethod => Gost_R3411_2012_256_HashAlgorithm.AlgorithmNameValue;
private HashAlgorithm GetHashAlgorithm() => new Gost_R3411_2012_256_HashAlgorithm();
public AsymmetricAlgorithm GetAsymmetricAlgorithm(X509Certificate2 certificate, string privateKeyPassword)
{
var provider = certificate.GetPrivateKeyAlgorithm();
if (!string.IsNullOrEmpty(privateKeyPassword))
{
var secureString = new SecureString();
foreach (var chr in privateKeyPassword)
secureString.AppendChar(chr);
if (provider is Gost_R3410_2012_256_AsymmetricAlgorithm alg1)
alg1.SetContainerPassword(secureString);
else if (provider is Gost_R3410_2012_512_AsymmetricAlgorithm alg2)
alg2.SetContainerPassword(secureString);
else throw new NotSupportedException(
$"Неизвестный тип алгоритма {provider.SignatureAlgorithm} для применения пароля");
}
return provider;
}
public Reference GetReference(string signedElementId, string signatureId)
{
var reference = new Reference
{
Uri = $"#{signedElementId}",
DigestMethod = DigestMethod,
Id = $"{signatureId}-ref{_referenceIndex}"
};
reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
reference.AddTransform(new XmlDsigExcC14NTransform());
_referenceIndex++;
return reference;
}
public AsymmetricSignatureFormatter GetSignatureFormatter(X509Certificate2 certificate)
{
var alg = certificate.GetPrivateKeyAlgorithm();
var signatureDescription =
(SignatureDescription)CryptoConfig.CreateFromName(alg.SignatureAlgorithm);
return signatureDescription.CreateFormatter(alg);
}
public XadesObject GetXadesObject(XadesInfo xadesInfo, string signatureId)
{
var xadesObject = new XadesObject
{
QualifyingProperties = new QualifyingProperties
{
Target = $"#{signatureId}",
SignedProperties = new SignedProperties { Id = $"{signatureId}-signedprops" }
}
};
var hashAlgorithm = GetHashAlgorithm();
var hashValue = hashAlgorithm.ComputeHash(xadesInfo.RawCertData);
var x509CertificateParser = new X509CertificateParser();
var bouncyCert = x509CertificateParser.ReadCertificate(xadesInfo.RawCertData);
var cert = new Cert
{
IssuerSerial = new IssuerSerial
{
X509IssuerName = bouncyCert.IssuerDN.ToX509IssuerName(),
X509SerialNumber = bouncyCert.SerialNumber.ToString()
},
CertDigest =
{
DigestValue = hashValue,
DigestMethod = new DigestMethod { Algorithm = DigestMethod }
}
};
var signedSignatureProperties = xadesObject.QualifyingProperties.SignedProperties.SignedSignatureProperties;
signedSignatureProperties.SigningCertificate.CertCollection.Add(cert);
signedSignatureProperties.SigningTime = xadesInfo.SigningDateTimeUtc.ToDateTimeOffset(xadesInfo.TimeZoneOffsetMinutes);
return xadesObject;
}
}
}

View File

@ -0,0 +1,72 @@
using Hcs.GostXades.Abstractions;
using Hcs.GostXades.Helpers;
using System;
namespace Hcs.GostXades
{
public class GostXadesBesService : IXadesService
{
CryptoProviderTypeEnum cryptoProviderType;
public GostXadesBesService(CryptoProviderTypeEnum cryptoProviderType)
{
this.cryptoProviderType = cryptoProviderType;
}
public void ValidateSignature(string xmlData, string elementId)
{
if (string.IsNullOrEmpty(xmlData))
{
throw new ArgumentNullException(nameof(xmlData));
}
if (string.IsNullOrWhiteSpace(elementId))
{
throw new ArgumentNullException(nameof(elementId));
}
var document = XmlDocumentHelper.Create(xmlData);
var signedXml = new XadesBesSignedXml(document, elementId)
{
CertificateMatcher = new CertificateMatcher(new GostCryptoProvider(this.cryptoProviderType))
};
signedXml.Validate();
}
public string Sign(string xmlData, string elementId, string certificateThumbprint, string certificatePassword)
{
if (string.IsNullOrEmpty(xmlData))
{
throw new ArgumentNullException(nameof(xmlData));
}
if (string.IsNullOrEmpty(elementId))
{
throw new ArgumentNullException(nameof(elementId));
}
if (string.IsNullOrEmpty(certificateThumbprint))
{
throw new ArgumentNullException(nameof(certificateThumbprint));
}
var originalDoc = XmlDocumentHelper.Create(xmlData);
var certificate = CertificateHelper.GetCertificateByThumbprint(certificateThumbprint);
var provider = new GostCryptoProvider(this.cryptoProviderType);
var xadesSignedXml = new XadesBesSignedXml(originalDoc)
{
SignedElementId = elementId,
CryptoProvider = provider
};
var element = xadesSignedXml.FindElement(elementId, originalDoc);
if (element == null)
{
throw new InvalidOperationException($"<22><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> c Id {elementId}");
}
xadesSignedXml.ComputeSignature(certificate, certificatePassword);
xadesSignedXml.InjectSignatureTo(originalDoc);
return originalDoc.OuterXml;
}
}
}

View File

@ -0,0 +1,38 @@
using System;
namespace Hcs.GostXades.Helpers
{
public static class ArrayHelper
{
public static bool AreEquals(byte[] first, byte[] second)
{
return AreEquals(first, second, (x, y) => x.Equals(y));
}
public static bool AreEquals(string[] first, string[] second)
{
return AreEquals(first, second, (x, y) => string.Equals(x, y, StringComparison.InvariantCultureIgnoreCase));
}
private static bool AreEquals<T>(T[] first, T[] second, Func<T, T, bool> comparator)
{
if (first == second)
{
return true;
}
if (first.Length != second.Length)
{
return false;
}
for (var i = 0; i < first.Length; i++)
{
var equals = comparator.Invoke(first[i], second[i]);
if (!equals)
{
return false;
}
}
return true;
}
}
}

View File

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
namespace Hcs.GostXades.Helpers
{
public static class CertificateHelper
{
/// <summary>
/// Получение сертификата из личного локального хранилища по отпечатку
/// </summary>
/// <param name="thumbprint">Отпечаток требуемого сертификата</param>
/// <returns>Сертификат с нужным отпечатком</returns>
public static X509Certificate2 GetCertificateByThumbprint(string thumbprint)
{
var certificateStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
try
{
certificateStore.Open(OpenFlags.ReadOnly);
var certificateCollection = certificateStore.Certificates
.Cast<X509Certificate2>()
.Where(i => string.Equals(i.Thumbprint, thumbprint, StringComparison.InvariantCultureIgnoreCase))
.ToArray();
if (!certificateCollection.Any())
{
throw new ArgumentException("Некорректный отпечаток сертификата");
}
var activeCertificates = certificateCollection.Where(i => DateTime.Parse(i.GetEffectiveDateString()) <= DateTime.Now &&
DateTime.Now <= DateTime.Parse(i.GetExpirationDateString()))
.ToArray();
if (activeCertificates.Any())
{
return activeCertificates[0];
}
throw new ArgumentException($"Сертификат с указанным отпечатком {thumbprint} недействителен");
}
finally
{
certificateStore.Close();
}
}
/// <summary>
/// Получение сертификатов из локального хранилища текущего пользователя
/// </summary>
/// <returns>Сертификаты из локального хранилища текущего пользователя</returns>
public static IEnumerable<X509Certificate2> GetCertificates()
{
var certificateStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
try
{
certificateStore.Open(OpenFlags.ReadOnly);
return certificateStore.Certificates.OfType<X509Certificate2>();
}
finally
{
certificateStore.Close();
}
}
}
}

View File

@ -0,0 +1,13 @@
using System.Collections;
using System.Linq;
namespace Hcs.GostXades.Helpers
{
public static class CollectionHelper
{
public static bool IsNotEmpty(this IEnumerable enumerable)
{
return enumerable != null && enumerable.OfType<object>().Any();
}
}
}

View File

@ -0,0 +1,12 @@
using System.Numerics;
namespace Hcs.GostXades.Helpers
{
public static class ConvertHelper
{
public static string BigIntegerToHex(string str)
{
return BigInteger.Parse(str).ToString("X");
}
}
}

View File

@ -0,0 +1,14 @@
using System;
namespace Hcs.GostXades.Helpers
{
public static class DateTimeHelper
{
public static DateTimeOffset ToDateTimeOffset(this DateTime dateTime, int timeZoneOffsetMinutes = 0)
{
var shiftedDateTime = dateTime.AddMinutes(timeZoneOffsetMinutes);
var unspecifiedDateTime = DateTime.SpecifyKind(shiftedDateTime, DateTimeKind.Unspecified);
return new DateTimeOffset(unspecifiedDateTime, new TimeSpan(0, timeZoneOffsetMinutes, 0));
}
}
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.Linq;
namespace Hcs.GostXades.Helpers
{
public static class EnumerableHelper
{
public static bool AreSequenceEquals(IEnumerable<string> first, IEnumerable<string> second)
{
var sortedFirst = first.OrderBy(x => x).ToArray();
var sortedSecond = second.OrderBy(x => x).ToArray();
return ArrayHelper.AreEquals(sortedFirst, sortedSecond);
}
}
}

View File

@ -0,0 +1,23 @@
using GostCryptography.Config;
using System;
namespace Hcs.GostXades.Helpers
{
public static class GostHashAlgorithmHelper
{
/// <summary>
/// Рассчитать хэш
/// </summary>
/// <param name="cryptoProviderType">Тип криптопровайдера</param>
/// <param name="bytes"></param>
/// <returns></returns>
public static string ComputeHash(CryptoProviderTypeEnum cryptoProviderType, byte[] bytes)
{
byte[] hashValue;
GostCryptoConfig.ProviderType = (GostCryptography.Base.ProviderType)cryptoProviderType;
var hashAlgorithm = new GostCryptography.Gost_R3411.Gost_R3411_2012_512_HashAlgorithm();
hashValue = hashAlgorithm.ComputeHash(bytes);
return BitConverter.ToString(hashValue).Replace("-", "");
}
}
}

View File

@ -0,0 +1,28 @@
using System;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;
using System.Xml;
namespace Hcs.GostXades.Helpers
{
public static class KeyInfoHelper
{
public static KeyInfo Create(X509Certificate2 certificate)
{
var xmlDocument = new XmlDocument();
var x509DataElement = xmlDocument.CreateElement("ds", "X509Data", KeyInfoNamespace);
var x509CertificateElement = xmlDocument.CreateElement("ds", "X509Certificate", KeyInfoNamespace);
x509CertificateElement.InnerText = Convert.ToBase64String(certificate.GetRawCertData());
x509DataElement.AppendChild(x509CertificateElement);
var keyInfo = new KeyInfo();
keyInfo.AddClause(new KeyInfoNode(x509DataElement));
return keyInfo;
}
private const string KeyInfoNamespace = "http://www.w3.org/2000/09/xmldsig#";
}
}

View File

@ -0,0 +1,74 @@
using Org.BouncyCastle.Asn1.X509;
using System.Linq;
using System.Text;
namespace Hcs.GostXades.Helpers
{
public static class X509NameHelper
{
/// <summary>
/// Исправить строку X509IssuerName для рукожопых пейсателей из Ланита
/// </summary>
/// <param name="x509Name">Исходная строка из сертификата</param>
/// <returns>Исправленная строка, чтобы ее понимал сервер ГИС ЖКХ</returns>
public static string ToX509IssuerName(this X509Name x509Name)
{
string x509IssuerName = x509Name.ToString();
var pairs = x509IssuerName
.Replace("\\,", "^_^")
.Split(',')
.Select(part => part.Split('='))
.Select(lrParts => new ReplacementPair
{
Key = lrParts[0],
Value = lrParts.Length == 2 ? lrParts[1] : string.Empty
}).ToList();
var nCount = pairs.Count;
var result = new StringBuilder();
var i = 0;
foreach (var pair in pairs)
{
switch (pair.Key.ToLower())
{
case "t":
case "title":
pair.Key = "2.5.4.12";
break;
case "g":
case "givenname":
pair.Key = "2.5.4.42";
break;
case "e":
pair.Key = "1.2.840.113549.1.9.1";
break;
case "sn":
case "surname":
pair.Key = "2.5.4.4";
break;
case "ou":
case "orgunit":
pair.Key = "2.5.4.11";
break;
case "unstructured-name":
case "unstructuredname":
pair.Key = "1.2.840.113549.1.9.2";
break;
}
result.Append($"{pair.Key}={pair.Value}{(i != (nCount - 1) ? ", " : string.Empty)}");
i++;
}
return result.ToString().Replace("^_^", "\\,");
}
}
internal class ReplacementPair
{
public string Key { get; set; }
public string Value { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using Microsoft.Xades;
using System.Xml;
namespace Hcs.GostXades.Helpers
{
public static class XadesSignedXmlHelper
{
public static void InjectSignatureTo(this XadesSignedXml signedXml, XmlDocument originalDoc)
{
var signatureElement = signedXml.GetXml();
var importSignatureElement = originalDoc.ImportNode(signatureElement, true);
var signedDataContainer = signedXml.GetIdElement(originalDoc, signedXml.SignedElementId);
signedDataContainer.InsertBefore(importSignatureElement, signedDataContainer.FirstChild);
}
}
}

View File

@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
namespace Hcs.GostXades.Helpers
{
public static class XmlDocumentHelper
{
/// <summary>
/// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> XmlDocument <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// </summary>
public static XmlDocument Create(string xmlData)
{
var xmlDocument = new XmlDocument { PreserveWhitespace = true };
try
{
xmlDocument.LoadXml(xmlData);
}
catch (XmlException xmlEx)
{
throw new InvalidOperationException($"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> xml <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {xmlEx.Message}", xmlEx);
}
return xmlDocument;
}
/// <summary>
/// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> XmlDocument <20><> <20><><EFBFBD><EFBFBD><EFBFBD>
/// </summary>
public static XmlDocument Load(string pathName)
{
var xmlDocument = new XmlDocument { PreserveWhitespace = true };
try
{
xmlDocument.Load(pathName);
}
catch (XmlException xmlEx)
{
throw new InvalidOperationException($"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> xml <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {xmlEx.Message}", xmlEx);
}
return xmlDocument;
}
/// <summary>
/// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> XmlDocument
/// </summary>
public static XmlNamespaceManager CreateNamespaceManager(this XmlDocument xml)
{
var manager = new XmlNamespaceManager(xml.NameTable);
foreach (var name in GetNamespaceDictionary(xml))
{
manager.AddNamespace(name.Key, name.Value);
}
return manager;
}
private static IDictionary<string, string> GetNamespaceDictionary(this XmlDocument xml)
{
var nameSpaceList = xml.SelectNodes(@"//namespace::*[not(. = ../../namespace::*)]").OfType<XmlNode>();
return nameSpaceList.Distinct(new LocalNameComparer()).ToDictionary(xmlNode => xmlNode.LocalName, xmlNode => xmlNode.Value);
}
private class LocalNameComparer : IEqualityComparer<XmlNode>
{
public bool Equals(XmlNode x, XmlNode y)
{
return x.LocalName == y.LocalName;
}
public int GetHashCode(XmlNode obj)
{
return obj.LocalName.GetHashCode();
}
}
}
}

View File

@ -0,0 +1,59 @@
using Hcs.GostXades.Abstractions;
using Hcs.GostXades.Helpers;
using Org.BouncyCastle.Asn1.X509;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace Hcs.GostXades
{
public class IssuerComparer : IIssuerComparer
{
static IssuerComparer()
{
var regexps = new Dictionary<string, string>
{
["^(ОГРН|OGRN|OID.1.2.643.100.1)="] = "1.2.643.100.1=",
["^(ОГРНИП|OGRNIP|OID.1.2.643.100.5)="] = "1.2.643.100.5=",
["^(ИНН|INN|OID.1.2.643.3.131.1.1)="] = "1.2.643.3.131.1.1=",
["^(E|Е|OID.1.2.840.113549.1.9.1)="] = "1.2.840.113549.1.9.1=",
["^S="] = "ST=",
["\\\""] = string.Empty,
[". "] = ".",
};
RegexpToReplace = regexps.ToDictionary(x => new Regex(x.Key), x => x.Value);
}
private static IEnumerable<string> ParseIssuer(string issuer)
{
return Tokenize(issuer).Select(ReplaceOids);
}
private static string ReplaceOids(string oidString)
{
foreach (var item in RegexpToReplace)
{
oidString = item.Key.Replace(oidString, item.Value);
}
return oidString;
}
private static IEnumerable<string> Tokenize(string issuer)
{
var tokenizer = new X509NameTokenizer(issuer);
while (tokenizer.HasMoreTokens())
{
yield return tokenizer.NextToken();
}
}
private static readonly Dictionary<Regex, string> RegexpToReplace;
public bool AreSameIssuer(string first, string second)
{
var firstTokens = ParseIssuer(first);
var secondTokens = ParseIssuer(second);
return EnumerableHelper.AreSequenceEquals(firstTokens, secondTokens);
}
}
}

View File

@ -0,0 +1,174 @@
using Hcs.GostXades.Abstractions;
using Hcs.GostXades.Helpers;
using Microsoft.Xades;
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Xml;
namespace Hcs.GostXades
{
public class XadesBesSignedXml : XadesSignedXml
{
public ICertificateMatcher CertificateMatcher { get; set; }
public ICryptoProvider CryptoProvider { get; set; }
/// <param name="document">Документ, который необходимо подписать с помощью XAdES-BES</param>
public XadesBesSignedXml(XmlDocument document) : base(document) { }
/// <param name="document">Документ, подписанный с помощью XAdES-BES</param>
/// <param name="elementId">Id подписанного элемента</param>
public XadesBesSignedXml(XmlDocument document, string elementId) : base(document, elementId)
{
var elementToVerify = FindElement(elementId, document);
var signatureNodeList = FindSignatureNodes(elementToVerify);
if (!signatureNodeList.Any())
{
throw new InvalidOperationException($"Элемент с Id {elementId} не содержит подписи");
}
if (signatureNodeList.Length > 1)
{
throw new InvalidOperationException($"Элемент с {elementId} подписан более одного раза");
}
var signatureNode = (XmlElement)signatureNodeList[0];
var standart = GetSignatureStandart(signatureNode);
if (standart != KnownSignatureStandard.Xades)
{
throw new InvalidOperationException($"Элемент с Id {elementId} подписан не по стандарту XADES-BES");
}
LoadXml(signatureNode);
}
/// <summary>
/// Валидация XAdES-BES подписи
/// </summary>
public void Validate()
{
X509Certificate2 matchedCert;
try
{
matchedCert = CertificateMatcher.GetSignatureCertificate(this);
}
catch (InvalidOperationException ex)
{
throw new XadesBesValidationException(ex.Message, ex);
}
if (matchedCert == null)
{
throw new XadesBesValidationException("XML подпись неверна");
}
ValidateCertificate(matchedCert);
ValidateAdditionalProperties();
}
/// <summary>
/// Подписывает XML-документ c помощью XAdES-BES
/// </summary>
/// <param name="certificate">Сертификат, с помощью которого производится подпись</param>
/// <param name="privateKeyPassword">Пароль от контейнера закрытого ключа используемого сертификата</param>
public void ComputeSignature(X509Certificate2 certificate, string privateKeyPassword)
{
var signatureId = $"xmldsig-{Guid.NewGuid().ToString().ToLower()}";
SigningKey = CryptoProvider.GetAsymmetricAlgorithm(certificate, privateKeyPassword);
Signature.Id = signatureId;
SignatureValueId = $"{signatureId}-sigvalue";
var reference = CryptoProvider.GetReference(SignedElementId, signatureId);
AddReference(reference);
SignedInfo.CanonicalizationMethod = XmlDsigCanonicalizationUrl;
SignedInfo.SignatureMethod = CryptoProvider.SignatureMethod;
var xadesInfo = new XadesInfo(certificate);
KeyInfo = KeyInfoHelper.Create(certificate);
var xadesObject = CryptoProvider.GetXadesObject(xadesInfo, signatureId);
AddXadesObject(xadesObject);
ComputeSignature();
HashAlgorithm hashAlgorithm;
GetSignedInfoHash(out hashAlgorithm);
var formatter = CryptoProvider.GetSignatureFormatter(certificate);
var signedHash = formatter.CreateSignature(hashAlgorithm.Hash);
Signature.SignatureValue = signedHash;
}
public XmlElement FindElement(string elementId, XmlDocument xmlDocument)
{
var elementToVerify = GetIdElement(xmlDocument, elementId);
if (elementToVerify == null)
{
throw new InvalidOperationException($"Элемент с Id {elementId} не найден");
}
return elementToVerify;
}
private KnownSignatureStandard GetSignatureStandart(XmlElement signatureElement)
{
return GetXadesObjectElement(signatureElement) == null ? KnownSignatureStandard.XmlDsig : KnownSignatureStandard.Xades;
}
private static XmlNode[] FindSignatureNodes(XmlElement elementToVerify)
{
var items = elementToVerify.ChildNodes
.OfType<XmlNode>()
.Where(x => string.Equals(x.LocalName, SignatureTagName, StringComparison.InvariantCulture)
&& string.Equals(x.NamespaceURI, XmlDsigNamespaceUrl, StringComparison.InvariantCultureIgnoreCase))
.ToArray();
return items;
}
private void ValidateAdditionalProperties()
{
var signedSignatureProperties = SignedSignatureProperties;
var signedDataObjectProperties = SignedDataObjectProperties;
// XAdES 1.4.2 clause G.2.2.6 Checking SignaturePolicyIdentifier
ThrowIfTrue(signedSignatureProperties?.SignaturePolicyIdentifier != null, "SignaturePolicyIdentifier");
// XAdES 1.4.2 clause G.2.2.8 Checking DataObjectFormat
ThrowIfTrue(signedDataObjectProperties?.DataObjectFormatCollection.IsNotEmpty(), "DataObjectFormat");
// XAdES 1.4.2 clause G.2.2.9 Checking CommitmentTypeIndication
ThrowIfTrue(signedDataObjectProperties?.CommitmentTypeIndicationCollection.IsNotEmpty(), "CommitmentTypeIndication");
// XAdES 1.4.2 clause G.2.2.11 Checking SignerRole
var signerRole = signedSignatureProperties?.SignerRole;
ThrowIfTrue(signerRole?.ClaimedRoles != null || signerRole?.CertifiedRoles != null, "SignerRole");
// XAdES 1.4.2 clause G.2.2.16.1.1 Checking AllDataObjectsTimeStamp
ThrowIfTrue(signedDataObjectProperties?.AllDataObjectsTimeStampCollection.IsNotEmpty(), "AllDataObjectsTimeStamp");
// XAdES 1.4.2 clause G.2.2.16.1.2 Checking IndividualDataObjectsTimeStamp
ThrowIfTrue(signedDataObjectProperties?.IndividualDataObjectsTimeStampCollection.IsNotEmpty(), "IndividualDataObjectsTimeStamp");
}
private static void ValidateCertificate(X509Certificate2 certificate)
{
if (DateTime.Now > certificate.NotAfter || DateTime.Now < certificate.NotBefore)
{
throw new XadesBesValidationException("Срок действия сертификата истек или еще не наступил");
}
var isCertValid = certificate.Verify();
if (!isCertValid)
{
throw new XadesBesValidationException("Неверный сертификат");
}
}
private static void ThrowIfTrue(bool? condition, string tagName)
{
if (condition == true)
{
throw new XadesBesValidationException($"Свойство xades:{tagName} не поддерживается");
}
}
private const string SignatureTagName = "Signature";
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Runtime.Serialization;
namespace Hcs.GostXades
{
[Serializable]
public class XadesBesValidationException : Exception
{
public XadesBesValidationException()
{
}
public XadesBesValidationException(string message) : base(message)
{
}
public XadesBesValidationException(string message, Exception inner) : base(message, inner)
{
}
protected XadesBesValidationException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Security.Cryptography.X509Certificates;
namespace Hcs.GostXades
{
public class XadesInfo
{
public byte[] RawCertData { get; }
public DateTime SigningDateTimeUtc { get; }
public int TimeZoneOffsetMinutes { get; }
public XadesInfo(X509Certificate certificate)
{
var offset = TimeZoneInfo.Local.GetUtcOffset(DateTime.Now);
RawCertData = certificate.GetRawCertData();
SigningDateTimeUtc = DateTime.UtcNow;
TimeZoneOffsetMinutes = Convert.ToInt32(offset.TotalMinutes);
}
}
}