PDFium Component menemukan awal struktur CMS bersarang dari panjang kontennya, bukan dengan menelusuri oktet panjangnya mundur, karena byte tepat sebelum kontennya adalah oktet panjang yang terakhir dan tidak mengatakan apa pun tentang berapa oktet yang mendahuluinya. CmsHeaderStart di FPdfCms.pas menurunkan panjang header dari ContentLen, yang oleh DER dibuat eksak, dan itulah yang menjaga AddSignatureTimestampToCms dari merusak setiap CMS yang set sertifikatnya lebih panjang dari 127 byte
Konteksnya adalah upgrade PAdES B-T. Atribut signature-time-stamp, yang didefinisikan ETSI EN 319 122-1 klausa 5.3 di bawah OID 1.2.840.113549.1.9.16.2.14, harus mendarat di unsignedAttrs milik SignerInfo yang dijelaskan RFC 5652 klausa 5.3, dan menurut definisinya ia hanya bisa ditambahkan setelah nilai signature-nya ada, karena timestamp token-nya dihitung atas nilai itu. Jadi CMS-nya sudah selesai dibangun dan sudah ditandatangani saat token itu tiba. Menambahkan satu atribut mengubah panjang SignerInfo, yang mengubah panjang SET signerInfos, lalu SignedData, lalu wrapper EXPLICIT [0], lalu ContentInfo terluarnya. Setiap header yang melingkupinya harus dikeluarkan ulang, dan semua yang tidak berada di jalur itu harus dibawa apa adanya. Pembahasan B-LT dan B-LTA membahas apa yang Anda dapatkan dari token itu; artikel ini membahas empat byte di depan set sertifikat yang terus salah dibangun ulang
Kenapa menambahkan timestamp butuh offset tag dari elemen sebelahnya?
Karena pembangunan ulangnya memakai ulang empat elemen sebelah SET signerInfos apa adanya, sementara reader-nya melaporkan di mana kontennya berada, bukan di mana tag-nya. TDerReader.ReadTlv mengembalikan byte tag, offset konten, panjang konten, dan offset TLV berikutnya. Itu permukaan yang tepat untuk turun ke dalam struktur, tapi untuk menyalin satu elemen utuh Anda butuh oktet tempat tag-nya berada, dan yang dipegang pemanggilnya hanya ContentOffs. CmsSliceTlv ada untuk menjembatani celah itu: diberi offset dan panjang kontennya, ia mengembalikan tag, oktet panjang, dan kontennya sebagai satu buffer, dan AddSignatureTimestampToCms memanggilnya untuk OID contentType, INTEGER version, SET digestAlgorithms, SEQUENCE encapContentInfo, dan, kalau ada, set certificates [0]
// Di dalam AddSignatureTimestampToCms: turun, potong elemen sebelahnya apa adanya
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;
// optional 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); // tag + oktet panjang + konten
R.Position:= CN;
end;
Dari kelima irisan itu, empat di antaranya kecil: OID sebelas byte, INTEGER tiga byte, set digest algorithm tujuh belas byte, encapContentInfo detached tiga belas byte. Set sertifikatnya yang membawa sertifikat penandatangan beserta rantainya, dan sertifikat X.509 sungguhan panjangnya setidaknya beberapa ratus byte. Set sertifikat karena itu satu-satunya irisan yang oktet panjangnya pernah dalam bentuk panjang, dan itulah irisan yang tidak bisa ditemukan helper lama
Kenapa oktet panjang DER tidak bisa ditelusuri mundur?
Karena jumlah oktet panjangnya disimpan di oktet pertamanya, dan membaca dari kontennya mundur berarti Anda menemui oktet terakhirnya lebih dulu. X.690 klausa 8.1.3.4 mendefinisikan bentuk pendek: satu oktet, bit 8 bersih, bit 7 sampai 1 memuat panjang 0 sampai 127. Klausa 8.1.3.5 mendefinisikan bentuk panjang: oktet awal dengan bit 8 set yang bit 7 sampai 1-nya memberi jumlah oktet berikutnya, diikuti oktet-oktet itu yang membawa panjangnya sebagai integer big-endian tanpa tanda. Tidak ada apa pun di aturan itu yang menandai sebuah oktet sebagai oktet lanjutan. Bit 8-nya adalah bit besaran seperti bit lainnya, jadi penelusuran mundur yang menguji bit teratas Buf[ContentOffs- 1] sedang menguji bit data lalu membaca tujuh bit bawahnya sebagai jumlah
// Helper lama, yang hanya diberi offset kontennya
function CmsHeaderStart(const Buf: TBytes; ContentOffs: Integer): Integer;
var
P, LenByte, LongLen: Integer;
begin
P:= ContentOffs- 1; // mendarat di oktet panjang TERAKHIR
if P< 0 then
Exit(ContentOffs);
LenByte:= Buf[P];
if (LenByte and $80)= 0 then // hanya bermakna untuk oktet PERTAMA
Result:= P- 1
else
begin
LongLen:= LenByte and $7F;
Result:= P- LongLen- 1;
end;
end;
// Header set sertifikat 1500 byte: A0 82 05 DC
// Buf[ContentOffs- 1]= $DC -> bit 8 set, $DC and $7F= 92
// Result= ContentOffs- 94 (tag-nya ada di ContentOffs- 4)
Ambil header set sertifikat yang memuat 1500 byte sertifikat, A0 82 05 DC. Penelusurannya mendarat di DC, melihat bit teratas yang set, mengekstrak 92 dari tujuh bit bawahnya lalu melaporkan tag-nya 94 byte sebelum kontennya, padahal jaraknya 4 byte. Di SignedData yang dibangun BuildSignedData, konten set sertifikatnya hanya berada beberapa puluh byte di dalam CMS-nya, jadi offset hasil hitungannya bukan cuma terlalu awal tapi negatif, dan kode lama menjagai ContentOffs- 1 agar tidak turun di bawah nol, bukan hasil akhirnya. CmsSliceTlv lalu mengambil irisan yang sembilan puluh sekian byte lebih panjang dari elemennya, dimulai sebelum buffer-nya, dan SignedData hasil bangun ulang membawa irisan itu di tempat set sertifikatnya seharusnya berada. Panjang tiga oktet yang oktet terakhirnya kebetulan di bawah $80, misalnya A0 82 05 10, gagal ke arah sebaliknya: penelusurannya menganggapnya oktet bentuk pendek dan memulai irisannya di 05, dua byte terlalu telat dan di dalam oktet panjangnya, tanpa tag sama sekali. Hasil akhirnya salah di kedua arah, cuma arahnya yang berbeda
Apa yang dijamin DER sehingga penurunan ke depan bisa eksak?
DER menjamin bahwa pengodean panjangnya adalah fungsi murni dari panjangnya. X.690 klausa 10.1 membatasi DER pada bentuk definit dan menuntut jumlah oktet minimum, yang menghapus dua kebebasan yang diberikan BER: bentuk tak tentu, dan penggemukan panjang bentuk panjang dengan oktet nol di depan. Di bawah aturan itu, panjang konten di bawah 128 punya tepat satu oktet panjang, dan panjang lainnya punya satu oktet awal plus oktet lanjutan sebanyak yang dibutuhkan byte signifikan panjangnya. Pemanggil CmsHeaderStart sudah memegang ContentLen, karena ReadTlv baru saja mengembalikannya, sehingga panjang header-nya bisa dihitung tanpa melihat satu byte pun dari buffer-nya
// Helper yang dirilis: turunkan header dari panjang kontennya.
// Di bawah X.690 10.1 oktet panjangnya adalah fungsi dari ContentLen
function CmsHeaderStart(const Buf: TBytes; ContentOffs, ContentLen: Integer): Integer;
var
LengthOctets, Remaining: Integer;
begin
if ContentLen< 128 then
LengthOctets:= 1 // bentuk pendek, X.690 8.1.3.4
else
begin
LengthOctets:= 1; // oktet awalnya, X.690 8.1.3.5
Remaining:= ContentLen;
while Remaining> 0 do
begin
Inc(LengthOctets); // satu per byte signifikan
Remaining:= Remaining shr 8;
end;
end;
Result:= ContentOffs- LengthOctets- 1;
if Result< 0 then
Result:= ContentOffs;
end;
Dua detail membuat ini aman dan bukan sekadar masuk akal. Pertama, asumsi bahwa inputnya DER ditegakkan di hulu: TDerReader.TryReadTlvAt, yang menjadi dasar ReadTlv, menolak bentuk tak tentu, menolak panjang bentuk panjang yang oktet lanjutan pertamanya nol, dan menolak oktet lanjutan tunggal yang di bawah $80. TLV yang sampai ke CmsSliceTlv sudah melewati pemeriksaan itu, jadi panjang bergaya BER yang tidak minimal tidak bisa mencapai penurunannya dan membuatnya berbohong. Kedua, fallback untuk hasil negatifnya sekarang menjagai jawaban yang sebenarnya, bukan nilai antara. Layak disebut bahwa reader-nya sudah tahu offset tag-nya sejak awal: TDerTlv membawa Offset dan HeaderLength sekaligus, dan hanya permukaan ReadTlv dengan empat parameter keluaran yang menjatuhkannya. Mengembalikannya akan menjadi antarmuka jangka panjang yang lebih bersih; perbaikan yang dirilis menjaga permukaan itu utuh dan membuat helper-nya benar atas dasar ukurannya sendiri
Kenapa uji timestamp lolos padahal bug-nya masih ada?
Karena setiap sertifikat fixture-nya cukup pendek untuk memakai bentuk pendek, dan penelusuran mundur memang benar untuk kasus itu. Tests.PadesTimestamp.pas membangun sertifikat penandatangannya dengan SetLength(SignerCertDer, 32) di satu uji dan 64 di uji lainnya, diisi ramp byte. Set sertifikat 32 byte terkode sebagai A0 20 dan yang 64 byte sebagai A0 40, masing-masing satu oktet panjang. Menelusuri mundur dari kontennya mendarat di satu oktet itu, bit teratasnya bersih karena ia oktet panjang pertama sekaligus satu-satunya, dan helper-nya menjawab dengan benar karena alasan yang salah. Suite berisi 1414 kasus itu hijau, CMS bertimestamp-nya ter-parse, validator tahap 1 melaporkan B-T, dan setiap pemeriksaan itu dijalankan atas set sertifikat yang tidak pernah dimiliki dokumen sungguhan mana pun
Aturan umumnya adalah bagian yang berguna. Kapan pun sebuah jalur kode bergantung pada bagaimana sebuah panjang dikodekan, fixture-nya harus melewati batas pengodeannya, dan untuk DER itu berarti konten lebih panjang dari 127 byte, yang memaksa bentuk panjang, dan idealnya juga lebih panjang dari 255 byte, yang memaksa oktet lanjutan kedua. Disiplin yang sama berlaku pada kasus lain di review itu yang self-verification-nya tidak bisa melihat penyimpangan DER: SET OF yang tidak terurut di signedAttrs tidak terlihat oleh round trip dari sumber yang sama karena alasan yang secara struktural identik, ujinya hanya menguji input yang di situ kode yang salah dan kode yang benar sepakat. Sketsa di bawah memanggil helper irisannya secara langsung, yang berarti ia harus diekspor dari FPdfCms.pas untuk build ujinya; batas yang sama bisa dicapai lewat permukaan publik dengan menyerahkan sertifikat rantai berukuran masing-masing ke BuildSignedData lalu mem-parse ulang hasil bertimestamp-nya
// Kunci batasnya: irisan melewati header bentuk panjang harus mulai di tag-nya
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;
// kontennya mulai tepat setelah header; irisannya harus seluruh 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;
Di mana pembangunan ulang masih punya batasnya
AddSignatureTimestampToCms ditulis untuk CMS yang dikeluarkan BuildSignedData, dan batasnya mengikuti dari situ. Penelusurannya mengharapkan satu SignerInfo dan hanya mengeluarkan ulang yang satu itu, jadi CMS multi-penandatangan dari luar akan kembali dengan satu penandatangan; ia mengenali set certificates [0] opsional tapi bukan set crls [1], dan CMS yang membawa salah satunya gagal secara berisik dengan exception signerInfos SET expected alih-alih salah memotong secara senyap. unsignedAttrs yang baru memuat satu atribut, jadi aturan pengurutan SET OF di X.690 klausa 11.6 terpenuhi secara trivial dan tidak butuh pengurutan. Dan bagian yang ditandatangani tidak tersentuh secara konstruksi: awalan SignerInfo sampai OCTET STRING signature-nya disalin apa adanya, itulah sebabnya validator yang menghitung ulang digest signedAttrs melihat byte yang sama sebelum dan sesudah timestamp-nya ditambahkan. Ketika ada yang tetap menolak dokumennya, penyebabnya biasanya di tempat lain dan layak punya daftar periksanya sendiri
Reader DER, writer-nya, pembangun CMS, dan injeksi timestamp ini semuanya dirilis sebagai sumber Pascal bersama komponen PDFium Delphi, dan bug berbentuk seperti ini adalah argumennya: ketika SignedData hasil bangun ulang keluar sembilan puluh byte terlalu panjang, Anda ingin membaca helper yang memotong irisannya dan klausa X.690 yang salah dipahaminya, bukan stack trace dari kotak hitam