Firma digital de PDF con certificado por software y C++

En este tutorial, se verá la implementación de firma digital desde una aplicación escrita en C++, haciendo uso de un par de claves y el certificado disponible en un contenedor PKCS#12.

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 el archivo "podofosign.cpp" que podrá ser reutilizado en otros proyectos para firmar.

Console Application Console Application Console Application

Dentro del archivo agregar las siguientes cabeceras:

  1. #include <podofo/podofo.h>
  2. #include <openssl/err.h>
  3. #include <openssl/pkcs12.h>
  4. using namespace PoDoFo;

Una vez importadas las cabeceras es necesario crear la siguiente función para calcular el espacio que ocupará la firma dentro del PDF.

  1. static bool load_cert_and_key(X509** out_cert, EVP_PKEY** out_pkey, pdf_int32& min_signature_size)
  2. {
  3. min_signature_size = 0;
  4. int l;
  5. if ((l = i2d_X509( *out_cert, NULL )) != -1) {
  6. min_signature_size += l + 418;
  7. } else {
  8. min_signature_size += 3072;
  9. }
  10. if ((l = EVP_PKEY_size( *out_pkey )) != -1) {
  11. min_signature_size += l + 1448;
  12. } else {
  13. min_signature_size += 1042;
  14. }
  15. return true;
  16. }

El certificado y llave privada de la función anterior se los obtendrá del archivo pfx mediante la siguiente función.

  1. void abrir(std::string filePfx, std::string password, X509** cert, EVP_PKEY** pkey)
  2. {
  3. FILE *fp = fopen(filePfx.c_str(), "rb");
  4. if (fp == NULL) {
  5. throw "No se pudo abrir el archivo.";
  6. }
  7. PKCS12 *p12_cert = NULL;
  8. STACK_OF(X509) *additional_certs = NULL;
  9. OpenSSL_add_all_algorithms();
  10. if (d2i_PKCS12_fp(fp, &p12_cert) < 0) {
  11. fclose(fp);
  12. throw "Error al leer el archivo.";
  13. }
  14. fclose(fp);
  15. if (PKCS12_parse(p12_cert, password.c_str(), pkey, cert, &additional_certs) == 0) {
  16. int err = ERR_get_error();
  17. switch (err) {
  18. case 218570875:
  19. case 218542222:
  20. case 218529960:
  21. throw "Formato de archvio incorrecto.";
  22. case 587686001:
  23. throw "Contraseña incorrecta.";
  24. default:
  25. throw "Error desconocido.";
  26. }
  27. }
  28. }

Para verificar que no existan otros campos de firma.

  1. static PdfObject* find_existing_signature_field(PdfAcroForm* pAcroForm, const PdfString& name)
  2. {
  3. if( !pAcroForm )
  4. PODOFO_RAISE_ERROR( ePdfError_InvalidHandle );
  5. PdfObject* pFields = pAcroForm->GetObject()->GetDictionary().GetKey( PdfName( "Fields" ) );
  6. if( pFields )
  7. {
  8. if( pFields->GetDataType() == ePdfDataType_Reference )
  9. pFields = pAcroForm->GetDocument()->GetObjects()->GetObject( pFields->GetReference() );
  10. if( pFields && pFields->GetDataType() == ePdfDataType_Array )
  11. {
  12. PdfArray &rArray = pFields->GetArray();
  13. PdfArray::iterator it, end = rArray.end();
  14. for( it = rArray.begin(); it != end; it++ )
  15. {
  16. // require references in the Fields array
  17. if( it->GetDataType() == ePdfDataType_Reference )
  18. {
  19. PdfObject *item = pAcroForm->GetDocument()->GetObjects()->GetObject( it->GetReference() );
  20. if( item && item->GetDictionary().HasKey( PdfName( "T" ) ) &&
  21. item->GetDictionary().GetKey( PdfName( "T" ) )->GetString() == name )
  22. {
  23. // found a field with the same name
  24. const PdfObject *pFT = item->GetDictionary().GetKey( PdfName( "FT" ) );
  25. if( !pFT && item->GetDictionary().HasKey( PdfName( "Parent" ) ) )
  26. {
  27. const PdfObject *pTemp = item->GetIndirectKey( PdfName( "Parent" ) );
  28. if( !pTemp )
  29. {
  30. PODOFO_RAISE_ERROR( ePdfError_InvalidDataType );
  31. }
  32. pFT = pTemp->GetDictionary().GetKey( PdfName( "FT" ) );
  33. }
  34. if( !pFT )
  35. {
  36. PODOFO_RAISE_ERROR( ePdfError_NoObject );
  37. }
  38. const PdfName fieldType = pFT->GetName();
  39. if( fieldType != PdfName( "Sig" ) )
  40. {
  41. std::string err = "Existing field '";
  42. err += name.GetString();
  43. err += "' isn't of a signature type, but '";
  44. err += fieldType.GetName().c_str();
  45. err += "' instead";
  46. PODOFO_RAISE_ERROR_INFO( ePdfError_InvalidName, err.c_str() );
  47. }
  48. return item;
  49. }
  50. }
  51. }
  52. }
  53. }
  54. return NULL;
  55. }

