Un validador rechaza una firma PAdES por uno de tres motivos en casi todos los casos que hemos depurado con PDFium Component en Delphi: la matriz /ByteRange todavía conserva sus marcadores de posición en cero, la hora de firma /M no es una cadena de fecha de PDF bien formateada, o una actualización incremental eliminó la entrada /Encrypt de un documento cifrado. Los tres casos producen archivos que se abren bien, se renderizan bien y fallan en el momento en que un validador compatible lee el diccionario de firmas
El escenario que motiva este artículo es dolorosamente específico. Usted firma un contrato con el flujo de trabajo de el artículo sobre firmas PAdES B-B, su propio código de inspección informa que la firma está presente y estructuralmente sana, cada prueba local da verde, y luego la otra parte carga el archivo en su plataforma de validación y obtiene una cruz roja. Nada de este fallo es visible en un visor, porque ninguno de estos tres defectos afecta al contenido de la página. Residen completamente en el diccionario de firmas y en el trailer del archivo, que es exactamente donde buscan los validadores y donde los visores generalmente no lo hacen
¿Por qué el ByteRange de mi firma PDF no es válido?
El rechazo de ByteRange casi siempre significa que los cuatro campos nunca se completaron, no que los rangos sean sutilmente incorrectos. La matriz /ByteRange [A B C D] declara dos tramos, los bytes de A a A+B y los bytes de C a C+D, y la norma EN 319 142-1 §6.3 (requisito k) exige que juntos cubran todo el archivo excepto la cadena /Contents codificada en hegadecimal. En números: A es 0, C >= A+B, C+D es igual a la longitud del archivo, y el espacio entre B y C contiene exactamente la cadena hexadecimal <...> (dos bytes de corchetes más dos caracteres hexadecimales por cada byte de datos CMS). Un validador que lee [0 0 0 0] concluye que la firma no cubre nada y la rechaza, sin importar qué tan correcta sea la estructura CMS dentro de /Contents
Vale la pena comprender la mecánica de cómo sucede esto porque el mismo patrón existe en cada pila de firmas. Un firmante no puede conocer los desplazamientos finales hasta que el archivo esté estructurado, por lo que el generador emite la matriz con marcadores de posición en cero de ancho fijo y los completa después del diseño. En las versiones de PDFium Component anteriores a la 2.14.1, ese llenado posterior buscaba el patrón de marcador de posición comenzando desde la posición de /Contents, pero /ByteRange se ubica antes de /Contents en el diccionario, por lo que la búsqueda no encontró nada y los tres reemplazos fallaron silenciosamente. El resumen del CMS se calculó sobre los rangos correctos, por lo que la criptografía era sólida; la declaración de esos rangos se mantuvo en cero, por lo que cada validador compatible rechazó el archivo. Los sellos de tiempo de documento de PAdES B-LTA, que utilizan el mismo diseño de diccionario, fallaban de la misma manera (importante si construye sobre firmas a largo plazo B-LT y B-LTA). La versión 2.14.1 realiza el llenado posterior desde el inicio del objeto de firma, y la solución está cubierta por una prueba de regresión que analiza el archivo producido y comprueba la aritmética siguiente
function SignedByteRangeCoversFile(const FileName: string): Boolean;
var
Raw: TBytes;
Text: AnsiString;
P, N: Integer;
F: array[0..3] of Int64;
begin
Raw := TFile.ReadAllBytes(FileName);
SetString(Text, PAnsiChar(@Raw[0]), Length(Raw));
P := Pos('/ByteRange', Text); // first signature only
Result := P > 0;
if not Result then Exit;
Inc(P, Length('/ByteRange'));
for N := 0 to 3 do
begin
while (P <= Length(Text)) and not (Text[P] in ['0'..'9']) do Inc(P);
F[N] := 0;
while (P <= Length(Text)) and (Text[P] in ['0'..'9']) do
begin
F[N] := F[N] * 10 + Ord(Text[P]) - Ord('0');
Inc(P);
end;
end;
// EN 319 142-1 §6.3 req k: spans cover everything except /Contents
Result := (F[0] = 0) and (F[2] >= F[0] + F[1]) and
(F[2] + F[3] = Int64(Length(Raw)));
end;
Veinte líneas de código RTL simple, sin llamadas a bibliotecas, y detectan toda la categoría de fallo en el momento de la producción. Si conserva una sola afirmación (assertion) de este artículo, conserve esta: analice su propio resultado firmado y compruebe las tres igualdades antes de que el archivo salga de su proceso
¿Por qué los validadores marcan la hora de firma /M como malformada?
Los validadores estrictos rechazan una hora de firma que no sea una cadena de fecha de PDF completa, y las dos partes que faltan con más frecuencia son el prefijo D: y el marcador de diferencia UTC. La norma ISO 32000-1 §7.9.4 define el formato de fecha como D:YYYYMMDDHHmmSS seguido de una diferencia (Z para UTC, o una relación con signo +HH'mm' respecto a este). Un simple 20260709143000 se analiza como una fecha en un lector flexible, pero un validador que aplique la gramática literalmente observa una cadena malformada en un campo de formato obligatorio y marca la firma. Las versiones de PDFium Component anteriores a la 2.14.4 escribían la entrada /M de TPdf.SignPades y SignPadesBytes exactamente en esa forma simple; desde la 2.14.4 la entrada incluye el prefijo D: y el marcador Z, por lo que la hora de firma declarada se lee como D:20260709143000Z
Una nota práctica se asocia a este campo. Primero, escriba en UTC y declárelo: una marca de tiempo sin un marcador de diferencia obliga al validador a adivinar la relación de zona horaria, y §7.9.4 trata la diferencia como parte del formato y no como un detalle opcional. Segundo, recuerde qué es /M (la hora declarada por el firmante, una afirmación y no una prueba). Un validador comprueba su formato aquí, no su veracidad; la hora comprobable proviene de una marca de tiempo RFC 3161 en PAdES B-T y superiores. Formatear un campo correctamente y confiar en él son decisiones independientes, y los validadores solo exigen lo primero
¿Por qué SignPades produce la excepción EPadesCrypto en un PDF cifrado?
El componente PDFium Component se niega a firmar un documento cifrado porque la alternativa es producir un archivo que los lectores compatibles dañarán al abrirlo. Una firma PAdES se añade como una actualización incremental, y la norma ISO 32000-1 §7.5.6 exige que el nuevo trailer de una sección de actualización contenga cada entrada del trailer anterior excepto /Prev (lo que en un documento cifrado incluye a /Encrypt). Si lo descarta, el último trailer declara que el archivo no está cifrado, por lo que un lector compatible analiza el cuerpo cifrado como texto sin formato y obtiene datos inservibles. Peor aún, los flujos y cadenas que añade el firmante tendrían que estar cifrados con la clave del documento para ser válidos, lo cual un inyector de firmas en texto sin formato no puede realizar. No hay forma de añadir una firma válida en texto sin formato a un archivo cifrado, por lo que desde la versión 2.14.2, TPdf.SignPades, SignPadesBytes e InjectPadesDssMarkers producen la excepción EPadesCrypto en lugar de emitir un resultado corrupto o imposible de verificar
try
if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
Writeln('Signing reported failure');
except
on E: EPadesCrypto do
begin
// e.g. 'SignPadesBytes: the source document is encrypted;
// remove encryption before signing'
Writeln('Cannot sign: ', E.Message);
end;
end;
La excepción es el resultado correcto, por lo que debe diseñar el flujo de trabajo en torno a ella en lugar de capturarla y reintentar. Descifre primero con autoridad de propietario, firme la copia en texto sin formato, y si el canal de distribución requiere cifrado, acepte que cifrar y luego firmar, o firmar y luego cifrar, producen elementos diferentes con historias de validación distintas. Una firma calculada sobre bytes en texto sin formato no puede sobrevivir a la reencriptación de esos bytes, por lo que las opciones reales son un archivo en texto sin formato firmado o una decisión de política documentada junto al código
Cómo dos errores pueden certificarse mutuamente
El defecto de ByteRange permaneció oculto durante tanto tiempo porque nuestro propio validador fallaba de una manera complementaria, y esa representa la lección más aplicable de este artículo. La comprobación de cobertura en ValidatePadesCompliance requería que el segundo tramo comenzara exactamente en A+B (un espacio en cero), lo que clasifica erróneamente el diseño estándar donde el espacio contiene la cadena hexadecimal de /Contents. Así que el validador interno rechazaba precisamente el diseño compatible y el generador interno nunca producía uno, y los flujos de trabajo de firmar y luego validar se mantenían correctos. Cada error le negaba al conjunto de pruebas el contraejemplo que habría revelado al otro. La versión 2.14.1 solucionó ambos lados en la misma versión: el generador completa los cuatro campos y el validador acepta C >= A+B con C+D igual a la longitud del archivo
La solución metodológica es la validación cruzada frente a una implementación que usted no haya escrito. Un generador y un validador que comparten una base de código, un autor o incluso solo un modelo mental del formato pueden ponerse de acuerdo sobre un malentendido común de forma indefinida; un validador independiente rompe esa simetría. Pase su resultado firmado por al menos un validador de conformidad externo antes de un lanzamiento y mantenga una autocomprobación estructural en la compilación como primera línea rápida: TPdf.ValidatePades, descrito en el artículo de inspección de firmas, informa el fallo de cobertura como un problema identificado
Una lista de comprobación de rechazo para resultados firmados
Cuando una plataforma rechace su firma PAdES, compruebe las causas estructurales sencillas antes de sospechar de los certificados o de las cadenas de confianza. Lea la matriz /ByteRange y verifique las tres igualdades: primer campo en cero, segundo tramo comenzando en o después del final del primero, segundo tramo finalizando exactamente al final del archivo. Lea la entrada /M y verifique que sea una cadena de fecha completa según §7.9.4 con el prefijo D: y un marcador de diferencia. Confirme que el documento de origen no estaba cifrado cuando se añadió la firma, y si su pila (stack) lo firmó de todos modos sin quejas, trate ese silencio como un error en la pila. Las tres comprobaciones se ejecutan sobre bytes sin formato en milisegundos y, en nuestra experiencia, explican el rechazo con mucha más frecuencia que cualquier causa criptográfica
Las firmas, inspección y validación utilizadas aquí —SignPades, ValidatePades y las comprobaciones de conformidad de PAdES con su aritmética de ByteRange corregida, formateo de fecha y control de cifrado— se distribuyen con PDFium Component para Delphi, C++Builder y Lazarus