Signieren einer PDF-Datei in JS und Einfügen einer Signatur in C # mit Crypto PRO

Also. Die Aufgabe ist gekommen. Laden Sie den Benutzer mit einem Browser ein, das PDF mit einer elektronischen Signatur (im Folgenden als EP bezeichnet) zu signieren. Der Benutzer muss über ein Token verfügen, das das Zertifikat sowie öffentliche und private Schlüssel enthält. Als Nächstes müssen Sie auf dem Server die Signatur in das PDF-Dokument einfügen. Danach müssen Sie die Signatur auf Gültigkeit überprüfen. Wir verwenden ASP.NET und dementsprechend C # als Backend.

Das ganze Salz ist, dass Sie eine Signatur im CAdES-X Long Type 1-Format und Russisch GOST R 34.10-2001, GOST R 34.10-2012 usw. verwenden müssen. Darüber hinaus kann es mehr als eine Signatur geben, dh Benutzer können abwechselnd eine Datei signieren. Vorherige Signaturen müssen jedoch gültig bleiben.

Bei der Lösung des Problems haben sie beschlossen, für uns zu komplizieren und die an den Kunden übertragene Datenmenge zu reduzieren. Übertragen Sie nur den Hash des Dokuments, nicht aber das Dokument selbst.

Im Quellcode werde ich die Momente weglassen, die für das Thema von geringer Bedeutung sind, ich werde nur das für die Kryptographie belassen. Ich werde den JS-Code nur für normale Browser geben, deren JS-Engines Promise und Funktionsgenerator unterstützen. Ich denke, wer muss für IE selbst schreiben (ich musste "nicht wollen").

Was Sie brauchen:

  1. Der Benutzer muss ein Schlüsselpaar und ein Zertifikat erhalten.
  2. Der Benutzer muss das Plug-In von Crypto PRO installieren. Ohne dies können wir mit JS-Tools nicht mit einem Kryptografiedienstanbieter zusammenarbeiten.

Anmerkungen:

  1. Für die Tests hatte ich ein Zertifikat, das vom Test Crypto PRO CA ausgestellt wurde, und einen normalen Token, den einer unserer Mitarbeiter erhalten hatte (zum Zeitpunkt des Schreibens von ~ 1500r mit einer jährlichen Lizenz für Crypto PRO und zwei Zertifikaten: aber das „neue“ und das „alte“ GOST).
  2. Sie sagen, dass das Plug-In mit ViPNet funktionieren kann, aber ich habe es nicht getestet.

Jetzt gehen wir davon aus, dass auf unserem Server ein PDF zum Signieren bereitsteht.
Fügen Sie der Seite ein Skript von Crypto PRO hinzu:

<script src="/Scripts/cadesplugin_api.js" type="text/javascript"></script> 

Dann müssen wir warten, bis das Cadesplugin-Objekt gebildet ist

 window.cadespluginLoaded = false; cadesplugin.then(function () { window.cadespluginLoaded = true; }); 

Wir fragen den Hash-Server. Zuvor müssen wir dazu noch wissen, welches Zertifikat und damit welcher Algorithmus der Benutzer signiert. Eine kleine Bemerkung: Ich habe alle Funktionen und "Variablen" für die Arbeit mit Kryptographie auf der Clientseite in einem CryptographyObject zusammengefasst.

Methode zum Ausfüllen des Zertifikatsfelds des CryptographyObject-Objekts:

  fillCertificates: function (failCallback) { cadesplugin.async_spawn(function*() { try { let oStore = yield cadesplugin.CreateObjectAsync("CAPICOM.Store"); oStore.Open(cadesplugin.CAPICOM_CURRENT_USER_STORE, cadesplugin.CAPICOM_MY_STORE, cadesplugin.CAPICOM_STORE_OPEN_MAXIMUM_ALLOWED); let certs = yield oStore.Certificates; certs = yield certs.Find(cadesplugin.CAPICOM_CERTIFICATE_FIND_TIME_VALID); let certsCount = yield certs.Count; for (let i = 1; i <= certsCount; i++) { let cert = yield certs.Item(i); CryptographyObject.certificates.push(cert); } oStore.Close(); } catch (exc) { failCallback(exc); } }); } 

