Teknik Makale

DER Uzunluk Oktetlerinde Geriye Yürümeyin: PDFium Component

PDFium Component, iç içe bir CMS yapısının başlangıcını içerik uzunluğundan buluyor, uzunluk oktetleri üzerinde geriye doğru yürüyerek asla değil; çünkü içerikten hemen önceki bayt son uzunluk oktetidir ve ondan önce kaç oktet geldiği hakkında hiçbir şey söylemez. FPdfCms.pas içindeki CmsHeaderStart bunun yerine başlık uzunluğunu ContentLen değerinden türetiyor; DER bunu kesin yapar ve AddSignatureTimestampToCmsi sertifika kümesi 127 bayttan uzun olan her CMS'i bozmaktan alıkoyan da budur

Söz konusu ayar PAdES B-T yükseltmesi. ETSI EN 319 122-1 madde 5.3'ün 1.2.840.113549.1.9.16.2.14 OID'i altında tanımladığı signature-time-stamp özniteliği, RFC 5652 madde 5.3'te tarif edilen SignerInfo öğesinin unsignedAttrs alanına yerleşmek zorundadır ve tanımı gereği ancak imza değeri oluştuktan sonra eklenebilir, çünkü timestamp token'ı o değer üzerinden hesaplanır. Yani token geldiğinde CMS zaten kurulmuş ve imzalanmıştır. Tek bir öznitelik eklemek SignerInfo uzunluğunu, o da signerInfos SET'inin, sonra SignedDatanın, sonra [0] EXPLICIT sarmalayıcısının, en sonunda dıştaki ContentInfonun uzunluğunu değiştirir. Bu yoldaki her kapsayıcı başlık yeniden yazılmak, bu yolda olmayan her şey ise bayt bayt taşınmak zorundadır. B-LT ve B-LTA yürüyüşü token'ın size ne kazandırdığını ele alıyor; bu makale ise yeniden kurmanın sürekli yanlış yaptığı, sertifika kümesinin önündeki dört bayt hakkında

Bir timestamp eklemek neden kardeş bir öğenin tag offset'ine ihtiyaç duyuyor?

Çünkü yeniden kurma, signerInfos SET'inin dört kardeşini olduğu gibi yeniden kullanıyor ve reader onların içeriğinin nerede olduğunu bildiriyor, tag'lerinin nerede olduğunu değil. TDerReader.ReadTlv tag baytını, içerik offset'ini, içerik uzunluğunu ve sonraki TLV'nin offset'ini geri verir. Bu, bir yapının içine inmek için doğru yüzey; ama bir öğeyi bütün olarak kopyalamak için tag'inin durduğu oktete ihtiyacınız var ve çağıranın elinde yalnızca ContentOffs var. CmsSliceTlv bu boşluğu kapatmak için var: bir içerik offset'i ve uzunluğu verildiğinde tag'i, uzunluk oktetlerini ve içeriği tek bir tampon olarak döndürür; AddSignatureTimestampToCms de onu contentType OID'i, version INTEGER'ı, digestAlgorithms SET'i, encapContentInfo SEQUENCE'i ve varsa certificates [0] kümesi için çağırır

// AddSignatureTimestampToCms içinde: içeri in, kardeşleri olduğu gibi dilimle
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;
// isteğe bağlı 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 + uzunluk oktetleri + içerik
  R.Position:= CN;
end;

Bu beş dilimden dördü küçücüktür: on bir baytlık bir OID, üç baytlık bir INTEGER, on yedi baytlık bir digest algoritması kümesi, on üç baytlık detached bir encapContentInfo. İmzalayan sertifikayı ve zincirini taşıyan ise sertifika kümesidir ve gerçek bir X.509 sertifikası en azından birkaç yüz bayt tutar. Bu yüzden uzunluk oktetleri uzun forma geçen tek dilim sertifika kümesidir ve eski yardımcının bulamadığı dilim de odur

