En validerare avvisar en PAdES-signatur av en av tre anledningar i nästan alla fall vi har felsökt med PDFium-komponenten i Delphi: /ByteRange-arrayen innehåller fortfarande sina noll-platshållare, signeringstiden /M är inte en välformad PDF-datumsträng, eller så har en inkrementell uppdatering tappat bort posten /Encrypt från ett krypterat dokument. Alla tre producerar filer som öppnas utan problem, renderas fint, men fallerar i samma ögonblick som en kompatibel validerare läser signaturordlistan (signature dictionary)
Scenariot som motiverar den här artikeln är smärtsamt specifikt. Du signerar ett kontrakt med arbetsflödet från artikeln om PAdES B-B-signering, din egen kontrollkod rapporterar att signaturen finns och är strukturellt sund, alla lokala tester är gröna — och sedan laddar motparten upp filen till sin valideringsplattform och får ett rött kryss. Inget av detta fel är synligt i en visare, eftersom ingen av dessa tre defekter rör sidans innehåll. De lever helt och hållet i signaturordlistan och filtrailern, vilket är precis där validerare letar och visare oftast inte gör det
Varför är mitt PDF-signaturs ByteRange ogiltigt?
Ett ByteRange-avvisande innebär nästan alltid att de fyra fälten aldrig fylldes i, inte att intervallen är subtilt felaktiga. Arrayen /ByteRange [A B C D] deklarerar två spann, byte A till A+B och byte C till C+D, och EN 319 142-1 §6.3 (krav k) kräver att de tillsammans täcker hela filen utom den hexadecimalt kodade /Contents-strängen. I siffer: A är 0, C >= A+B, C+D är lika med filens längd, och gapet mellan B och C rymmer exakt <...>-hexsträngen — två klammerbyte plus två hex-tecken per byte av CMS-data. En validerare som läser [0 0 0 0] drar slutsatsen att signaturen inte täcker någonting och avvisar den, oavsett hur korrekt CMS-strukturen inuti /Contents är
Mekaniken bakom hur detta sker är värd att förstå eftersom samma mönster finns i alla signeringsstackar. En signerare kan inte veta de slutliga förskjutningarna förrän filen har lagts ut, så skrivaren genererar arrayen med noll-platshållare med fast bredd och fyller i dem efter layouten. I utgåvor av PDFium-komponenten före 2.14.1 sökte den ifyllningen efter platshållarmönstret med början från /Contents-positionen — men /ByteRange ligger före /Contents i ordlistan, så sökningen hittade ingenting och alla tre ersättningar misslyckades tyst. CMS-kontrollsumman (digest) beräknades över korrekta intervall, så kryptografin var sund; men deklarationen av dessa intervall förblev noll, så varje kompatibel validerare avvisade filen. Tidsstämplar för PAdES B-LTA-dokument, som använder samma ordlistelayout, misslyckades på samma sätt — relevant om du bygger på långsiktiga B-LT- och B-LTA-signaturer. Version 2.14.1 gör ifyllnad från början av signaturobjektet, och korrigeringen täcks av ett regressionstest som tolkar den producerade filen och bekräftar aritmetiken nedan
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;
Tjugo rader RTL-kod, inga externa bibliotek, och den upptäcker hela felklassen vid tillverkningstillfället. Om du bara sparar ett kontrollpåstående från den här artikeln, spara detta: tolka dina egna signerade utdata och verifiera de tre likheterna innan filen lämnar processen
Varför flaggar validerare signeringstiden /M som felaktig?
Strikta validerare avvisar en signeringstid som inte är en fullständig PDF-datumsträng, och de två delar som oftast saknas är prefixet D: och UTC-tidszonsmarkeringen. ISO 32000-1 §7.9.4 definierar datumformatet som D:YYYYMMDDHHmmSS följt av en förskjutning — Z för UTC, eller en signerad +HH'mm'-relation till det. En ren 20260709143000 tolkas som ett datum av en generös läsare, men en validerare som tillämpar grammatiken bokstavligt ser en felaktig sträng i ett obligatoriskt formaterat fält och flaggar signaturen. Utgåvor av PDFium-komponenten före 2.14.4 skrev posten /M i TPdf.SignPades och SignPadesBytes i just den rena formen; sedan 2.14.4 bär posten prefixet D: och markeringen Z, så att den deklarerade signeringstiden lyder D:20260709143000Z
Två praktiska anteckningar hör till detta fält. För det första, skriv UTC och ange det: en tidsstämpel utan tidszonsmarkering tvingar valideraren att gissa tidszonsrelationen, och §7.9.4 behandlar tidszonen som en del av formatet snarare än en valfri finess. För det andra, kom ihåg vad /M är — signerarens deklarerade tid, ett påstående snarare än ett bevis. En validerare kontrollerar dess format här, inte dess sanningshalt; bevisbar tid kommer från en RFC 3161-tidsstämpel vid PAdES B-T och högre. Att formatera ett fält korrekt och att lita på det är separata beslut, och validerare tvingar bara fram det första
Varför utlöser SignPades EPadesCrypto på en krypterad PDF?
PDFium-komponenten vägrar att signera ett krypterat dokument eftersom alternativet är att producera en fil som kompatibla läsare kommer att förstöra vid öppning. En PAdES-signatur läggs till som en inkrementell uppdatering, och ISO 32000-1 §7.5.6 kräver att den nya trailern för en uppdateringssektion bär med sig varje post från den föregående trailern utom /Prev — i ett krypterat dokument inkluderar det /Encrypt. Om den tas bort deklarerar den senaste trailern att filen är okrypterad, så en kompatibel läsare tolkar den krypterade kroppen som klartext och får bara skräp. Värre är att de strömmar och strängar signeraren lägger till själva måste krypteras med dokumentnyckeln för att vara giltiga, vilket en klartext-signaturinjektor inte kan göra. Det finns inget sätt att lägga till en giltig klartextsignatur till en krypterad fil, så sedan version 2.14.2 utlöser TPdf.SignPades, SignPadesBytes och InjectPadesDssMarkers undantaget EPadesCrypto istället för att generera ett korrupt eller icke-verifierbart 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;
Undantaget är det korrekta utfallet, så designa arbetsflödet kring det istället för att fånga felet och försöka igen. Avkryptera först med ägarbehörighet, signera klartextkopian, och om distributionskanalen kräver kryptering, acceptera att kryptering-sedan-signering och signering-sedan-kryptering producerar olika artefakter med olika valideringshistorik. En signatur beräknad över klartextbyte kan inte överleva omkryptering av dessa byte, så de ärliga alternativen är en signerad klartextfil eller ett policybeslut dokumenterat bredvid koden
Hur två buggar kan certifiera varandra
ByteRange-defekten gömde sig så länge som den gjorde eftersom vår egen validerare hade fel på ett kompletterande sätt, och det är den mest överförbara lärdomen i den här artikeln. Täckningskontrollen i ValidatePadesCompliance krävde att det andra spannet startade exakt vid A+B — ett noll-gap — vilket felaktigt klassificerar standardlayouten där gapet rymmer hexsträngen /Contents. Så den interna valideraren avvisade just den kompatibla layouten och den interna generatorn producerade aldrig en sådan, och end-to-end-pipelinen för signera-sedan-validera förblev grön. Varje bugg nekade testsviten det motexempel som skulle ha avslöjat den andra. Version 2.14.1 fixade båda sidor i samma utgåva: generatorn fyller i alla fyra fälten, och valideraren accepterar C >= A+B med C+D lika med filens längd
Den metodologiska lösningen är korskontroll mot en implementering du inte själv har skrivit. En generator och en validerare som delar kodbas, författare eller bara en mental modell av formatet kan vara överens om ett delat missförstånd på obestämd tid; en oberoende validerare bryter symmetrin. Kör dina signerade utdata genom minst en extern efterlevnadskontroll före en release, och behåll en strukturell självkontroll i bygget som en snabb första linje — TPdf.ValidatePades, som beskrivs i artikeln om signaturinspektion, rapporterar täckningsfelet som ett namngivet 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 checklist för avvisade signaturer
När en plattform avvisar din PAdES-signatur, kontrollera de enkla strukturella orsakerna innan du misstänker certifikat eller förtroendekedjor. Läs arrayen /ByteRange och verifiera de tre likheterna: första fältet noll, andra spannet startar vid eller efter slutet av det första, andra spannet slutar exakt vid filens slut. Läs posten /M och verifiera att det är en fullständig datumsträng enligt §7.9.4 med prefixet D: och en tidszonsmarkering. Bekräfta att källdokumentet inte var krypterat när signaturen lades till — och om din stack signerade det ändå utan anmärkning, behandla den tystnaden som en bugg i stacken. Alla tre kontrollerna körs på råa byte på millisekunder, och enligt vår erfarenhet förklarar de avvisningen betydligt oftare än någon kryptografisk orsak
Signerings-, inspektions- och valideringsanropen som används här — SignPades, ValidatePades och PAdES-efterlevnadskontrollerna med deras korrigerade ByteRange-aritmetik, datumformatering och krypteringsspärr — levereras med PDFium Component för Delphi, C++Builder och Lazarus