PDFium Component začetek gnezdene strukture CMS poišče iz dolžine njene vsebine in nikoli z vzvratno hojo po oktetih dolžine, ker je bajt tik pred vsebino zadnji oktet dolžine in ne pove nič o tem, koliko jih je pred njim. CmsHeaderStart v FPdfCms.pas dolžino glave izpelje iz ContentLen, kar DER naredi natančno, in prav to preprečuje, da bi AddSignatureTimestampToCms pokvaril vsak CMS, katerega nabor potrdil je daljši od 127 bajtov
Nastavitev je nadgradnja PAdES B-T. Atribut časovnega žiga podpisa, tisti, ki ga ETSI EN 319 122-1 razdelek 5.3 definira pod OID 1.2.840.113549.1.9.16.2.14, se mora znajti v unsignedAttrs zapisa SignerInfo, opisanega v RFC 5652 razdelek 5.3, po definiciji pa ga je mogoče dodati šele, ko vrednost podpisa obstaja, ker se žeton časovnega žiga izračuna nad to vrednostjo. Ko žeton prispe, je CMS torej že zgrajen in že podpisan. Dodatek enega atributa spremeni dolžino SignerInfo, to spremeni dolžino niza SET signerInfos, nato SignedData, nato ovoja EXPLICIT [0], nato zunanjega ContentInfo. Vsako glavo, ki koga objema, je treba oddati znova, vse, kar ni na tej poti, pa je treba prenesti bajt za bajtom. Pregled B-LT in B-LTA pokriva, kaj vam žeton prinese; ta članek je o štirih bajtih pred naborom potrdil, ki jih je obnova vedno znova zamočila
Zakaj dodajanje časovnega žiga potrebuje odmik oznake sorodnika?
Ker obnova dobesedno uporabi štiri sorodnike niza SET signerInfos, bralnik pa poroča, kje je njihova vsebina, in ne, kje je njihova oznaka. TDerReader.ReadTlv vrne bajt oznake, odmik vsebine, dolžino vsebine in odmik naslednjega TLV. To je prava površina za spuščanje v strukturo, a za kopiranje celega elementa potrebujete oktet, na katerem sedi njegova oznaka, klicatelj pa drži le ContentOffs. CmsSliceTlv obstaja zato, da premosti to vrzel: iz odmika in dolžine vsebine vrne oznako, oktete dolžine in vsebino kot en vmesni pomnilnik, AddSignatureTimestampToCms pa ga pokliče za OID contentType, INTEGER version, niz SET digestAlgorithms, SEQUENCE encapContentInfo in, kadar je prisoten, nabor certificates [0]
// Znotraj AddSignatureTimestampToCms: spusti se, sorodnike odreži dobesedno
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;
// neobvezni 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); // oznaka + okteti dolžine + vsebina
R.Position:= CN;
end;
Od teh petih izrezkov so štirje drobni: OID z enajstimi bajti, INTEGER s tremi, niz algoritmov zgoščevanja s sedemnajstimi in ločen encapContentInfo s trinajstimi. Nabor potrdil je tisti, ki nosi potrdilo podpisnika in njegovo verigo, resnično potrdilo X.509 pa sega vsaj nekaj sto bajtov daleč. Nabor potrdil je zato edini izrezek, katerega okteti dolžine so kdaj v dolgi obliki, in prav tega izrezka stari pomožni element ni znal najti
Zakaj oktetov dolžine DER ni mogoče brati vzvratno?
Ker je število oktetov dolžine shranjeno v prvem od njih, pri branju od vsebine nazaj pa najprej naletite na zadnjega. X.690 razdelek 8.1.3.4 določa kratko obliko: en oktet, bit 8 ni nastavljen, biti od 7 do 1 nosijo dolžino od 0 do 127. Razdelek 8.1.3.5 določa dolgo obliko: začetni oktet z nastavljenim bitom 8, katerega biti od 7 do 1 povedo število naslednjih oktetov, tem pa sledijo tisti okteti, ki nosijo dolžino kot nepredznačeno veliko-endenjsko celo število. Nobeno pravilo ne označi naslednjega okteta kot naslednjega. Njegov bit 8 je bit velikosti kot vsak drug, zato vzvratna hoja, ki preizkusi zgornji bit Buf[ContentOffs- 1], preizkuša podatkovni bit in nato njegovih spodnjih sedem bitov prebere kot število
// Stari pomožni element, ki dobi le odmik vsebine
function CmsHeaderStart(const Buf: TBytes; ContentOffs: Integer): Integer;
var
P, LenByte, LongLen: Integer;
begin
P:= ContentOffs- 1; // pristane na ZADNJEM oktetu dolžine
if P< 0 then
Exit(ContentOffs);
LenByte:= Buf[P];
if (LenByte and $80)= 0 then // smiselno le za PRVEGA
Result:= P- 1
else
begin
LongLen:= LenByte and $7F;
Result:= P- LongLen- 1;
end;
end;
// Glava nabora potrdil s 1500 bajti: A0 82 05 DC
// Buf[ContentOffs- 1]= $DC -> bit 8 nastavljen, $DC and $7F= 92
// Result= ContentOffs- 94 (oznaka je pri ContentOffs- 4)
Vzemite glavo nabora potrdil s 1500 bajti potrdil, A0 82 05 DC. Hoja pristane na DC, vidi nastavljen zgornji bit, iz spodnjih sedmih bitov izlušči 92 in prijavi oznako 94 bajtov pred vsebino, medtem ko je 4 bajte pred njo. V SignedData, ki ga zgradi BuildSignedData, vsebina nabora potrdil sedi le nekaj deset bajtov v CMS, zato izračunani odmik ni bil le prezgodnji, ampak negativen, stara koda pa je ContentOffs- 1 varovala pred padcem pod nič in ne svojega končnega rezultata. CmsSliceTlv je nato vzel izrezek devetdeset in nekaj bajtov daljši od elementa, ki se je začel pred vmesnim pomnilnikom, in obnovljeni SignedData je ta izrezek nosil tam, kjer bi moral biti njegov nabor potrdil. Dolžina s tremi okteti, katere zadnji oktet je slučajno padel pod $80, recimo A0 82 05 10, pa je odpovedala v drugo smer: hoja ga je imela za oktet kratke oblike in izrezek začela pri 05, dva bajta prepozno in znotraj oktetov dolžine, brez vsakršne oznake. Izid je bil napačen v obe smeri, razlikovala se je le smer
Kaj DER jamči, da izpeljava naprej postane natančna?
DER jamči, da je kodiranje dolžine čista funkcija dolžine. X.690 razdelek 10.1 omeji DER na določeno obliko in zahteva najmanjše število oktetov, kar odpravi dve svoboščini, ki jih BER dovoljuje: nedoločeno obliko in oblazinjenje dolžine v dolgi obliki z vodilnimi ničelnimi okteti. Po tem pravilu ima dolžina vsebine pod 128 natanko en oktet dolžine, vsaka druga dolžina pa en začetni oktet plus natanko toliko naslednjih oktetov, kolikor pomembnih bajtov potrebuje dolžina. Klicatelj CmsHeaderStart že ima ContentLen, ker mu ga je ReadTlv pravkar vrnil, zato je dolžina glave izračunljiva, ne da bi pogledali en sam bajt vmesnega pomnilnika
// Dobavljeni pomožni element: glavo izpelje iz dolžine vsebine.
// Po X.690 10.1 so okteti dolžine funkcija ContentLen
function CmsHeaderStart(const Buf: TBytes; ContentOffs, ContentLen: Integer): Integer;
var
LengthOctets, Remaining: Integer;
begin
if ContentLen< 128 then
LengthOctets:= 1 // kratka oblika, X.690 8.1.3.4
else
begin
LengthOctets:= 1; // začetni oktet, X.690 8.1.3.5
Remaining:= ContentLen;
while Remaining> 0 do
begin
Inc(LengthOctets); // po eden na pomemben bajt
Remaining:= Remaining shr 8;
end;
end;
Result:= ContentOffs- LengthOctets- 1;
if Result< 0 then
Result:= ContentOffs;
end;
Dve podrobnosti naredita to varno in ne le verjetno. Prvič, predpostavko, da je vhod DER, uveljavlja že prejšnji sloj: TDerReader.TryReadTlvAt, na katerem je zgrajen ReadTlv, zavrne nedoločeno obliko, zavrne dolžino v dolgi obliki, katere prvi naslednji oktet je nič, in zavrne en sam naslednji oktet pod $80. TLV, ki pride do CmsSliceTlv, je te preglede že prestal, zato neminimalna dolžina v slogu BER ne more priti do izpeljave in je speljati na laž. Drugič, nadomestna rešitev za negativen rezultat zdaj varuje pravi odgovor in ne vmesne vrednosti. Velja povedati, da je bralnik odmik oznake poznal ves čas: TDerTlv nosi tako Offset kot HeaderLength, samo površina ReadTlv s štirimi izhodnimi parametri ju spusti. Vračanje teh vrednosti bi bil čistejši dolgoročni vmesnik; dobavljeni popravek to površino ohrani nedotaknjeno in pomožni element naredi pravilen po njegovih lastnih pravilih
Zakaj so preskusi časovnega žiga šli skozi s hroščem na mestu?
Ker je bilo vsako vzorčno potrdilo dovolj kratko za kratko obliko, vzvratna hoja pa je pravilna natanko v tem primeru. Tests.PadesTimestamp.pas svoje potrdilo podpisnika zgradi s SetLength(SignerCertDer, 32) v enem preskusu in s 64 v drugem, napolnjeno z naraščajočimi bajti. Nabor potrdil s 32 bajti se zakodira kot A0 20, tisti s 64 pa kot A0 40, vsak z enim samim oktetom dolžine. Vzvratna hoja od vsebine pristane na tem enem oktetu, njegov zgornji bit ni nastavljen, ker je prvi in edini oktet dolžine, in pomožni element odgovori pravilno iz napačnega razloga. Niz 1414 primerov je bil zelen, CMS s časovnim žigom se je razčlenil, preverjevalnik prve stopnje je prijavil B-T, in vsako od teh preverjanj se je izvajalo nad naborom potrdil, kakršnega ni vseboval še noben resničen dokument
Splošno pravilo je tisto koristno. Kadar koli je pot kode odvisna od tega, kako je dolžina zakodirana, mora vzorec prestopiti mejo kodiranja, pri DER pa to pomeni vsebino, daljšo od 127 bajtov, kar vsili dolgo obliko, in v idealnem primeru tudi daljšo od 255 bajtov, kar vsili še drugi naslednji oktet. Ista disciplina velja za drugi primer iz tistega pregleda, kjer samopreverjanje ni videlo odstopanja od DER: nerazvrščeni SET OF v signedAttrs je bil za obhod znotraj istega izvora neviden iz strukturno enakega razloga, preskus je preizkušal le vhode, pri katerih se napačna in pravilna koda strinjata. Spodnja skica pokliče pomožni element za rezanje neposredno, kar pomeni, da ga je treba za preskusno gradnjo izvoziti iz FPdfCms.pas; ista meja je dosegljiva prek javne površine tako, da BuildSignedData podate verižno potrdilo vsake velikosti in znova razčlenite rezultat s časovnim žigom
// Pripni mejo: izrezek skozi glavo v dolgi obliki se mora začeti pri oznaki
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;
// vsebina se začne takoj za glavo; izrezek mora biti celoten 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;
Kje obnova še vedno potegne svoje meje
AddSignatureTimestampToCms je napisan za CMS, kakršnega odda BuildSignedData, in njegove meje izhajajo iz tega. Hoja pričakuje en sam SignerInfo in ponovno odda le tega, zato bi se tuj CMS z več podpisniki vrnil z enim podpisnikom; prepozna neobvezni nabor certificates [0], ne pa nabora crls [1], in CMS, ki ga nosi, glasno odpove z izjemo signerInfos SET expected in ne tiho z napačnim rezanjem. Novi unsignedAttrs nosi en atribut, zato je pravilo razvrščanja SET OF iz X.690 razdelek 11.6 trivialno izpolnjeno in ne potrebuje razvrščanja. Podpisani del pa ostane po zgradbi nedotaknjen: predpona SignerInfo do OCTET STRING podpisa se kopira dobesedno, zato preverjevalnik, ki znova zgošča signedAttrs, vidi enake bajte pred dodanim časovnim žigom in po njem. Kadar kljub temu dokument zavrne, so vzroki običajno drugje in si zaslužijo svoj kontrolni seznam
Bralnik DER, pisec, gradnik CMS in to vbrizgavanje časovnega žiga vsi izhajajo kot izvorna koda Pascal v komponenti PDFium za Delphi, in hrošč takšne oblike je argument za to: ko je obnovljeni SignedData devetdeset bajtov predolg, želite prebrati pomožni element, ki je rezal izrezek, in razdelek X.690, ki ga je narobe prebral, ne pa sledi sklada iz črne škatle