Bir CMS'te PAdES B-T timestamp eklemenin neleri yeniden kurduğu: CmsSliceTlv contentType, version, digestAlgorithms, encapContentInfo ve certificates kümesini bayt bayt kopyalar, kısa uzunluk formunun dışına çıkacak kadar uzun olan tek dilim sertifika kümesidir ve SignerInfo'dan ContentInfo'ya kadar her kapsayıcı başlık yeniden yazılır
İmzalanan bölgeye kuruluşu gereği dokunulmaz çünkü SignerInfo öneki imza OCTET STRING'ine kadar olduğu gibi kopyalanır; signedAttrs değerini yeniden hashleyen bir doğrulayıcı timestamp yerleşmeden önce ve sonra aynı baytları görür

DER uzunluk oktetleri neden geriye doğru yürünemez?

Çünkü uzunluk oktetlerinin sayısı onların ilkinde saklanır ve içerikten geriye doğru okuduğunuzda önce sonuncusuyla karşılaşırsınız. X.690 madde 8.1.3.4 kısa formu tanımlar: tek oktet, bit 8 sıfır, 7'den 1'e kadar bitler 0 ile 127 arasında bir uzunluk taşır. Madde 8.1.3.5 uzun formu tanımlar: bit 8'i ayarlı bir başlangıç oktetinin 7-1 bitleri sonraki oktetlerin sayısını verir, ardından gelen o oktetler uzunluğu işaretsiz big-endian bir tam sayı olarak taşır. Kuralda hiçbir şey bir sonraki okteti sonraki olarak işaretlemez. Onun bit 8'i de herhangi bir büyüklük bitidir; bu yüzden Buf[ContentOffs- 1] ifadesinin üst bitini test eden geriye yürüyüş, bir veri bitini test edip sonra onun alt yedi bitini sayı olarak okur

// Eski yardımcı, elinde yalnızca içerik offset'i varken
function CmsHeaderStart(const Buf: TBytes; ContentOffs: Integer): Integer;
var
  P, LenByte, LongLen: Integer;
begin
  P:= ContentOffs- 1;            // SON uzunluk oktetine düşer
  if P< 0 then
    Exit(ContentOffs);
  LenByte:= Buf[P];
  if (LenByte and $80)= 0 then   // yalnızca İLK oktet için anlamlı
    Result:= P- 1
  else
  begin
    LongLen:= LenByte and $7F;
    Result:= P- LongLen- 1;
  end;
end;

// 1500 baytlık bir sertifika kümesinin başlığı:  A0 82 05 DC
//   Buf[ContentOffs- 1]= $DC  -> bit 8 ayarlı, $DC and $7F= 92
//   Result= ContentOffs- 94     (tag ContentOffs- 4 konumunda)

1500 bayt sertifika tutan bir sertifika kümesinin başlığını alın: A0 82 05 DC. Yürüyüş DC üzerine düşer, üst bitin ayarlı olduğunu görür, alt yedi bitten 92 çıkarır ve tag'i içerikten 94 bayt önce bildirir; oysa tag 4 bayt öncededir. BuildSignedData ile kurulmuş bir SignedData içinde sertifika kümesinin içeriği CMS'in yalnızca birkaç düzine bayt içindedir, yani hesaplanan offset sadece erken değil negatifti ve eski kod ContentOffs- 1 ifadesini sıfırın altına düşmeye karşı koruyordu, nihai sonucunu değil. Ardından CmsSliceTlv öğeden doksan küsur bayt uzun, tamponun öncesinden başlayan bir dilim aldı ve yeniden kurulan SignedData bu dilimi sertifika kümesinin olması gereken yerde taşıdı. Son okteti $80 altına düşen üç oktetlik bir uzunluk, diyelim A0 82 05 10, bu kez ters yönde başarısız oldu: yürüyüş onu kısa form bir oktet sandı ve dilimi 05 konumunda, iki bayt geç ve uzunluk oktetlerinin içinde, hiç tag olmadan başlattı. Sonuç her iki durumda da yanlıştı, yalnızca yönü değişiyordu