Y para crear la firma se utilizará la siguiente función.

  1. static void sign_with_signer( PdfSignOutputDevice &signer, X509 *cert, EVP_PKEY *pkey )
  2. {
  3. if( !cert )
  4. PODOFO_RAISE_ERROR_INFO( ePdfError_InvalidHandle, "cert == NULL" );
  5. if( !pkey )
  6. PODOFO_RAISE_ERROR_INFO( ePdfError_InvalidHandle, "pkey == NULL" );
  7. unsigned int uBufferLen = 65535, len;
  8. char *pBuffer;
  9. while( pBuffer = reinterpret_cast<char *>( podofo_malloc( sizeof( char ) * uBufferLen) ), !pBuffer )
  10. {
  11. uBufferLen = uBufferLen / 2;
  12. if( !uBufferLen )
  13. break;
  14. }
  15. if( !pBuffer )
  16. PODOFO_RAISE_ERROR (ePdfError_OutOfMemory);
  17. int rc;
  18. BIO *mem = BIO_new( BIO_s_mem() );
  19. if( !mem )
  20. {
  21. podofo_free( pBuffer );
  22. throw "Failed to create input BIO";
  23. }
  24. unsigned int flags = PKCS7_DETACHED | PKCS7_BINARY;
  25. PKCS7 *pkcs7 = PKCS7_sign( cert, pkey, NULL, mem, flags );
  26. if( !pkcs7 )
  27. {
  28. BIO_free( mem );
  29. podofo_free( pBuffer );
  30. throw "PKCS7_sign failed";
  31. }
  32. while( len = signer.ReadForSignature( pBuffer, uBufferLen ), len > 0 )
  33. {
  34. rc = BIO_write( mem, pBuffer, len );
  35. if( static_cast<unsigned int>( rc ) != len )
  36. {
  37. PKCS7_free( pkcs7 );
  38. BIO_free( mem );
  39. podofo_free( pBuffer );
  40. throw "BIO_write failed";
  41. }
  42. }
  43. podofo_free( pBuffer );
  44. if( PKCS7_final( pkcs7, mem, flags ) <= 0 )
  45. {
  46. PKCS7_free( pkcs7 );
  47. BIO_free( mem );
  48. throw "PKCS7_final failed";
  49. }
  50. bool success = false;
  51. BIO *out = BIO_new( BIO_s_mem() );
  52. if( !out )
  53. {
  54. PKCS7_free( pkcs7 );
  55. BIO_free( mem );
  56. throw "Failed to create output BIO";
  57. }
  58. char *outBuff = NULL;
  59. long outLen;
  60. i2d_PKCS7_bio( out, pkcs7 );
  61. outLen = BIO_get_mem_data( out, &outBuff );
  62. if( outLen > 0 && outBuff )
  63. {
  64. if( static_cast<size_t>( outLen ) > signer.GetSignatureSize() )
  65. {
  66. PKCS7_free( pkcs7 );
  67. BIO_free( out );
  68. BIO_free( mem );
  69. std::ostringstream oss;
  70. oss << "Requires at least " << outLen << " bytes for the signature, but reserved is only " << signer.GetSignatureSize() << " bytes";
  71. PODOFO_RAISE_ERROR_INFO( ePdfError_ValueOutOfRange, oss.str().c_str() );
  72. }
  73. PdfData signature( outBuff, outLen );
  74. signer.SetSignature( signature );
  75. success = true;
  76. }
  77. PKCS7_free( pkcs7 );
  78. BIO_free( out );
  79. BIO_free( mem );
  80. if( !success )
  81. throw "Failed to get data from the output BIO";
  82. }

