Artículo técnico

PAdES Digital Signatures in Delphi: Signing and Validation with PDF Library for Delphi

Validar una firma PAdES significa comprobar tres elementos independientes, y una marca verde en un visor solo informa sobre el tercero. Primero, el arreglo /ByteRange debe cubrir los bytes correctos: los tramos que indica tienen que reconstruir la entrada exacta sobre la que se calculó el resumen CMS, sin bytes firmados fuera de ellos. Segundo, el certificado dentro del CMS debe encadenarse a una raíz de confianza y llevar el atributo firmado de certificado de firma que exige PAdES. Tercero, si el perfil afirma tener una marca de tiempo, un token RFC 3161 debe vincular el valor de la firma a un momento anterior a la expiración del certificado. Acrobat reduce los tres a un icono; un verificador de conformidad los mantiene separados, y también debería hacerlo el código que produce estos archivos. losLab PDF Library (PDF Library for Delphi) ofrece el lado de firma, la reincrustación de marcas de tiempo y las llamadas de auditoría para inspeccionar un ByteRange antes de confiar en él

Una distinción confunde casi toda primera implementación de PAdES, así que vale la pena aclararla antes de escribir código. Una firma escrita con /SubFilter /adbe.pkcs7.detached es una firma ISO 32000-1 §12.8 perfectamente válida que Acrobat informará como correcta. Tampoco es una firma PAdES, porque ETSI EN 319 142-1 exige ETSI.CAdES.detached en todos los niveles de referencia. Un verificador de conformidad eIDAS rechaza la primera y acepta la segunda aunque la criptografía sea idéntica. El perfil es una afirmación que el documento hace sobre sí mismo, y hacer esa afirmación correctamente es una llamada en PDF Library for Delphi

Qué convierte una firma PDF en una firma PAdES

ETSI EN 319 142-1 define cuatro niveles de referencia apilados sobre el formato CMS. PAdES-B-B es el punto de entrada: una firma CAdES en un campo de firma PDF con el SubFilter ETSI.CAdES.detached y un atributo firmado de certificado de firma. PAdES-B-T agrega una marca de tiempo RFC 3161 sobre el valor de la firma, que demuestra que la firma existía antes de un momento que nadie puede antedatar. PAdES-B-LT incorpora los certificados, CRL y respuestas OCSP necesarios para la validación en un Document Security Store, de modo que el archivo siga siendo verificable después de que la CA emisora retire su infraestructura. PAdES-B-LTA corona la pila con una marca de tiempo del documento que vuelve a proteger la evidencia acumulada a medida que los algoritmos se debilitan

PDF Library for Delphi asigna estos conceptos a su API de proceso de firma. El marcador de perfil es SetSignProcessCustomSubFilter. Si la política necesita una indicación de tipo de compromiso (prueba de origen, prueba de aprobación o uno de los otros identificadores ETSI numerados del 1 al 6), se indica mediante SetSignProcessCommitmentType. Una política de firma explícita se adjunta con SetSignProcessSignaturePolicy, que recibe el OID de la política y su resumen. Un valor predeterminado merece atención: con el algoritmo de resumen en automático, la biblioteca selecciona SHA-256 para las firmas ETSI y adbe.pkcs7.detached y recurre a SHA-1 solo en la ruta heredada adbe.pkcs7.sha1. Establézcalo explícitamente de todos modos. Los auditores preguntan qué hash se usó, y un valor explícito en el código es más fácil de defender que un valor predeterminado que se debe buscar en el manual para explicar

Escalera de niveles base PAdES B-B, B-T, B-LT y B-LTA construida con PDF Library for Delphi, que muestra cómo cada nivel agrega marcas de tiempo, evidencia DSS o una marca de tiempo de documento renovable sobre el núcleo ETSI.CAdES.detached
Cada nivel de línea base ETSI apila una garantía más sobre el mismo núcleo CAdES, desde los atributos firmados hasta una marca de tiempo de documento renovable

Producción de la firma de referencia

