Teknisk artikel

Verifiera digitala PDF-signaturer i Delphi med HotPDF

HotPDF verifierar digitala signaturer i inlästa PDF-dokument genom tre THotPDF-metoder: GetLoadedSignatureInfo, VerifyLoadedSignature och VerifyLoadedSignatureEx, introducerade i v2.259.0. Komponenten gör en ny hashing av filens /ByteRange-segment, kontrollerar attributet CMS messageDigest och utför en RSA PKCS#1 v1.5-verifiering mot det inbäddade certifikatet för undertecknaren, och returnerar svValid när dokumentets byte är intakta

Scenariot är vardagligt men det är inte insatserna. En motpart returnerar ett signerat avtal, ditt arbetsflöde behöver arkivera det, och någon ställer den enda frågan som betyder något: är detta dokumentet vi skickade, byte för byte, signerat med det certifikat det påstår? Att svara på det i kod är verifieringssidan av signaturhistorien; signeringssidan, att bygga och bädda in PAdES-signaturer från början, behandlas i följeslagsartikeln om att skapa digitala PAdES-signaturer med HotPDF. Den här artikeln handlar om den andra riktningen: en PDF anländer redan signerad, och du vill ha ett programmatiskt utlåtande snarare än en skärmdump av Akrobats gröna bock

Hur bevisar en signerad PDF att den inte har manipulerats?

En PDF-signatur skyddar specifika byteintervall i filen, inte en abstrakt uppfattning om ”dokumentet”. ISO 32000-1 §12.8 definierar mekanismen: signaturens formulärfält bär på en ordbok vars post /Contents innehåller en CMS SignedData-behållare (RFC 5652) och vars array /ByteRange anger de exakta filområden som signaturen täcker, enligt §12.8.1. Arrayen är en lista över förskjutnings- och längdpar, i praktiken två segment: allt före den hexadecimala strängen /Contents och allt efter den. Signaturvärdet kan inte täcka sig självt, så filen hashas runt det hålet

Den designen har en konsekvens som formar hela API:et: verifieringen måste göra en hash på de ursprungliga serialiserade byten, exakt som de ligger på disken. En tolkad objektmodell är värdelös för detta, eftersom en om-serialisering av även ett oförändrat dokument ger andra byte. HotPDF verifierar därför mot källfilen som dokumentet lästes in från, eller mot en TStream med råa byte som du tillhandahåller, aldrig mot dess representation i minnet

Läsa signaturens metadata innan du verifierar något

GetLoadedSignatureInfo tolkar signaturordboken och dess CMS-behållare utan att röra en enda byte i dokumentet, vilket gör det till det rätta första anropet när du bara behöver visa vem som signerade och när. Signaturfält indexeras från 0 i formulärfältsordning, och GetLoadedSignatureFieldCount anger hur många som finns. Den returnerade THPDFSignatureInfo-posten bär med sig fältnamn, /SubFilter, undertecknarens certifikats allmänna namn (Common Name), distinkta namn för subjekt och utfärdare, serienummer, giltighetsdatum, signeringstid (från det signerade attributet när det finns, annars ordbokens post /M), namnet på digest-algoritmen samt strängarna /Reason, /Location och /ContactInfo. Dess Status-medlem förblir svNotVerified, en ärlig etikett för ”tolkad, inte kontrollerad”

var
  Pdf: THotPDF;
  Info: THPDFSignatureInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('signed-contract.pdf');
    for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
    begin
      Info := Pdf.GetLoadedSignatureInfo(I);
      Writeln('Field:     ', Info.FieldName);
      Writeln('Signer:    ', Info.SignerName);
      Writeln('Issuer:    ', Info.IssuerDN);
      Writeln('Algorithm: ', Info.HashAlgorithm);
      Writeln('SubFilter: ', Info.SubFilter);
    end;
  finally
    Pdf.Free;
  end;
end;

Köra den kryptografiska kontrollen

VerifyLoadedSignatureEx utför hela verifieringen för ett dokument som lästs in från en fil och lämnar tillbaka den ifyllda infoposten i ett enda anrop: den öppnar källfilen igen, hashar /ByteRange-segmenten med SignerInfo-digest-algoritmen, jämför resultatet med det signerade attributet messageDigest (RFC 5652 §5.4), och verifierar sedan RSA-signaturen över DER SET-omkodningen av de signerade attributen. När en signatur inte bär några signerade attribut körs RSA-kontrollen direkt över dokumentets hash istället. Signaturer som stöds är RSA PKCS#1 v1.5 med SHA-1, SHA-256, SHA-384 eller SHA-512-digester, vilket täcker subfiltren adbe.pkcs7.detached och ETSI.CAdES.detached som produceras av ledande signeringsverktyg

var
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  Status := Pdf.VerifyLoadedSignatureEx(0, Info);
  case Status of
    svValid:
      if Info.CoversWholeDocument then
        Writeln('Giltig; signaturen täcker hela filen')
      else
        Writeln('Giltig; filen utökades efter signering');
    svDigestMismatch:
      Writeln('Dokumentets byte ändrades efter signering');
    svSignatureInvalid:
      Writeln('RSA-kontrollen misslyckades över signerade attribut');
    svUnsupportedAlgorithm:
      Writeln('Icke-RSA-nyckel eller okänd digest-algoritm');
    svMalformed:
      Writeln('CMS-behållaren kunde inte tolkas');
    svSourceUnavailable:
      Writeln('Inga källbyte; använd överlagringen med TStream');
  end;
