En validator afviser en PAdES-signatur af en ud af tre årsager i næsten alle de tilfælde, vi har fejlfundet med PDFium Component i Delphi: /ByteRange-arrayet indeholder stadig sine nul-pladsholdere, /M-signeringstiden er ikke en velformet PDF-datostreng, eller en trinvis opdatering tabte /Encrypt-posten fra et krypteret dokument. Alle tre producerer filer, der åbner fint, renderer fint og fejler i det øjeblik, en overensstemmende validator læser signaturordbogen
Det scenarie, der motiverer denne artikel, er smerteligt specifikt. Du underskriver en kontrakt med arbejdsgangen fra artiklen om PAdES B-B-signering, din egen inspektionskode rapporterer, at signaturen er til stede og strukturelt sund, hver lokal test er grøn — og derefter uploader modparten filen til deres valideringsplatform og får et rødt kryds. Intet ved fejlen er synligt i en fremviser, fordi ingen af disse tre defekter berører sideindholdet. De lever helt og holdent i signaturordbogen og filens trailer, hvilket er præcis der, hvor validatorer kigger, og fremvisere for det meste ikke gør
Hvorfor er mit PDF-signatur ByteRange ugyldigt?
En ByteRange-afvisning betyder næsten altid, at de fire felter aldrig blev udfyldt, ikke at områderne er subtilt forkerte. /ByteRange [A B C D]-arrayet erklærer to spænd, bytes A til A+B og bytes C til C+D, og EN 319 142-1 §6.3 (krav k) kræver, at de tilsammen dækker hele filen undtagen den hex-kodede /Contents-streng. I tal: A er 0, C >= A+B, C+D er lig med fillængden, og hullet mellem B og C indeholder præcis <...>-hexstrengen — to parentesbytes plus to hex-tegn pr. byte CMS-data. En validator, der læser [0 0 0 0], konkluderer, at signaturen intet dækker, og afviser den, uanset hvor korrekt CMS-strukturen inde i /Contents er
Mekanikken i, hvordan dette sker, er værd at forstå, fordi det samme mønster findes i enhver signeringsstak. En underskriver kan ikke kende de endelige forskydninger (offsets), før filen er sat op, så skriveren udsender arrayet med fastbredde nul-pladsholdere og efterudfylder dem efter opsætningen. I udgivelser af PDFium Component før 2.14.1 søgte denne efterudfyldning efter pladsholdermønsteret startende fra /Contents-positionen — men /ByteRange sidder før /Contents i ordbogen, så søgningen fandt intet, og alle tre erstatninger fejlede lydløst. CMS-sammendraget (digest) blev beregnet over de korrekte områder, så kryptografien var sund; erklæringen af disse områder forblev på nul, så enhver overensstemmende validator afviste filen. PAdES B-LTA Dokument-tidsstempler, som bruger samme ordbogsopsætning, fejlede på samme måde — relevant, hvis du bygger på langsigtede B-LT- og B-LTA-signaturer. Version 2.14.1 efterudfylder fra signaturobjektets start, og rettelsen er dækket af en regresstest, der parser den producerede fil og bekræfter aritmetikken nedenfor
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;
Tyve linjer almindelig RTL-kode, ingen bibliotekskald, og it catches the entire failure class at the point of production. Hvis du beholder én kontrol (assertion) fra denne artikel, så behold denne: Pars dit eget signerede output og kontroller de tre ligheder, før filen forlader din proces
Hvorfor flager validatorer /M-signeringstiden som fejlbehæftet?
Strenge validatorer afviser en signeringstid, der ikke er en komplet PDF-datostreng, og de to dele, der oftest mangler, er D:-præfikset og UTC-forskydningsmarkøren. ISO 32000-1 §7.9.4 definerer datoformatet som D:YYYYMMDDHHmmSS efterfulgt af en forskydningsmæssig værdi — Z for UTC, eller et fortegnsvurderet +HH'mm' forhold til det. En rå 20260709143000 fortolkes som en dato af en eftergivende læser, men en validator, der anvender grammatikken bogstaveligt, ser en forkert formateret streng i et felt med obligatorisk format og markerer signaturen. Udgivelser af PDFium Component før 2.14.4 skrev /M-posten for TPdf.SignPades og SignPadesBytes i netop den rå form; siden 2.14.4 bærer posten D:-præfikset og Z-markøren, så den erklærede signeringstid lyder D:20260709143000Z
To praktiske bemærkninger knytter sig til dette felt. For det første: Skriv UTC og angiv det: Et tidsstempel uden en forskydningsmarkør tvinger validatoren til at gætte tidszonens forhold, og §7.9.4 behandler forskydningen som en del af formatet snarere end en valgfri detalje. For det andet: Husk hvad /M er — underskriverens erklærede tid, en påstand snarere end et bevis. En validator kontrollerer dens format her, ikke dens sandfærdighed; beviselig tid kommer fra et RFC 3161-tidsstempel ved PAdES B-T og derover. At formatere et felt korrekt og at stole på det er to forskellige beslutninger, og validatorer håndhæver kun den første
Hvorfor rejser SignPades EPadesCrypto på en krypteret PDF?
PDFium Component nægter at underskrive et krypteret dokument, fordi alternativet er at producere en fil, som overensstemmende læsere vil ødelægge ved åbning. En PAdES-signatur tilføjes som en trinvis opdatering, og ISO 32000-1 §7.5.6 kræver, at den nye trailer i en opdateringssektion bærer enhver post fra den foregående trailer undtagen /Prev — i et krypteret dokument inkluderer det /Encrypt. Drop det, og den seneste trailer erklærer filen ukrypteret, så en overensstemmende læser fortolker den krypterede krop som klartekst og får vrøvl. Værte, de strømme og strenge, som underskriveren tilføjer, skulle selv være krypteret med dokumentnøglen for at være lovlige, hvilket en klartekst-signaturinjektor ikke kan gøre. Der er ingen måde at tilføje en gyldig klartekstsignatur til en krypteret fil, så siden version 2.14.2 rejser TPdf.SignPades, SignPadesBytes og InjectPadesDssMarkers undtagelsen EPadesCrypto i stedet for at udsende et beskadiget eller uverificerbart resultat
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;
Undtagelsen er det korrekte resultat, så design arbejdsgangen omkring den frem for at fange den og prøve igen. Dekrypter først med ejerrettighed, underskriv klartekstkopien, og hvis distributionskanalen kræver kryptering, må du acceptere, at krypter-og-underskriv samt underskriv-og-krypter producerer forskellige artefakter med forskellige valideringsforløb. En signatur beregnet over klartekstbytes kan ikke overleve en genkryptering af disse bytes, så de reelle muligheder er en signeret klartekstfil eller en politikbeslutning dokumenteret ved siden af koden
Hvordan to fejl kan godkende hinanden
ByteRange-defekten forblev skjult så længe, fordi vores egen validator var forkert på en komplementær måde, og det er den mest overførbare lektion i denne artikel. Dækningskontrollen i ValidatePadesCompliance krævede, at det andet spænd startede præcis ved A+B — et nul-gab — hvilket fejlklassificerer det standardlayout, hvor gabet indeholder /Contents-hexstrengen. Så den interne validator afviste præcis det overensstemmende layout, og den interne generator producerede aldrig et, og ende-til-ende underskriv-og-valider-pipelines forblev tilfældigvis grønne. Hver fejl nægtede testsuiten det modeksempel, der ville have afsløret den anden. Version 2.14.1 rettede begge sider i samme udgivelse: Generatoren efterudfylder alle fire felter, og validatoren accepterer C >= A+B med C+D lig med fillængden
Den metodiske løsning er krydsvalidering mod en implementering, du ikke selv har skrevet. En generator og a validator that share a codebase, an author, or even just a mental model of the format can agree on a shared misunderstanding indefinitely; an independent validator breaks the symmetry. Kør dit signerede output gennem mindst én ekstern overensstemmelseskontrol før en udgivelse, og behold en strukturel selvkontrol i buildet som den hurtige første linje — TPdf.ValidatePades, beskrevet i artiklen om signaturinspektion, rapporterer dækningsfejlen som et navngivet problem
var
R: TPadesValidationResult;
begin
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
R := Pdf.ValidatePades;
if ppeiByteRangeNotCoveringFile in R.Issues then
Writeln('ByteRange does not cover the file');
if not R.IsCompliant then
Writeln('Structural issues present: do not ship this file');
end;
En tjekliste for afvisning af signeret output
Når en platform afviser din PAdES-signatur, skal du kontrollere de billige strukturelle årsager, før du mistænker certifikater eller tillidskæder. Læs /ByteRange-arrayet og bekræft de tre ligheder: Første felt nul, andet spænd startende ved eller efter slutningen af det første, andet spænd slutter præcis ved slutningen af filen. Læs /M-posten og bekræft, at det is a full §7.9.4 date string with the D: prefix and an offset marker. Bekræft, at kildedokumentet ikke var krypteret, da signaturen blev tilføjet — og hvis din stak alligevel underskrev det uden klager, skal du behandle denne tavshed som en fejl i stakken. Alle tre kontroller kører på rå bytes på millisekunder, og efter vores erfaring forklarer de afvisningen langt oftere end nogen kryptografisk årsag
De underskrifts-, inspektions- og valideringskald, der er brugt her — SignPades, ValidatePades og PAdES-overensstemmelseskontrollerne med deres korrigerede ByteRange-aritmetik, datoformatering og krypteringsspærring — leveres med PDFium Component til Delphi, C++Builder og Lazarus