Sehubungan dengan transisi ke Linux, menjadi perlu untuk mem-port salah satu sistem server kami yang ditulis dalam C # ke Mono. Sistem ini bekerja dengan tanda tangan digital yang disempurnakan, jadi salah satu tugas yang kami hadapi adalah memeriksa kinerja sertifikat GOST dari CryptoPro in mono. CryptoPro sendiri telah mengimplementasikan CSP untuk Linux untuk beberapa waktu, tetapi upaya pertama untuk menggunakannya menunjukkan bahwa kelas kriptografi Mono asli (mirip dengan yang ada di pangkalan. Net - X509Store, X509Certificate2, dll.) Tidak hanya tidak bekerja dengan kunci tamu, mereka bahkan tidak melihat mereka di brankas mereka. Karena ini, pekerjaan dengan kriptografi harus terhubung langsung melalui pustaka CryptoPro.
Instalasi Sertifikat
Sebelum Anda menerapkan kode, Anda harus menginstal sertifikat dan memastikan bahwa itu berfungsi dengan baik.
Instalasi SertifikatCryptoPro CSP versi versi 3.9 diinstal pada Centos 7 di folder / opt / cprocsp. Untuk menghindari konflik antara utilitas mono dan CryptoPro dengan nama yang sama (misalnya, certmgr), path ke folder tidak ditambahkan ke variabel lingkungan dan semua utilitas dipanggil dalam path lengkap.
Pertama, kami mendefinisikan daftar pembaca:
/opt/cprocsp/bin/amd64/csptest -enum -info -type PP_ENUMREADERS | iconv -f cp1251
Jika tidak ada pembaca dari folder pada disk (HDIMAGE) di antara daftar, letakkan:
/opt/cprocsp/sbin/amd64/cpconfig -hardware reader -add HDIMAGE store
Kemudian Anda bisa membuat wadah dari bentuk '\\. \ HDIMAGE \ {nama wadah}' dengan membuat wadah baru dengan kunci:
/opt/cprocsp/bin/amd64/csptest -keyset -provtype 75 -newkeyset -cont '\\.\HDIMAGE\test'
atau dengan membuat folder / var / opt / cprocsp / keys / root / {nama wadah} .000, yang berisi kumpulan standar file kontainer CryptoPro (* .key, * .mask, dll.).
Setelah itu, sertifikat dari wadah dapat dipasang di toko sertifikat:
/opt/cprocsp/bin/amd64/certmgr -inst mMy -cont '\\.\HDIMAGE\{ }'
Sertifikat yang diinstal dapat dilihat dengan perintah berikut:
/opt/cprocsp/bin/amd64/certmgr -list mMy
Pengoperasian sertifikat dapat diverifikasi sebagai berikut:
/opt/cprocsp/bin/amd64/cryptcp – sign -norev -thumbprint {} {} { }
/opt/cprocsp/bin/amd64/cryptcp – verify -norev { }
Jika semuanya baik-baik saja dengan sertifikat, maka Anda dapat melanjutkan ke koneksi dalam kode.
Koneksi dalam kode
Meskipun proses porting ke Linux, sistem itu seharusnya terus berfungsi di lingkungan Windows, jadi secara lahiriah, bekerja dengan kriptografi harus dilakukan melalui metode umum dari bentuk "byte [] SignData (byte [] _arData, X509Certificate2 _ppert)", yang seharusnya bekerja sama seperti di Linux dan juga di Windows.
Analisis metode perpustakaan kriptografi ternyata berhasil, karena CryptoPro menerapkan perpustakaan "libcapi20.so" yang sepenuhnya meniru perpustakaan enkripsi standar Windows - "crypt32.dll" dan "advapi32.dll". Mungkin, tentu saja, tidak sepenuhnya, tetapi semua metode yang diperlukan untuk bekerja dengan kriptografi tersedia di sana, dan hampir semua bekerja.
Oleh karena itu, kami membentuk dua kelas statis "WCryptoAPI" dan "LCryptoAPI", yang masing-masing akan mengimpor serangkaian metode yang diperlukan sebagai berikut:
[DllImport(LIBCAPI20, SetLastError = true)] internal static extern bool CertCloseStore(IntPtr _hCertStore, uint _iFlags);
Sintaks koneksi untuk setiap metode dapat dibuat secara independen, atau menggunakan situs web pinvoke , atau menyalin dari sumber .Net (kelas CAPISafe ). Dari modul yang sama, Anda dapat menggambar konstanta dan struktur yang terkait dengan kriptografi, yang keberadaannya selalu membuat hidup lebih mudah ketika bekerja dengan perpustakaan eksternal.
Dan kemudian kita membentuk kelas statis "UCryptoAPI" yang, tergantung pada sistem, akan memanggil metode salah satu dari dua kelas:
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); } }
Dengan demikian, menggunakan metode dari kelas UCryptoAPI, Anda dapat mengimplementasikan kode yang hampir seragam untuk kedua sistem.
Pencarian Sertifikat
Bekerja dengan kriptografi biasanya dimulai dengan pencarian sertifikat, untuk ini dalam crypt32.dll ada dua metode CertOpenStore (membuka toko sertifikat yang ditentukan) dan CertOpenSystemStore sederhana (membuka sertifikat pribadi pengguna). Karena kenyataan bahwa bekerja dengan sertifikat tidak terbatas pada sertifikat pengguna pribadi, kami menghubungkan yang pertama:
Pencarian Sertifikat 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 {
Pencarian berlangsung dalam beberapa tahap:
- pembukaan penyimpanan;
- pembentukan struktur data yang kami cari;
- pencarian sertifikat;
- jika diperlukan, maka verifikasi sertifikat (dijelaskan dalam bagian terpisah);
- menutup repositori dan melepaskan struktur dari poin 2 (karena di mana-mana ada pekerjaan dengan memori .Net yang tidak dikelola, tidak ada yang akan dilakukan bagi kita untuk membersihkannya);
Ada beberapa poin halus saat mencari sertifikat.
CryptoPro di Linux bekerja dengan string ANSI, dan pada Windows dengan UTF8, oleh karena itu:
- saat menghubungkan metode membuka penyimpanan di Linux, perlu untuk secara eksplisit menentukan jenis marshaling [In, MarshalAs (UnmanagedType.LPStr)];
- melewati string pencarian (misalnya, dengan nama Subjek) itu harus dikonversi menjadi satu set byte dengan pengkodean yang berbeda;
- untuk semua konstanta kriptografi yang memiliki variasi berdasarkan jenis string (misalnya, CERT_FIND_SUBJECT_STR_A dan CERT_FIND_SUBJECT_STR_W) di Windows, Anda harus memilih * _W, dan di Linux * _A;
Metode MapX509StoreFlags dapat diambil langsung dari sumber Microsoft tanpa perubahan, itu hanya membentuk topeng akhir berdasarkan bendera .Net.
Nilai di mana pencarian berlangsung tergantung pada jenis pencarian (periksa dengan MSDN untuk CertFindCertificateInStore ), contoh menunjukkan dua opsi yang paling umum digunakan - untuk format string (nama Subjek, Penerbit, dll.) Dan biner (sidik jari, nomor seri).
Proses membuat sertifikat dari IntPtr di Windows dan Linux sangat berbeda. Windows akan membuat sertifikat dengan cara sederhana:
new X509Certificate2(hCert);
di Linux, Anda harus membuat sertifikat dalam dua langkah:
X509Certificate2(new X509Certificate(hCert));
Di masa depan, kita memerlukan akses ke hCert untuk bekerja, dan itu harus disimpan di objek sertifikat. Di Windows, nanti dapat diambil dari properti Handle, tetapi Linux mengubah struktur CERT_CONTEXT yang mengikuti tautan hCert menjadi tautan ke struktur x509_st (OpenSSL) dan mendaftarkannya di Handle. Oleh karena itu, ada baiknya membuat pewaris dari X509Certificate2 (ISDP_X509Cert dalam contoh), yang akan menyimpan hCert di kedua sistem di bidang yang terpisah.
Jangan lupa bahwa ini adalah tautan ke area memori yang tidak dikelola dan harus dibebaskan setelah pekerjaan selesai. Karena di .Net 4.5 X509Certificate2 tidak dapat digunakan - pembersihan menggunakan metode CertFreeCertificateContext harus dilakukan di destructor.
Formasi tanda tangan
Saat bekerja dengan sertifikat GOST, tanda tangan terputus dengan satu penanda tangan hampir selalu digunakan. Untuk membuat tanda tangan seperti itu, diperlukan blok kode yang cukup sederhana:
Formasi tanda tangan public static int SignDataCP(byte[] _arData, X509Certificate2 _pCert, out byte[] _arRes, ref string _sError) { _arRes = new byte[0];
Selama pekerjaan metode, struktur dengan parameter terbentuk dan metode penandatanganan disebut. Struktur parameter dapat memungkinkan Anda untuk menyimpan sertifikat di tanda tangan untuk membentuk rantai lengkap (bidang cMsgCert dan rgpMsgCert, yang pertama menyimpan jumlah sertifikat, daftar tautan kedua ke struktur sertifikat ini).
Metode penandatanganan dapat menerima satu atau lebih dokumen untuk ditandatangani secara bersamaan dengan satu tanda tangan. Omong-omong, ini tidak bertentangan dengan Undang-Undang Federal 63 dan sangat nyaman, karena pengguna tidak mungkin senang dengan kebutuhan untuk mengklik tombol “tanda” beberapa kali.
Keanehan utama dari metode ini adalah bahwa ia tidak bekerja dalam mode dua panggilan, yang khas untuk sebagian besar metode perpustakaan yang bekerja dengan blok memori yang besar (yang pertama dengan nol - mengembalikan panjang buffer yang diperlukan, yang kedua mengisi buffer). Oleh karena itu, perlu untuk membuat buffer besar, dan kemudian mempersingkat panjang aktualnya.
Satu-satunya masalah serius adalah pencarian OID dari algoritma hash (Intisari) yang digunakan ketika masuk - dalam bentuk eksplisit itu tidak dalam sertifikat (hanya ada algoritma dari tanda tangan itu sendiri). Dan jika pada Windows Anda dapat menentukannya dengan string kosong - itu akan mengambil secara otomatis, tetapi Linux akan menolak untuk menandatangani jika algoritme tidak sama.
Tetapi ada trik - dalam informasi tentang algoritma tanda tangan (struktur CRYPT_OID_INFO) tanda tangan OID disimpan dalam pszOID, dan pengidentifikasi algoritma hash disimpan di Algid. Dan mengubah Algid menjadi OID sudah merupakan masalah teknis:
Mendapatkan OID dari algoritma hash 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));
Setelah membaca kode dengan hati-hati, Anda mungkin terkejut bahwa pengidentifikasi algoritma diperoleh dengan cara sederhana (CertOIDToAlgId) dan Oid di dalamnya rumit (CryptFindOIDInfo). Adalah logis untuk mengasumsikan penggunaan metode yang kompleks atau keduanya sederhana, dan di Linux kedua opsi bekerja dengan sukses. Namun, pada Windows, opsi sulit untuk mendapatkan pengidentifikasi dan hanya mendapatkan OID tidak stabil, sehingga hibrida aneh seperti itu akan menjadi solusi yang stabil.
Verifikasi Tanda Tangan
Verifikasi tanda tangan dilakukan dalam dua tahap, pada awalnya tanda tangan itu sendiri diverifikasi, dan kemudian sertifikat yang dengannya tanda tangan itu diverifikasi (rantai, tanggal penandatanganan, dll.).
Seperti halnya saat menandatangani, Anda harus menentukan set data yang akan ditandatangani, parameter tanda tangan, dan tanda tangan itu sendiri:
Verifikasi Tanda Tangan 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();
Untuk kenyamanan, proses pembentukan struktur dengan parameter telah dipindahkan ke metode terpisah (GetStdSignVerifyPar). Setelah itu, tanda tangan itu sendiri diperiksa dan penandatangan pertama diekstraksi (untuk kebaikan itu perlu untuk mengekstrak semua, tetapi tanda tangan yang mengandung beberapa penandatangan masih eksotis).
Setelah mengekstraksi sertifikat penandatangan, kami akan mengonversinya ke kelas kami dan memverifikasinya (jika ditentukan dalam parameter metode). Untuk verifikasi, tanggal penandatanganan dari penanda tangan pertama digunakan (lihat bagian tentang mengekstraksi informasi dari tanda tangan, dan bagian tentang memeriksa sertifikat).
Ekstrak Informasi Tanda Tangan
Seringkali, sistem kriptografi membutuhkan representasi cetak dari tanda tangan. Dalam setiap kasus, ini berbeda, jadi lebih baik untuk membuat kelas informasi tentang tanda tangan, yang akan berisi informasi dalam bentuk yang nyaman untuk digunakan dan dengan bantuannya menyediakan presentasi yang dicetak. Di .Net ada kelas seperti itu - SignedCms, namun analognya dengan mono dengan tanda tangan CritoPro menolak untuk bekerja di yang pertama, kedua berisi pengubah yang disegel dan ketiga hampir semua propertinya dilindungi oleh penulisan, jadi Anda harus membuat analog Anda sendiri.
Tanda tangan itu sendiri mengandung dua elemen utama - daftar sertifikat dan daftar penandatangan. Daftar sertifikat mungkin kosong, atau mungkin berisi semua sertifikat untuk verifikasi, termasuk rantai lengkap. Daftar penandatangan menunjukkan jumlah tanda tangan nyata. Komunikasi di antara mereka dilakukan oleh nomor seri dan penerbit (Penerbit). Secara teoritis, dalam satu tanda tangan mungkin ada dua sertifikat dari penerbit berbeda dengan nomor seri yang sama, tetapi dalam praktiknya ini dapat diabaikan dan dicari hanya dengan nomor seri.
Membaca tanda tangan adalah sebagai berikut:
Ekstrak Informasi Tanda Tangan public int Decode(byte[] _arSign, ref string _sError) { IntPtr hMsg = IntPtr.Zero;
Tanda tangan diuraikan dalam beberapa tahap, pertama struktur pesan (CryptMsgOpenToDecode) terbentuk, kemudian data tanda tangan nyata (CryptMsgUpdate) dimasukkan ke dalamnya. Masih memverifikasi bahwa ini adalah tanda tangan asli dan pertama mendapatkan daftar sertifikat, dan kemudian daftar penandatangan. Daftar sertifikat diambil secara berurutan:
Mendapatkan daftar sertifikat 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; }
Pertama, jumlah sertifikat ditentukan dari parameter CMSG_CERT_COUNT_PARAM, dan kemudian informasi tentang setiap sertifikat diambil secara berurutan. Proses pembuatan konteks sertifikat dan atas dasar sertifikat itu sendiri melengkapi proses pembuatan.
Mengambil data penanda tangan lebih sulit. Mereka berisi indikasi sertifikat dan daftar parameter tanda tangan (misalnya, tanggal penandatanganan). Proses ekstraksi data adalah sebagai berikut:
Mengambil Informasi Penandatangan 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);
, .
Seperti yang sudah jelas dari pendahuluan, memeriksa sertifikat untuk validitas adalah salah satu tugas yang paling sulit. Itulah sebabnya perpustakaan memiliki banyak metode untuk mengimplementasikan setiap item secara individual. Oleh karena itu, untuk menyederhanakan, mari kita beralih ke sumber .Net untuk metode X509Certificate2.Verify () dan menjadikannya sebagai dasar.
Verifikasi terdiri dari dua tahap:- membentuk rantai sertifikat sampai ke akar;
- periksa setiap sertifikat di dalamnya (untuk pencabutan, waktu, dll.);
Verifikasi semacam itu harus dilakukan sebelum menandatangani dan mengenkripsi pada tanggal saat ini, dan pada saat verifikasi tanda tangan pada tanggal penandatanganan. Metode verifikasi itu sendiri kecil:
Verifikasi Sertifikat 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)));
Pertama, rantai dibentuk menggunakan metode BuildChain, dan kemudian diperiksa. Selama pembentukan rantai, struktur parameter, tanggal verifikasi dan tanda centang terbentuk:
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 .
Kesimpulan
:
- 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 .
Referensi
- CAPILite
- c #
- .Net:
- CAPIBase
- X509Certificate2
- SignedCMS
- SignerInfo
- mono:
- X509Certificate2
- X509CertificateImplBtls