HotPDF, Delphi-PDF-komponentti, torjuu nyt allekirjoitusten kietoutumisen: versiosta v2.759.0 alkaen sekä VerifyLoadedSignatureEx että erävalidaattori vaativat, että kahden /ByteRange-sekmentin välinen rako on täsmälleen /Contents-heksamerkkijono erottimineen, ja v2.761.0 lisää metodin AddLoadedSignedSignatureField, jolloin toinen allekirjoitus voidaan liittää jo allekirjoitettuun PDF:ään siistinä inkrementaalisena revisiona. Kaksi muutosta kuuluvat yhteen, koska oikea toinen allekirjoitus on täsmälleen se asettelu, jota tiukempi verifioija odottaa
Tilanne, joka paljasti ongelman, on tavallinen. Sopimuksen allekirjoittaa toimittaja, minkä jälkeen se kulkee hyväksyjälle, jonka on allekirjoitettava häiritsemättä ensimmäistä allekirjoitusta. Toinen revisio liitetään ensimmäisen perään, sen oma /ByteRange ulottuu koko kasvaneeseen tiedostoon, ja molempien allekirjoitusten pitäisi verifioitua. Sinne pääseminen käsin merkitsi inkrementaalisen osion kirjoittamista itse, ja testifixture, joka teki juuri niin, osoittautui oppikirjamaiseksi allekirjoitusten kietoutumisen rakenteeksi, jonka vanha verifioija hyväksyi mielellään. Jos et ole aiemmin katsonut verifiointi-API:a, opas PDF-digitaalisten allekirjoitusten verifiointi HotPDF:llä kattaa perusteet, joille tämä artikkeli rakentuu
Mitä ByteRange-rakoon täsmälleen kuuluu?
Rakon on sisällettävä täydellinen /Contents-arvo eikä mitään muuta: ISO 32000-1 §12.8.3.3 sanoo, että heksamerkkijono, <- ja >-erottimineen, mahtuu täsmälleen kahden tavualueen väliseen tilaan, ja ISO 32000-2 §12.8.1 vie saman säännön eteenpäin. Taulukko 252 ja PAdES-asiakirjat sanovat vain, että tiivistys sulkee pois Contents-arvon, mikä on helppo lukea niin, että vain heksanumerot suljetaan pois. Aiemmat HotPDF-julkaisut lukivat sen noin: PreparePDFForSigning ja striimaava CMS-valmistelu tiivistivät myös kulmasulkeet, ja lähdekoodin kommentti väitti, että sulkeiden on kuuluttava kattavuuteen. Validaattorit, jotka vertaavat rakoa allekirjoitusarvoon, liputtavat kyseisen asettelun epäkelvoksi tavualueeksi, joten v2.759.0 siirtää molemmat erottimet allekirjoitettujen alueiden ulkopuolelle. Nopea itsenäinen tarkistus mille tahansa allekirjoitetulle tiedostolle on katsoa kahta tavua: tavun offsetissa ByteRange[1] on oltava < ja tavun offsetissa ByteRange[2] - 1 on oltava >
Miksi ei-tyhjän raon tarkistus ohittaa allekirjoitusten kietoutumisen?
Ei-tyhjän raon tarkistus todistaa vain, että jotain jätettiin tiivistyksen ulkopuolelle, ei sitä, mitä jätettiin, ja juuri se on koko hyökkäyspinta. /Contents-paikanpitäjä varataan tuhansilla nollanumeroilla, kun taas oikea CMS-säiliö täyttää sen harvoin. Hyökkääjä voi sulkea heksamerkkijonon aikaisin tuon nollatäytteen sisällä >:llä, kirjoittaa uusia objekteja tai väärennetyn revision loppuun varattua tilaa ja jättää tavualueet koskemattomiksi. CMS-allekirjoitus verifioituu yhä, koska jokainen allekirjoitettu tavu on muuttumaton, alueet alkavat yhä arvosta 0 ja päättyvät tiedoston kokoon, ja vanha HotPDF-verifioija raportoi tilan svValid arvolla CoversWholeDocument True. PDF-lukija puolestaan jäsentää sen, mitä tuossa allekirjoittamattomassa reiässä istuu
HotPDF kohtelee rakoa nyt datana, joka on validoitava tavu kerrallaan. Verifioija lukee raon, irrottaa erottimet, hyväksyy vain heksanumerot sekä PDF:n tyhjät merkit (sarkain, rivinsyöttö, lomakkeensyöttö, vaununpalautus, välilyönti), dekoodaa numerot ja vaatii tuloksen olevan täsmälleen yhtä kuin allekirjoitussanakirjan /Contents. Mikä tahansa muu alentaa tuloksen tilaksi svInvalidByteRange. Tarkistus ajaa sekä yksittäisen allekirjoituksen polulla että ValidateLoadedSignatureBatchissa, joka piti oman kattavuuslogiikkansa ja tarvitsi saman korjauksen. HotPDF:n tuottamat tiedostot ennen v2.759.0:a, joiden rakossa oli vain numeroita sulkeiden istuessa juuri alueiden sisäpuolella, verifioituvat yhä, joten arkistoidut asiakirjat eivät yhtäkkiä muutu punaisiksi
var
Pdf: THotPDF;
Info: THPDFSignatureInfo;
Status: THPDFSignatureVerifyStatus;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('SignedTwice.pdf');
for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
begin
Status := Pdf.VerifyLoadedSignatureEx(I, Info);
case Status of
svValid:
if Info.CoversWholeDocument then
Writeln(Info.FieldName, ': valid, covers the whole file')
else
Writeln(Info.FieldName, ': valid, ',
Info.UnsignedTrailingBytes, ' bytes appended later');
svInvalidByteRange:
Writeln(Info.FieldName, ': ByteRange gap rejected (wrapping?)');
else
Writeln(Info.FieldName, ': failed, status ', Ord(Status));
end;
end;
finally
Pdf.Free;
end;
end;
Miten allekirjoitettuun PDF:ään lisätään toinen allekirjoitus?
Avaa allekirjoitettu tiedosto metodilla BeginIncrementalUpdate, kutsu metodia AddLoadedSignedSignatureField, tallenna metodilla SaveIncrementalUpdate ja allekirjoita sitten valmisteltu tiedosto luokkafunktiolla THotPDF.SignPDFWithPFX. Ennen v2.761.0 dokumentoitu resepti, jonka mukaan kutsutaan metodia THPDFPage.AddSignedSignatureField metodin BeginIncrementalUpdate jälkeen, ei voinut toimia, koska CurrentPage on nil inkrementaalisessa tilassa eikä mikään voinut kiinnittää /V-paikanpitäjää ladatun asiakirjan kenttään. Uusi metodi luo widgetin ladatulle sivulle ja ripustaa saman paikanpitäjäsanakirjan, jota uuden asiakirjan polku käyttää, /V:n alle, joten molemmat allekirjoitusreitit jakavat yhden sarjoinnin. Ensimmäiseen allekirjoitukseen itseensä kirjoitus PAdES-digitaalisten allekirjoitusten luominen Delphissä kävelee läpi PFX-putken
var
Pdf: THotPDF;
FieldIndex: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.BeginIncrementalUpdate('Signed.pdf');
// Sivu 0, widgetin suorakulmio pisteinä, CMS:lle varattu 8192 tavua
FieldIndex := Pdf.AddLoadedSignedSignatureField(0, 320, 60, 520, 120,
'ApproverSignature', 8192);
if FieldIndex < 0 then
raise Exception.Create('Page index out of range');
Pdf.SaveIncrementalUpdate('Prepared.pdf');
finally
Pdf.Free;
end;
if not THotPDF.SignPDFWithPFX('Prepared.pdf', 'SignedTwice.pdf',
'approver.pfx', 'pfx-password') then
raise Exception.Create('Second signature failed');
end;
AddLoadedSignedSignatureField on tahallaan hiljaisempi kuin sisarensa. Muut AddLoaded*-kenttien luojat asettavat /NeedAppearances true:n AcroFormille, mikä käskee katselimen uudelleengeneroimaan kenttien ulkoasut; allekirjoitetulla asiakirjalla tuo uudelleengenerointi voi kirjoittaa uudelleen allekirjoitettua sisältöä, joten uusi metodi poistaa lipun jälleen, ellei lähde jo kantanut sitä. /SigFlags pitää alkuperäisen arvonsa OR 3 (SignaturesExist plus AppendOnly, ISO 32000-1 taulukko 219). Sinun ei myöskään tarvitse kutsua MarkDirtya sivulla: lisääminen /Annotsiin ja /Fieldsiin välittää dirty-lipun omistavalle epäsuoralle objektille, ja eksplisiittinen sivumerkintä vain raahaisi muuttumattoman sivusanakirjan uuteen revisioon, jonka revisioanalyysi raportoi sitten sivun muutoksena. Lopuksi paikanpitäjä kirjoittaa /ByteRangein ennen /Contentsia, koska paikkaaja paikantaa sentinelin /ByteRangein ensin ja etsii eteenpäin täsmäävää heksamerkkijonoa
Mitä muuttuu, kun ulkoinen allekirjoittaja tai HSM tuottaa CMS:n?
Työnkulussa ei muutu mitään, mutta offsetit tarkoittavat nyt sitä, mitä spesifikaatio sanoo. PreparePDFForSigning palauttaa kaksi nollapohjaista aluetta, joiden rako on koko /Contents-merkkijono, ja ContentsHexStart on ensimmäisen heksanumeron yhdestä alkava indeksi AnsiStringissä. Lyhyempi CMS täytetään arvolla 0 loppuun, ennen sulkevaa >:ä. Koska PreparePDFForSigning paikkaa ensin löytämänsä paikkaamattoman sentinelin, valmistele täsmälleen yksi paikanpitäjä per revisio ja suosi metodia InsertSignatureHexAt palautetuilla offseteilla hakupohjaisen InsertSignatureHexin sijaan, kun tiedostossa on jo aiempia allekirjoituksia
var
Bytes, ToSign, CmsHex: AnsiString;
R1Start, R1Len, R2Start, R2Len, HexStart, HexLen: Integer;
begin
Bytes := LoadFileAsAnsiString('Prepared.pdf'); // oma apurisi
if not THotPDF.PreparePDFForSigning(Bytes, R1Start, R1Len,
R2Start, R2Len, HexStart, HexLen) then
raise Exception.Create('No signature placeholder found');
// Rako on koko heksamerkkijono: '<' päättää alueen 1, '>' edeltää aluetta 2
Assert(Bytes[R1Start + R1Len + 1] = '<');
Assert(Bytes[R2Start] = '>');
ToSign := Copy(Bytes, R1Start + 1, R1Len) + Copy(Bytes, R2Start + 1, R2Len);
CmsHex := SignDetachedWithHsm(ToSign); // oma CMS-allekirjoittajasi, DER heksana
if not THotPDF.InsertSignatureHexAt(Bytes, HexStart, HexLen, CmsHex) then
raise Exception.Create('CMS does not fit the reserved space');
SaveAnsiStringToFile(Bytes, 'SignedTwice.pdf'); // oma apurisi
end;
Missä uusien tarkistusten rajat ovat?
Rakotarkistus sulkee yhden tietyn reiän, eikä sitä pidä myydä yli. svValid tarkoittaa yhä tavueheyttä plus avainta, joka täsmää upotettuun sertifikaattiin; luottamus kyseiseen sertifikaattiin on erillinen päätös. Rako validoidaan vain, kun verifioijalla on lähdetavut, jotka VerifyLoadedSignatureEx lukee ladatusta tiedostosta ja joiden TStream-overloadit otat itse. Kaksiallekirjoitetun tiedoston ensimmäiselle allekirjoitukselle CoversWholeDocument on oikein False, ja se, lisäsikö liitetty revisio vain allekirjoituksen vai muuttiko myös sivuja, on kysymys kirjoitukselle DocMDP, FieldMDP ja revisioanalyysi HotPDF:ssä. Huomaa myös, että liitetty PDF MAC -tarkistus vertaa offseteja <- ja >-paikkoihin, joten se hyväksyy sekä vanhan että uuden asettelun; mikä tahansa oma työkalusi, joka kovakoodaa versiota edeltävät offsetit, kaatuu ensin kohdatessaan tuoreesti allekirjoitetun tiedoston
Jos Delphi- tai C++Builder-sovelluksesi allekirjoittaa, vastallekirjoittaa tai auditoi PDF:eitä, turvallisin polku on antaa yhden kirjaston tuottaa ja verifioida sama asettelu. HotPDF, natiivi Delphi PDF -komponentti toimittaa tiukemman rakovalidoinnin, inkrementaaliset toiset allekirjoitukset ja yllä näytetyt ulkoisten allekirjoittajien koukut yhdessä komponentissa