Artículo técnico

Verificar firmas digitales de PDF en Delphi con HotPDF

HotPDF verifica firmas digitales en documentos PDF cargados a través de tres métodos de THotPDF: GetLoadedSignatureInfo, VerifyLoadedSignature y VerifyLoadedSignatureEx, introducidos en la versión v2.259.0. El componente vuelve a calcular el hash de los segmentos /ByteRange del archivo original, comprueba el atributo messageDigest de CMS y ejecuta una verificación RSA PKCS#1 v1.5 con respecto al certificado del firmante incrustado, devolviendo svValid cuando los bytes del documento están intactos

El escenario es trivial y lo que está en juego no lo es. Una contraparte devuelve un contrato firmado, su flujo de trabajo necesita archivarlo y alguien hace la única pregunta que importa: ¿es este el documento que enviamos, byte por byte, firmado por el certificado que declara? Responder a eso en código es el lado de la verificación de la historia de la firma; el lado de la firma, es decir, la construcción e incrustación de firmas PAdES en primer lugar, se cubre en el artículo complementario sobre la creación de firmas digitales PAdES con HotPDF. Este artículo trata sobre la otra dirección: llega un PDF ya firmado y usted desea un veredicto programático en lugar de una captura de pantalla de la marca de verificación verde de Acrobat

¿Por qué la firma de un PDF protege rangos de bytes específicos del archivo?

Una firma de PDF protege rangos de bytes específicos del archivo, no una noción abstracta del "documento". La norma ISO 32000-1 §12.8 define el mecanismo: el campo de formulario de firma lleva un diccionario cuya entrada /Contents contiene un contenedor CMS SignedData (RFC 5652) y cuya matriz /ByteRange nombra las regiones exactas del archivo que cubre la firma, según §12.8.1. La matriz es una lista de pares de desplazamiento y longitud, en la práctica dos segmentos: todo lo que está antes de la cadena hexadecimal /Contents y todo lo que está después de ella. El valor de la firma no puede cubrirse a sí mismo, por lo que se calcula el hash del archivo alrededor de ese hueco

Ese diseño tiene una consecuencia que da forma a toda la API: la verificación debe calcular el hash de los bytes serializados originales, tal como se encuentran en el disco. Un modelo de objetos analizado es inútil para esto, porque volver a serializar incluso un documento sin cambios produce bytes diferentes. Por lo tanto, HotPDF verifica con respecto al archivo de origen desde el que se cargó el documento, o con respecto a un TStream de bytes sin procesar que usted proporcione, nunca con respecto a su representación en memoria

Leer los metadatos de la firma antes de verificar nada

GetLoadedSignatureInfo analiza el diccionario de firmas y su contenedor CMS sin tocar un solo byte del documento, lo que la convierte en la primera llamada correcta cuando solo necesita mostrar quién firmó y cuándo. Los campos de firma se indexan desde 0 en el orden de los campos de formulario, y GetLoadedSignatureFieldCount le indica cuántos existen. El registro THPDFSignatureInfo devuelto contiene el nombre del campo, /SubFilter, el nombre común del certificado del firmante, los nombres distinguidos del asunto y del emisor, el número de serie, las fechas de validez, la hora de la firma (a partir del atributo firmado cuando está presente, de lo contrario, la entrada /M del diccionario), el nombre del algoritmo de resumen y las cadenas /Reason, /Location y /ContactInfo. Su miembro Status permanece como svNotVerified, una etiqueta honesta para "analizado, no verificado"

var
  Pdf: THotPDF;
  Info: THPDFSignatureInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('signed-contract.pdf');
    for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
    begin
      Info := Pdf.GetLoadedSignatureInfo(I);
      Writeln('Field:     ', Info.FieldName);
      Writeln('Signer:    ', Info.SignerName);
      Writeln('Issuer:    ', Info.IssuerDN);
      Writeln('Algorithm: ', Info.HashAlgorithm);
      Writeln('SubFilter: ', Info.SubFilter);
    end;
  finally
    Pdf.Free;
  end;
end;

Ejecutar la comprobación criptográfica

VerifyLoadedSignatureEx realiza la verificación completa de un documento cargado desde un archivo y devuelve el registro de información completado en una sola llamada: vuelve a abrir el archivo de origen, calcula el hash de los segmentos /ByteRange con el algoritmo de resumen SignerInfo, compara el resultado con el atributo firmado messageDigest (RFC 5652 §5.4) y luego verifica mediante RSA la firma sobre la codificación de nuevo DER SET de los atributos firmados. Cuando una firma no contiene atributos firmados, la comprobación RSA se ejecuta directamente sobre el hash del documento. Las firmas admitidas son RSA PKCS#1 v1.5 con resúmenes SHA-1, SHA-256, SHA-384 o SHA-512, lo que cubre los subfiltros adbe.pkcs7.detached y ETSI.CAdES.detached producidos por las herramientas de firma habituales

var
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  Status := Pdf.VerifyLoadedSignatureEx(0, Info);
  case Status of
    svValid:
      if Info.CoversWholeDocument then
        Writeln('Valid; signature covers the whole file')
      else
        Writeln('Valid; file was extended after signing');
    svDigestMismatch:
      Writeln('Document bytes changed after signing');
    svSignatureInvalid:
      Writeln('RSA check failed over signed attributes');
    svUnsupportedAlgorithm:
      Writeln('Non-RSA key or unknown digest algorithm');
    svMalformed:
      Writeln('CMS container could not be parsed');
    svSourceUnavailable:
      Writeln('No source bytes; use the TStream overload');
  end;
