PDFium Component įdėtos CMS struktūros pradžią randa pagal jos turinio ilgį, o niekada neidamas atgal per ilgio oktetus, nes baitas prieš pat turinį yra paskutinis ilgio oktetas ir nieko nesako apie tai, kiek jų yra prieš jį. CmsHeaderStart FPdfCms.pas faile antraštės ilgį išveda iš ContentLen, ką DER padaro tiksliu, ir būtent tai neleidžia AddSignatureTimestampToCms sugadinti kiekvieno CMS, kurio sertifikatų rinkinys ilgesnis nei 127 baitai
Aptariamas nustatymas yra PAdES B-T pakėlimas. Parašo laiko žymos atributas — tas, kurį ETSI EN 319 122-1 5.3 punktas apibrėžia su OID 1.2.840.113549.1.9.16.2.14 — turi atsidurti SignerInfo struktūros unsignedAttrs, aprašyto RFC 5652 5.3 punkte, ir pagal apibrėžimą jis gali būti pridėtas tik po to, kai parašo reikšmė jau egzistuoja, nes laiko žymos tokenas skaičiuojamas būtent nuo tos reikšmės. Tad CMS jau yra sukurtas ir jau pasirašytas, kai tokenas ateina. Vieno atributo pridėjimas pakeičia SignerInfo ilgį, o tai pakeičia signerInfos SET ilgį, paskui SignedData, paskui [0] EXPLICIT apvalkalo, paskui išorinio ContentInfo. Kiekviena apimanti antraštė turi būti išvesta iš naujo, o viskas, kas tame kelyje nėra, turi būti pernešta baitas į baitą. B-LT ir B-LTA apžvalga aptaria, ką tas tokenas duoda; šis straipsnis apie keturis baitus prieš sertifikatų rinkinį, kuriuos perstatymas vis išvesdavo klaidingai
Kodėl pridedant laiko žymą reikia brolio žymos poslinkio?
Nes perstatymas pažodžiui panaudoja keturis signerInfos SET brolius, o skaitytuvas praneša, kur yra jų turinys, o ne kur yra jų žyma. TDerReader.ReadTlv grąžina žymos baitą, turinio poslinkį, turinio ilgį ir kito TLV poslinkį. Tai teisingas paviršius leidimuisi į struktūrą, bet norint nukopijuoti visą elementą reikia oketo, kuriame sėdi jo žyma, o kvietėjas teturi ContentOffs. CmsSliceTlv egzistuoja tam tarpui užpildyti: gavęs turinio poslinkį ir ilgį, jis grąžina žymą, ilgio oktetus ir turinį kaip vieną buferį, o AddSignatureTimestampToCms jį kviečia contentType OID, version INTEGER, digestAlgorithms SET, encapContentInfo SEQUENCE ir, kai yra, certificates [0] rinkiniui
// AddSignatureTimestampToCms viduje: nusileisti, paimti brolius pažodžiui
if not R.ReadTlv(Tag, CO, CL, CN) or (Tag<> Byte(asnSequence)) then
raise Exception.Create('CMS: encapContentInfo SEQUENCE expected');
SdEncapTlv:= CmsSliceTlv(CmsDer, CO, CL);
R.Position:= CN;
// neprivalomas certificates [0]
HasCerts:= (R.Position< SdEnd) and (CmsDer[R.Position]= $A0);
if HasCerts then
begin
if not R.ReadTlv(Tag, CO, CL, CN) or (Tag<> $A0) then
raise Exception.Create('CMS: certificates [0] malformed');
SdCertsTlv:= CmsSliceTlv(CmsDer, CO, CL); // žyma + ilgio oktetai + turinys
R.Position:= CN;
end;
Iš tų penkių iškirptų dalių keturios yra mažytės: vienuolikos baitų OID, trijų baitų INTEGER, septyniolikos baitų digest algoritmų rinkinys, trylikos baitų atskirtas encapContentInfo. Sertifikatų rinkinys yra tas, kuris neša pasirašytojo sertifikatą ir jo grandinę, o tikras X.509 sertifikatas siekia bent kelis šimtus baitų. Tad sertifikatų rinkinys yra vienintelė iškirpta dalis, kurios ilgio oktetai kada nors būna ilgojoje formoje, ir būtent jos senasis pagalbinis metodas nesurasdavo
Kodėl DER ilgio oktetų negalima eiti atgal?
Nes ilgio oktetų skaičius saugomas pirmame iš jų, o skaitant nuo turinio atgal pirmiausia sutinkamas paskutinis. X.690 8.1.3.4 punktas apibrėžia trumpąją formą: vienas oktetas, 8 bitas išjungtas, 7–1 bituose ilgis nuo 0 iki 127. 8.1.3.5 punktas apibrėžia ilgąją formą: pradinis oktetas su įjungtu 8 bitu, kurio 7–1 bitai nurodo tolesnių oktetų skaičių, o po jų tie oktetai neša ilgį kaip beženklį big-endian sveikąjį skaičių. Niekas toje taisyklėje nepažymi tolesnio oketo kaip tolesnio. Jo 8 bitas yra toks pat dydžio bitas kaip ir visi kiti, tad ėjimas atgal, tikrinantis Buf[ContentOffs- 1] viršutinį bitą, tikrina duomenų bitą ir paskui skaito jo septynis žemus bitus kaip skaičių
// Senasis pagalbinis metodas, gavęs tik turinio poslinkį
function CmsHeaderStart(const Buf: TBytes; ContentOffs: Integer): Integer;
var
P, LenByte, LongLen: Integer;
begin
P:= ContentOffs- 1; // patenka į PASKUTINĮ ilgio oktetą
if P< 0 then
Exit(ContentOffs);
LenByte:= Buf[P];
if (LenByte and $80)= 0 then // prasminga tik PIRMAM
Result:= P- 1
else
begin
LongLen:= LenByte and $7F;
Result:= P- LongLen- 1;
end;
end;
// 1500 baitų sertifikatų rinkinio antraštė: A0 82 05 DC
// Buf[ContentOffs- 1]= $DC -> bit 8 set, $DC and $7F= 92
// Result= ContentOffs- 94 (žyma yra ties ContentOffs- 4)
Imkime 1500 baitų sertifikatų rinkinio antraštę A0 82 05 DC. Ėjimas patenka į DC, pamato įjungtą viršutinį bitą, iš septynių žemų bitų ištraukia 92 ir praneša žymą 94 baitus prieš turinį, kai ji yra 4 baitus prieš jį. BuildSignedData sukurtoje SignedData sertifikatų rinkinio turinys sėdi vos keliasdešimt baitų nuo CMS pradžios, tad apskaičiuotas poslinkis būdavo ne tik per ankstyvas, bet ir neigiamas, o senasis kodas saugojo ContentOffs- 1 nuo nukritimo žemiau nulio, o ne savo galutinį rezultatą. CmsSliceTlv tada paimdavo devyniasdešimt keliais baitais ilgesnę iškirptą dalį, prasidedančią prieš buferį, ir perstatyta SignedData nešdavo tą dalį ten, kur turėjo būti jos sertifikatų rinkinys. Trijų oktetų ilgis, kurio paskutinis oktetas atsitiktinai nukrisdavo žemiau $80 — tarkim, A0 82 05 10 — klysdavo priešingai: ėjimas jį palaikydavo trumpąja forma ir pradėdavo iškirptą dalį ties 05, dviem baitais per vėlai ir ilgio oktetus viduje, visai be žymos. Rezultatas buvo klaidingas abiem atvejais, keitėsi tik kryptis
Ką DER garantuoja, kad išvedimas pirmyn būtų tikslus?
DER garantuoja, kad ilgio kodavimas yra gryna ilgio funkcija. X.690 10.1 punktas apriboja DER iki apibrėžtosios formos ir reikalauja mažiausio oktetų skaičiaus, o tai panaikina dvi laisves, kurias leidžia BER: neapibrėžtąją formą ir ilgosios formos ilgio paminkštinimą pradiniais nuliniais oktetais. Pagal tą taisyklę turinio ilgis, mažesnis už 128, turi lygiai vieną ilgio oktetą, o bet kuris kitas ilgis turi vieną pradinį oktetą plius lygiai tiek tolesnių oktetų, kiek reikšmingų baitų reikia ilgiui. CmsHeaderStart kvietėjas jau laiko ContentLen, nes ReadTlv ką tik jį grąžino, tad antraštės ilgį galima apskaičiuoti nepažvelgus į nė vieną buferio baitą
// Pasirodęs pagalbinis metodas: antraštę išvesti iš turinio ilgio.
// Pagal X.690 10.1 ilgio oktetai yra ContentLen funkcija
function CmsHeaderStart(const Buf: TBytes; ContentOffs, ContentLen: Integer): Integer;
var
LengthOctets, Remaining: Integer;
begin
if ContentLen< 128 then
LengthOctets:= 1 // trumpoji forma, X.690 8.1.3.4
else
begin
LengthOctets:= 1; // pradinis oktetas, X.690 8.1.3.5
Remaining:= ContentLen;
while Remaining> 0 do
begin
Inc(LengthOctets); // po vieną už kiekvieną reikšmingą baitą
Remaining:= Remaining shr 8;
end;
end;
Result:= ContentOffs- LengthOctets- 1;
if Result< 0 then
Result:= ContentOffs;
end;
Du dalykai daro tai saugiu, o ne vien įtikėtinu. Pirma, prielaida, kad įvestis yra DER, užtikrinama aukščiau: TDerReader.TryReadTlvAt, ant kurio pastatytas ReadTlv, atmeta neapibrėžtąją formą, atmeta ilgosios formos ilgį, kurio pirmas tolesnis oktetas yra nulis, ir atmeta vieną tolesnį oktetą, mažesnį už $80. TLV, kuris pasiekia CmsSliceTlv, tuos patikrinimus jau praėjo, tad BER stiliaus ne minimalus ilgis negali pasiekti išvedimo ir priversti jį meluoti. Antra, neigiamo rezultato atsarginis kelias dabar saugo tikrąjį atsakymą, o ne tarpinį. Verta pasakyti, kad skaitytuvas žymos poslinkį žinojo visą laiką: TDerTlv neša ir Offset, ir HeaderLength, ir tik keturių išvesties parametrų ReadTlv paviršius juos numeta. Juos grąžinti būtų švaresnė ilgalaikė sąsaja; pasirodžiusi pataisa tą paviršių palieka nepaliestą ir padaro pagalbinį metodą teisingą jo paties sąlygomis
Kodėl laiko žymos testai praėjo su klaida vietoje?
Nes kiekvienas bandymų sertifikatas buvo pakankamai trumpas trumpajai formai, o ėjimas atgal yra teisingas būtent tam atvejui. Tests.PadesTimestamp.pas savo pasirašytojo sertifikatą kuria su SetLength(SignerCertDer, 32) viename teste ir 64 kitame, užpildydamas baitų seka. 32 baitų sertifikatų rinkinys koduojamas kaip A0 20, o 64 baitų — kaip A0 40, po vieną ilgio oktetą kiekvienas. Ėjimas atgal nuo turinio patenka į tą vieną oktetą, jo viršutinis bitas išjungtas, nes jis yra pirmas ir vienintelis ilgio oktetas, ir pagalbinis metodas atsako teisingai dėl neteisingos priežasties. 1414 atvejų rinkinys buvo žalias, CMS su laiko žyma išsianalizavo, pirmo etapo validatorius pranešė B-T, ir kiekviena iš tų patikrų buvo paleista prieš sertifikatų rinkinį, kokio nė vienas tikras dokumentas niekada neturėjo
Bendra taisyklė yra naudingoji dalis. Kai tik koks nors kodo kelias priklauso nuo to, kaip užkoduotas ilgis, bandymų duomenys turi kirsti kodavimo ribą, o DER tai reiškia turinį, ilgesnį nei 127 baitai, kuris verčia ilgąją formą, ir idealiu atveju dar ilgesnį nei 255 baitai, kuris verčia antrą tolesnį oktetą. Ta pati disciplina galioja ir kitam tos apžvalgos atvejui, kuriame savikontrolė negalėjo pamatyti DER nukrypimo: nesurikiuotas SET OF signedAttrs viduje buvo nematomas kelionei pirmyn ir atgal tame pačiame šaltinyje dėl struktūriškai identiškos priežasties — testas tikrino tik tokias įvestis, kuriose klaidingas kodas ir teisingas kodas sutampa. Žemiau esantis metmenys kviečia iškirpimo pagalbinį metodą tiesiogiai, o tai reiškia, kad jį reikia eksportuoti iš FPdfCms.pas testiniam rinkiniui; ta pati riba pasiekiama ir per viešąjį paviršių, padavus BuildSignedData po vieną kiekvieno dydžio grandinės sertifikatą ir iš naujo išanalizavus rezultatą su laiko žyma
// Įtvirtinti ribą: iškirpta dalis per ilgosios formos antraštę turi prasidėti ties žyma
const
Lens: array[0..6] of Integer= (127, 128, 255, 256, 1500, 65535, 65536);
procedure TCmsSliceTests.HeaderStart_LongFormLengths;
var
W: TDerWriter;
Content, Tlv: TBytes;
I, Len: Integer;
begin
W:= TDerWriter.Create;
try
for I:= Low(Lens) to High(Lens) do
begin
Len:= Lens[I];
SetLength(Content, Len);
Tlv:= W.Wrap($A0, Content); // A0 7F / A0 81 80 / A0 82 05 DC ...
W.Clear;
// turinys prasideda iškart po antraštės; iškirpta dalis turi būti visas TLV
Assert.AreEqual(Length(Tlv),
Length(CmsSliceTlv(Tlv, Length(Tlv)- Len, Len)),
'slice through header of a '+ IntToStr(Len)+ '-byte content');
end;
finally
W.Free;
end;
end;
Kur perstatymas vis dar brėžia savo ribas
AddSignatureTimestampToCms rašytas tam CMS, kurį išveda BuildSignedData, ir jo ribos iš to išplaukia. Ėjimas tikisi vieno SignerInfo ir išveda tik jį, tad svetimas kelių pasirašytojų CMS grįžtų su vienu pasirašytoju; jis atpažįsta neprivalomą certificates [0] rinkinį, bet ne crls [1] rinkinį, ir CMS su tokiu krinta garsiai su signerInfos SET expected išimtimi, o ne tyliai iškerpa neteisingai. Naujasis unsignedAttrs laiko vieną atributą, tad X.690 11.6 punkto SET OF rikiavimo taisyklė patenkinama trivialiai ir jokio rikiavimo nereikia. O pasirašytoji dalis konstrukciškai lieka nepaliesta: SignerInfo priešdėlis iki parašo OCTET STRING nukopijuojamas pažodžiui, ir todėl validatorius, iš naujo maišuojantis signedAttrs, mato tuos pačius baitus iki laiko žymos pridėjimo ir po jo. Kai jis vis tiek atmeta dokumentą, priežastys paprastai būna kitur ir vertos atskiro kontrolinio sąrašo
DER skaitytuvas, rašytojas, CMS kūrėjas ir šis laiko žymos įterpimas — visa tai platinama kaip Pascal šaltinis su PDFium Delphi komponentu, ir tokios formos klaida yra argumentas už tai: kai perstatyta SignedData išeina devyniasdešimt baitų per ilga, norisi perskaityti pagalbinį metodą, kuris kirpo dalį, ir X.690 punktą, kurį jis perskaitė neteisingai, o ne juodos dėžės išklotinę