DER uzunluk oktetlerinin neden geriye doğru yürünemeyeceği: A0 82 05 DC dizisini sondan okumak son oktet DC üzerine düşer, onun ayarlı üst biti 92 gibi uydurma bir sayı verir ve tag'i 94 oktet erken konumlandırır; A0 82 05 10 ise ters yönde başarısız olup dilimi uzunluk oktetlerinin içinde iki oktet geç başlatır
Eski CmsHeaderStart ara çıkarma işlemini koruyordu, nihai sonucunu değil; bu yüzden bir dilim tamponun öncesinden bile başlayabiliyordu ve yeniden kurulan SignedData o dilimi sertifika kümesinin yerinde taşıyordu

İleri türetmeyi kesin yapan DER garantisi nedir?

DER, uzunluk kodlamasının uzunluğun saf bir fonksiyonu olmasını garanti eder. X.690 madde 10.1 DER'i definite forma kısıtlar ve en az sayıda oktet ister; bu, BER'in izin verdiği iki serbestliği ortadan kaldırır: indefinite form ve uzun form bir uzunluğu baştaki sıfır oktetlerle doldurma. Bu kural altında 128'in altındaki bir içerik uzunluğu tam olarak bir uzunluk oktetine sahiptir; diğer her uzunluk ise bir başlangıç okteti artı uzunluğun anlamlı bayt ihtiyacı kadar sonraki oktetten oluşur. CmsHeaderStart çağıranı ContentLen değerini zaten elinde tutar, çünkü ReadTlv onu az önce döndürmüştür; yani başlık uzunluğu tamponun tek bir baytına bakmadan hesaplanabilir

DER'in garanti ettiği ileri türetme: 127 içerik uzunluğu A0 7F, 128 A0 81 80, 255 A0 81 FF, 256 A0 82 01 00 ve 1500 A0 82 05 DC olarak kodlanır; yani başlık uzunluğu tek başına ContentLen değerinden çıkar ve TryReadTlvAt minimal olmayan her BER formunu zaten reddetmiştir
32 ve 64 baytlık sertifikalarla kurulan fixture değerleri kısa formun içinde kalıyordu ve geriye yürüyüş orada yanlış nedenden dolayı doğru yanıt veriyordu; bu yüzden sınır takımı artık 127, 128, 255 ve 256 baytı geçiyor
// Yayınlanan yardımcı: başlığı içerik uzunluğundan türet.
// X.690 10.1 altında uzunluk oktetleri ContentLen değerinin fonksiyonudur
function CmsHeaderStart(const Buf: TBytes; ContentOffs, ContentLen: Integer): Integer;
var
  LengthOctets, Remaining: Integer;
begin
  if ContentLen< 128 then
    LengthOctets:= 1                 // kısa form, X.690 8.1.3.4
  else
  begin
    LengthOctets:= 1;                // başlangıç okteti, X.690 8.1.3.5
    Remaining:= ContentLen;
    while Remaining> 0 do
    begin
      Inc(LengthOctets);             // anlamlı bayt başına bir
      Remaining:= Remaining shr 8;
    end;
  end;
  Result:= ContentOffs- LengthOctets- 1;
  if Result< 0 then
    Result:= ContentOffs;
end;

Bunu makul olmaktan çıkarıp güvenli yapan iki ayrıntı var. Birincisi, girdinin DER olduğu varsayımı yukarı akışta zorlanıyor: ReadTlvin üzerine kurulduğu TDerReader.TryReadTlvAt, indefinite formu, ilk sonraki okteti sıfır olan uzun form bir uzunluğu ve $80 altında tek sonraki okteti reddediyor. CmsSliceTlve ulaşan bir TLV bu kontrolleri zaten geçmiştir, yani BER tarzı minimal olmayan bir uzunluk türetmeye ulaşıp onu yalancı yapamaz. İkincisi, negatif sonuç için yedek artık bir ara değeri değil gerçek sonucu koruyor. Şunu da söylemek gerekir: reader tag offset'ini baştan beri biliyordu; TDerTlv hem Offset hem HeaderLength taşır ve bunları düşüren tek şey dört çıkış parametreli ReadTlv yüzeyidir. Onları döndürmek uzun vadede daha temiz bir arayüz olurdu; yayınlanan düzeltme o yüzeyi olduğu gibi koruyup yardımcıyı kendi koşullarında doğru yapıyor

Hata yerindeyken timestamp testleri neden geçiyordu?