Finalmente la función que hace toda la magia.

  1. const char* sign(const char *inputfile, const char *filePfx, const char *password)
  2. {
  3. EVP_PKEY *pkey;
  4. X509 *cert;
  5. abrir(filePfx, password, &cert, &pkey);
  6. const char *outputfile = "/tmp/signed.pdf";
  7. const char *reason = "I agree";
  8. const char *sigsizestr = NULL;
  9. const char *annot_position = NULL;
  10. const char *field_name = NULL;
  11. int annot_page = 0;
  12. double annot_left = 0.0, annot_top = 0.0, annot_width = 0.0, annot_height = 0.0;
  13. bool annot_print = false;
  14. bool field_use_existing = false;
  15. PdfError::EnableDebug( false );
  16. int sigsize = -1;
  17. if( sigsizestr )
  18. {
  19. sigsize = atoi( sigsizestr );
  20. if( sigsize <= 0 )
  21. {
  22. throw (std::string("Invalid value for signature size specified (") + sigsizestr + "), use a positive integer, please").c_str();
  23. }
  24. }
  25. if( outputfile && strcmp( outputfile, inputfile ) == 0)
  26. {
  27. // even I told you not to do it, you still specify the same output file
  28. // as the input file. Just ignore that.
  29. outputfile = NULL;
  30. }
  31. #ifdef PODOFO_HAVE_OPENSSL_1_1
  32. OPENSSL_init_crypto(0, NULL);
  33. #else
  34. OpenSSL_add_all_algorithms();
  35. ERR_load_crypto_strings();
  36. ERR_load_PEM_strings();
  37. ERR_load_ASN1_strings();
  38. ERR_load_EVP_strings();
  39. #endif
  40. pdf_int32 min_signature_size = 0;
  41. if( !load_cert_and_key( &cert, &pkey, min_signature_size ) )
  42. {
  43. throw "Error al cargar el certificado y clave.";
  44. }
  45. if( sigsize > 0 )
  46. min_signature_size = sigsize;
  47. PdfSignatureField *pSignField = NULL;
  48. PdfAnnotation *pTemporaryAnnot = NULL; // for existing signature fields
  49. try
  50. {
  51. PdfMemDocument document;
  52. document.Load( inputfile, true );
  53. if( !document.GetPageCount() )
  54. PODOFO_RAISE_ERROR_INFO( ePdfError_PageNotFound, "The document has no page. Only documents with at least one page can be signed" );
  55. PdfAcroForm* pAcroForm = document.GetAcroForm();
  56. if( !pAcroForm )
  57. PODOFO_RAISE_ERROR_INFO( ePdfError_InvalidHandle, "acroForm == NULL" );
  58. if( !pAcroForm->GetObject()->GetDictionary().HasKey( PdfName( "SigFlags" ) ) ||
  59. !pAcroForm->GetObject()->GetDictionary().GetKey( PdfName( "SigFlags" ) )->IsNumber() ||
  60. pAcroForm->GetObject()->GetDictionary().GetKeyAsLong( PdfName( "SigFlags" ) ) != 3 )
  61. {
  62. if( pAcroForm->GetObject()->GetDictionary().HasKey( PdfName( "SigFlags" ) ) )
  63. pAcroForm->GetObject()->GetDictionary().RemoveKey( PdfName( "SigFlags" ) );
  64. pdf_int64 val = 3;
  65. pAcroForm->GetObject()->GetDictionary().AddKey( PdfName( "SigFlags" ), PdfObject( val ) );
  66. }
  67. if( pAcroForm->GetNeedAppearances() )
  68. {
  69. pAcroForm->SetNeedAppearances( false );
  70. }
  71. PdfOutputDevice outputDevice( outputfile, true );
  72. PdfSignOutputDevice signer( &outputDevice );
  73. PdfString name;
  74. PdfObject* pExistingSigField = NULL;
  75. if( field_name )
  76. {
  77. name = PdfString( field_name );
  78. pExistingSigField = find_existing_signature_field( pAcroForm, name );
  79. if( pExistingSigField && !field_use_existing)
  80. {
  81. std::string err = "Signature field named '";
  82. err += name.GetString();
  83. err += "' already exists";
  84. PODOFO_RAISE_ERROR_INFO( ePdfError_WrongDestinationType, err.c_str() );
  85. }
  86. }
  87. else
  88. {
  89. char fldName[96]; // use bigger buffer to make sure sprintf does not overflow
  90. sprintf( fldName, "PodofoSignatureField%" PDF_FORMAT_INT64, static_cast<pdf_int64>( document.GetObjects().GetObjectCount() ) );
  91. name = PdfString( fldName );
  92. }
  93. if( pExistingSigField )
  94. {
  95. if( !pExistingSigField->GetDictionary().HasKey( "P" ) )
  96. {
  97. std::string err = "Signature field named '";
  98. err += name.GetString();
  99. err += "' doesn't have a page reference";
  100. PODOFO_RAISE_ERROR_INFO( ePdfError_PageNotFound, err.c_str() );
  101. }
  102. PdfPage* pPage;
  103. pPage = document.GetPagesTree()->GetPage( pExistingSigField->GetDictionary().GetKey( "P" )->GetReference() );
  104. if( !pPage )
  105. PODOFO_RAISE_ERROR( ePdfError_PageNotFound );
  106. pTemporaryAnnot = new PdfAnnotation( pExistingSigField, pPage );
  107. if( !pTemporaryAnnot )
  108. PODOFO_RAISE_ERROR_INFO( ePdfError_OutOfMemory, "Cannot allocate annotation object for existing signature field" );
  109. pSignField = new PdfSignatureField( pTemporaryAnnot );
  110. if( !pSignField )
  111. PODOFO_RAISE_ERROR_INFO( ePdfError_OutOfMemory, "Cannot allocate existing signature field object" );
  112. pSignField->EnsureSignatureObject();
  113. }
  114. else
  115. {
  116. PdfPage* pPage = document.GetPage( annot_page );
  117. if( !pPage )
  118. PODOFO_RAISE_ERROR( ePdfError_PageNotFound );
  119. PdfRect annot_rect;
  120. if( annot_position )
  121. {
  122. annot_rect = PdfRect( annot_left, pPage->GetPageSize().GetHeight() - annot_top - annot_height, annot_width, annot_height );
  123. }
  124. PdfAnnotation* pAnnot = pPage->CreateAnnotation( ePdfAnnotation_Widget, annot_rect );
  125. if( !pAnnot )
  126. PODOFO_RAISE_ERROR_INFO( ePdfError_OutOfMemory, "Cannot allocate annotation object" );
  127. if( annot_position && annot_print )
  128. pAnnot->SetFlags( ePdfAnnotationFlags_Print );
  129. else if( !annot_position && ( !field_name || !field_use_existing ) )
  130. pAnnot->SetFlags( ePdfAnnotationFlags_Invisible | ePdfAnnotationFlags_Hidden );
  131. pSignField = new PdfSignatureField( pAnnot, pAcroForm, &document );
  132. if( !pSignField )
  133. PODOFO_RAISE_ERROR_INFO( ePdfError_OutOfMemory, "Cannot allocate signature field object" );
  134. }
  135. // use large-enough buffer to hold the signature with the certificate
  136. signer.SetSignatureSize( min_signature_size );
  137. pSignField->SetFieldName( name );
  138. pSignField->SetSignatureReason( PdfString( reinterpret_cast<const pdf_utf8 *>( reason ) ) );
  139. pSignField->SetSignatureDate( PdfDate() );
  140. pSignField->SetSignature( *signer.GetSignatureBeacon() );
  141. // The outputfile != NULL means that the write happens to a new file,
  142. // which will be truncated first and then the content of the inputfile
  143. // will be copied into the document, follwed by the changes.
  144. document.WriteUpdate( &signer, outputfile != NULL );
  145. if( !signer.HasSignaturePosition() )
  146. PODOFO_RAISE_ERROR_INFO( ePdfError_SignatureError, "Cannot find signature position in the document data" );
  147. // Adjust ByteRange for signature
  148. signer.AdjustByteRange();
  149. // Read data for signature and count it
  150. // We seek at the beginning of the file
  151. signer.Seek( 0 );
  152. sign_with_signer( signer, cert, pkey );
  153. signer.Flush();
  154. }
  155. catch( PdfError & e )
  156. {
  157. if (strcmp(e.what(), "ePdfError_NoPdfFile") == 0)
  158. {
  159. throw "Formato de archivo incorrecto.";
  160. }
  161. throw (std::string("Error: ") + e.what()).c_str();
  162. }
  163. #ifndef PODOFO_HAVE_OPENSSL_1_1
  164. ERR_free_strings();
  165. #endif
  166. if( pSignField )
  167. delete pSignField;
  168. if( pTemporaryAnnot )
  169. delete pTemporaryAnnot;
  170. return outputfile;
  171. }

Nota.- Las funciones anteriores fueron modificadas de: PoDoFo

Para poder reutilizar la funcion sign será necesario crear un nuevo archivo de cabecera:

Console Application Console Application Console Application

Dentro de la cabecera simplemente habrá que hacer accesible el método sign.

  1. /*! Método para firmar documentos haciendo uso de PoDoFo. */
  2. const char* sign(const char *inputfile, const char *filePfx, const char *password);

Y en el método main del archivo main será decirle que archivo y certificado por software utilizar.

  1. cout << sign("/home/alberto/Documentos/prueba.pdf", "/home/alberto/Documentos/token.p12", "12345678") << endl;

Si se intenta compilar se producirá un error al no estar referenciadas las librerías. Para referenciar elegir la opción Build options del menú contextual.

Console Application Console Application

En el panel Other linker options, se especifica las librerías que serán enlazadas, que para este caso son openssl y podofo.

Si se compila y ejecuta se firmará el archivo prueba.pdf y se guardará el archivo firmado en /tmp/firmado.pdf.

Si desea descargar un ejemplo de archivo firmado puede hacerlo a través del siguiente link: Archivo firmado de ejemplo

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

© ADSIB 2019 Bolivia