Tarkistusohjelma hylkää PAdES-allekirjoituksen lähes kaikissa Delphin PDFium-komponentilla vianmäärittämissämme tapauksissa jostain näistä kolmesta syystä: /ByteRange-taulukko sisältää edelleen nollapaikkamerkkinsä, /M-allekirjoitusaika ei ole oikein muotoiltu PDF-päivämäärämerkkijono, tai inkrementaalinen päivitys pudotti /Encrypt-merkinnän suojatusta dokumentista. Kaikki kolme tuottavat tiedostoja, jotka avautuvat ja renderöityvät hienosti, mutta epäonnistuvat heti, kun standardia noudattava tarkistusohjelma lukee allekirjoitussanakirjan
Tämän artikkelin taustalla oleva skenaario on tuskallisen tarkka. Allekirjoitat sopimuksen PAdES B-B -allekirjoitusta käsittelevän artikkelin mukaisella työnkululla, oma tarkistuskoodisi ilmoittaa allekirjoituksen olevan olemassa ja rakenteellisesti kunnossa, ja kaikki paikalliset testit näyttävät vihreää – ja sitten vastapuoli lataa tiedoston tarkistusalustalleen ja saa punaisen rastin. Virheestä ei näy mitään katseluohjelmassa, koska mikään näistä kolmesta puutteesta ei koske sivun sisältöä. Ne elävät täysin allekirjoitussanakirjassa ja tiedoston lopussa (trailer), minne tarkistusohjelmat katsovat ja katseluohjelmat yleensä eivät
Miksi PDF-allekirjoitukseni ByteRange on virheellinen?
ByteRange-hylkäys tarkoittaa lähes aina sitä, että neljää kenttää ei koskaan täytetty, eikä sitä, että alueet olisivat hienovaraisesti väärin. /ByteRange [A B C D] -taulukko ilmoittaa kaksi aluetta, tavut A:sta A+B:hen ja tavut C:stä C+D:hen, ja EN 319 142-1 §6.3 (vaatimus k) edellyttää, että ne yhdessä kattavat koko tiedoston lukuun ottamatta heksamuotoista /Contents-merkkijonoa. Lukuina: A on 0, C >= A+B, C+D vastaa tiedoston pituutta, ja B:n ja C:n välinen rako sisältää tarkalleen <...>-heksamerkkijonon – kaksi sulkumerkkitavua plus kaksi heksamerkkiä per CMS-tieto-tavu. Tarkistusohjelma, joka lukee [0 0 0 0], päättelee allekirjoituksen olevan tyhjä ja hylkää sen riippumatta siitä, kuinka oikea /Contents-osion sisäinen CMS-rakenne on
Tämän tapahtumamekanismi on hyödyllistä ymmärtää, koska sama kaava toistuu jokaisessa allekirjoitusohjelmistossa. Allekirjoittaja ei voi tietää lopullisia siirtymiä ennen kuin tiedoston asettelu on valmis, joten kirjoittaja luo taulukon kiinteänlevyisillä nollapaikkamerkeillä ja täyttää ne asettelun jälkeen. PDFium-komponentin versioissa ennen 2.14.1 tämä jälkitäyttö etsi paikkamerkkikuviota alkaen /Contents-kohdasta – mutta /ByteRange sijaitsee ennen /Contents-osiota sanakirjassa, joten haku ei löytänyt mitään ja kaikki kolme korvausta epäonnistuivat hiljaisesti. CMS-tiiviste laskettiin oikeilta alueilta, joten kryptografia oli kunnossa; näiden alueiden ilmoitus jäi nollaksi, joten jokainen standardia noudattava tarkistusohjelma hylkäsi tiedoston. PAdES B-LTA -dokumenttiaikaleimat, jotka käyttävät samaa sanakirja-asettelua, epäonnistuivat samalla tavalla – mikä on tärkeää, jos käytät pitkäaikaisia B-LT- ja B-LTA-allekirjoituksia. Versio 2.14.1 tekee jälkitäytön allekirjoitusobjektin alusta alkaen, ja korjaus varmistetaan regressiotestillä, joka jäsentää luodun tiedoston ja varmistaa alla olevan matematiikan
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;
Kaksikymmentä riviä tavallista RTL-koodia ilman kirjastokutsuja riittää paljastamaan koko virheluokan heti tiedoston luontivaiheessa. Jos otat tästä artikkelista käyttöön vain yhden tarkistuksen, valitse tämä: jäsennä oma allekirjoitettu tulosteesi ja tarkista kolme yhtäläisyyttä ennen kuin tiedosto lähtee prosessistasi
Miksi tarkistusohjelmat merkitsevät /M-allekirjoitusajan virheelliseksi?
Tiukat tarkistusohjelmat hylkäävät allekirjoitusajan, joka ei ole täydellinen PDF-päivämäärämerkkijono. Kaksi useimmiten puuttuvaa osaa ovat D:-etuliite ja UTC-siirtymämerkki. ISO 32000-1 §7.9.4 määrittelee päivämäärämuodoksi D:YYYYMMDDHHmmSS, jota seuraa siirtymä – Z UTC-ajalle tai etumerkillä varustettu +HH'mm'-suhde siihen. Paljas 20260709143000 jäsentyy päivämääräksi sallivalle lukijalle, mutta kielioppia kirjaimellisesti soveltava tarkistusohjelma näkee virheellisen merkkijonon pakollisessa kentässä ja merkitsee allekirjoituksen virheelliseksi. PDFium-komponentin versiot ennen 2.14.4 kirjoittivat TPdf.SignPades- ja SignPadesBytes-metodien /M-merkinnän juuri tässä paljaassa muodossa; versiosta 2.14.4 lähtien merkintä sisältää D:-etuliitteen ja Z-merkin, joten ilmoitettu allekirjoitusaika on muotoa D:20260709143000Z
Tähän kenttään liittyy kaksi käytännön huomautusta. Ensinnäkin kirjoita UTC-aika ja ilmaise se selvästi: aikaleima ilman siirtymämerkkiä pakottaa tarkistusohjelman arvaamaan aikavyöhykesuhteen, ja pykälä §7.9.4 käsittelee siirtymää osana muotoilua eikä valinnaisena hienoutena. Toiseksi muusta, mikä /M on – se on allekirjoittajan ilmoittama aika eli väite, ei todiste. Tarkistusohjelma tarkistaa sen muodon, ei sen totuudellisuutta; todistettava aika saadaan RFC 3161 -aikaleimasta tasolla PAdES B-T ja sitä korkeammalla. Kentän oikea muotoilu ja siihen luottaminen ovat eri päätöksiä, ja tarkistusohjelmat valvovat vain ensimmäistä
Miksi SignPades nostaa EPadesCrypto-poikkeuksen suojatussa PDF-tiedostossa?
PDFium-komponentti kieltäytyy allekirjoittamasta suojattua dokumenttia, koska vaihtoehtona on tuottaa tiedosto, jonka standardia noudattavat lukijat korruptoivat avattaessa. PAdES-allekirjoitus lisätään inkrementaalisena päivityksenä, ja ISO 32000-1 §7.5.6 vaatii päivitysosion uuden trailerin sisältävän jokaisen edellisen trailerin merkinnän paitsi /Prev-avaimen – suojatussa dokumentissa tämä sisältää myös /Encrypt-avaimen. Jos se jätetään pois, uusin trailer ilmoittaa tiedoston olevan suojaamaton, jolloin lukija yrittää tulkita suojattua runkoa selväkielisenä ja saa tulokseksi roskatietoa. Vielä pahempaa on, että allekirjoittajan lisäämät virrat ja merkkijonot pitäisi itse salata dokumentin avaimella ollakseen laillisia, mitä selväkielisen allekirjoituksen injektoija ei voi tehdä. Suojattuun tiedostoon ei ole mahdollista lisätä kelvollista selväkielistä allekirjoitusta, joten versiosta 2.14.2 alkaen TPdf.SignPades, SignPadesBytes ja InjectPadesDssMarkers nostavat EPadesCrypto-poikkeuksen sen sijaan, että ne tuottaisivat korruptoituneen tai kelvottoman tuloksen
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;
Poikkeus on oikea lopputulos, joten suunnittele työnkulku sen ympärille sen sijaan, että vain ottaisit sen kiinni ja yrittäisit uudelleen. Poista suojaus ensin omistajan oikeuksilla, allekirjoita selväkielinen kopio, ja jos jakelukanava vaatii salausta, hyväksy se, että salaus-sitten-allekirjoitus ja allekirjoitus-sitten-salaus tuottavat erilaisia tiedostoja erilaisilla tarkistustarinoilla. Selväkielisten tavujen yli laskettu allekirjoitus ei voi säilyä näiden tavujen uudelleensalauksesta, joten rehelliset vaihtoehdot ovat joko allekirjoitettu suojaamaton tiedosto tai koodin viereen dokumentoitu käytäntöpäätös
Miten kaksi virhettä voivat varmentaa toisensa
ByteRange-virhe pysyi piilossa niin pitkään, koska oma tarkistusohjelmamme oli virheellinen yhteensopivalla tavalla, ja se on tämän artikkelin tärkein opetus. Kattavuustarkistus ValidatePadesCompliance-metodissa vaati, että toinen alue alkaa tarkalleen kohdasta A+B – eli nollarako – mikä luokittelee väärin standardin asettelun, jossa raossa on /Contents-heksamerkkijono. Joten sisäinen tarkistusohjelma hylkäsi juuri standardin mukaisen asettelun ja sisäinen generaattori ei koskaan tuottanut sellaista, ja päästä päähän -allekirjoitus- ja tarkistusputket sattuivat pysymään vihreinä. Kumpikin virhe varmensi toisensa. Versio 2.14.1 korjasi molemmat puolet samassa julkaisussa: generaattori täyttää kaikki neljä kenttää ja tarkistusohjelma hyväksyy ehdon C >= A+B, jossa C+D vastaa tiedoston pituutta
Metodologinen korjaus on ristiintarkistus sellaista toteutusta vastaan, jota et ole itse kirjoittanut. Generaattori ja tarkistusohjelma, jotka jakavat saman koodikannan, kirjoittajan tai edes vain saman käsityksen tiedostomuodosta, voivat olla samaa mieltä jaetusta väärinkäsityksestä loputtomiin; riippumaton tarkistusohjelma rikkoo tämän symmetrian. Aja allekirjoitettu tulosteesi vähintään yhden ulkoisen tarkistusohjelman läpi ennen julkaisua ja pidä rakenteellinen itsetarkistus koontiversiossa nopeana ensimmäisenä linjana – TPdf.ValidatePades, jota käsitellään allekirjoitusten tarkistusta käsittelevässä artikkelissa, raportoi kattavuusvirheen nimettynä ongelmana
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;
Hylkäysten tarkistuslista allekirjoitetulle tulosteelle
Kun alusta hylkää PAdES-allekirjoituksesi, tarkista helpot rakenteelliset syyt ennen kuin epäilet varmenteita tai luottamusketjuja. Lue /ByteRange-taulukko ja varmista kolme yhtäläisyyttä: ensimmäinen kenttä nolla, toinen alue alkaa ensimmäisen päättymisen jälkeen ja toinen alue päättyy tarkalleen tiedoston loppuun. Lue /M-merkintä ja varmista, että se on täydellinen pykälän §7.9.4 mukainen päivämäärämerkkijono, jossa on D:-etuliite ja siirtymämerkki. Varmista, että lähdedokumentti ei ollut suojattu, kun allekirjoitus lisättiin – ja jos ohjelmistosi allekirjoitti sen silti valittamatta, käsittele tätä hiljaisuutta virheenä ohjelmistossa. Kaikki kolme tarkistusta ajetaan raakatavuista millisekunneissa, ja kokemuksemme mukaan ne selittävät hylkäyksen paljon useammin kuin mikään kryptografinen syy
Tässä käytetyt allekirjoitus-, tarkistus- ja validointikutsut – SignPades, ValidatePades sekä PAdES-yhteensopivuustarkistukset korjattuine ByteRange-laskentoineen, päivämäärän muotoiluineen ja salauksen estämisineen – toimitetaan Delphin, C++Builderin ja Lazaruksen PDFium-komponentin mukana