Technisch artikel

Waarom validators PAdES-handtekeningen afwijzen: PDFium in Delphi

Een validator wijst een PAdES-handtekening in bijna elk geval dat we met het PDFium Component in Delphi hebben gedebugd om een van de volgende drie redenen af: de /ByteRange-array bevat nog de tijdelijke nul-aanduidingen, de /M-ondertekeningstijd is geen goed gevormde PDF-datumstring, of een incrementele update heeft de vermelding /Encrypt weggelaten uit een versleuteld document. Alle drie leiden ze tot bestanden die prima openen en renderen, maar falen op het moment dat een conformerende validator het handtekeningenwoordenboek (signature dictionary) leest

Het scenario dat aanleiding geeft tot dit artikel is pijnlijk specifiek. U ondertekent een contract met de workflow uit het artikel over PAdES B-B-ondertekening, uw eigen inspectiecode meldt dat de handtekening aanwezig en structureel in orde is, elke lokale test is groen — en vervolgens uploadt de tegenpartij het bestand naar hun validatieplatform en krijgt een rood kruis. Niets van de fout is zichtbaar in een viewer, omdat geen van deze drie defecten de paginainhoud raakt. Ze bevinden zich volledig in het handtekeningenwoordenboek en de staart van het bestand (trailer), wat precies is waar validators kijken en viewers meestal niet

Waarom is de ByteRange van mijn PDF-handtekening ongeldig?

Een ByteRange-afwijzing betekent bijna altijd dat de vier velden nooit zijn ingevuld, niet dat de bereiken subtiel onjuist zijn. De array /ByteRange [A B C D] declareert twee bereiken, bytes A tot A+B en bytes C tot C+D, en EN 319 142-1 §6.3 (vereiste k) eist dat deze samen het hele bestand beslaan, behalve de hex-gecodeerde /Contents-string. In getallen: A is 0, C >= A+B, C+D is gelijk aan de bestandsgrootte, en het gat tussen B en C bevat exact de <...>-hexstring — twee haakjesbytes plus twee hex-tekens per byte aan CMS-gegevens. Een validator die [0 0 0 0] leest, concludeert dat de handtekening niets dekt en wijst deze af, ongeacht hoe correct de CMS-structuur binnen /Contents is

Het is de moeite waard om de werking hiervan te begrijpen, omdat ditzelfde patroon in elke ondertekeningsstack voorkomt. Een ondertekenaar kan de uiteindelijke offsets pas weten als het bestand is ingedeeld, dus de schrijver verzendt de array met tijdelijke nul-aanduidingen van vaste breedte en vult deze na de indeling achteraf in. In releases van het PDFium Component vóór 2.14.1 zocht dat invullen naar het patroon van de tijdelijke aanduiding vanaf de positie van /Contents — maar /ByteRange bevindt zich vóór /Contents in het woordenboek, waardoor de zoekopdracht niets vond en alle drie de vervangingen geruisloos faalden. De CMS-hash werd berekend over de juiste bereiken, waardoor de cryptografie in orde was; de declaratie van die bereiken bleef echter op nul staan, waardoor elke conformerende validator het bestand weigerde. Documenttijdstempels voor PAdES B-LTA, die dezelfde woordenboeklay-out gebruiken, faalden op dezelfde manier — relevant als u voortbouwt op langdurige B-LT- en B-LTA-handtekeningen. Versie 2.14.1 vult in vanaf het begin van het handtekeningobject, en de oplossing wordt gedekt door een regressietest die het geproduceerde bestand ontleedt en de onderstaande berekening valideert

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);          // alleen eerste handtekening
  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: bereiken dekken alles behalve /Contents
  Result := (F[0] = 0) and (F[2] >= F[0] + F[1]) and
            (F[2] + F[3] = Int64(Length(Raw)));
end;

Twintig regels eenvoudige RTL-code, geen bibliotheekaanroepen, en het vangt de hele foutklasse af op het moment van productie. Als u één assertie uit dit artikel bewaart, bewaar dan deze: ontleed uw eigen ondertekende uitvoer en controleer de drie gelijkheden voordat het bestand uw proces verlaat

Waarom markeren validators de /M-ondertekeningstijd als misvormd?

Strikte validators wijzen een ondertekeningstijd af die geen complete PDF-datumstring is, en de twee delen die het vaakst ontbreken zijn het voorvoegsel D: en de UTC-offsetmarker. ISO 32000-1 §7.9.4 definieert het datumformaat als D:YYYYMMDDHHmmSS gevolgd door een offset — Z voor UTC, of een ondertekende +HH'mm'-relatie daarmee. Een kale 20260709143000 ontleedt als een datum voor een tolerante lezer, maar een validator die de grammatica letterlijk toepast, ziet een misvormde string in een verplicht opgemaakt veld en markeert de handtekening. Releases van het PDFium Component vóór 2.14.4 schreven de vermelding /M van TPdf.SignPades en SignPadesBytes in exact die kale vorm; sinds 2.14.4 bevat de vermelding het voorvoegsel D: en de Z-marker, zodat de gedeclareerde ondertekeningstijd leest als D:20260709143000Z

Twee praktische opmerkingen horen bij dit veld. Ten eerste: schrijf UTC en geef dat ook aan. Een tijdstempel zonder offset-indicator dwingt de validator om de relatie met de tijdzone te raden, en §7.9.4 behandelt de offset als onderdeel van het formaat in plaats van een optionele aardigheid. Ten tweede: onthoud wat /M is — de door de ondertekenaar gedeclareerde tijd, een claim in plaats van een bewijs. Een validator controleert hier het formaat, niet de waarheid; bewijsbaar tijdstip is afkomstig van een RFC 3161-tijdstempel bij PAdES B-T en hoger. Het correct formatteren van een veld en het vertrouwen ervan zijn afzonderlijke beslissingen, en validators handhaven alleen de eerste

