与向Linux的过渡有关,有必要将我们用C#编写的服务器系统之一移植到Mono。 该系统可使用增强的数字签名,因此我们面临的任务之一就是以单声道方式测试CryptoPro的GOST证书的性能。 CryptoPro本身已经在Linux上实现了CSP ,但是使用它的第一次尝试表明,本机Mono密码学类(类似于基本的.Net-X509Store,X509Certificate2等)不仅不能用于来宾密钥,它们甚至不要在他们的保险库中看到他们。 因此,必须通过CryptoPro库直接连接加密技术。
证书安装
在实施代码之前,必须安装证书并确保它可以正常工作。
证书安装CryptoPro CSP组件版本3.9安装在Centos 7的/ opt / cprocsp文件夹中。 为了避免具有相同名称的mono和CryptoPro实用程序(例如certmgr)之间发生冲突,未将文件夹的路径输入到环境变量中,并且在完整路径中调用了所有实用程序。
首先,我们定义一个读者列表:
/opt/cprocsp/bin/amd64/csptest -enum -info -type PP_ENUMREADERS | iconv -f cp1251
如果列表中磁盘上的文件夹(HDIMAGE)中没有读取器,则将其放置:
/opt/cprocsp/sbin/amd64/cpconfig -hardware reader -add HDIMAGE store
然后,您可以通过使用键创建新的容器来创建格式为'\\。\ HDIMAGE \ {container name}'的容器:
/opt/cprocsp/bin/amd64/csptest -keyset -provtype 75 -newkeyset -cont '\\.\HDIMAGE\test'
或通过创建/ var / opt / cprocsp / keys / root / {容器名称} .000文件夹,其中包含标准的CryptoPro容器文件集(* .key,* .mask等)。
之后,可以将来自容器的证书安装在证书存储中:
/opt/cprocsp/bin/amd64/certmgr -inst mMy -cont '\\.\HDIMAGE\{ }'
可以使用以下命令查看已安装的证书:
/opt/cprocsp/bin/amd64/certmgr -list mMy
证书的操作可以通过以下方式验证:
/opt/cprocsp/bin/amd64/cryptcp – sign -norev -thumbprint {} {} { }
/opt/cprocsp/bin/amd64/cryptcp – verify -norev { }
如果证书一切正常,则可以继续执行代码中的连接。
代码连接
尽管有移植到Linux的过程,该系统仍应在Windows环境中继续运行,因此从表面上看,必须通过“ byte [] SignData(byte [] _arData,X509Certificate2 _pCert)”形式的通用方法来进行加密工作,该方法应与在Linux和Windows上。
事实证明,对密码库方法的分析是成功的,因为CryptoPro实现了“ libcapi20.so”库,该库完全模仿了标准的加密库Windows-“ crypt32.dll”和“ advapi32.dll”。 当然,也许不是全部,但那里提供了所有使用密码的必要方法,几乎所有方法都可以使用。
因此,我们形成两个静态类“ WCryptoAPI”和“ LCryptoAPI”,它们各自将导入必要的方法集,如下所示:
[DllImport(LIBCAPI20, SetLastError = true)] internal static extern bool CertCloseStore(IntPtr _hCertStore, uint _iFlags);
每种方法的连接语法既可以独立创建,也可以使用pinvoke网站,也可以从.Net源代码( CAPISafe类)进行复制。 在同一模块中,您可以绘制与加密相关的常数和结构,当使用外部库时,它们的存在总是使生活变得更轻松。
然后,我们形成静态类“ UCryptoAPI”,根据系统的不同,它将调用以下两个类之一的方法:
internal static bool CertCloseStore(IntPtr _hCertStore, uint _iFlags) { if (fIsLinux) return LCryptoAPI.CertCloseStore(_hCertStore, _iFlags); else return WCryptoAPI.CertCloseStore(_hCertStore, _iFlags); } public static bool fIsLinux { get { int iPlatform = (int) Environment.OSVersion.Platform; return (iPlatform == 4) || (iPlatform == 6) || (iPlatform == 128); } }
因此,使用UCryptoAPI类的方法,您可以为两个系统实现几乎统一的代码。
证书搜寻
加密工作通常从证书搜索开始,为此,在crypt32.dll中有两个CertOpenStore方法(打开指定的证书存储)和一个简单的CertOpenSystemStore(打开用户的个人证书)。 由于使用证书不仅限于个人用户证书,因此我们连接了第一个证书:
证书搜寻 public static int FindCertificateCP(string _pFindValue, out X509Certificate2 _pCert, ref string _sError, StoreLocation _pLocation = StoreLocation.CurrentUser, StoreName _pName = StoreName.My, X509FindType _pFindType = X509FindType.FindByThumbprint, bool _fVerify = false) { _pCert = null; IntPtr hCert = IntPtr.Zero; GCHandle hInternal = new GCHandle(); GCHandle hFull = new GCHandle(); IntPtr hSysStore = IntPtr.Zero; try {
搜索分几个阶段进行:
- 储存开口;
- 我们正在寻找的数据结构的形成;
- 证书搜索;
- 如果需要,则进行证书验证(在单独的部分中进行说明);
- 关闭存储库并从第2点开始释放结构(由于到处都有非托管.Net内存的工作,我们将无法对其进行清理);
搜索证书时有一些细微之处。
Linux上的CryptoPro使用ANSI字符串,而Windows上的CryptoPro使用UTF8,因此:
- 当连接Linux中打开存储的方法时,有必要为存储代码参数明确指示封送处理的类型[In,MarshalAs(UnmanagedType.LPStr)];
- 传递搜索字符串(例如,主题名称)时,必须将其转换为一组具有不同编码的字节;
- 对于Windows中所有因字符串类型而异的加密常量(例如CERT_FIND_SUBJECT_STR_A和CERT_FIND_SUBJECT_STR_W),必须选择* _W,在Linux中选择* _A;
MapX509StoreFlags方法可以直接从Microsoft来源获取而无需更改,它只是基于.Net标志形成最终的掩码。
执行搜索的值取决于搜索的类型(与MSDN 一起检查CertFindCertificateInStore ),该示例显示两个最常用的选项-字符串格式(名称Subject,Issuer等)和二进制格式(指纹,序列号)。
在Windows和Linux上从IntPtr创建证书的过程非常不同。 Windows将以一种简单的方式创建证书:
new X509Certificate2(hCert);
在Linux上,您必须分两步创建证书:
X509Certificate2(new X509Certificate(hCert));
将来,我们需要访问hCert才能工作,并且必须将其存储在证书对象中。 在Windows上,以后可以从Handle属性中检索它,但是Linux会将hCert链接后面的CERT_CONTEXT结构转换为x509_st(OpenSSL)结构的链接,并将其注册到Handle中。 因此,值得从X509Certificate2(示例中为ISDP_X509Cert)创建继承程序,该继承程序会将hCert存储在两个系统的单独字段中。
不要忘记这是到非托管内存区域的链接,在工作结束后必须将其释放。 因为 .NET 4.5中的X509Certificate2不是一次性的-使用CertFreeCertificateContext方法进行的清洁必须在析构函数中执行。
签名形成
使用GOST证书时,几乎总是使用带有一个签名者的断开签名。 为了创建这样的签名,需要一个相当简单的代码块:
签名形成 public static int SignDataCP(byte[] _arData, X509Certificate2 _pCert, out byte[] _arRes, ref string _sError) { _arRes = new byte[0];
在该方法的工作期间,将形成带有参数的结构,并调用签名方法。 参数结构可以使您将证书保存在签名中以形成完整的链(cMsgCert和rgpMsgCert字段,第一个存储证书的数量,第二个存储这些证书的结构的链接列表)。
签名方法可以接收一个或多个文档,以使用一个签名同时进行签名。 顺便说一下,这并不违反联邦法律63,并且非常方便,因为用户不太可能会对需要多次单击“签名”按钮感到高兴。
这种方法的主要缺点是,它不能在两次调用模式下工作,这对于大多数处理大型内存块的库方法来说是典型的(第一个为null -返回所需的缓冲区长度,第二个填充缓冲区)。 因此,有必要创建一个大的缓冲区,然后将其缩短到其实际长度。
唯一严重的问题是搜索签名时使用的哈希算法(摘要)的OID-以显式形式,它不在证书中(仅签名本身的算法)。 如果在Windows上可以用空字符串指定它-它会自动启动,但是如果算法不同,Linux会拒绝签名。
但是有一个技巧-在有关签名算法(结构CRYPT_OID_INFO)的信息中,签名OID存储在pszOID中,而哈希算法标识符存储在Algid中。 将Algid转换为OID已经是一个技术问题:
获取哈希算法的OID internal static int GetHashAlgoritmOID(IntPtr _hCertHandle, out string _sOID, ref string _sError) { _sOID = ""; IntPtr hHashAlgInfo = IntPtr.Zero; IntPtr hData = IntPtr.Zero; try { CERT_CONTEXT pContext = (CERT_CONTEXT)Marshal.PtrToStructure(_hCertHandle, typeof(CERT_CONTEXT)); CERT_INFO pCertInfo = (CERT_INFO)Marshal.PtrToStructure(pContext.pCertInfo, typeof(CERT_INFO));
仔细阅读代码后,您可能会惊讶地发现以简单的方式获得算法标识符(CertOIDToAlgId),而其中的Oid却很复杂(CryptFindOIDInfo)。 假设同时使用复杂方法或同时使用这两种方法是合乎逻辑的,并且在Linux中,这两个选项都可以成功工作。 但是,在Windows上,获取标识符和仅获取OID的困难选择是不稳定的,因此,这种奇怪的混合方式将是一种稳定的解决方案。
签名验证
签名验证分两个阶段进行,首先对签名本身进行验证,然后对生成签名的证书进行验证(链,签名日期等)。
在签名时,还必须指定要签名的数据集,签名参数和签名本身:
签名验证 internal static CRYPT_VERIFY_MESSAGE_PARA GetStdSignVerifyPar() { CRYPT_VERIFY_MESSAGE_PARA pVerifyParams = new CRYPT_VERIFY_MESSAGE_PARA(); pVerifyParams.cbSize = (int)Marshal.SizeOf(pVerifyParams); pVerifyParams.dwMsgEncodingType = UCConsts.PKCS_7_OR_X509_ASN_ENCODING; pVerifyParams.hCryptProv = 0; pVerifyParams.pfnGetSignerCertificate = IntPtr.Zero; pVerifyParams.pvGetArg = IntPtr.Zero; return pVerifyParams; } public static int CheckSignCP(byte[] _arData, byte[] _pSign, out X509Certificate2 _pCert, ref string _sError, bool _fVerifyOnlySign = true, X509RevocationMode _pRevMode = X509RevocationMode.Online, X509RevocationFlag _pRevFlag = X509RevocationFlag.ExcludeRoot){ _pCert = null; IntPtr pHData = Marshal.AllocHGlobal(_arData.Length); GCHandle pCertContext = GCHandle.Alloc(IntPtr.Zero, GCHandleType.Pinned); try { Marshal.Copy(_arData, 0, pHData, _arData.Length); CRYPT_VERIFY_MESSAGE_PARA pVerParam = UCUtils.GetStdSignVerifyPar();
为了方便起见,使用参数形成结构的过程已移至单独的方法(GetStdSignVerifyPar)。 之后,将检查签名本身并提取第一个签名者(虽然有必要提取所有签名,但是包含多个签名者的签名仍然很奇怪)。
提取签署者的证书后,我们会将其转换为我们的类并进行验证(如果在方法参数中指定)。 为了进行验证,将使用第一个签名者的签名日期(请参阅有关从签名中提取信息的部分以及有关检查证书的部分)。
提取签名信息
密码系统通常需要签名的印刷表示。 在每种情况下,它都是不同的,因此最好创建一类有关签名的信息,其中将以方便使用的形式包含信息,并在其帮助下提供印刷的演示文稿。 在.Net中有一个此类-SignedCms,但是,它的带有CritiPro pro签名的单声道类似物首先无法使用,其次它包含密封的修饰符,其次几乎所有属性都被写保护,因此您必须创建自己的类似物。
签名本身包含两个主要元素-证书列表和签名者列表。 证书列表可能为空,或者可能包含用于验证的所有证书,包括完整的链。 签名者列表指示实际签名的数量。 它们之间的通信由序列号和发行者(发行者)进行。 从理论上讲,在一个签名中,可以有两个来自不同发布者的证书,且具有相同的序列号,但实际上,只能通过序列号来忽略和搜索。
读取签名如下:
提取签名信息 public int Decode(byte[] _arSign, ref string _sError) { IntPtr hMsg = IntPtr.Zero;
签名分为几个阶段进行解析,首先是形成消息结构(CryptMsgOpenToDecode),然后将真实的签名数据(CryptMsgUpdate)输入到其中。 仍然需要验证这是真实的签名,并首先获取证书列表,然后再获取签名者列表。 证书列表按顺序检索:
获取证书列表 internal static X509Certificate2Collection GetSignCertificates(IntPtr _hMsg) { X509Certificate2Collection certificates = new X509Certificate2Collection(); uint iCnt = GetCryptMsgParam<uint>(_hMsg, UCConsts.CMSG_CERT_COUNT_PARAM); for (int i = 0; i < iCnt; i++) { IntPtr hInfo = IntPtr.Zero; IntPtr hCert = IntPtr.Zero; try { uint iLen = 0; if (!GetCryptMsgParam(_hMsg, UCConsts.CMSG_CERT_PARAM, out hInfo, out iLen)) continue; hCert = UCryptoAPI.CertCreateCertificateContext(UCConsts.PKCS_7_OR_X509_ASN_ENCODING, hInfo, iLen); if (hCert != IntPtr.Zero) { certificates.Add(new ISDP_X509Cert(hCert)); hCert = IntPtr.Zero; } } finally { if (hInfo != IntPtr.Zero) Marshal.FreeHGlobal(hInfo); if (hInfo != IntPtr.Zero) Marshal.FreeHGlobal(hCert); } } return certificates; }
首先,从CMSG_CERT_COUNT_PARAM参数确定证书的数量,然后顺序检索有关每个证书的信息。 创建证书上下文并基于证书本身的过程完成了创建过程。
检索签名者数据更加困难。 它们包含证书指示和签名参数列表(例如,签名日期)。 数据提取过程如下:
检索签名者信息 public int Decode(IntPtr _hMsg, int _iIndex, ISDPSignedCms _pSignedCms, ref string _sError) {
, CMSG_SIGNER_INFO. . , .
, — ( , ).
internal static CryptographicAttributeObjectCollection ReadCryptoAttrsCollection(CRYPT_ATTRIBUTES _pAttrs) { CryptographicAttributeObjectCollection pRes = new CryptographicAttributeObjectCollection(); for (int i = 0; i < _pAttrs.cAttr; i++) { IntPtr hAttr = new IntPtr((long)_pAttrs.rgAttr + (i * Marshal.SizeOf(typeof(CRYPT_ATTRIBUTE)))); CRYPT_ATTRIBUTE pAttr = (CRYPT_ATTRIBUTE) Marshal.PtrToStructure(hAttr, typeof(CRYPT_ATTRIBUTE)); CryptographicAttributeObject pAttrInfo = new CryptographicAttributeObject(new Oid(pAttr.pszObjId), GetAsnEncodedDataCollection(pAttr)); pRes.Add(pAttrInfo); } return pRes; }
Oid – ( ASN.1). :
internal static Pkcs9AttributeObject Pkcs9AttributeFromOID(string _sName) { switch (_sName) { case UCConsts.S_SIGN_DATE_OID : return new Pkcs9SigningTime();
Pkcs9AttributeObject. , mono . Mono .
— — SignedCms, .
, , . (, , ).
public static int EncryptDataCP(byte[] _arInput, X509Certificate2 _pCert, out byte[] _arRes, ref string _sError) { _arRes = new byte[0]; try {
— , . , , .
, , .
. , ( ). :
internal static int GetEncodeAlgoritmOID(IntPtr _hCertHandle, out string _sOID, ref string _sError) { bool fNeedRelease = false; _sOID = ""; uint iKeySpec = 0; IntPtr hCrypto = IntPtr.Zero; try {
. ( , , , .), . (UCConsts.CRYPT_ENCRYPT_ALG_OID_GROUP_ID). — .
( ).
, , . . — , :
public static int DecryptDataCP(byte[] _arInput, out X509Certificate2 _pCert, out byte[] _arRes, ref string _sError) { _arRes = new byte[0]; _pCert = null; IntPtr hSysStore = UCryptoAPI.CertOpenSystemStore(IntPtr.Zero, UCConsts.AR_CRYPTO_STORE_NAME[(int)StoreName.My]); GCHandle GC = GCHandle.Alloc(hSysStore, GCHandleType.Pinned); IntPtr hOutCertL = IntPtr.Zero; IntPtr hOutCert = IntPtr.Zero; try {
, . , ( Linux ).
, , , , . , . :
- ( , , . .);
- — ;
- — ;
- , , (CRL);
, .
从介绍中已经很清楚,检查证书的有效性是最困难的任务之一。这就是为什么图书馆有很多方法可以单独实现每个项目。因此,为简化起见,让我们转到X509Certificate2.Verify()方法的.Net源代码并以此为基础。
验证包括两个阶段:- 从头到尾形成证书链;
- 检查其中的每个证书(用于吊销,时间等);
此类验证必须在当前日期进行签名和加密之前,以及在签名日期进行签名验证时进行。验证方法本身很小:
证书验证 internal static int VerifyCertificate (IntPtr _hCert, X509RevocationMode _iRevMode, X509RevocationFlag _iRevFlag, DateTime _rOnDate, TimeSpan _iCTLTimeout, IntPtr _hPolicy, ref string _sError) { if (_hCert == IntPtr.Zero) { _sError = UCConsts.S_CRYPTO_CERT_CHECK_ERR; return UConsts.E_NO_CERTIFICATE; } CERT_CHAIN_POLICY_PARA pPolicyParam = new CERT_CHAIN_POLICY_PARA(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_PARA))); CERT_CHAIN_POLICY_STATUS pPolicyStatus = new CERT_CHAIN_POLICY_STATUS(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_STATUS)));
首先,使用BuildChain方法形成一个链,然后对其进行检查。在链形成过程中,将形成参数的结构,验证日期和检查标志:
internal static int BuildChain (IntPtr _hChainEngine, IntPtr _hCert, X509RevocationMode _iRevMode, X509RevocationFlag _iRevFlag, DateTime _rOnDate, TimeSpan _rCTLTimeOut, ref IntPtr _hChain, ref string _sError) {
, Microsoft. hCertPolicy hAppPolicy OID-, , . , , .
(, ).
MapRevocationFlags — .Net — uint .
结论
:
- 10 ;
- ;
- byte[] {1, 2, 3, 4, 5};
- ;
- ;
- byte[] {1, 2, 3, 4, 5};
- ;
Windows Linux 1-, 10- 50- , Linux . Linux - - ( , ), «» . (deadlock-) ( «Access Violation»).
UCryptoAPI . fpCPSection object :
private static object fpCPSection = new object(); internal static bool CryptMsgClose(IntPtr _hCryptMsg) { lock (pCPSection) { if (fIsLinux) return LCryptoAPI.CryptMsgClose(_hCryptMsg); else return WCryptoAPI.CryptMsgClose(_hCryptMsg); } } public static object pCPSection { get { return fpCPSection;} }
, Linux- .
mono Issuer Subject . , , mono X500DistinguishedName . , mono ( ), (impl.issuerName impl.subjectName). (Reflection) X500DistinguishedName, CERT_CONTEXT .
参考文献
- CAPILite
- c #
- .Net:
- CAPIBase
- X509Certificate2
- SignedCMS
- SignerInfo
- mono:
- X509Certificate2
- X509CertificateImplBtls