end;

Två implementeringsdetaljer är värda att känna till eftersom de förklarar misslyckanden som ser mystiska ut utifrån. För det första är kontrollen av signerade attribut petig med kodningen: inuti filen är attributen taggade som [0] IMPLICIT, men signaturen beräknades över deras DER SET OF-form, så verifieraren taggar om innan den hashar, exakt enligt kraven i RFC 5652 §5.4. En hemmabyggd verifierare som hashar byten som de visas i filen kommer att avvisa varje korrekt signerat dokument. För det andra är /Contents konventionellt nollfylld till en reserverad bytebudget, så verifieraren trunkerar DER-blobben till den faktiska längden på dess yttre SEQUENCE före tolkning; skräpliknande avslutande nollor är normala, inte korruption. Samma familj av ASN.1-tolkningsrisker, på certifikatimportsidan, behandlas i artikeln om PKCS#12- och ASN.1-säkerhetshärdning i HotPDF

Vad garanterar egentligen en giltig signatur?

svValid betyder exakt detta: de byte som anges av /ByteRange hashar till det värde som undertecknaren signerade, och signaturen verifieras under den offentliga nyckeln för certifikatet inbäddat i CMS-behållaren. Det är byte-integritet plus nyckelbindning, och ingenting mer. Certifikatkedje- och förtroendevalidering ligger uttryckligen utanför HotPDF-verifierarens räckvidd: den går inte igenom kedjan till en rot, kontrollerar inte återkallande (revocation) eller konsulterar något förtroendelager. Ett självsignerat certifikat från en angripare som signerat om ett modifierat dokument kommer att verifieras som svValid, eftersom matematiken är internt konsekvent. Huruvida undertecknaren är den de utger sig för att vara, och om någon bör lita på dem, är ett policybeslut som hör hemma i ett separat lager, vare sig det är din organisations vitlista för certifikat, Windows certifikatarkiv eller en valideringsauktoritet

Flaggan CoversWholeDocument skyddar mot ett finare glapp. En signatur täcker endast sin /ByteRange, och PDF-filens inkrementella uppdateringsmekanism tillåter att innehåll läggs till efter en signatur utan att ogiltigförklara den, vilket är avsiktligt och är hur arbetsflöden med flera signaturer fungerar. Flaggan beräknas under verifieringen och är sann endast när de två segmenten plus /Contents-luckan spänner över hela filen. När svValid kommer med CoversWholeDocument som falskt är den signerade revisionen intakt men filen innehåller senare tillägg, och om ditt arbetsflöde ska tolerera det som dessa tillägg ändrade är något du själv måste besluta om

Ströminlästa och krypterade dokument behöver sina egna källbyte

De parameterlösa VerifyLoadedSignature och VerifyLoadedSignatureEx beror på att komponenten kommer ihåg vilken fil dokumentet kom ifrån. Läs in dokumentet från en ström och det finns inget filnamn att öppna igen; detsamma gäller efter omladdningsvägen för lösenord som används för krypterade dokument, arbetsflödet som beskrivs i artikeln om AES-256 PDF-kryptering med HotPDF. I båda fallen returnerar de filbaserade överlagringarna svSourceUnavailable istället för att gissa. Lösningen är överlagringen med TStream, som låter dig lämna över de ursprungliga råa byten från var du än förvarade dem, en fil du still har, en minnesbuffert eller en databasblob

var
  Src: TFileStream;
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  // Ströminläst dokument: komponenten har inget källfilnamn,
  // så tillhandahåll de ursprungliga byten själv.
  Src := TFileStream.Create('signed-contract.pdf',
    fmOpenRead or fmShareDenyWrite);
  try
    Status := Pdf.VerifyLoadedSignature(0, Src, Info);
    if Status <> svValid then
      Writeln('Verification failed: ', Ord(Status));
  finally
    Src.Free;
  end;
end;

Rapportera vad du inte kan verifiera

En verifierare som bara känner till ”giltig” och ”ogiltig” kommer att felaktigt rapportera dokument som den helt enkelt inte förstår, så statusuppräkningen skiljer de fall som ditt användargränssnitt bör skilja på. svDigestMismatch betyder att dokumentets byte ändrades efter signering, den klassiska signalen för manipulering. svSignatureInvalid betyder att byten hashar korrekt men RSA-kontrollen misslyckades, vilket pekar på ett skadat eller förfalskat signaturvärde. svUnsupportedAlgorithm är det ärliga svaret för ECDSA-nycklar och okända digester: signaturen kan vara helt utmärkt, HotPDF kan helt enkelt inte kontrollera den, och att rapportera den som ”ogiltig” skulle smutskasta ett friskt dokument. svMalformed flaggar en CMS-behållare som inte kunde tolkas alls. För grindkontroller returnerar VerifyAllLoadedSignatures sant endast när minst ett signaturfält finns och vart och ett av dem verifieras som svValid, en bekväm enkel boolesk funktion för en arkivmottagningspipeline som vägrar att ta emot något mindre

Signaturverifiering, PAdES-signering, AES-256-kryptering och redigerings-API:et för inlästa dokument levereras alla i samma inbyggda VCL-bibliotek för Delphi och C++Builder, utan externa DLL-beroenden; hela funktionslistan och IDE-versioner som stöds finns på produktsidan för HotPDF Component