Validando una firma digital en documentos PDF desde una aplicación en C++

En este tutorial, se verá la implementación de validación de firma digital en documentos PDF con firma digital, desde una aplicación escrita en C++. Para este propósito, es necesario realizar las siguientes validaciones:

  • Verificar la autenticidad del documento.- Este paso consiste en comprobar que el documento firmado no haya sido modificado después de la firma o que el mismo sea una falsificación. Esto se logra a partir de la generación de un HASH (código que identifica al documento de manera única, al igual que una huella digital identifica a una persona). La firma digital se constituye en el HASH del documento que se está firmando encriptado con la clave privada del signatario.
    Para validar la autenticidad del documento, se debe desencriptar la firma con la clave pública y comparar el HASH obtenido con un nuevo HASH calculado a partir del documento.
    - Si no se puede desencriptar la firma, significa que no corresponde al propietario de la clave pública.
    - Si el HASH obtenido de la firma no corresponde al HASH del documento, significa que el documento fue modificado.
  • Verificar la propiedad y legalidad de la clave pública.- Una vez verificado el paso anterior podemos estar seguros de que el documento fue firmado por el propietario del par de claves y que el mismo no fue modificado después de la firma. Sin embargo, también es necesario identificar al propietario del par de claves. Esto se logra a través del certificado de firma digital, que contiene los datos de identidad y clave pública del signatario y además está firmado por una ECA (Entidad Certificadora Autorizada) que para nuestro caso es ADSIB.
    Revisando el certificado se puede obtener la identidad del signatario, pero para estar seguros de que los mismos no fueron falseados, se debe verificar la firma de la ECA en el certificado, en este caso con la clave pública de la ADSIB.
    La firma del certificado se verifica como en el paso anterior, pero utilizando certificado de firma digital de la ADSIB, que a su vez contiene la clave pública y se encuentra en: Certificado ADSIB
  • Verificar la vigencia del certificado.- Hasta este punto ya se tiene la certeza de que el documento fue firmado por el propietario del par de claves identificado a través del certificado de firma digital. Sin embargo, para evitar que por ejemplo un tercero firme documentos a nombre de un difunto, se tienen políticas que limitan la vida útil del certificado a un tiemo determinado.
    Por este motivo, es necesario además verificar que el documento fue firmado dentro del periodo de vigencia del certificado, simplemente comparando la fecha en la que se firmó el documento y el periodo de vigencia del certificado.
  • Verificar la revocación del certificado.- Adicionalmente también es posible que el propietario del par de claves pierda el Token encargado de resguardar su par de claves o el mismo sea robado, en cuyo caso lo más aconsejable es reportar este hecho a la ECA. Una vez reportado el hecho, la ECA publica el estado del certificado como REVOCADO, invalidando cualquier firma posterior a ocurrido el hecho.
    Finalmente entonces será necesario revisar la lista para verificar que el certificado no haya sido revocado.

En el caso de los archivos PDF, se maneja el contenido por secciones, ya que si comparamos el documento original con el firmado, serán diferentes por el contenido de la firma. Sin embargo el contenido propiamente del documento no cambia.

Por lo anterior, el cálculo del HASH se realiza sobre el contenido del PDF como se explica en el siguiente enlace: PDF

El mayor reto al momento de firmar un documento PDF es la manipulación del mismo, por lo que es necesario apoyarse en alguna librería. En nuestro caso se utilizará la librería publicada en: PoDoFo.

Para Debian 10 la librería viene incluída en los repositorios, por lo que solo hará falta ejecutar la siguiente instrucción:

  1. sudo apt install build-essential libpodofo-dev codeblocks

En caso de utilizar una versión previa a Debian 10, se deberá compilar la versión de libpodofo 0.9.6. Adicionalmente a las dependencias se instaló codeblocks que se utilizará para escribir el programa de manera más amigable, por lo que el primer paso será ingresar a codeblocks y crear un proyecto "Console Application".

Console Application

Seguir los siguientes pasos hasta finalizar el asistente.

Console Application Console Application Console Application

Teniendo el proyecto creado, lo primero será agregar las librerías a utilizarse. Para la edición del PDF como ya se mencionó se utilizará PoDoFo y para validar la firma se utilizará OpenSSL:

  1. #include <iostream>
  2. #include <openssl/err.h>
  3. #include <openssl/bio.h>
  4. #include <openssl/pkcs7.h>
  5. #include <openssl/pem.h>
  6. #include <podofo/podofo.h>

Una vez agregadas las librerías, no se podrá compilar el proyecto debido a que no se tiene las referencias a las librerías compartidas. Para agregar la referencia a las librerías compartidas, editar las opciones de contrucción del proyecto (Build options)

Console Application

Para el proyecto seleccionar la pestaña Linker settings y agregar las banderas -lcrypto y lpodofo como se muestra en la imagen.