Waarom roept SignPades EPadesCrypto op bij een versleutelde PDF?

Het PDFium Component weigert een versleuteld document te ondertekenen, omdat het alternatief het produceren van een bestand is dat conformerende lezers bij het openen zullen vernietigen. Een PAdES-handtekening wordt toegevoegd als een incrementele update, en ISO 32000-1 §7.5.6 vereist dat de nieuwe trailer van een update-sectie elke vermelding van de vorige trailer bevat, behalve /Prev — bij een versleuteld document omvat dat ook /Encrypt. Laat dit weg en de nieuwste trailer verklaart het bestand onversleuteld, zodat een conformerende lezer het versleutelde lichaam als platte tekst ontleedt en rommel krijgt. Erger nog, de stromen en strings die de ondertekenaar toevoegt, zouden zelf moeten worden versleuteld met de documentcode om geldig te zijn, wat een platte tekst handtekening-injector niet kan. Er is geen manier om een geldige handtekening in platte tekst toe te voegen aan een versleuteld bestand. Sinds versie 2.14.2 roepen TPdf.SignPades, SignPadesBytes en InjectPadesDssMarkers daarom EPadesCrypto op in plaats van een beschadigd of onverifieerbaar resultaat te genereren

try
  if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
    Writeln('Signing reported failure');
except
  on E: EPadesCrypto do
  begin
    // bijv. 'SignPadesBytes: the source document is encrypted;
    //       remove encryption before signing'
    Writeln('Kan niet ondertekenen: ', E.Message);
  end;
end;

De uitzondering (exception) is de juiste uitkomst, dus ontwerp de workflow hieromheen in plaats van deze op te vangen en opnieuw te proberen. Decrypteer eerst met eigenaarsrechten (owner authority), onderteken de kopie in platte tekst, en als het distributiekanaal versleuteling vereist, accepteer dan dat encrypt-then-sign en sign-then-encrypt verschillende artefacten produceren met verschillende validatieverhalen. Een handtekening berekend over bytes in platte tekst kan een herversleuteling van die bytes niet overleven, dus de eerlijke opties zijn een ondertekend bestand in platte tekst of een beleidsbeslissing die naast de code is gedocumenteerd

Hoe twee bugs elkaar kunnen certificeren

Het ByteRange-defect bleef zo lang verborgen omdat onze eigen validator op een complementaire manier onjuist was, en dat is de meest overdraagbare les in dit artikel. De bereikcontrole in ValidatePadesCompliance vereiste dat het tweede bereik exact op A+B begon — een gat van nul — wat de standaardlay-out, waarbij het gat de /Contents-hexstring bevat, verkeerd classificeert. De interne validator wees dus juist de conformerende lay-out af en de interne generator produceerde er nooit een, waardoor end-to-end onderteken- en validatie-pijplijnen toevallig groen bleven. Elke bug onthield de testsuite het tegenvoorbeeld dat de andere zou hebben blootgelegd. Versie 2.14.1 loste beide zijden in dezelfde release op: the generator vult alle vier de velden in, en de validator accepteert C >= A+B met C+D gelijk aan de bestandsgrootte

De methodologische oplossing is kruislingse validatie (cross-validation) tegen een implementatie die u niet zelf hebt geschreven. Een generator en een validator die een codebase, een auteur of zelfs maar een mentaal model van het formaat delen, kunnen het eindeloos eens zijn over een gedeeld misverstand; een onafhankelijke validator doorbreekt de symmetrie. Voer uw ondertekende uitvoer door ten minste één externe conformiteitscontrole vóór een release, en behoud een structurele zelfcontrole in de build als de snelle eerste lijn — TPdf.ValidatePades, beschreven in het artikel over handtekeningeninspectie, rapporteert de bereikfout als een benoemd probleem

var
  R: TPadesValidationResult;
begin
  Pdf.FileName := 'contract-signed.pdf';
  Pdf.Active := True;
  R := Pdf.ValidatePades;
  if ppeiByteRangeNotCoveringFile in R.Issues then
    Writeln('ByteRange dekt het bestand niet');
  if not R.IsCompliant then
    Writeln('Structurele problemen aanwezig: lever dit bestand niet uit');
end;

Een afvinklijst voor afwijzingen van ondertekende uitvoer

Wanneer een platform uw PAdES-handtekening afwijst, controleer dan de eenvoudige structurele oorzaken voordat u certificaten of vertrouwensketens verdenkt. Lees de /ByteRange-array en controleer de drie gelijkheden: eerste veld nul, tweede bereik beginnend op of na het einde van het eerste, tweede bereik eindigend exact op het einde van het bestand. Lees de vermelding /M en controleer of dit een volledige §7.9.4-datumstring is met het voorvoegsel D: en een offset-indicator. Bevestig dat het brondocument niet versleuteld was toen de handtekening werd toegevoegd — en als uw stack deze toch zonder klagen heeft ondertekend, behandel die stilte dan als een bug in de stack. Alle drie de controles worden binnen milliseconden op ruwe bytes uitgevoerd en in onze ervaring verklaren ze de afwijzing veel vaker dan welke cryptografische oorzaken dan ook

De ondertekenings-, inspectie- en validatie-aanroepen die hier worden gebruikt — SignPades, ValidatePades en de PAdES-conformiteitscontroles met hun gecorrigeerde ByteRange-berekening, datumopmaak en versleutelingspoort — worden geleverd met het PDFium Component voor Delphi, C++Builder en Lazarus