end;

Vale la pena conocer dos detalles de implementación porque explican fallos que parecen misteriosos desde el exterior. Primero, la comprobación de los atributos firmados es estricta con la codificación: dentro del archivo, los atributos están etiquetados como [0] IMPLICIT, pero la firma se calculó sobre su forma DER SET OF, por lo que el verificador vuelve a etiquetar antes de calcular el hash, exactamente como exige la norma RFC 5652 §5.4. Un verificador personalizado que calcule el hash de los bytes tal como aparecen en el archivo rechazará cualquier documento firmado correctamente. Segundo, /Contents se rellena convencionalmente con ceros hasta un presupuesto de bytes reservado, por lo que el verificador trunca el bloque (blob) DER a la longitud real de su SEQUENCE externa antes de analizarlo; los ceros finales que parecen basura son normales, no corrupción. La misma familia de peligros de análisis de ASN.1, en el lado de la importación de certificados, es el tema del artículo sobre el endurecimiento de la seguridad de PKCS#12 y ASN.1 en HotPDF

¿Qué garantiza realmente una firma válida?

svValid significa exactamente esto: los bytes nombrados por /ByteRange calculan su hash al valor que firmó el firmante, y la firma se verifica bajo la clave pública del certificado incrustado en el contenedor CMS. Eso es integridad de bytes más vinculación de clave, y nada más. La validación de la cadena de certificados y de la confianza están explícitamente fuera del alcance del verificador de HotPDF: no recorre la cadena hasta una raíz, no comprueba la revocación ni consulta ningún almacén de confianza. Un certificado autofirmado de un atacante que volvió a firmar un documento modificado se verificará como svValid, porque la matemática es internamente coherente. Si el firmante es quien dice ser, y si alguien debería confiar en él, es una decisión de política que pertenece a una capa independiente, ya sea la lista blanca de certificados de su organización, el almacén de certificados de Windows o una autoridad de validación

La bandera CoversWholeDocument protege una brecha más sutil. Una firma solo cubre su /ByteRange, y el mecanismo de actualización incremental de PDF permite añadir contenido después de una firma sin invalidarla, lo cual es por diseño y es cómo funcionan los flujos de trabajo de firma múltiple. La bandera se calcula durante la verificación y es verdadera solo cuando los dos segmentos más el hueco de /Contents abarcan todo el archivo. Cuando llega svValid con CoversWholeDocument falso, la revisión firmada está intacta pero el archivo contiene adiciones posteriores, y su flujo de trabajo debería decidir si tolera lo que cambiaron esas adiciones

Los documentos cargados por flujo y cifrados necesitan sus propios bytes de origen

Los métodos sin parámetros VerifyLoadedSignature y VerifyLoadedSignatureEx dependen de que el componente recuerde de qué archivo provino el documento. Cargue el documento desde un flujo y no habrá ningún nombre de archivo para volver a abrir; lo mismo se aplica después de la ruta de recarga de contraseña utilizada para documentos cifrados, el flujo de trabajo descrito en el artículo sobre cifrado de PDF AES-256 con HotPDF. En ambos casos, las sobrecargas devuelven svSourceUnavailable en lugar de adivinar. La solución es la sobrecarga de TStream, que le permite entregar los bytes sin procesar originales desde donde los guardó: un archivo que aún conserva, un búfer de memoria o un blob de base de datos

var
  Src: TFileStream;
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  // Stream-loaded document: the component holds no source
  // file name, so supply the original bytes yourself.
  Src := TFileStream.Create('signed-contract.pdf',
    fmOpenRead or fmShareDenyWrite);
  try
    Status := Pdf.VerifyLoadedSignature(0, Src, Info);
    if Status <> svValid then
      Writeln('Verification failed: ', Ord(Status));
  finally
    Src.Free;
  end;
end;

Informar de lo que no se puede verificar

Un verificador que solo conoce "válido" e "inválido" informará erróneamente sobre documentos que simplemente no comprende, por lo que la enumeración de estados separa los casos que su interfaz de usuario debería distinguir. svDigestMismatch significa que los bytes del documento cambiaron después de la firma, la señal clásica de alteración. svSignatureInvalid significa que el hash de los bytes es correcto pero falló la comprobación RSA, lo que apunta a un valor de firma dañado o falsificado. svUnsupportedAlgorithm is la respuesta honesta para claves ECDSA y resúmenes no reconocidos: la firma puede ser perfectamente buena, simplemente HotPDF no puede comprobarla, y notificarla como "inválida" difamaría un documento sano. svMalformed marca un contenedor CMS que no se pudo analizar en absoluto. Para comprobaciones de estilo puerta de enlace (gate-style), VerifyAllLoadedSignatures devuelve verdadero solo cuando existe al menos un campo de firma y cada uno de ellos se verifica como svValid, un conveniente booleano único para una canalización de ingesta de archivos que rechaza cualquier cosa inferior

La verificación de firmas, la firma PAdES, el cifrado AES-256 y la API de edición de documentos cargados se distribuyen en la misma biblioteca VCL nativa para Delphi y C++Builder, sin dependencias de DLL externas; la lista completa de características y las versiones de IDE compatibles se encuentran en la página del producto HotPDF Component