Console Application

Ya teniendo todo listo para el desarrollo, comenzamos escribiendo en el método main el siguiente código:

  1. PoDoFo::PdfError::EnableDebug(false);
  2. try {
  3. PoDoFo::PdfMemDocument mDoc;
  4. mDoc.Load("prueba.pdf", true);
  5. } catch (PoDoFo::PdfError & e) {
  6. cout << e.what() << endl;
  7. }

El código anterior instancia un objeto de la clase PdfMemDocument que permite modificar documentos PDF y a continuación abre el archivo prueba.pdf, que como no existe en el directorio del proyecto si lo ejecutamos mostrará un mensaje de error indicando que no se encontró el archivo. Podemos firmar un documento o descargar uno de prueba de la siguiente url: PDF y lo guardamos en el directorio del proyecto.

Una vez abierto el archivo lo que corresponde es buscar las firmas que contiene. Para esto utilizaremos un iterador de tipo TCIVecObjects recorriendo objeto por objeto encontrato e ingresando en caso de que sea de tipo Dictionary.

  1. PoDoFo::TCIVecObjects it = mDoc.GetObjects().begin();
  2. int nroSignatures = 0;
  3. while (it != mDoc.GetObjects().end()) {
  4. if ((*it)->IsDictionary()) {
  5. }
  6. it++;
  7. }

La iteración anterior recorrerá el PDF deteniéndose en los objetos que son de tipo Dictionary, ya que a partir de este tipo de objetos se identificará las firmas como se muestra a continuación con el código dentro del if:

  1. PoDoFo::PdfDictionary dict = (*it)->GetDictionary();
  2. if (dict.HasKey("V") && dict.HasKey("Type") && dict.HasKey("Subtype") && dict.HasKey("FT")
  3. && strcmp(dict.GetKey("Type")->GetName().GetEscapedName().c_str(), "Annot") == 0
  4. && strcmp(dict.GetKey("Subtype")->GetName().GetEscapedName().c_str(), "Widget") == 0
  5. && strcmp(dict.GetKey("FT")->GetName().GetEscapedName().c_str(), "Sig") == 0) {
  6. nroSignatures++;
  7. }

Finalmente con el objeto Dictionary se verifica que sea de tipo "Annot", subtipo "Widget" y con firma. Si se cumplen todas esas condiciones se habrá identificado una firma en el documento PDF aperturado al inicio. Teniendo las firmas, lo que corresponde es procesarlas para lo que primeramente se obtendrá y mostrará por consola la etiqueta de la firma:

  1. cout << (*it)->GetDictionary().GetKey("T")->GetString().GetString() << endl;
  2. PoDoFo::PdfDictionary sig_dict = mDoc.GetObjects().GetObject((*it)->GetDictionary().GetKey("V")->GetReference())->GetDictionary();
  3. PoDoFo::PdfString signature = sig_dict.GetKey("Contents")->GetString();
  4. unsigned char *signatureuchar = (unsigned char *)malloc(signature.GetLength());
  5. memcpy(signatureuchar, signature.GetString(), signature.GetLength());

Se muestra la etiqueta de la firma y se obtiene el diccionario que contiene la firma, a partir del cual se obtiene el contenido, es decir la estructura PKCS7 creada al momento de firmar el PDF.

Para poder interpretar el contenido del PKCS7 se requiere una librería criptográfica que para el ejemplo será OpenSSL.

  1. BIO* mem = BIO_new(BIO_s_mem());
  2. BIO_write(mem, signatureuchar, signature.GetLength() + 1);
  3. PKCS7 *pkcs7 = 0;
  4. d2i_PKCS7_bio(mem, &pkcs7);
  5. BIO_free(mem);

Haciendo uso de la librería OpenSSL se carga en un buffer de memoria el contenido de la firma y se lo parsea en un objeto de tipo PKCS7 para poder utilizarlo.

La validación de la firma con OpenSSL es bastante simple, sin embargo se requiere los certificados que hacen la cadena de confianza mencionados en el subtítulo "Verificar la propiedad y legalidad de la clave pública" y que deben descargarse en el directorio del proyecto. Lo primero será cargar en memoria estos certificados de la siguiente manera:

  1. X509_STORE *ca = X509_STORE_new();
  2. FILE *f = fopen("firmadigital_bo.crt", "rb");
  3. STACK_OF(X509_INFO)* cai = PEM_X509_INFO_read(f, NULL, NULL, NULL);
  4. fclose(f);
  5. for (int i = 0; i < sk_X509_INFO_num(cai); i++) {
  6. X509_INFO* item = sk_X509_INFO_value(cai, i);
  7. X509_STORE_add_cert(ca, item->x509);
  8. }

Como se puede observar el código anterior crear un objeto de tipo X509_STORE que contendrá los certificados de la cadena de confianza, abre el archivo firmadigital_bo.crt que contiene los certificados y los carga en el objeto X509_STORE.