La API plana controla la firma como una máquina de estados de una sola operación: abre un proceso sobre el archivo de origen, lo configura, finaliza en un archivo de salida y lee el código de resultado. La secuencia siguiente produce una firma PAdES-B-B con SHA-256. La línea más importante no tiene nada que ver con la firma en sí. Es la reserva deliberadamente sobredimensionada de /Contents, porque es lo único que no se puede cambiar después si alguna vez se debe agregar una marca de tiempo a esta firma

var
  Pdf: TPDFlib;
  SignId: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    SignId := Pdf.NewSignProcessFromFile('invoice.pdf', '');
    if SignId = 0 then
      raise Exception.Create('cannot open source PDF');
    Pdf.SetSignProcessField(SignId, 'Sig1');
    Pdf.SetSignProcessPFXFromFile(SignId, 'company.pfx', PfxPassword);
    Pdf.SetSignProcessInfo(SignId, 'Approved', 'Vienna', 'billing@example.com');
    Pdf.SetSignProcessCustomSubFilter(SignId, 'ETSI.CAdES.detached');
    Pdf.SetSignProcessDigestAlgorithm(SignId, 2);          // SHA-256
    Pdf.SetSignProcessReserveContentsBytes(SignId, 8192);  // room for a timestamp later
    Pdf.EndSignProcessToFile(SignId, 'invoice-signed.pdf');
    if Pdf.GetSignProcessResult(SignId) <> 1 then
      raise Exception.CreateFmt('signing failed, code %d',
        [Pdf.GetSignProcessResult(SignId)]);
    Pdf.ReleaseSignProcess(SignId);
  finally
    Pdf.Free;
  end;
end;

NewSignProcessFromFile devuelve 0 cuando la fuente no puede abrirse en absoluto. Después de eso, GetSignProcessResult separa los modos de falla que realmente ocurren en producción: 4 significa una contraseña PDF incorrecta, 7 una contraseña PFX incorrecta, 9 un archivo de certificado sin clave privada, 10 una ruta de salida no escribible y 11 una falla al aplicar los bytes de firma. Registrar el código numérico junto al nombre del archivo de entrada convierte un ticket de soporte impreciso en un diagnóstico de un minuto

Agregar la marca de tiempo RFC 3161 que la biblioteca no obtendrá por usted

PDF Library for Delphi no incluye cliente TSA, y ese es un límite deliberado, no una omisión. La biblioteca calcula el hash que la autoridad de marcas de tiempo debe contrafirmar y después reincrusta el CMS aumentado; el intercambio HTTP y la cirugía CMS intermedia pertenecen a quien llama. Hay una razón técnica estricta para la separación. El control de Windows CryptoAPI que nominalmente agrega atributos sin firmar, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, falla con CRYPT_E_INVALID_INDEX en el diseño SignedData separado que usa PAdES. Por ello, el CMS mejorado debe proceder de un codificador CMS bajo control propio. Ninguna biblioteca puede incorporar silenciosamente el token con una llamada al sistema, y cualquiera que afirme hacerlo está realizando la cirugía en algún lugar que no se puede ver

Pipeline para agregar una marca de tiempo RFC 3161 a una firma PAdES en Delphi, separando el hashing e incrustación de PDF Library for Delphi de la solicitud TSA del llamador y la recodificación CMS dentro del espacio /Contents reservado
La biblioteca calcula los hashes y vuelve a incrustar mientras su código obtiene el token y realiza la cirugía CMS, y el resultado debe caer dentro de la reserva de 8192 bytes de /Contents
var
  Pdf: TPDFlib;
  StsId: Integer;
  HashHex, TstDer, TsAttr, AugmentedCms: AnsiString;
