Tehnični članak

Preverjanje digitalnih podpisov PDF v Delphiju s HotPDF

HotPDF preverja digitalne podpise v naloženih dokumentih PDF prek treh metod THotPDF: GetLoadedSignatureInfo, VerifyLoadedSignature in VerifyLoadedSignatureEx, uvedenih v različici v2.259.0. Komponenta ponovno zgošči segmente /ByteRange izvirne datoteke, preveri atribut CMS messageDigest in izvede preverjanje RSA PKCS#1 v1.5 v primerjavi z vgrajenim potrdilom podpisnika ter vrne svValid, ko so bajti dokumenta nedotaknjeni

Scenarij je vsakdanji, vložki pa ne. Nasprotna stranka vrne podpisano pogodbo, vaš delovni tok jo mora shraniti, nekdo pa zastavi edino vprašanje, ki je pomembno: ali je to dokument, ki smo ga poslali, bajt za bajtom, in ali je podpisan s potrdilom, ki ga navaja? Odgovor na to v kodi je stran preverjanja v zgodbi o podpisih; stran podpisovanja, torej gradnja in vgrajevanje podpisov PAdES, pa je obravnavana v spremljevalnem članku o ustvarjanju digitalnih podpisov PAdES s HotPDF. Ta članek govori o nasprotni smeri: PDF prispe že podpisan, vi pa želite programsko razsodbo namesto posnetka zaslona Acrobatove zelene kljukice

Kako podpisan PDF dokazuje, da vanj ni bilo poseženo?

Podpis PDF varuje določene bajtne obsege datoteke in ne nekega abstraktnega koncepta "dokumenta". ISO 32000-1 §12.8 določa mehanizem: polje obrazca podpisa vsebuje slovar, katerega vnos /Contents vsebuje vsebnik CMS SignedData (RFC 5652), njegovo polje /ByteRange pa navaja natančna območja datoteke, ki jih podpis pokriva, v skladu s §12.8.1. Polje je seznam parov odmikov in dolžin, v praksi dveh segmentov: vse pred heksadecimalnim nizom /Contents in vse po njem. Vrednost podpisa ne more pokrivati same sebe, zato se datoteka zgošča okoli te luknje

Ta zasnova ima posledico, ki oblikuje celoten API: preverjanje mora zgoščevati izvirne serializirane bajte natanko tako, kot ležijo na disku. Analiziran model objekta je za to neuporaben, saj ponovna serializacija celo nespremenjenega dokumenta ustvari drugačne bajte. HotPDF zato preverja glede na izvorno datoteko, iz katere je bil dokument naložen, ali glede na tok TStream surovih bajtov, ki jih zagotovite sami, nikoli pa glede na njegovo predstavitev v pomnilniku

Diagram, kako HotPDF v Delphi preveri podpisan PDF s ponovnim razprševanjem obeh razponov ByteRange izvorne datoteke okoli luknje Contents, medtem ko se razčlenjen model v pomnilniku nikoli ne razprši, ker ponovna serializacija spremeni bajte
Overjanje zgosti oba segmenta ByteRange izvornih bajtov točno tako, kot so serializirani; razčlenjen model v pomnilniku je neuporaben, ker ponovna serializacija tudi nespremenjenega dokumenta proizvede drugačne bajte

Branje metapodatkov podpisa pred preverjanjem česar koli

Klic GetLoadedSignatureInfo analizira slovar podpisa in njegov vsebnik CMS, ne da bi se dotaknil enega samega bajta dokumenta, zaradi česar je to pravi prvi klic, ko želite le prikazati, kdo je podpisal in kdaj. Polja podpisov so indeksirana od 0 v vrstnem redu polj obrazca, klic GetLoadedSignatureFieldCount pa vam pove, koliko jih obstaja. Vrnjeni zapis THPDFSignatureInfo vsebuje ime polja, /SubFilter, splošno ime potrdila podpisnika, razločevalna imena prejemnika in izdajatelja (subject in issuer distinguished names), serijsko številko, datume veljavnosti, čas podpisovanja (iz podpisanega atributa, ko je prisoten, sicer iz vnosa /M v slovarju), ime algoritma zgoščevanja ter nize /Reason, /Location in /ContactInfo. Njegov član Status ostane svNotVerified, kar je poštena oznaka za "analizirano, ni preverjeno"

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;

