Tikrinimo įrankis atmeta PAdES parašą dėl vienos iš trijų priežasčių beveik kiekvienu atveju, kurį derinom su „PDFium Component“ Delphi aplinkoje: masyvas „/ByteRange“ vis dar turi nulines laikinąsias reikšmes, pasirašymo laikas „/M“ nėra teisingo formato PDF datos eilutė arba inkrementinis atnaujinimas pašalino „/Encrypt“ įrašą iš šifruoto dokumento. Dėl visų trijų priežasčių sukuriami failai, kurie atsidaro ir atvaizduojami puikiai, tačiau sugenda tą pačią sekundę, kai standartinis tikrinimo įrankis nuskaito parašo žodyną
Scenarijus, kuris paskatino parašyti šį straipsnį, yra labai konkretus. Jūs pasirašote sutartį pagal darbo eigą iš PAdES B-B pasirašymo straipsnio, jūsų pačių tikrinimo kodas praneša, kad parašas yra ir jo struktūra teisinga, visi vietiniai testai žali — o tada kita šalis įkelia failą į savo tikrinimo platformą ir gauna raudoną kryžių. Peržiūros programoje ši klaida nematoma, nes nė vienas iš šių trijų defektų neliečia puslapio turinio. Jie gyvena tik parašo žodyne bei failo pabaigoje (trailer), kur tikrinimo įrankiai žiūri, o peržiūros programos dažniausiai ne
Kodėl mano PDF parašo „ByteRange“ yra negaliojantis?
„ByteRange“ atmetimas beveik visada reiškia, kad keturi laukai niekada nebuvo užpildyti, o ne tai, kad patys rėžiai yra šiek tiek neteisingi. Masyvas „/ByteRange [A B C D]“ deklaruoja dvi sritis: baitus nuo „A“ iki „A+B“ ir baitus nuo „C“ iki „C+D“, o standartas EN 319 142-1 §6.3 (k reikalavimas) reikalauja, kad jie kartu apimtų visą failą, išskyrus šešioliktainiu kodu užrašytą „/Contents“ eilutę. Skaičiais: „A“ yra 0, „C >= A+B“, „C+D“ yra lygus failo ilgiui, o tarpas tarp „B“ ir „C“ turi tiksliai „<...>“ šešioliktainę eilutę — du skliaustelių baitus ir du šešioliktainius simbolius kiekvienam CMS duomenų baitui. Tikrinimo įrankis, nuskaitęs „[0 0 0 0]“, daro išvadą, kad parašas neapima nieko, ir jį atmeta, nepriklausomai nuo to, kokia teisinga yra CMS struktūra lauke „/Contents“
Mechanizmą, kaip tai nutinka, verta suprasti, nes tas pats šablonas egzistuoja kiekvienoje pasirašymo sistemoje. Pasirašantis asmuo negali žinoti galutinių poslinkių, kol failas nėra suformuotas, zodžiu, rašymo funkcija sukuria masyvą su fiksuoto pločio nuliniais užpildais ir juos užpildo po failo suformavimo. „PDFium Component“ versijose iki v2.14.1 šis užpildymas ieškojo užpildo šablono pradedant nuo „/Contents“ pozicijos, tačiau „/ByteRange“ žodyne yra prieš „/Contents“, todėl paieška nieko nerado ir visi trys pakeitimai tyliai nepavyko. CMS santrauka (digest) buvo suskaičiuota teisingiems rėžiams, todėl kriptografija buvo teisinga; tačiau šių rėžių deklaracija liko nulinė, todėl kiekvienas standartinis tikrinimo įrankis atmetė failą. „PAdES B-LTA“ dokumentų laiko žymos (kurios naudoja tą patį žodyno išdėstymą) sugesdavo taip pat (tai svarbu, jei kuriate ilgalaikius B-LT ir B-LTA parašus). Versija 2.14.1 užpildo reikšmes nuo parašo objekto pradžios, ir šis pataisymas yra padengtas regresiniu testu, kuris analizuoja sukurtą failą ir patikrina žemiau pateiktą aritmetiką
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;
Dvidešimt eilučių paprasto RTL kodo, jokių išorinių bibliotekų iškvietimų, ir tai leidžia aptikti visą šią klaidų klasę jau gamybos metu. Jei norite pasilikti tik vieną patikrinimą iš šio straipsnio, pasilikite šį: išanalizuokite savo pačių pasirašytą failą ir patikrinkite tris lygybes prieš failui paliekant jūsų programos procesą
Kodėl tikrinimo įrankiai pažymi pasirašymo laiką /M kaip sugadintą?
Griežti tikrinimo įrankiai atmeta pasirašymo laiką, kuris nėra pilna PDF datos eilutė, o dvi dažniausiai trūkstamos dalys yra priešdėlis „D:“ ir UTC laiko juostos žymeklis. ISO 32000-1 §7.9.4 apibrėžia datos formatą kaip „D:YYYYMMDDHHmmSS“, po kurio seka poslinkis — „Z“ UTC laikui arba pasirašytas santykis „+HH'mm'“ su juo. Paprastas skaičius „20260709143000“ atlaidesniam skaitytuvui gali pasirodyti kaip data, tačiau tikrinimo įrankis, pažodžiui taikantis taisykles, mato sugadintą eilutę privalomo formato lauke ir atmeta parašą. „PDFium Component“ versijos iki v2.14.4 rašė „/M“ įrašą metoduose „TPdf.SignPades“ bei „SignPadesBytes“ būtent tokiu pliku formatu; nuo v2.14.4 versijos įrašas turi priešdėlį „D:“ ir „Z“ žymeklį, todėl deklaruojamas laikas atrodo taip: „D:20260709143000Z“
Dvi praktinės pastabos dėl šio lauko. Pirma, rašykite UTC laiką ir aiškiai jį nurodykite: laiko žyma be poslinkio žymeklio verčia tikrinimo įrankį spėlioti laiko juostos santykį, o §7.9.4 traktuoja poslinkį kaip formato dalį, o ne kaip pasirenkamą smulkmeną. Antra, atsiminkite, kas yra „/M“ — tai pasirašiusiojo deklaruojamas laikas, kuris yra tik teiginys, o ne įrodymas. Tikrinimo įrankis čia tikrina tik jo formatą, o ne teisingumą; įrodomas laikas gaunamas iš RFC 3161 laiko žymos „PAdES B-T“ ir aukštesniuose lygiuose. Teisingas lauko formatavimas ir pasitikėjimas juo yra skirtingi dalykai, ir tikrinimo įrankiai reikalauja tik pirmojo
Kodėl „SignPades“ sukelia „EPadesCrypto“ išimtį šifruotame PDF faile?
„PDFium Component“ atsisako pasirašyti šifruotą dokumentą, nes kitaip būtų sukurtas failas, kurį standartinės peržiūros programos sugadins atidarymo metu. PAdES parašas pridedamas kaip inkrementinis atnaujinimas, o ISO 32000-1 §7.5.6 reikalauja, kad nauja atnaujinimo sekcijos pabaiga (trailer) turėtų visus ankstesnės pabaigos įrašus, išskyrus „/Prev“ — šifruotame dokumente tai apima ir „/Encrypt“. Jei jį pašalinsime, paskutinė pabaiga deklaruos failą kaip nešifruotą, todėl standartinis skaitytuvas šifruotą kūną interpretuos kaip paprastą tekstą ir gaus šiukšles. Dar blogiau, srautai ir eilutės, kuriuos prideda pasirašytojas, patys turėtų būti šifruojami su dokumento raktu, ko paprastas parašo įterpiklis negali padaryti. Nėra galimybės pridėti galiojantį nešifruotą parašą prie šifruoto failo, todėl nuo v2.14.2 versijos metodai „TPdf.SignPades“, „SignPadesBytes“ bei „InjectPadesDssMarkers“ sukelia išimtį „EPadesCrypto“, užuot sukūrę sugadintą ar nepatikrinamą rezultatą
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;
Išimtis yra teisingas rezultatas, todėl suplanuokite darbo eigą pagal tai, užuot bandę vėl ir vėl. Pirmiausia iššifruokite dokumentą turėdami savininko teises, pasirašykite nešifruotą kopiją, o jei platinimo kanalui reikalingas šifravimas — susitaikykite su tuo, kad pasirašymas po šifravimo ir šifravimas po pasirašymo sukuria skirtingus failus su skirtingomis tikrinimo istorijomis. Parašas, suskaičiuotas nešifruotiems baitams, negali išgyventi šių baitų pakartotinio šifravimo, todėl sąžiningi pasirinkimai yra pasirašytas nešifruotas failas arba politikos sprendimas, dokumentuotas kode
Kaip dvi klaidos gali patvirtinti viena kitą
„ByteRange“ defektas išliko nematoma taip ilgai, nes mūsų pačių tikrinimo įrankis turėjo papildomą klaidą, ir tai yra pati svarbiausia pamoka šiame straipsnyje. Apimties patikra metode „ValidatePadesCompliance“ reikalauja, kad antroji sritis prasidėtų tiksliai ties „A+B“ (nulinis tarpas), kas klaidingai klasifikavo standartinį išdėstymą, kur tarpas turi „/Contents“ šešioliktainę eilutę. Todėl vidinis tikrinimo įrankis atmetė būtent standartą atitinkantį išdėstymą, o vidinis generatorius jo niekada nekūrė, ir visas pasirašymo-tikrinimo procesas išlikdavo žalias. Kiekviena klaida neleido testų rinkiniui gauti priešingo pavyzdžio, kuris būtų atskleidęs kitą klaidą. Versija 2.14.1 ištaisė abi puses tame pačiame leidime: generatorius užpildo visus keturis laukus, o tikrinimo įrankis priima „C >= A+B“, kai „C+D“ yra lygus failo ilgiui
Metodologinis pataisymas yra patikrinimas pagal kitą, ne jūsų pačių parašytą realizaciją. Generatorius ir tikrinimo įrankis, kurie dalijasi tuo pačiu kodu, autoriumi ar net tiesiog supratimu apie formatą, gali sutarti dėl bendro klaidingo supratimo neribotą laiką; nepriklausomas tikrinimo įrankis sugriauna šią simetriją. Prieš išleisdami programą, praleiskite pasirašytą failą per bent vieną išorinį tikrinimo įrankį ir išlaikykite struktūrinį patikrinimą savo programoje — metodas „TPdf.ValidatePades“, aprašytas parašų tikrinimo straipsnyje, praneša apie „ByteRange“ problemą kaip apie žinomą klaidą
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;
Pasirašyto failo atmetimo kontrolinis sąrašas
Kai sistema atmeta jūsų PAdES parašą, pirmiausia patikrinkite paprastas struktūrines priežastis, užuot įtarę sertifikatus ar pasitikėjimo grandines. Nustatykite masyvą „/ByteRange“ ir patikrinkite tris lygybes: pirmasis laukas nulinis, antroji sritis prasideda pirmosios pabaigoje arba už jos, antroji sritis baigiasi tiksliai failo pabaigoje. Nustatykite įrašą „/M“ ir patikrinkite, ar tai yra pilna §7.9.4 datos eilutė su priešdėliu „D:“ ir laiko juostos žymekliu. Įsitikinkite, kad pradinis dokumentas nebuvo šifruotas, kai buvo pridedamas parašas — o jei jūsų sistema vis tiek jį pasirašė be jokių problemų, laikykite šią tylą pačios sistemos klaida. Visi trys patikrinimai atliekami su paprastais baitais per milisekundes, ir, mūsų patirtimi, jie paaiškina atmetimą žymiai dažniau nei bet kokia kriptografinė priežastis
Pasirašymo, tikrinimo bei patvirtinimo iškvietimai — „SignPades“, „ValidatePades“ ir PAdES atitikties patikros su pataisyta „ByteRange“ aritmetika, datos formatavimu bei šifravimo valdymu — tiekiami „PDFium Component“ komponente, skirtame Delphi, C++Builder ir Lazarus