begin
  Pdf := TPDFlib.Create;
  try
    StsId := Pdf.NewPAdESSignatureTimeStampProcessFromFile('invoice-signed.pdf', '');
    Pdf.SetPAdESSignatureTimeStampField(StsId, 'Sig1');
    Pdf.SetPAdESSignatureTimeStampDigestAlgorithm(StsId, 2);
    HashHex := Pdf.GetPAdESSignatureValueHashHex(StsId);
    // las dos llamadas de abajo son código de tu aplicación: un HTTP POST a tu TSA,
    // y un re-encode CMS que adjunta el token como atributo sin firmar
    TstDer := RequestTimeStampToken(HashHex);
    TsAttr := Pdf.BuildPAdESSignatureTimeStampAttribute(TstDer);
    AugmentedCms := AttachUnsignedAttribute(Pdf.GetPAdESSignatureCMSBytes(StsId), TsAttr);
    Pdf.SetPAdESSignatureCMSBytes(StsId, AugmentedCms);
    Pdf.EndPAdESSignatureTimeStampProcessToFile(StsId, 'invoice-bt.pdf');
    if Pdf.GetPAdESSignatureTimeStampProcessResult(StsId) <> 1 then
      raise Exception.Create('timestamp embedding failed');
    Pdf.ReleasePAdESSignatureTimeStampProcess(StsId);
  finally
    Pdf.Free;
  end;
end;

Observe los códigos de resultado aquí: 12 significa que el campo de firma nombrado no existe, 11 que no se pudo analizar el CMS existente y 13 que el CMS aumentado ya no cabe en el marcador de posición /Contents reservado. El código 13 es el que duele, porque la única solución es volver a firmar: un token de marca de tiempo típico con su cadena de certificados ocupa de 4 a 6 KB, y la reserva de 8192 bytes realizada durante el paso B-B existe precisamente para que este paso tenga dónde alojarse

La validación comienza en ByteRange, no en la cadena de certificados

Una marca verde en un visor es una decisión de confianza frente al almacén de certificados de esa máquina, no un veredicto estructural sobre el archivo. La validación programática debe empezar más abajo, con la pregunta que las actualizaciones incrementales hacen sutil: ¿qué bytes cubre realmente cada firma? Cada mejora analizada aquí, ya sea una segunda firma, un diccionario DSS o una marca de tiempo del documento, llega mediante una actualización incremental, y cada actualización agrega bytes fuera del /ByteRange de la firma anterior. Esos bytes agregados son legítimos. Un validador aún tiene que clasificarlos frente a la política de modificación del documento, y el nivel DocMDP por campo donde reside esa política se puede leer con GetSignatureDocMDPLevelByName

Auditoría de disposición de bytes de un PDF firmado en Delphi que muestra los tramos cubiertos por ByteRange, los bytes de /Contents excluidos, las actualizaciones incrementales adjuntas fuera del rango y el veredicto de cobertura contra el tamaño del archivo
Dos intervalos cubiertos con los bytes de la propia firma excluidos cuentan la historia real de la cobertura, y las actualizaciones agregadas se clasifican según la política DocMDP en lugar de temérselas
var
  Doc: TPDFlibSignDoc;
  Names: TStringList;
  I: Integer;
  B0, B1, B2, B3, FileSize: Int64;
begin
  FileSize := TFile.GetSize('invoice-bt.pdf');  // before Open: SignDoc holds a share lock
  Doc := TPDFlibSignDoc.Create;
  try
    if not Doc.Open('invoice-bt.pdf', '', False) then
      raise Exception.Create('cannot open for audit');
    Names := TStringList.Create;
    try
      Doc.GetSignatureFieldNames(Names);
      for I := 0 to Names.Count - 1 do
        if Doc.GetSignatureValueObjNum(Names[I]) > 0 then   // >0 significa que realmente está firmado
        begin
          B0 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
          B1 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
          B2 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
          B3 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
          if (B0 = 0) and (B2 + B3 = FileSize) then
            Writeln(Names[I], ': covers the file to EOF')
          else
            Writeln(Names[I], ': earlier revision, or unexpected ByteRange layout');
        end;
    finally
      Names.Free;
    end;
    Doc.Close;
  finally
    Doc.Free;
  end;
end;

