In einem früheren Artikel wurde der Prozess der Integration der GOST-Zertifikate von CryptoPro in Mono beschrieben. Ebenso beschäftigen wir uns mit der Verbindung von RSA-Zertifikaten.
Wir haben weiterhin eines unserer in C # geschriebenen Serversysteme auf Linux portiert, und die Warteschlange hat den RSA-bezogenen Teil erreicht. Wenn die Schwierigkeiten beim Verbinden beim letzten Mal leicht durch das Zusammenspiel zweier Systeme erklärt werden konnten, die ursprünglich nicht miteinander verbunden waren, erwartete niemand beim Verbinden von „normalen“ RSA-Zertifikaten von Mono einen Haken.
Die Installation des Zertifikats und des Schlüssels verursachte keine Probleme, und das System sah es sogar im Standardspeicher. Es war jedoch nicht mehr möglich, Daten aus einer zuvor generierten Signatur zu signieren, zu verschlüsseln oder zu extrahieren - mono fiel mit einem Fehler stabil aus. Ich musste, wie im Fall von CryptoPro, eine direkte Verbindung zur Verschlüsselungsbibliothek herstellen. Für RSA-Zertifikate unter Linux ist OpenSSL der Hauptkandidat für eine solche Verbindung.
Installation des Zertifikats
Glücklicherweise verfügt Centos 7 über eine integrierte Version von OpenSSL - 1.0.2k. Um keine zusätzlichen Schwierigkeiten in das System einzuführen, haben wir uns entschlossen, eine Verbindung zu dieser Version herzustellen. Mit OpenSSL können Sie jedoch spezielle Datei-Zertifikatspeicher erstellen:
- Ein solches Geschäft enthält Zertifikate und CRLs, keine privaten Schlüssel. In diesem Fall müssen sie separat gespeichert werden.
- Das Speichern von Zertifikaten und privaten Schlüsseln in Windows auf einer Festplatte in unsicherer Form ist „extrem unsicher“ (die Verantwortlichen für digitale Sicherheit beschreiben dies normalerweise umfangreicher und weniger zensiert). Um ehrlich zu sein, ist dies unter Linux nicht sehr sicher, aber in der Tat üblich üben;
- Die Koordination des Speicherorts solcher Repositorys unter Windows und Linux ist recht problematisch.
- Im Falle einer manuellen Implementierung des Speichers ist ein Dienstprogramm erforderlich, um den Satz von Zertifikaten zu verwalten.
- mono selbst verwendet Festplattenspeicher mit einer OpenSSL-Struktur und speichert private Schlüssel in offener Form in der Nähe.
Aus diesen Gründen verwenden wir die Standardspeicher für .NET- und Mono-Zertifikate, um OpenSSL zu verbinden. Unter Linux müssen dazu zunächst das Zertifikat und der private Schlüssel im Mono-Repository abgelegt werden.
Installation des ZertifikatsWir werden hierfür das Standarddienstprogramm certmgr verwenden. Installieren Sie zunächst den privaten Schlüssel von pfx:
certmgr -importKey -c -p {password} My {pfx file}
Dann legen wir das Zertifikat von diesem pfx ab, der private Schlüssel stellt automatisch eine Verbindung dazu her:
certmgr -add -c My {cer file}
Wenn Sie den Schlüssel im Speicher des Computers installieren möchten, müssen Sie die Option -m hinzufügen.
Danach kann das Zertifikat im Repository angezeigt werden:
certmgr -list -c -v My
Achten Sie auf die Ausstellung. Es ist zu beachten, dass das Zertifikat für das System sichtbar ist und an den zuvor heruntergeladenen privaten Schlüssel gebunden ist. Danach können Sie mit der Verbindung im Code fortfahren.
Verbindung im Code
Wie beim letzten Mal sollte das System, obwohl es auf Linux portiert wurde, weiterhin in der Windows-Umgebung funktionieren. Daher sollte die Arbeit mit Kryptographie äußerlich mit allgemeinen Methoden der Form „Byte [] SignData (Byte [] _arData, X509Certificate2 _pCert)“ durchgeführt werden, die sowohl unter Linux als auch unter Windows gleich funktionieren sollten.
Idealerweise sollte es Methoden geben, die genau wie unter Windows funktionieren - unabhängig vom Zertifikatstyp (unter Linux über OpenSSL oder CryptoPro je nach Zertifikat und unter Windows über crypt32).
Die Analyse der OpenSSL-Bibliotheken ergab, dass unter Windows die Hauptbibliothek "libeay32.dll" und unter Linux "libcrypto.so.10" lautet. Wie beim letzten Mal bilden wir zwei Klassen WOpenSSLAPI und LOpenSSLAPI, die eine Liste von Bibliotheksbibliotheksmethoden enthalten:
[DllImport(CTRYPTLIB, CharSet = CharSet.Auto, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] internal static extern void OPENSSL_init();
Beachten Sie im Gegensatz zu CryptoPro die Aufrufkonvention - hier muss sie explizit angegeben werden. Die Syntax zum Verbinden der einzelnen Methoden muss dieses Mal unabhängig basierend auf * .h OpenSSL-Quelldateien generiert werden .
Die Grundregeln zum Generieren der Aufrufsyntax in C # basierend auf Daten aus .h-Dateien lauten wie folgt:
- alle Links zu Strukturen, Zeichenfolgen usw. - IntPtr, einschließlich Links innerhalb der Strukturen selbst;
- Links zu Links - Ref IntPtr, wenn diese Option nicht funktioniert, dann nur IntPtr. In diesem Fall muss der Link selbst manuell eingefügt und entfernt werden.
- Arrays - Byte [];
- long in C (OpenSSL) ist ein int in C # (ein kleiner Fehler kann auf den ersten Blick zu stundenlangem Suchen nach der Quelle unvorhersehbarer Fehler werden);
In der Deklaration können Sie aus Gewohnheit SetLastError = true angeben, die Bibliothek ignoriert dies jedoch - Fehler sind über Marshal.GetLastWin32Error () nicht verfügbar. OpenSSL verfügt über eigene Methoden für den Zugriff auf Fehler.
Und dann bilden wir die bereits bekannte statische Klasse „UOpenSSLAPI“, die je nach System die Methode einer von zwei Klassen aufruft:
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); } }
Wir stellen sofort fest, dass ein kritischer Abschnitt vorhanden ist. OpenSSL bietet theoretisch Arbeit in einer Multithread-Umgebung. Zunächst wird jedoch in der Beschreibung gesagt, dass dies nicht garantiert ist:
Sie können die meisten Objekte jedoch nicht gleichzeitig in mehreren Threads verwenden.
Und zweitens ist die Verbindungsmethode nicht die trivialste. Die übliche Zwei-Kern-VM (Server mit dem Intel Xeon E5649-Prozessor im Hyper-Threading-Modus) liefert bei Verwendung eines solchen kritischen Abschnitts etwa 100 vollständige Zyklen (siehe Testalgorithmus aus dem vorherigen Artikel ) oder 600 Signaturen pro Sekunde, was für die meisten Aufgaben grundsätzlich ausreicht ( Bei starker Belastung wird ohnehin die Microservice- oder Knotenarchitektur des Systems verwendet.
OpenSSL initialisieren und entladen
Im Gegensatz zu CryptoPro erfordert OpenSSL bestimmte Aktionen, bevor Sie es verwenden und nachdem Sie die Arbeit mit der Bibliothek beendet haben:
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(); }
Fehlerinformationen
OpenSSL speichert Fehlerinformationen in internen Strukturen, für die es spezielle Methoden in der Bibliothek gibt. Leider sind einige der einfachen Methoden, wie z. B. ERR_error_string, instabil, sodass Sie komplexere Methoden verwenden müssen:
Fehlerinformationen abrufen public static string GetErrStrPart(ulong _iErr, int _iPart) {
Ein Fehler in OpenSSL enthält Informationen über die Bibliothek, in der er aufgetreten ist, die Methode und den Grund. Nach Erhalt des Fehlercodes selbst müssen daher alle drei Teile separat extrahiert und in einer Textzeile zusammengefasst werden. Die Länge jeder Zeile beträgt laut OpenSSL- Dokumentation nicht mehr als 120 Zeichen. Da wir verwalteten Code verwenden, müssen wir die Zeile sorgfältig aus dem Link extrahieren:
Abrufen eines Strings durch 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 ""; } }
Fehler beim Überprüfen von Zertifikaten fallen nicht in die allgemeine Liste und müssen je nach Überprüfungskontext mit einer separaten Methode extrahiert werden:
Zertifikatüberprüfungsfehler erhalten public static string GetCertVerifyErr(IntPtr _hStoreCtx) { int iErr = UOpenSSLAPI.X509_STORE_CTX_get_error(_hStoreCtx); return PtrToFirstStr(UOpenSSLAPI.X509_verify_cert_error_string(iErr)); }
Zertifikatsuche
Wie immer beginnt die Kryptografie mit einer Zertifikatsuche. Wir verwenden Vollzeitspeicher, daher suchen wir mit regulären Methoden:
Zertifikatsuche 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) {
Achten Sie auf den kritischen Abschnitt. Mono mit Zertifikaten funktioniert auch über OpenSSL, jedoch nicht über UOpenSSLAPI. Wenn Sie dies hier nicht tun, können unter Last Speicherlecks und unverständliche Fehler auftreten.
Das Hauptmerkmal ist die Erstellung eines Zertifikats. Im Gegensatz zur Version für CryptoPro erhalten wir in diesem Fall das Zertifikat selbst (X509Certificate2) aus dem Store, und der Link in Handle darin ist bereits auf die OpenSSL-Struktur X509_st gerichtet. Es scheint notwendig zu sein, aber es gibt keinen Zeiger auf EVP_PKEY (Link zur privaten Schlüsselstruktur in OpenSSL) im Zertifikat.
Wie sich herausstellte, wird der private Schlüssel selbst im internen Feld des Zertifikats - impl / fallback / _cert / _rsa / rsa - gespeichert. Dies ist die RSAManaged-Klasse, und ein kurzer Blick auf ihren Code (z. B. die DecryptValue- Methode) zeigt, wie schlecht Mono mit Kryptografie ist. Anstatt OpenSSL-Kryptografietechniken ehrlich zu verwenden, scheinen sie mehrere Algorithmen manuell implementiert zu haben. Diese Annahme wird durch ein leeres Suchergebnis für ihr Projekt für OpenSSL-Methoden wie CMS_final, CMS_sign oder CMS_ContentInfo_new unterstützt. Und ohne sie ist die Bildung einer Standard-CMS-Signaturstruktur kaum vorstellbar. Gleichzeitig wird die Arbeit mit Zertifikaten teilweise über OpenSSL ausgeführt.
Dies bedeutet, dass der private Schlüssel von Mono entladen und über PEM in EVP_PKEY geladen werden muss. Aus diesem Grund benötigen wir erneut den Klassenerben von X509Certificate, in dem alle zusätzlichen Links gespeichert werden.
Hier sind nur Versuche, wie im Fall von CryptoPro, ein neues Zertifikat aus Handle zu erstellen - führen ebenfalls nicht zum Erfolg (Mono stürzt mit einem Fehler ab), und das Erstellen auf der Grundlage des empfangenen Zertifikats führt zu Speicherverlusten. Daher besteht die einzige Möglichkeit darin, ein Zertifikat basierend auf einem Byte-Array mit pem zu erstellen. Das PEM-Zertifikat kann wie folgt erworben werden:
Erhalt eines PEM-Zertifikats 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);
Das Zertifikat wird ohne privaten Schlüssel erhalten und wir verbinden es selbst und bilden ein separates Feld für einen Link zu ENV_PKEY:
Generieren von ENV_PKEY basierend auf dem privaten PEM-Schlüssel internal static IntPtr GetENV_PKEYOS(byte[] _arData) { IntPtr hBIOPem = IntPtr.Zero; try {
Beim Hochladen des privaten Schlüssels in PEM ist die Aufgabe viel komplizierter als das PEM-Zertifikat, wird jedoch hier bereits beschrieben. Beachten Sie, dass das Entladen des privaten Schlüssels ein „äußerst unsicheres“ Geschäft ist und dies in jeder Hinsicht vermieden werden sollte. Und da ein solches Entladen für die Arbeit mit OpenSSL erforderlich ist, ist es unter Windows besser, die crypt32.dll-Methoden oder reguläre .Net-Klassen zu verwenden, wenn Sie diese Bibliothek verwenden. Unter Linux muss man vorerst so arbeiten.
Es ist auch zu beachten, dass die generierten Links einen nicht verwalteten Speicherbereich anzeigen und freigegeben werden müssen. Weil in .Net 4.5 ist X509Certificate2 nicht verfügbar, dann müssen Sie dies im Destruktor tun
Unterschreiben
Um OpenSSL zu signieren, können Sie die vereinfachte CMS_sign-Methode verwenden. Bei der Auswahl eines Algorithmus wird jedoch eine Konfigurationsdatei verwendet, die für alle Zertifikate gleich ist. Daher ist es besser, sich auf den Code dieser Methode zu verlassen, um eine ähnliche Signaturgenerierung zu implementieren:
Datensignatur 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 {
Der Fortschritt des Algorithmus ist wie folgt. Konvertieren Sie zunächst das eingehende Zertifikat (falls es sich um X509Certificate2 handelt) in unseren Typ. Weil Da wir mit Links zu einem nicht verwalteten Speicherbereich arbeiten, müssen wir diese sorgfältig überwachen. Einige Zeit, nachdem der Link zum Zertifikat außerhalb des Gültigkeitsbereichs liegt, wird der Destruktor gestartet. Und darin haben wir bereits genau die Methoden vorgeschrieben, die erforderlich sind, um den gesamten damit verbundenen nicht verwalteten Speicher zu löschen. Dieser Ansatz ermöglicht es uns, keine Zeit damit zu verschwenden, diese Links direkt innerhalb der Methode zu verfolgen.
Nachdem wir uns mit dem Zertifikat befasst haben, bilden wir ein BIO mit Daten und einer Signaturstruktur. Dann fügen wir die Daten des Unterzeichners hinzu, setzen das Flag zum Trennen der Signatur und beginnen mit der endgültigen Bildung der Signatur. Das Ergebnis wird an BIO übertragen. Es bleibt nur ein Array von Bytes aus dem BIO zu extrahieren. Das Konvertieren von BIO in eine Reihe von Bytes und umgekehrt wird häufig verwendet. Es ist daher besser, sie in eine separate Methode zu setzen:
BIO in Byte [] und umgekehrt 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 {
Wie bei CryptoPro müssen Informationen zum Signatur-Hashing-Algorithmus aus dem Zertifikat extrahiert werden. Bei OpenSSL wird es jedoch direkt im Zertifikat gespeichert:
Abrufen eines Hash-Algorithmus 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); }
Die Methode stellte sich als ziemlich schwierig heraus, aber sie funktioniert. Sie finden die Methode EVP_get_digestbynid in der Dokumentation 1.0.2, die Bibliotheken der von uns verwendeten Version exportieren sie jedoch nicht. Deshalb bilden wir zuerst nid und auf seiner Basis einen Kurznamen. Und bereits mit einem Kurznamen können Sie den Algorithmus auf die normale Weise extrahieren, indem Sie nach Namen suchen.
Überprüfung der Unterschrift
Die empfangene Unterschrift muss überprüft werden. OpenSSL überprüft die Signatur wie folgt:
Überprüfung der Unterschrift 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 {
Zunächst werden die Signaturdaten vom Byte-Array in die CMS-Struktur konvertiert:
Bildung der CMS-Struktur 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;
, .
Fazit
, . X509Certificate2 (mono) . .
, Windows . . Linux , , .
CSP 5.0 , RSA . , , , RSA, , .
Referenzen
- OpenSSL 1.0.2 ManPages ;
- OpenSSL 1 2 ;
- OpenSSL :
- cms_smime.c;
- Wiki OpenSSL ;
- mono:
- RSAManaged ;