Izvajanje kriptografskega preverjanja

Klic VerifyLoadedSignatureEx izvede popolno preverjanje za dokument, naložen iz datoteke, in vrne izpolnjen zapis s podatki v enem klicu: ponovno odpre izvorno datoteko, zgošči segmente /ByteRange z algoritmom zgoščevanja SignerInfo, primerja rezultat s podpisanim atributom messageDigest (RFC 5652 §5.4) in nato izvede preverjanje RSA nad ponovnim kodiranjem DER SET podpisanih atributov. Kadar podpis ne vsebuje podpisanih atributov, se preverjanje RSA izvede neposredno nad zgoščeno vrednostjo dokumenta. Podprti podpisi so RSA PKCS#1 v1.5 z zgoščenimi vrednostmi SHA-1, SHA-256, SHA-384 ali SHA-512, kar pokriva podfiltre adbe.pkcs7.detached in ETSI.CAdES.detached, ki jih ustvarijo glavna orodja za podpisovanje

Cevovod VerifyLoadedSignatureEx v HotPDF za Delphi: ponovno odpri izvorno datoteko, razprši razpone ByteRange, primerjaj s podpisanim atributom CMS messageDigest, nato preveri z RSA PKCS#1 v1.5, kar izda svValid, svDigestMismatch ali svSignatureInvalid
VerifyLoadedSignatureEx znova zgosti segmente ByteRange, primerja jih s podpisanim atributom messageDigest in RSA-overi DER SET podpisanih atributov, preden poroča svValid ali določeno stanje napake
var
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  Status := Pdf.VerifyLoadedSignatureEx(0, Info);
  case Status of
    svValid:
      if Info.CoversWholeDocument then
        Writeln('Valid; signature covers the whole file')
      else
        Writeln('Valid; file was extended after signing');
    svDigestMismatch:
      Writeln('Document bytes changed after signing');
    svSignatureInvalid:
      Writeln('RSA check failed over signed attributes');
    svUnsupportedAlgorithm:
      Writeln('Non-RSA key or unknown digest algorithm');
    svMalformed:
      Writeln('CMS container could not be parsed');
    svSourceUnavailable:
      Writeln('No source bytes; use the TStream overload');
  end;
end;

Dve podrobnosti implementacije sta vredni omembe, saj pojasnjujeta neuspehe, ki so od zunaj videti skrivnostni. Prvič, preverjanje podpisanih atributov je izbirčno glede kodiranja: znotraj datoteke so atributi označeni kot [0] IMPLICIT, vendar je bil podpis izračunan nad njihovo obliko DER SET OF, zato preverjalnik pred zgoščevanjem ponovno označi elemente, natanko tako, kot zahteva RFC 5652 §5.4. Doma narejen preverjalnik, ki zgošča bajte, kot se pojavljajo v datoteki, bo zavrnil vsak pravilno podpisan dokument. Drugič, polje /Contents je običajno dopolnjeno z ničlami do rezerviranega proračuna bajtov, zato preverjalnik pred analiziranjem skrajša blok DER na dejansko dolžino njegove zunanje SEQUENCE; odvečne končne ničle so običajne in ne pomenijo poškodbe. Enaka družina nevarnosti pri analizi ASN.1 na strani uvoza potrdil je tema članka o varnostnem utrjevanju PKCS#12 in ASN.1 v HotPDF

Kaj veljaven podpis dejansko zagotavlja?

Status svValid pomeni natanko to: bajti, navedeni v /ByteRange, se zgoščijo v vrednost, ki jo je podpisnik podpisal, podpis pa se preveri z javnim ključem potrdila, vgrajenega v vsebniku CMS. To je celovitost bajtov in vezava ključa ter nič drugega. Preverjanje verige potrdil in zaupanja sta izrecno izven obsega preverjalnika HotPDF: ne prehodi verige do korena, ne preveri preklica in ne preveri nobene shrambe zaupanja. Samopodpisano potrdilo napadalca, ki je ponovno podpisal spremenjen dokument, se bo preverilo kot svValid, ker je matematika interno skladna. Ali je podpisnik tisti, za katerega se izdaja, in ali bi mu morali zaupati, je odločitev o politiki, ki pripada ločeni plasti, najsi bo to seznam dovoljenih potrdil vaše organizacije, shramba potrdil Windows ali organ za potrjevanje