Hay dos trampas en esta ruta de auditoría. TPDFlibSignDoc.Open mantiene el archivo con un bloqueo exclusivo de uso compartido, por lo que un validador que también quiera aplicar hash a los bytes del archivo sin procesar para la verificación CMS debe leer el archivo en memoria antes de abrirlo para auditoría. Invierta ese orden y la lectura falla por un bloqueo que usted mismo estableció. La segunda trampa es silenciosa en lugar de ruidosa: la contraparte de API plana GetSignProcessByteRange devuelve Integer mientras que los desplazamientos subyacentes son Int64, así que después de 2 GB la llamada plana trunca sin avisar, razón por la que este ejemplo obtiene los desplazamientos mediante la clase de auditoría. También vale la pena mencionar una ausencia. La capa plana no tiene ningún envoltorio VerifySignature. Los veredictos criptográficos proceden de TPDFlibSignatureVerifier en el nivel de clase, que devuelve vsValid, vsInvalid o vsUnknown, o de un validador externo en el que la política de conformidad ya confía

Validación a largo plazo: DSS, VRI y la marca de tiempo del documento

PAdES-B-LT existe porque la infraestructura de revocación es mortal. ETSI EN 319 142-1 §5.4.2.2 especifica el Document Security Store: un diccionario de nivel de documento que contiene certificados, CRL y respuestas OCSP, indexados opcionalmente por firma mediante entradas VRI codificadas por el hash del /Contents de cada firma. El flujo de PDF Library for Delphi refleja el diseño de la marca de tiempo. NewPAdESDSSProcessFromFile abre el proceso; AddPAdESDSSCertificate, AddPAdESDSSCRL y AddPAdESDSSOCSP aceptan blobs DER; AddPAdESDSSVRI vincula material seleccionado a una firma; EndPAdESDSSProcessToFile escribe todo como una actualización incremental. La parte difícil sigue de su lado. Obtener el material de revocación y juzgar si es lo bastante reciente como para que valga la pena incrustarlo es tarea de quien llama. La biblioteca garantiza que los diccionarios son estructuralmente conformes; no puede garantizar que su respondedor OCSP dijera la verdad

El punto final de archivo, B-LTA, agrega una marca de tiempo del documento: un campo de firma separado cuyo tipo es DocTimeStamp en vez de Sig, producido mediante SetSignProcessDocTimeStamp con una longitud de firma reservada. No sustituye la marca de tiempo de firma del paso B-T. La marca de tiempo de firma prueba cuándo existía una firma en particular; la marca de tiempo del documento protege todo el archivo, incluida la evidencia DSS, y es el elemento que un archivo a largo plazo renueva cada pocos años a medida que los algoritmos se debilitan. Un perfil de archivo maduro incluye ambas. Para lectores anteriores a estas estructuras, TPDFlibSignDoc.EnsurePAdESExtensions registra la extensión de desarrollador ESIC en el catálogo del documento, anunciando que el archivo utiliza funciones definidas por ETSI

Conviene anticipar una reacción a todo esto, porque parece un error y no lo es. Un visor a menudo informa "validity unknown" en un archivo cuya estructura PAdES es totalmente correcta. La confianza y la estructura son ejes independientes. El visor simplemente no puede encadenar al firmante con una raíz en la que confíe en esa máquina, algo rutinario con CA privadas y certificados de prueba, incluso cuando la auditoría ByteRange y la verificación CMS aprueban. La solución es distribuir correctamente el certificado raíz o evaluar frente a las listas de confianza de la UE cuando el estado eIDAS cualificado es el objetivo real, en vez de tocar el código de firma

Para la perspectiva del lado de auditoría, es decir, enumerar campos de firma en un corpus, volcar diseños ByteRange y leer niveles DocMDP en masa, consulte el artículo complementario sobre el banco de trabajo de conformidad y firma. Los documentos firmados que también deben satisfacer una política de archivo pertenecen al flujo de trabajo descrito en preflight PDF/A y PDF/UA en Delphi. La documentación completa de la API y las descargas de evaluación se encuentran en la página de producto de losLab PDF Library for Delphi