Kommentar: Versuchen Sie, einen Zertifikatspeicher zu öffnen. An diesem Punkt warnt Sie das System des Benutzers, dass die Site versucht, etwas mit Zertifikaten, Kryptografie und anderem magischen, unverständlichen Unsinn zu tun. Der Benutzer muss auf "Ja" klicken.
Als nächstes erhalten wir zeitlich gültige (nicht abgelaufene) Zertifikate und fügen sie in das Zertifikatsarray ein. Dies muss aufgrund der asynchronen Natur von Cadesplugin erfolgen (für IE ist alles anders;)).

Hash-Methode:

 getHash: function (certIndex, successCallback, failCallback, -  ) { try { cadesplugin.async_spawn(function*() { let cert = CryptographyObject.certificates[certIndex]; let certPublicKey = yield cert.PublicKey(); let certAlgorithm = yield certPublicKey.Algorithm; let algorithmValue = yield certAlgorithm.Value; let hashAlgorithm; //           if (algorithmValue === "1.2.643.7.1.1.1.1") { hashAlgorithm = "2012256"; } else if (algorithmValue === "1.2.643.7.1.1.1.2") { hashAlgorithm = "2012512"; } else if (algorithmValue === "1.2.643.2.2.19") { hashAlgorithm = "3411"; } else { failCallback("      ."); return; } $.ajax({ url: "/Services/SignService.asmx/GetHash", method: "POST", contentType: "application/json; charset=utf-8 ", dataType: "json", data: JSON.stringify({ //-     //          hashAlgorithm: hashAlgorithm, }), complete: function (response) { //   ,       if (response.status === 200) { CryptographyObject.signHash(response.responseJSON, function(data) { $.ajax({ url: CryptographyObject.signServiceUrl, method: "POST", contentType: "application/json; charset=utf-8", dataType: "json", data: JSON.stringify({ Signature: data.Signature, //-     //       }), complete: function(response) { if (response.status === 200) successCallback(); else failCallback(); } }); }, certIndex); } else { failCallback(); } } }); }); } catch (exc) { failCallback(exc); } } 

Kommentar: Achten Sie auf cadesplugin.async_spawn, die Generatorfunktion wird an cadesplugin.async_spawn übergeben, worauf next () nacheinander aufgerufen wird, was zum Übergang zum Yield führt.
Somit erhalten wir von C # ein bestimmtes Analogon von asynchronem Warten. Alles sieht synchron aus, funktioniert aber asynchron.

Was passiert nun auf dem Server, wenn ein Hash angefordert wurde?

Zunächst müssen Sie das iTextSharp-Nuget-Paket installieren (zum Zeitpunkt des Schreibens sollte die aktuelle Version 5.5.13 sein).

Zweitens wird CryptoPro.Sharpei benötigt, es geht zum Laden für Crypto PRO .NET SDK

Jetzt können Sie Hash bekommen

  // hash- HashAlgorithm hashAlgorithm; switch (hashAlgorithmName) { case "3411": hashAlgorithm = new Gost3411CryptoServiceProvider(); break; case "2012256": hashAlgorithm = new Gost3411_2012_256CryptoServiceProvider(); break; case "2012512": hashAlgorithm = new Gost3411_2012_512CryptoServiceProvider(); break; default: GetLogger().AddError("  ", $"hashAlgorithmName: {hashAlgorithmName}"); return HttpStatusCode.BadRequest; } // hash   ,  cadesplugin string hash; using (hashAlgorithm) //downloadResponse.RawBytes -     PDF  using (PdfReader reader = new PdfReader(downloadResponse.RawBytes)) { //    int existingSignaturesNumber = reader.AcroFields.GetSignatureNames().Count; using (MemoryStream stream = new MemoryStream()) { //      using (PdfStamper st = PdfStamper.CreateSignature(reader, stream, '\0', null, true)) { PdfSignatureAppearance appearance = st.SignatureAppearance; //        ,        appearance.SetVisibleSignature(new Rectangle(36, 100, 164, 150), reader.NumberOfPages, //  ,       $"{SignatureFieldNamePrefix}{existingSignaturesNumber + 1}"); //,     ExternalBlankSignatureContainer external = new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED); //  -         //  , .. CAdES-X Long Type 1          MakeSignature.SignExternalContainer(appearance, external, 65536); // ,   ,     using (Stream contentStream = appearance.GetRangeStream()) { // hash     ,  cadesplugin hash = string.Join(string.Empty, hashAlgorithm.ComputeHash(contentStream).Select(x => x.ToString("X2"))); } } // stream  ,   ,      } } 

Auf dem Client, der Hash vom Server hat, signieren wir ihn

  //certIndex -    .           hash   signHash: function (data, callback, certIndex, failCallback) { try { cadesplugin.async_spawn(function*() { certIndex = certIndex | 0; let oSigner = yield cadesplugin.CreateObjectAsync("CAdESCOM.CPSigner"); let cert = CryptographyObject.certificates[certIndex]; oSigner.propset_Certificate(cert); oSigner.propset_Options(cadesplugin.CAPICOM_CERTIFICATE_INCLUDE_WHOLE_CHAIN); //     TSP .      oSigner.propset_TSAAddress("https://www.cryptopro.ru/tsp/"); let hashObject = yield cadesplugin.CreateObjectAsync("CAdESCOM.HashedData"); let certPublicKey = yield cert.PublicKey(); let certAlgorithm = yield certPublicKey.Algorithm; let algorithmValue = yield certAlgorithm.Value; if (algorithmValue === "1.2.643.7.1.1.1.1") { yield hashObject.propset_Algorithm(cadesplugin.CADESCOM_HASH_ALGORITHM_CP_GOST_3411_2012_256); oSigner.propset_TSAAddress(CryptographyObject.tsaAddress2012); } else if (algorithmValue === "1.2.643.7.1.1.1.2") { yield hashObject.propset_Algorithm(cadesplugin.CADESCOM_HASH_ALGORITHM_CP_GOST_3411_2012_512); oSigner.propset_TSAAddress(CryptographyObject.tsaAddress2012); } else if (algorithmValue === "1.2.643.2.2.19") { yield hashObject.propset_Algorithm(cadesplugin.CADESCOM_HASH_ALGORITHM_CP_GOST_3411); oSigner.propset_TSAAddress(CryptographyObject.tsaAddress2001); } else { alert("    "); return; } //   hash    hash   yield hashObject.SetHashValue(data.Hash); let oSignedData = yield cadesplugin.CreateObjectAsync("CAdESCOM.CadesSignedData"); oSignedData.propset_ContentEncoding(cadesplugin.CADESCOM_BASE64_TO_BINARY); //   base64 let signatureHex = yield oSignedData.SignHash(hashObject, oSigner, cadesplugin.CADESCOM_CADES_X_LONG_TYPE_1); data.Signature = signatureHex; callback(data); }); } catch (exc) { failCallback(exc); } } 

Kommentar: Senden Sie die empfangene Signatur an den Server (siehe oben)

Nun, schließlich fügen Sie die Signatur in das Dokument auf der Serverseite ein

 //   //downloadResponse.RawBytes -   PDF      using (PdfReader reader = new PdfReader(downloadResponse.RawBytes)) { using (MemoryStream stream = new MemoryStream()) { //requestData.Signature -     IExternalSignatureContainer external = new SimpleExternalSignatureContainer(Convert.FromBase64String(requestData.Signature)); //lastSignatureName -  ,      hash MakeSignature.SignDeferred(reader, lastSignatureName, stream, external); //   } } 

Kommentar: SimpleExternalSignatureContainer ist die einfachste Klasse, die die IExternalSignatureContainer-Schnittstelle implementiert.

  /// <summary> ///      /// </summary> private class SimpleExternalSignatureContainer : IExternalSignatureContainer { private readonly byte[] _signedBytes; public SimpleExternalSignatureContainer(byte[] signedBytes) { _signedBytes = signedBytes; } public byte[] Sign(Stream data) { return _signedBytes; } public void ModifySigningDictionary(PdfDictionary signDic) { } } 

Mit der Unterzeichnung des PDF ist das alles. Die Überprüfung wird in der Fortsetzung des Artikels beschrieben. Ich hoffe sie wird ...



Korrekturen aus dem Kommentar zum Empfang des Oid-Signaturalgorithmus. Vielen Dank

Source: https://habr.com/ru/post/de426087/


All Articles