Zastavica CoversWholeDocument varuje pred subtilnejšo vrzeljo. Podpis vedno pokriva le svoj /ByteRange, PDF-jev mehanizem inkrementalnih posodobitev pa omogoča dodajanje vsebine po podpisu brez njegove razveljavitve, kar je namerno in omogoča delovanje delovnih tokov z več podpisi. Zastavica se izračuna med preverjanjem in je resnična le, ko dva segmenta in vrzel /Contents obsegajo celotno datoteko. Če status svValid pride z zastavico CoversWholeDocument, ki je napačna (false), je podpisana revizija nedotaknjena, vendar datoteka vsebuje kasnejše dodatke, vaš delovni tok pa se mora odločiti, ali bo te dodatke toleriral

Diagram obsega obljub svValid pri preverjanju podpisov HotPDF: celovitost bajtov ByteRange in vezava ključa, medtem ko prehoj verige, razveljavitev in shrambe zaupanja ostanejo izven obsega, CoversWholeDocument pa označi pripete prirastne posodobitve
svValid pomeni bajtno celovitost plus vez ključa in nič več; obhod verige, preklic in shrambe zaupanja pripadajo ločeni plasti politik, CoversWholeDocument=false pa signalizira bajte, dodane po podpisu

Dokumenti, naloženi iz tokov in šifrirani dokumenti, potrebujejo lastne izvorne bajte

Klica VerifyLoadedSignature in VerifyLoadedSignatureEx brez parametrov sta odvisna od tega, ali si komponenta zapomni, iz katere datoteke je dokument prišel. Naložite dokument iz toka in ime datoteke za ponovno odpiranje ne bo na voljo; enako velja po poti ponovnega nalaganja z geslom, ki se uporablja za šifrirane dokumente, kar je delovni tok, opisan v članku o šifriranju PDF z AES-256 v HotPDF. V obeh primerih preobremenitve, podprte z datotekami, vrnejo svSourceUnavailable namesto ugibanja. Rešitev je preobremenitev TStream, ki vam omogoča, da predate izvirne surove bajte od tam, kjer ste jih shranili — datoteke, ki jo še imate, pomnilniškega odložišča ali podatkovne baze

var
  Src: TFileStream;
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  // Dokument, naložen iz toka: komponenta ne hrani izvornega
  // imena datoteke, zato izvirne bajte posredujte sami.
  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;

Poročanje o tistem, česar ne morete preveriti

Preverjalnik, ki pozna le "veljavno" in "neveljavno", bo napačno poročal o dokumentih, ki jih preprosto ne razume, zato enumeracija stanja ločuje primere, ki jih mora vaš uporabniški vmesnik razlikovati. svDigestMismatch pomeni, da so se bajti dokumenta po podpisovanju spremenili, kar je klasičen znak nepooblaščenega spreminjanja. svSignatureInvalid pomeni, da se bajti pravilno zgoščijo, vendar je preverjanje RSA spodletelo, kar kaže na poškodovano ali ponarejeno vrednost podpisa. svUnsupportedAlgorithm je pošten odgovor za ključe ECDSA in neznane algoritme zgoščevanja: podpis je lahko povsem dober, HotPDF ga preprosto ne more preveriti, poročanje o tem kot o "neveljavnem" pa bi škodovalo zdravemu dokumentu. svMalformed označuje vsebnik CMS, ki ga sploh ni bilo mogoče analizirati. Za preverjanja tipa vrat funkcija VerifyAllLoadedSignatures vrne true le, ko obstaja vsaj eno polje podpisa in se vsako od njih preveri kot svValid, kar je priročna enojna logična vrednost (boolean) za cevovod za prevzem arhivov, ki zavrača vse ostalo

Preverjanje podpisov, podpisovanje PAdES, šifriranje AES-256 in urejanje naloženih dokumentov so na voljo v isti izvorni knjižnici VCL za Delphi in C++Builder brez zunanjih odvisnosti od datotek DLL; celoten seznam funkcij in podprtih različic IDE je na strani izdelka HotPDF Delphi Component