Firmando un PDF en JS y pegando una firma en C # usando Crypto PRO

Entonces La tarea ha llegado. Con un navegador, invite al usuario a firmar el PDF con una firma electrónica (en lo sucesivo, EP). El usuario debe tener un token que contenga el certificado, las claves públicas y privadas. A continuación, en el servidor debe insertar la firma en el documento PDF. Después de eso, debe verificar la validez de la firma. Usamos ASP.NET y, en consecuencia, C # como back-end.

Toda la sal es que necesita usar una firma en el formato CAdES-X Long Type 1 y el ruso GOST R 34.10-2001, GOST R 34.10-2012, etc. Además, puede haber más de una firma, es decir, los usuarios pueden turnarse para firmar un archivo. Sin embargo, las firmas anteriores deben seguir siendo válidas.

En el proceso de resolver el problema, decidieron complicarnos y reducir la cantidad de datos transmitidos al cliente. Solo transmita el hash del documento, pero no el documento en sí.

En el código fuente omitiré los momentos que son de poca importancia para el tema, dejaré solo eso con respecto a la criptografía. Daré el código JS solo para navegadores normales cuyos motores JS admiten Promise y generador de funciones. Creo que quién necesita escribir para IE ellos mismos (tuve que "no quiero").

Lo que necesitas

  1. El usuario debe recibir un par de claves y un certificado.
  2. El usuario debe instalar el complemento desde Crypto PRO. Sin esto, no podremos trabajar con un proveedor de servicios criptográficos utilizando herramientas JS.

Observaciones:

  1. Para las pruebas, tenía un certificado emitido por la prueba Crypto PRO CA y un token normal recibido por uno de nuestros empleados (al momento de escribir ~ 1500r con una licencia anual para Crypto PRO y dos certificados: pero el "nuevo" y el "antiguo" GOST)
  2. Dicen que el complemento puede funcionar con ViPNet, pero no lo he probado.

Ahora suponemos que en nuestro servidor hay un PDF listo para firmar.
Agregue un script de Crypto PRO a la página:

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

Luego tenemos que esperar hasta que se forme el objeto cadesplugin

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

Le preguntamos al servidor hash. Anteriormente, para esto todavía necesitamos saber qué certificado, y por lo tanto el algoritmo, firmará el usuario. Un pequeño comentario: combiné todas las funciones y "variables" para trabajar con criptografía en el lado del cliente en un CryptographyObject.

Método para llenar el campo de certificados del objeto CryptographyObject:

  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); } }); } 

Comentario: intente abrir un almacén de certificados. En este punto, el sistema del usuario le advertirá que el sitio está tratando de hacer algo con certificados, criptografía y otras tonterías mágicas e incomprensibles. El usuario deberá hacer clic en "Sí"
A continuación, obtenemos certificados que son válidos a tiempo (no caducados) y los colocamos en la matriz de certificados. Esto debe hacerse debido a la naturaleza asincrónica de cadesplugin (para IE, todo es diferente;)).

Método hash:

 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); } } 

Comentario: preste atención a cadesplugin.async_spawn, se le pasa la función de generador, a la que next () se llama secuencialmente, lo que conduce a la transición al rendimiento.
Por lo tanto, obtenemos un cierto análogo de async-wait de C #. Todo parece síncrono, pero funciona de forma asíncrona.

Ahora, qué sucede en el servidor cuando se ha solicitado hash.

Primero, debe instalar el paquete nuget iTextSharp (en el momento de la redacción, la versión actual 5.5.13 debería ser)

En segundo lugar, se necesita CryptoPro.Sharpei, va a la carga de Crypto PRO .NET SDK

Ahora puedes obtener hash

  // 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  ,   ,      } } 

En el cliente, que tiene hash del servidor, lo firmamos

  //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); } } 

Comentario: envíe la firma recibida al servidor (ver arriba)

Bueno, finalmente, inserte la firma en el documento en el lado del servidor

 //   //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); //   } } 

Comentario: SimpleExternalSignatureContainer es la clase más simple que implementa la interfaz IExternalSignatureContainer.

  /// <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) { } } 

En realidad, con la firma del PDF, eso es todo. La verificación se describirá en la continuación del artículo. Espero que ella sea ...



Se hicieron correcciones del comentario al recibir el algoritmo de firma de Oid. Gracias

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


All Articles