Çünkü her test sertifikası kısa formu kullanacak kadar kısaydı ve geriye yürüyüş tam da o durumda doğrudur. Tests.PadesTimestamp.pas imzalayan sertifikasını bir testte SetLength(SignerCertDer, 32), diğerinde 64 ile kuruyor ve bir bayt rampasıyla dolduruyor. 32 baytlık bir sertifika kümesi A0 20, 64 baytlık olan A0 40 olarak kodlanır; her biri tek bir uzunluk oktetidir. İçerikten geriye yürümek o tek oktete düşer, ilk ve tek uzunluk okteti olduğu için üst biti sıfırdır ve yardımcı yanlış nedenden dolayı doğru yanıt verir. 1414 vakalık takım yeşildi, timestamp eklenmiş CMS ayrıştırılıyordu, 1. aşama doğrulayıcı B-T bildiriyordu ve bu kontrollerin hepsi gerçek hiçbir belgenin içermediği bir sertifika kümesi üzerinde çalıştırılmıştı

Asıl faydalı kısım genel kural. Bir kod yolu uzunluğun nasıl kodlandığına bağlı olduğunda, test verisi kodlama sınırını geçmek zorundadır; DER için bu, uzun formu zorlayan 127 bayttan uzun içerik ve ideal olarak ikinci bir sonraki okteti zorlayan 255 bayttan uzun içerik demektir. Aynı disiplin, o incelemedeki öz doğrulamanın bir DER sapmasını göremediği diğer durum için de geçerli: signedAttrs içindeki sıralanmamış SET OF yapısal olarak benzer bir nedenden aynı kaynaklı bir round-trip'e görünmezdi, çünkü test yalnızca yanlış kodun ve doğru kodun hemfikir olduğu girdileri çalıştırıyordu. Aşağıdaki taslak dilim yardımcısını doğrudan çağırıyor, yani test derlemesi için onu FPdfCms.pas dosyasından dışa aktarmak gerekiyor; aynı sınıra genel yüzeyden de ulaşılabilir: BuildSignedData fonksiyonuna her boyuttan bir zincir sertifikası verip timestamp eklenmiş sonucu yeniden ayrıştırın

// Sınırı sabitle: uzun form başlık üzerinden bir dilim tag'de başlamalı
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;
      // içerik başlıktan hemen sonra başlar; dilim TLV'nin tamamı olmalı
      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;

Yeniden kurmanın sınırlarını hâlâ nerede çizdiği

AddSignatureTimestampToCms, BuildSignedDataın ürettiği CMS için yazılmıştır ve sınırları da bundan çıkar. Yürüyüş tek bir SignerInfo bekler ve yalnızca onu yeniden yazar, yani yabancı bir çok imzalı CMS tek imzalı olarak geri döner; isteğe bağlı bir certificates [0] kümesini tanır ama crls [1] kümesini tanımaz ve böyle bir kümeyi taşıyan CMS sessizce yanlış dilimlemek yerine signerInfos SET expected istisnasıyla yüksek sesle başarısız olur. Yeni unsignedAttrs tek bir öznitelik tutar, bu yüzden X.690 madde 11.6'nın SET OF sıralama kuralı kolayca sağlanır ve hiçbir sıralama gerekmez. İmzalanan bölge ise kuruluşu gereği dokunulmamıştır: SignerInfo öneki, imza OCTET STRING'ine kadar olduğu gibi kopyalanır; signedAttrsi yeniden hashleyen bir doğrulayıcının timestamp eklenmeden önce ve sonra aynı baytları görmesinin nedeni budur. Bir doğrulayıcı belgeyi yine de reddediyorsa nedenler genellikle başka yerdedir ve kendi kontrol listesini hak eder

DER reader, writer, CMS kurucusu ve bu timestamp enjeksiyonu Pascal kaynağı olarak PDFium Delphi component ile birlikte geliyor ve bu biçimdeki bir hata bunun gerekçesi: yeniden kurulan bir SignedData doksan bayt uzun çıktığında, bir kara kutudan gelen stack trace'i değil, dilimi kesen yardımcıyı ve yanlış okuduğu X.690 maddesini okumak istersiniz