Add migrated to .NET 8.0 variant of Hcs.Client
This commit is contained in:
18
Hcs.ClientNet/GostXades/Abstractions/ICertificateMatcher.cs
Normal file
18
Hcs.ClientNet/GostXades/Abstractions/ICertificateMatcher.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using Microsoft.Xades;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace 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);
|
||||
}
|
||||
}
|
||||
58
Hcs.ClientNet/GostXades/Abstractions/ICryptoProvider.cs
Normal file
58
Hcs.ClientNet/GostXades/Abstractions/ICryptoProvider.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using Microsoft.Xades;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography.Xml;
|
||||
|
||||
namespace 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);
|
||||
}
|
||||
}
|
||||
16
Hcs.ClientNet/GostXades/Abstractions/IIssuerComparer.cs
Normal file
16
Hcs.ClientNet/GostXades/Abstractions/IIssuerComparer.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace 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);
|
||||
}
|
||||
}
|
||||
23
Hcs.ClientNet/GostXades/Abstractions/IXadesService.cs
Normal file
23
Hcs.ClientNet/GostXades/Abstractions/IXadesService.cs
Normal file
@ -0,0 +1,23 @@
|
||||
namespace 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);
|
||||
}
|
||||
}
|
||||
129
Hcs.ClientNet/GostXades/CertificateMatcher.cs
Normal file
129
Hcs.ClientNet/GostXades/CertificateMatcher.cs
Normal file
@ -0,0 +1,129 @@
|
||||
using GostXades.Abstractions;
|
||||
using 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 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
|
||||
}
|
||||
}
|
||||
13
Hcs.ClientNet/GostXades/CryptoProviderTypeEnum.cs
Normal file
13
Hcs.ClientNet/GostXades/CryptoProviderTypeEnum.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace 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
|
||||
}
|
||||
}
|
||||
130
Hcs.ClientNet/GostXades/GostCryptoProvider.cs
Normal file
130
Hcs.ClientNet/GostXades/GostCryptoProvider.cs
Normal file
@ -0,0 +1,130 @@
|
||||
using GostCryptography.Config;
|
||||
using GostCryptography.Gost_R3410;
|
||||
using GostCryptography.Gost_R3411;
|
||||
using GostXades.Abstractions;
|
||||
using 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 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
72
Hcs.ClientNet/GostXades/GostXadesBesService.cs
Normal file
72
Hcs.ClientNet/GostXades/GostXadesBesService.cs
Normal file
@ -0,0 +1,72 @@
|
||||
using GostXades.Abstractions;
|
||||
using GostXades.Helpers;
|
||||
using System;
|
||||
|
||||
namespace 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Hcs.ClientNet/GostXades/Helpers/ArrayHelper.cs
Normal file
38
Hcs.ClientNet/GostXades/Helpers/ArrayHelper.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
|
||||
namespace 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
Hcs.ClientNet/GostXades/Helpers/CertificateHelper.cs
Normal file
67
Hcs.ClientNet/GostXades/Helpers/CertificateHelper.cs
Normal file
@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Hcs.ClientNet/GostXades/Helpers/CollectionHelper.cs
Normal file
13
Hcs.ClientNet/GostXades/Helpers/CollectionHelper.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
|
||||
namespace GostXades.Helpers
|
||||
{
|
||||
public static class CollectionHelper
|
||||
{
|
||||
public static bool IsNotEmpty(this IEnumerable enumerable)
|
||||
{
|
||||
return enumerable != null && enumerable.OfType<object>().Any();
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Hcs.ClientNet/GostXades/Helpers/ConvertHelper.cs
Normal file
12
Hcs.ClientNet/GostXades/Helpers/ConvertHelper.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace GostXades.Helpers
|
||||
{
|
||||
public static class ConvertHelper
|
||||
{
|
||||
public static string BigIntegerToHex(string str)
|
||||
{
|
||||
return BigInteger.Parse(str).ToString("X");
|
||||
}
|
||||
}
|
||||
}
|
||||
14
Hcs.ClientNet/GostXades/Helpers/DateTimeHelper.cs
Normal file
14
Hcs.ClientNet/GostXades/Helpers/DateTimeHelper.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
15
Hcs.ClientNet/GostXades/Helpers/EnumerableHelper.cs
Normal file
15
Hcs.ClientNet/GostXades/Helpers/EnumerableHelper.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Hcs.ClientNet/GostXades/Helpers/GostHashAlgorithmHelper.cs
Normal file
23
Hcs.ClientNet/GostXades/Helpers/GostHashAlgorithmHelper.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using GostCryptography.Config;
|
||||
using System;
|
||||
|
||||
namespace 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("-", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Hcs.ClientNet/GostXades/Helpers/KeyInfoHelper.cs
Normal file
28
Hcs.ClientNet/GostXades/Helpers/KeyInfoHelper.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography.Xml;
|
||||
using System.Xml;
|
||||
|
||||
namespace 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#";
|
||||
}
|
||||
}
|
||||
74
Hcs.ClientNet/GostXades/Helpers/X509NameHelper.cs
Normal file
74
Hcs.ClientNet/GostXades/Helpers/X509NameHelper.cs
Normal file
@ -0,0 +1,74 @@
|
||||
using Org.BouncyCastle.Asn1.X509;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace 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; }
|
||||
}
|
||||
}
|
||||
16
Hcs.ClientNet/GostXades/Helpers/XadesSignedXmlHelper.cs
Normal file
16
Hcs.ClientNet/GostXades/Helpers/XadesSignedXmlHelper.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using Microsoft.Xades;
|
||||
using System.Xml;
|
||||
|
||||
namespace 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
77
Hcs.ClientNet/GostXades/Helpers/XmlDocumentHelper.cs
Normal file
77
Hcs.ClientNet/GostXades/Helpers/XmlDocumentHelper.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
|
||||
namespace 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
59
Hcs.ClientNet/GostXades/IssuerComparer.cs
Normal file
59
Hcs.ClientNet/GostXades/IssuerComparer.cs
Normal file
@ -0,0 +1,59 @@
|
||||
using GostXades.Abstractions;
|
||||
using GostXades.Helpers;
|
||||
using Org.BouncyCastle.Asn1.X509;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
174
Hcs.ClientNet/GostXades/XadesBesSignedXml.cs
Normal file
174
Hcs.ClientNet/GostXades/XadesBesSignedXml.cs
Normal file
@ -0,0 +1,174 @@
|
||||
using GostXades.Abstractions;
|
||||
using GostXades.Helpers;
|
||||
using Microsoft.Xades;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Xml;
|
||||
|
||||
namespace 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";
|
||||
}
|
||||
}
|
||||
25
Hcs.ClientNet/GostXades/XadesBesValidationException.cs
Normal file
25
Hcs.ClientNet/GostXades/XadesBesValidationException.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
21
Hcs.ClientNet/GostXades/XadesInfo.cs
Normal file
21
Hcs.ClientNet/GostXades/XadesInfo.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user