El segundo elemento requerido para validar una firma digital es el contenido firmado que para los archivos PDF vendrá a ser el body. Para esto se deberá extraer el condenido del PDF enmarcado por el atributo ByteRange del objeto firma del documento, a continuación del código de ejemplo:

  1. FILE *fp = fopen("prueba.pdf", "rb");
  2. fseek(fp, 0L, SEEK_END);
  3. long int fileLength = ftell(fp);
  4. BIO *indata = BIO_new(BIO_s_mem());
  5. PoDoFo::PdfArray byte_range = sig_dict.GetKey("ByteRange")->GetArray();
  6. for (long unsigned int k = 0; k < byte_range.size() / 2; k++) {
  7. if (!byte_range[k * 2].IsNumber() || !byte_range[k * 2 + 1].IsNumber()) {
  8. cout << "Valor ilegal en ByteRange." << endl;
  9. return 0;
  10. }
  11. PoDoFo::pdf_int64 offset = byte_range[k * 2].GetNumber();
  12. PoDoFo::pdf_int64 len = byte_range[k * 2 + 1].GetNumber();
  13. if (offset < 0 || offset >= fileLength || len < 0 || len > fileLength || offset + len > fileLength) {
  14. cout << "Valor ilegal en ByteRange." << endl;
  15. }
  16. fseek(fp, offset, SEEK_SET);
  17. const int BLOCK_SIZE = 4096;
  18. unsigned char *signed_data_buffer = (unsigned char*)malloc(BLOCK_SIZE);
  19. long int l = 0;
  20. while(l < len) {
  21. long unsigned int bytes_left = len - l;
  22. if (bytes_left < BLOCK_SIZE) {
  23. fread(signed_data_buffer, 1, bytes_left, fp);
  24. BIO_write(indata, signed_data_buffer, bytes_left);
  25. l = len;
  26. } else {
  27. fread(signed_data_buffer, 1, BLOCK_SIZE, fp);
  28. BIO_write(indata, signed_data_buffer, BLOCK_SIZE);
  29. l += BLOCK_SIZE;
  30. }
  31. }
  32. }
  33. fclose(fp);

Lo que hace el código anterior es identificar el rango que correponde al body del PDF a través del campo ByteRange de la firma y lo carga en un buffer denominado "indata" de manera que el hash se calcule sobre este rango del archivo.

Finalmente ya se puede validar la firma del documento con la función PKCS7_verify de OpenSSL como se muestra a continuación.

  1. BIO* out = BIO_new(BIO_s_mem());
  2. if (PKCS7_verify(pkcs7, NULL, ca, indata, out, 0)) {
  3. cout << " - Estado firma: valido" << endl;
  4. } else {
  5. unsigned long err = ERR_get_error();
  6. cout << " - Estado firma: " << ERR_reason_error_string(err) << endl;
  7. }
  8. BIO_free(out);
  9. BIO_free(indata);

La función PKCS7_verify de OpenSSL se encarga de realizar las validaciones a la firma y en caso de no ser válida mostrará el mensaje con la causa.

Finalmente si se desea obtener información del certificado se puede utilizar la función PKCS7_get0_signers de OpenSSL que obtendrá el certificado del signatario almacenado en la estructura PKCS7.

  1. STACK_OF(X509)* certs = PKCS7_get0_signers(pkcs7, NULL, 0);
  2. for (int i = 0; i < sk_X509_num(certs); i++) {
  3. X509* cert = sk_X509_value(certs, i);
  4. X509_NAME* subject_name = X509_get_subject_name(cert);
  5. char nombre[65];
  6. if (X509_NAME_get_text_by_NID(subject_name, OBJ_sn2nid("CN"), nombre, 65) > 0) {
  7. cout << " - Nombre del signatario: " << nombre << endl;
  8. }
  9. char pais[65];
  10. if (X509_NAME_get_text_by_NID(subject_name, OBJ_sn2nid("C"), pais, 65) > 0) {
  11. cout << " - Pais: " << pais << endl;
  12. }
  13. cout << " - Fecha firma: " << sig_dict.GetKey("M")->GetString().GetString() << endl;
  14. }

Una vez obtenido el certificado, se puede obtener la información del signatario a través de la función X509_get_subject_name y por ejemplo mostrar el nombre o país al que corresponde.

Si desea descargar el ejemplo puede hacerlo a través del siguiente link: Código de ejemplo

Telf: (591-2)2200720-2200730 interno 1006-1007-1021 Correo Electrónico: soporte@firmadigital.bo
Fax: (591-2)2200740 Geo#: 6mpd1sdnm
Casilla: 6500 Dirección: Calle Jaime Mendoza No 981 casi esquina Peñaranda, Zona Sur - San Miguel
© ADSIB 2019 Bolivia