在上一篇文章中,介绍了将CryptoPro的GOST证书与mono集成的过程。 同样,我们将重点介绍连接RSA证书。
我们继续将用C#编写的服务器系统之一移植到Linux,并且队列到达了与RSA相关的部分。 如果最后一次连接上的困难可以通过最初没有相互连接的两个系统的交互来轻松解释,那么当从mono连接“普通” RSA证书时,没有人会遇到麻烦。
安装证书和密钥不会引起问题,系统甚至可以在标准存储中看到它。 但是,不再可能对以前生成的签名进行签名,加密或提取数据-单声道稳定地出现错误。 与CryptoPro一样,我必须直接连接到加密库。 对于Linux中的RSA证书,这种连接的主要候选者是OpenSSL。
证书安装
幸运的是,Centos 7具有内置版本的OpenSSL-1.0.2k。 为了不给系统带来其他麻烦,我们决定连接到该版本。 OpenSSL允许您创建特殊的文件证书存储,但是:
- 这样的商店包含证书和CRL,而不是私钥,因此在这种情况下,它们将必须单独存储;
- 在Windows中以不安全的形式将证书和私钥存储在磁盘上是“极度不安全的”(负责数字安全性的人员通常将其描述得更宽泛且审查较少),说实话,这在Linux中不是很安全,但是实际上很常见练习
- 在Windows和Linux上协调此类存储库的位置非常困难;
- 在手动实施存储的情况下,将需要实用程序来管理证书集;
- mono本身使用具有OpenSSL结构的磁盘存储,并且还在附近以开放形式存储私钥;
由于这些原因,我们将使用标准的.Net和mono证书存储区来连接OpenSSL。 为此,在Linux上,必须首先将证书和私钥放在mono存储库中。
证书安装我们将为此使用标准certmgr实用程序。 首先,从pfx安装私钥:
certmgr -importKey -c -p {password} My {pfx file}
然后我们将这个pfx中的证书放入,私钥将自动连接到它:
certmgr -add -c My {cer file}
如果要将密钥安装在计算机的存储器中,则必须添加-m选项。
之后,可以在存储库中看到证书:
certmgr -list -c -v My
注意发行。 应该注意的是,证书对系统是可见的,并且与早先下载的私钥绑定。 之后,您可以继续进行代码中的连接。
代码连接
与上次一样,该系统尽管已移植到Linux,但仍应在Windows环境中继续运行。 因此,从外部看,应通过“ byte [] SignData(byte [] _arData,X509Certificate2 _pCert)”形式的通用方法来进行加密,这在Linux和Windows中均应相同。
理想情况下,无论证书的类型如何(在Linux上通过OpenSSL或CryptoPro取决于证书,在Windows上通过crypt32),都应具有与Windows一样有效的方法。
对OpenSSL库的分析表明,在Windows上,主库是“ libeay32.dll”,在Linux上,主库是“ libcrypto.so.10”。 与上次一样,我们形成两个类WOpenSSLAPI和LOpenSSLAPI,其中包含库库方法的列表:
[DllImport(CTRYPTLIB, CharSet = CharSet.Auto, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] internal static extern void OPENSSL_init();
与CryptoPro不同,请注意调用约定,在此必须明确指定。 这次必须基于* .h OpenSSL源文件独立生成用于连接每种方法的语法。
基于.h文件中的数据在C#中生成调用语法的基本规则如下:
- -结构,字符串等的任何链接。-IntPtr,包括结构本身内部的链接;
- 链接到链接-ref IntPtr,如果此选项不起作用,则仅IntPtr。 在这种情况下,必须自己手动放置和删除链接。
- 数组-字节[];
- C语言中的long(OpenSSL)是C#中的int(乍一看,很小的错误可能会花费数小时来寻找不可预测的错误的来源);
在声明中,出于习惯,您可以指定SetLastError = true,但是该库将忽略此错误-通过Marshal.GetLastWin32Error()无法获得错误。 OpenSSL拥有自己的错误访问方法。
然后,我们形成已经熟悉的静态类“ UOpenSSLAPI”,根据系统的不同,该类将调用以下两个类之一的方法:
private static object fpOSSection = new object(); public static void OPENSSL_init() { lock (pOSSection) { if (fIsLinux) LOpenSSLAPI.OPENSSL_init(); else WOpenSSLAPI.OPENSSL_init(); } } public static object pOSSection { get { return fpOSSection; } } public static bool fIsLinux { get { int iPlatform = (int) Environment.OSVersion.Platform; return (iPlatform == 4) || (iPlatform == 6) || (iPlatform == 128); } }
我们立即注意到存在关键部分。 理论上讲, OpenSSL在多线程环境中提供工作。 但是,首先,在描述中立即说不能保证:
但是您仍然不能同时在多个线程中使用大多数对象。
其次,连接方法不是最简单的。 使用这样的关键部分时,通常的双核VM(带有Intel Xeon E5649处理器的服务器处于超线程模式)在使用这样的关键部分时大约每秒提供100个完整周期(请参阅上一篇文章的测试算法)或600个签名,基本上足以应付大多数任务(在重负载下,无论如何都将使用系统的微服务或节点体系结构。
初始化和卸载OpenSSL
与CryptoPro不同,OpenSSL在开始使用它和完成使用库之后需要采取某些措施:
public static void InitOpenSSL() { UOpenSSLAPI.OPENSSL_init(); UOpenSSLAPI.ERR_load_crypto_strings(); UOpenSSLAPI.ERR_load_RSA_strings(); UOpenSSLAPI.OPENSSL_add_all_algorithms_conf(); UOpenSSLAPI.OpenSSL_add_all_ciphers(); UOpenSSLAPI.OpenSSL_add_all_digests(); } public static void CleanupOpenSSL() { UOpenSSLAPI.EVP_cleanup(); UOpenSSLAPI.CRYPTO_cleanup_all_ex_data(); UOpenSSLAPI.ERR_free_strings(); }
错误信息
OpenSSL将错误信息存储在库中有特殊方法的内部结构中。 不幸的是,某些简单的方法(例如ERR_error_string)是不稳定的,因此您必须使用更复杂的方法:
获取错误信息 public static string GetErrStrPart(ulong _iErr, int _iPart) {
OpenSSL中的错误包含有关发生错误的库,方法和原因的信息。 因此,在接收到错误代码本身之后,有必要分别提取所有这三个部分,并将它们放在文本行中。 根据OpenSSL 文档,每行的长度不超过120个字符,并且由于我们使用托管代码,因此我们必须谨慎地从链接中提取该行:
通过IntPtr获取字符串 public static string PtrToFirstStr(IntPtr _hPtr, int _iLen = 256) { if(_hPtr == IntPtr.Zero) return ""; try { byte[] arStr = new byte[_iLen]; Marshal.Copy(_hPtr, arStr, 0, arStr.Length); string[] arRes = Encoding.ASCII.GetString(arStr).Split(new char[] { (char)0 }, StringSplitOptions.RemoveEmptyEntries); if (arRes.Length > 0) return arRes[0]; return ""; }catch { return ""; } }
检查证书时的错误不属于常规列表,必须根据验证上下文通过单独的方法提取它们:
收到证书验证错误 public static string GetCertVerifyErr(IntPtr _hStoreCtx) { int iErr = UOpenSSLAPI.X509_STORE_CTX_get_error(_hStoreCtx); return PtrToFirstStr(UOpenSSLAPI.X509_verify_cert_error_string(iErr)); }
证书搜寻
与往常一样,加密从证书搜索开始。 我们使用全时存储,因此我们将使用常规方法进行搜索:
证书搜寻 internal static int FindCertificateOS(string _pFindValue, out X509Certificate2 _pCert, ref string _sError, StoreLocation _pLocation = StoreLocation.CurrentUser, StoreName _pName = StoreName.My, X509FindType _pFindType = X509FindType.FindByThumbprint, bool _fVerify = false) { lock (UOpenSSLAPI.pOSSection) {
注意关键部分。 具有证书的单声道也可以通过OpenSSL,但不能通过UOpenSSLAPI使用。 如果您在此处不执行此操作,则可能会导致内存泄漏并在负载下浮动难以理解的错误。
主要功能是创建证书。 与CryptoPro的版本不同,在这种情况下,我们从商店中获取证书本身(X509Certificate2),并且其中Handle中的链接已定向到OpenSSL结构X509_st。 似乎有必要,但是证书中没有指向EVP_PKEY(指向OpenSSL中的私钥结构的链接)的指针。
事实证明,私钥本身以明文形式存储在证书的内部字段中-impl / fallback / _cert / _rsa / rsa。 这是RSAManaged类,快速浏览其代码(例如, DecryptValue方法)可显示加密技术的单声道有多糟糕。 他们似乎没有诚实地使用OpenSSL加密技术,而是手动实现了几种算法。 他们的项目的OpenSSL方法(例如CMS_final,CMS_sign或CMS_ContentInfo_new)的空搜索结果支持此假设。 没有它们,很难想象标准CMS签名结构的形成。 同时,使用证书的工作部分是通过OpenSSL进行的。
这意味着私钥将必须从mono卸载并通过pem加载到EVP_PKEY中。 因此,我们再次需要X509Certificate的类继承者,该类继承者将存储所有其他链接。
这只是尝试,例如在CryptoPro中从Handle创建新证书的情况下-也不会成功(由于错误而导致的单次崩溃),并且基于接收到的证书进行创建会导致内存泄漏。 因此,唯一的选择是基于包含pem的字节数组创建证书。 可以通过以下方式获得PEM证书:
获得PEM证书 public static int ToCerFile(this X509Certificate2 _pCert, out byte[] _arData, ref string _sError, bool _fBase64 = true) { _arData = new byte[0]; try { byte[] arData = _pCert.Export(X509ContentType.Cert);
无需私人密钥即可获取证书,我们将其自身连接起来,形成一个单独的字段来链接到ENV_PKEY:
基于PEM私钥生成ENV_PKEY internal static IntPtr GetENV_PKEYOS(byte[] _arData) { IntPtr hBIOPem = IntPtr.Zero; try {
上传的PEM私钥,任务比PEM证书要困难得多,但它已经被描述在这里 。 请注意,卸载私钥是一项“极其不安全”的工作,应从各个方面避免这种情况。 并且由于要使用OpenSSL必须进行此类卸载,因此在Windows中,使用此库时最好使用crypt32.dll方法或常规.Net类。 在Linux中,目前,您必须像这样工作。
还应记住,生成的链接指示非托管内存区域,必须将其释放。 因为 .Net 4.5中的X509Certificate2不是一次性的,那么您需要在析构函数中执行此操作
签收
要签名OpenSSL,可以使用简化的CMS_sign方法,但是,它依赖于选择算法的配置文件,该算法对于所有证书都是相同的。 因此,最好依靠此方法的代码来实现类似的签名生成:
数据签名 internal static int SignDataOS(byte[] _arData, X509Certificate2 _pCert, out byte[] _arRes, ref string _sError) { _arRes = new byte[0]; uint iFlags = UCConsts.CMS_DETACHED; IntPtr hData = IntPtr.Zero; IntPtr hBIORes = IntPtr.Zero; IntPtr hCMS = IntPtr.Zero; try {
算法的进展如下。 首先,将传入的证书(如果是X509Certificate2)转换为我们的类型。 因为 由于我们使用到非托管内存区域的链接,因此我们必须仔细监视它们。 证书链接超出范围后的某个时间,.Net将启动析构函数。 在其中,我们已经精确地规定了清除与之关联的所有非托管内存所必需的方法。 这种方法使我们不必浪费时间直接在方法内部跟踪这些链接。
处理完证书后,我们形成了一个具有数据和签名结构的BIO。 然后,我们添加签名者的数据,设置用于断开签名的标志,并开始最终形成签名。 结果被传送到BIO。 它仅保留从BIO提取字节数组。 通常将BIO转换为一组字节,反之亦然,因此最好将它们放在单独的方法中:
字节[]中的BIO,反之亦然 internal static int ReadFromBIO_OS(IntPtr _hBIO, out byte[] _arRes, ref string _sError, uint _iLen = 0) { _arRes = new byte[0]; IntPtr hRes = IntPtr.Zero; uint iLen = _iLen; if(iLen == 0) iLen = int.MaxValue; try {
与CryptoPro一样,有必要从证书中提取有关签名哈希算法的信息。 但对于OpenSSL,它将直接存储在证书中:
获取哈希算法 public static IntPtr GetDigestAlgOS(IntPtr _hCert) { x509_st pCert = (x509_st)Marshal.PtrToStructure(_hCert, typeof(x509_st)); X509_algor_st pAlgInfo = (X509_algor_st)Marshal.PtrToStructure(pCert.sig_alg, typeof(X509_algor_st)); IntPtr hAlgSn = UOpenSSLAPI.OBJ_nid2sn(UOpenSSLAPI.OBJ_obj2nid(pAlgInfo.algorithm)); return UOpenSSLAPI.EVP_get_digestbyname(hAlgSn); }
该方法原来很棘手,但可以。 您可以在文档1.0.2中找到EVP_get_digestbynid方法,但是我们使用的版本的库不会导出该方法。 因此,首先我们形成nid,并在其基础上形成简称。 并且已经有了一个简短的名称,您可以按名称搜索的常规方式提取算法。
签名验证
收到的签名需要验证。 OpenSSL验证签名如下:
签名验证 internal static int CheckSignOS(byte[] _arData, byte[] _arSign, out X509Certificate2 _pCert, ref string _sError, bool _fVerifyOnlySign = true, StoreLocation _pLocation = StoreLocation.CurrentUser){ _pCert = null; IntPtr hBIOData = IntPtr.Zero; IntPtr hCMS = IntPtr.Zero; IntPtr hTrStore = IntPtr.Zero; try {
首先,将签名数据从字节数组转换为CMS结构:
CMS结构的形成 internal static int GetCMSFromBytesOS(byte[] _arData, out IntPtr _hCMS, ref string _sError) { _hCMS = IntPtr.Zero; IntPtr hBIOCMS = IntPtr.Zero; IntPtr hCMS = IntPtr.Zero; try {
, BIO. , , ( ) :
internal static int GetTrustStoreOS(StoreLocation _pLocation, out IntPtr _hStore, ref string _sError) { _hStore = IntPtr.Zero; IntPtr hStore = IntPtr.Zero; try { List<X509Certificate2> pCerts = GetCertList(_pLocation, StoreName.Root, TCryptoPath.cpOpenSSL); pCerts.AddRange(GetCertList(_pLocation, StoreName.AuthRoot, TCryptoPath.cpOpenSSL));
, ( , ). CMS_Verify, .
(, CRL), iFlag .
. , , . .Net — SignedCms, .
( , ) . — , .
internal int DecodeOS(byte[] _arSign, byte[] _arContent, ref string _sError) { IntPtr hBIOData = IntPtr.Zero; IntPtr hCMS = IntPtr.Zero; IntPtr hCerts = IntPtr.Zero; try {
, BIO ( ) CMS, . , — .
(STACK_OF(X509)), sk_pop, . , sk_value.
, CMS_get0_signers CMS_get1_certs. , . , , :
CRYPTO_add(&cch->d.certificate->references, 1, CRYPTO_LOCK_X509)
1.1.0 X509_up_ref, .
:
public int DecodeOS(IntPtr _hSignerInfo, ref string _sError) { try {
, , . ASN.1. asn1_string_st Pkcs9SigningTime.
:
internal static int GetSignerInfoCertOS(IntPtr _hSignerInfo, X509Certificate2Collection _pCerts, out X509Certificate2 _pCert, ref string _sError) { _pCert = null; try {
, . asn1_string_st, hex :
hex ANS.1 internal static int GetBinaryHexFromASNOS(IntPtr _hASN, out string _sHexData, ref string _sError) { _sHexData = ""; try { asn1_string_st pSerial = (asn1_string_st)Marshal.PtrToStructure(_hASN, typeof(asn1_string_st)); byte[] arStr = new byte[pSerial.iLength]; Marshal.Copy(pSerial.hData, arStr, 0, (int)pSerial.iLength); _sHexData = arStr.ToHex().ToUpper(); return UConsts.S_OK; } catch (Exception E) { _sError = UCConsts.S_HEX_ASN_BINARY_ERR.Frm(E.Message); return UConsts.E_GEN_EXCEPTION; } }
, , .
OpenSSL :
internal static int EncryptDataOS(byte[] _arInput, List<X509Certificate2> _pReceipients, out byte[] _arRes, ref string _sError) { _arRes = new byte[0]; uint iFlags = UCConsts.CMS_BINARY; IntPtr hData = IntPtr.Zero; IntPtr hReceipts = IntPtr.Zero; IntPtr hBIORes = IntPtr.Zero; IntPtr hCMS = IntPtr.Zero; try {
BIO — . . , BIO . OpenSSL , , , . EVP_des_ede3_cbc, .
, . . OpenSSL:
public static int GetCertsStackOS(List<X509Certificate2> _pCerts, out IntPtr _hStack, ref string _sError) { _hStack = IntPtr.Zero; IntPtr hStack = IntPtr.Zero; try { hStack = UOpenSSLAPI.sk_new_null(); foreach (X509Certificate2 pCert in _pCerts) {
, . :
- , ;
- ;
- ;
- ;
- BIO ;
internal static int DecryptDataOS(byte[] _arInput, out X509Certificate2 _pCert, out byte[] _arRes, ref string _sError, StoreLocation _pLocation = StoreLocation.CurrentUser ) { _arRes = new byte[0]; _pCert = null; uint iFlag = UCConsts.CMS_BINARY; IntPtr hBIORes = IntPtr.Zero; IntPtr hCMS = IntPtr.Zero; X509Certificate2 pCert; try {
. , CMS_RecipientInfo_set0_pkey, CMS, .
, , . , . :
internal static int GetRecepInfoCertOS(IntPtr _hRecep, StoreLocation _pLocation, out X509Certificate2 _pCert, ref string _sError) { _pCert = null; try {
CMS_RecipientInfo_ktri_get0_signer_id , hSNO . .
C , , . ktri — . OpenSSL : CMS_RecipientInfo_kari_*, CMS_RecipientInfo_kekri_* CMS_RecipientInfo_set0_password pwri.
, . . , . . . OpenSSL . ( ), , .
, , , :
internal static int VerifyCertificateOS(IntPtr _hCert, X509RevocationMode _iRevMode, X509RevocationFlag _iRevFlag, StoreLocation _pLocation, DateTime _rOnDate, ref string _sError) { IntPtr hStore = IntPtr.Zero; IntPtr hStoreCtx = IntPtr.Zero; try {
(X509_STORE_CTX) . :
public static void SetStoreCtxCheckDate(IntPtr _hStoreCtx, DateTime _rDate) { uint iFlags = UCConsts.X509_V_FLAG_USE_CHECK_TIME | UCConsts.X509_V_FLAG_X509_STRICT | UCConsts.X509_V_FLAG_CRL_CHECK_ALL;
, .
结论
, . X509Certificate2 (mono) . .
, Windows . . Linux , , .
CSP 5.0 , RSA . , , , RSA, , .
参考文献
- OpenSSL 1.0.2 ManPages ;
- OpenSSL 1 2 ;
- OpenSSL :
- cms_smime.c;
- Wiki OpenSSL ;
- mono:
- RSAManaged ;