Teknik Makale

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

A0 82 05 DC dört baytı, 1500 bayt içerik taşıyan context-specific bir [0] elemanı açar. Soldan sağa okunduğunda tek anlamlıdır: tag A0, iki oktet daha geleceğini bildiren başlangıç uzunluk okteti 82 ve ardından 05 DC. İlk içerik baytından başlayıp sağdan sola okursanız karşılaştığınız ilk oktet DCdir. Üst biti ayarlıdır ve düşük yedi biti 92 der; ona güvenen bir yardımcı, tagi içerikten 94 bayt önce bildirir. Gerçek mesafe 4

Kimse bilerek geriye yürümez. O, çoğu DER okuyucusunun her elemanın içerik ofsetiyle uzunluğunu vermesinden olur; içine inmek için gereken tam olarak budur, oysa bir kardeş elemanı değiştirmeden kopyalamak taginin ofsetini ister. İçerik ofsetinden çıkarmak iki satırlık bir iş gibi görünür ve her uzunluk tek bir oktete sığıdıkça doğru kalır. Gerçek bir sertifika kümesinin uzunluğu hiçbir zaman tek oktete sığmaz

Bir imza timestampi imzalı bir CMSe ne yapar?

Bir PAdES B-T timestampi eklemek, CMSin her kapsayıcı başlığının yeniden kodlanmasını zorunlu kılar; beş kardeş eleman ise bayt bayt karşıya geçmek zorundadır. ETSI EN 319 122-1 bölüm 5.3ün OID 1.2.840.113549.1.9.16.2.14 altında tanımladığı signature-time-stamp özniteliği, RFC 5652 bölüm 5.3teki SignerInfonun unsignedAttrsünde yaşar ve içindeki RFC 3161 tokeni imza değeri üzerinden hesaplanır. Yani CMS, token TSAya ulaştığında çoktan kurulmuş ve çoktan imzalanmıştır. Tek bir ek öznitelik SignerInfoyu uzatır; bu da signerInfos SETini, sonra SignedDatayı, sonra [0] EXPLICIT sarmalayıcısını, sonra dış ContentInfoyu uzatır

PDFium Componentte o sıra, FPdfCms.pas ile FPdfTsa.pasdaki üç dışa aktarılmış çağrıdır. SignPadesBytes, TPadesSignOptions.Level plB_T ya da üzerindeyken ve TsaUrl ayarlıyken onları sizin için koşar ve düz bir B-B imzası yazmak yerine B-T seviyesi boş bir TsaUrlle istendiğinde EPadesCrypto fırlatır. CMSi BuildSignedData ile kendiniz kuruyorsanız zincir şöyledir:

// CmsDer, BuildSignedData'nın döndürdüğü ContentInfo'yu tutar.
// Options.Level >= plB_T iken SignPadesBytes aynı üç adımı uygular
var
  SigValue: TBytes;
  Ts: TPadesTimestamp;
begin
  if not ExtractSignatureValue(CmsDer, SigValue) then
    raise Exception.Create('CMS carries no signature value');
  // TSA messageImprint'i, imza değeri oktetleri üzerinden hesaplanır
  Ts:= RequestTimestamp(TsaUrl, SigValue, Nonce);
  // SignerInfo'dan ContentInfo'ya yeniden kodlar, kardeşleri değiştirmeden kopyalar
  AddSignatureTimestampToCms(CmsDer, Ts.TokenDer);
end;

AddSignatureTimestampToCms beş elemanı hiç dokunmadan karşıya taşır: contentType OIDi, SignedData version INTEGERi, digestAlgorithms SETi, encapContentInfo SEQUENCEi ve varsa certificates [0] kümesi. BuildSignedDatanın SHA-256 ile yaydığı CMSde o dördü miniciktir: OID için 11 bayt, version için 3, digest algoritması kümesi için 17, detached encapContentInfo için 13. Sertifika kümesi, imzalayan sertifikasını ve ardından geçtiğiniz her zincir sertifikasını taşır. Gerçek bir X.509 sertifikası birkaç yüz bayta koşar. Uzunluk oktetleri hep uzun formu kullanan tek dilim budur

Bir CMS'te PAdES B-T timestamp eklemenin neleri yeniden kurduğu: AddSignatureTimestampToCms, contentType ile 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 yürünemez?

Uzunluk oktetlerinin sayısı ilkinde saklanır ve geriye bir yürüyüş önce sonuncuyla karşılaşır. X.690 bölüm 8.1.3.4 kısa formu tanımlar: bir oktet, 8. bit temiz, 7 ile 1 arası bitler 0 ile 127 arası bir uzunluk tutar. Bölüm 8.1.3.5 uzun formu tanımlar. Başlangıç oktetinin 8. biti ayarlıdır ve 7 ile 1 arası bitler, uzunluğu işaretsiz big-endian bir tamsayı olarak tutan sonraki oktetleri sayar. Hiçbir şey bir sonraki okteti sonraki olarak işaretlemez. Onun 8. biti, öbür yedi gibi bir büyüklük bitidir. Bu yüzden Buf[ContentOffs- 1]in üst bitini deneyen bir yardımcı önce veriyi sınar, sonra veriyi bir sayı olarak okur

// Cazip yardımcı, elinde yalnızca içerik ofsetiyle. Bunu göndermeyin
function NaiveHeaderStart(const Buf: TBytes; ContentOffs: Integer): Integer;
var
  P, LenByte: 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İNDE anlamlı
    Result:= P- 1
  else
    Result:= P- (LenByte and $7F)- 1;
end;

// A0 82 05 DC (1500 bayt): Buf[ContentOffs- 1]= $DC, $DC and $7F= 92
//   Result= ContentOffs- 94; tag, ContentOffs- 4'tedir
// A0 82 05 10 (1296 bayt): $10'un 8. biti temiz; kısa form sanılır
//   Result= ContentOffs- 2; uzunluk oktetlerinin içindeki 05'i gösterir

İki başarısızlık yönü de sıradan boyutlarda ortaya çıkar. 1500 baytlık bir sertifika kümesi başlığını DC ile bitirir ve yardımcı dilimi 90 bayt erken başlatır. 1296 baytlık bir küme 10 ile biter; yardımcı onu kısa-form bir uzunluk sanır ve dilim, hiç tag olmadan iki bayt geç başlar. Belli bir zincirin birinci mi ikinci mi vakaya düşeceği, toplam uzunluğunun düşük baytından fazlasına değil, tam ona bağlıdır

Erken vaka tamponun önünden de taşabilir. BuildSignedDatanın düzenlediği gibi düzenlenmiş, her kapsayıcı uzunluğun iki oktet aldığı bir CMSe sertifika kümesinin içeriği 60. baytta başlar: ContentInfo başlığı için 4, OID için 11, [0] ile SignedData başlıklarının her biri için 4, sonra küçük kardeşler için 3, 17 ve 13 ve sertifika kümesi başlığı için 4. 60tan 94ü çıkarın, yardımcı -34 döndürür. Tek kontrolü ContentOffs- 1in üzerindedir; negatif sonuç düpedüz kopyaya girer

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
Ara çıkarma işlemini nihai sonucu yerine koruyan bir geriye yürüyüş, bir dilimi tamponun öncesinden bile başlatabilir; yeniden kurulan SignedData o dilimi sertifika kümesinin yerinde taşır

X.690 bölüm 10.1 neyi sabitler?

DER, uzunluk kodlamasını uzunluğun saf bir fonksiyonu yapar. X.690 bölüm 10.1, DERi kesin forma kısıtlar ve en az oktet sayısını ister; bu da BERin tanıdığı iki özgürlüğü kaldırır: belirsiz form ve baştaki sıfır oktetlerle dolgulanmış bir uzun-form uzunluk. O kural altında 128in altındaki bir içerik uzunluğu daima tam olarak bir uzunluk okteti alır. Başka her uzunluk, bir başlangıç okteti artı değerin anlamlı bayt sayısı kadar tam olarak sonraki oktet alır. Bu, 128 için 81 80 verir. 256da başlık 82 01 00a büyür ve 65536, 83 01 00 00 ister. İçerik uzunluğunu halihazırda tutan kod, tamponun tek bir baytını okumadan başlık boyutunu hesaplayabilir

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
128 bayttan kısa içerik kısa formun içinde kalır ve geriye yürüyüş orada yanlış nedenden dolayı doğru yanıt verir; bu yüzden sınır testleri 127, 128, 255 ve 256 baytı geçmek zorundadır

Türetme, girdinin gerçekten DER olduğu iddiası kadar iyidir. TDerReader.TryReadTlvAt, herhangi bir dilimlemeden önce o iddiayı zorlar. $80lik bir başlangıç uzunluk oktetini (belirsiz form), ilk sonraki okteti sıfır olan bir uzun-form uzunluğu, kısa form olması gereken $80 altındaki tek bir sonraki okteti ve dörtten fazla sonraki okteti reddeder. ReadTlv, TryReadTlvAt üzerine kuruludur; dolayısıyla AddSignatureTimestampToCmsin gezdiği her eleman o kontrolleri çoktan geçmiştir. BER tarzı dolgulu bir uzunluk, aritmetiğe asla ulaşmaz

AddSignatureTimestampToCmsin içinde her kardeşin tag ofseti, içerik ofseti ve içerik uzunluğundan bu yolla türetilir. Türetilen başlık, tamponun başlangıcından öne düşerse - ofsetlerin birbirine ait olamayacağı anlamına gelir - prosedür, mesajı CMS: TLV header precedes the buffer start olan bir exception fırlatır ve hiçbir CMS üretmez. Yine de ayrıştırılan bozuk bir yeniden kurulum, ofsetlerin uyuşmayı bıraktığı noktadaki bir exceptiondan çok daha zor izlenir

Türetme bir varsayım daha taşır: tanımlayıcı tek oktet alır. X.690, 31 ve üzeri tag sayıları için çok oktetli tanımlayıcılara izin verir ve TryReadTlvAt onları ayrıştırır. CMS SignedData hiçbir zaman birini kullanmaz; tagleri evrensel SEQUENCE, SET, INTEGER ve OID artı context-specific [0] ile [1]dir; dolayısıyla varsayım, timestamp yeniden kurulumunun dokunduğu her eleman için geçerlidir

Kendi DER kodunuzda kardeş elemanları kopyalamak

TDerReadera karşı kod yazıyorsanız türetmeyi tamamen atlayın. PeekTlv ile ReadNextTlv, Offseti çoktan tagi gösteren ve HeaderLengthi tag artı uzunluk oktetlerinin ölçülmüş boyutu olan bir TDerTlv kaydını doldurur; EncodedBytes, elemanı Offsetten NextOffsete döndürür. O yol, hiçbir uzunluk aritmetiğinin kurtaramayacağı çok oktetli tanımlayıcılarla da başa çıkar. Dört çıkış parametreli ReadTlv iniş için kullanışlıdır; yalnızca içerik tarafını bildirdiği için içeriğe ihtiyacınız olduğunda, bütün elemana ihtiyacınız olduğunda ise kayıt biçimine uzanın

// SignedData'nın her çocuğu eksiksiz bir TLV olarak: tag, uzunluk oktetleri, içerik.
// TDerTlv.Offset tag'i gösterir; başlık aritmetiği işe karışmaz
function SignedDataChildren(const CmsDer: TBytes): TDerByteArrays;
var
  Root, Ci, Explicit0, Sd: TDerReader;
  Tlv: TDerTlv;
begin
  Result:= nil;
  Root.Initialize(CmsDer);
  if not Root.ReadNextTlv(Tlv) or not Root.EnterConstructed(Tlv, Ci) then
    raise Exception.Create('ContentInfo SEQUENCE expected');
  if not Ci.SkipTlv then                            // contentType OID
    raise Exception.Create('contentType OID expected');
  if not Ci.ReadNextTlv(Tlv) or not Ci.EnterConstructed(Tlv, Explicit0) then
    raise Exception.Create('[0] EXPLICIT content expected');
  if not Explicit0.ReadNextTlv(Tlv) or not Explicit0.EnterConstructed(Tlv, Sd) then
    raise Exception.Create('SignedData SEQUENCE expected');
  while not Sd.AtEnd do
  begin
    if not Sd.ReadNextTlv(Tlv) then
      raise Exception.Create('SignedData child malformed');
    SetLength(Result, Length(Result)+ 1);
    Result[High(Result)]:= Sd.EncodedBytes(Tlv);   // mesela A0 82 05 DC ...
  end;
end;

Başka bir API'nin size içerik ofseti ve uzunluk dışında hiçbir şey vermediği durumlar için ileri türetme kısadır. Bir sonraki bölümde sınır kontrolüyle gösterilen DerHeaderStart sizin kodunuzdur; bölüm 10.1deki kuralı yansıtır ve birbirine bağlanamayan ofsetler için -1 döndürür; böylece çağıranın elinde, makul görünen bir başlangıç baytı yerine reddedebileceği bir değer olur

Kısa test sertifikaları hatayı neden gizler?

Küçük bir yer tutucu sertifika, sertifika kümesini kısa formda tutar ve geriye yürüyüş tam o durum için doğrudur. 32 baytlık bir küme A0 20 olarak, 64 baytlık bir küme A0 40 olarak kodlanır. İçerikten geriye yürümek o tek uzunluk oktetine düşer ve üst biti temizdir; çünkü o hem ilktir hem tektir. Doğru cevap, yanlış neden. O kadar küçük sertifikalar üzerine kurulu bir timestamp testi, sonucu yeniden ayrıştırabilir, özniteliği bulabilir, B-T bildirebilir ve gerçekçi boyutta bir sertifika kümesini dilimden hiç bir kez geçirmemiş olabilir

Bir uzunluğun nasıl kodlandığına bağımlı her kod yolu, kodlama sınırını geçen test girdisi ister. DER için bu, uzun formu zorlayan 128 bayt ve üzeri ile ikinci bir sonraki oktet zorlayan 256 bayt ve üzeri içerik demektir. Aşağıdaki kontrol, DerHeaderStartı okuyucunun ayrıştırırken ölçtüğü başlıkla karşılaştırır; iki sınırda ve açılıştaki 1500 baytlık vakada. signedAttrsteki sıralanmamış SET OF da aynı yapısal nedenle aynı-kökenli gidiş dönüşleri aşar: test, yanlış kodla doğru kodun uzlaştığı girdileri yalnızca ona verir

// Yalnızca içerik ofseti ve uzunluk bildiren API'ler için kendi yardımcınız.
// Tek oktetli bir tanımlayıcı varsayar; CMS SignedData'daki her tag için doğru
function DerHeaderStart(ContentOffs, ContentLen: Integer): Integer;
var
  LengthOctets, Remaining: Integer;
begin
  LengthOctets:= 1;                  // short form, X.690 8.1.3.4
  if ContentLen>= 128 then
  begin
    Remaining:= ContentLen;          // long form, X.690 8.1.3.5
    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:= -1;                     // tutarsız ofsetler: çağıran fırlatsın
end;

// Sınır kontrolü, kendi test kodunuz olarak yazılmış: türetilen tag ofseti
// okuyucunun başlığı ayrıştırırken ölçtüğü ofsete eşit olmalı
procedure CheckHeaderDerivation;
const
  Lens: array[0..6] of Integer= (127, 128, 255, 256, 1500, 65535, 65536);
var
  W: TDerWriter;
  R: TDerReader;
  Tlv: TDerTlv;
  Content: TBytes;
  I: Integer;
begin
  W:= TDerWriter.Create;
  try
    for I:= Low(Lens) to High(Lens) do
    begin
      SetLength(Content, Lens[I]);
      R.Initialize(W.Wrap($A0, Content));   // A0 7F / A0 81 80 / A0 82 05 DC ...
      W.Clear;
      if not R.PeekTlv(Tlv) then
        raise Exception.CreateFmt('unreadable TLV for %d bytes', [Lens[I]]);
      if DerHeaderStart(Tlv.ContentOffset, Tlv.ContentLength)<> Tlv.Offset then
        raise Exception.CreateFmt('header start wrong for %d bytes', [Lens[I]]);
    end;
  finally
    W.Free;
  end;
end;

Aynı sınır, dışa aktarılan fonksiyonlar üzerinden uçtan uca ulaşılabilirdir. BuildSignedDataye toplam boyutu sertifika kümesini 128 ile 256 baytın hemen altına ve üstüne koyan zincir sertifikaları verin, her sonuç üzerinde AddSignatureTimestampToCms koşun, sonra çıktıyı SignedDataChildrenle gezin ve sertifika kümesi elemanını, timestamp eklenmeden önceki CMStekiyle bayt bayt karşılaştırın

AddSignatureTimestampToCms neyi kabul eder

AddSignatureTimestampToCms, BuildSignedDatanın yaydığı CMS için yazılmıştır: tek bir SignerInfo, isteğe bağlı bir certificates [0] kümesi, henüz crls [1] yok ve unsignedAttrs yok. Birden çok SignerInfo tutan bir signerInfos SETi verin; ilk imzalayanı tutup gerisini düşürmek yerine fırlatır. Bir crls [1] kümesi taşıyan bir CMS, CMS: signerInfos SET expected exceptionında durur; çünkü sertifikalardan sonraki eleman, yürüyüşün aradığı SET değildir. İki red de, CmsDere geri bir şey yazılmadan önce olur

Yeni unsignedAttrs tam olarak bir öznitelik tutar; X.690 bölüm 11.6nın SET OF sıralama kuralı böylece sırasız sağlanır. SignerInfonun imzalı kısmı, versiondan imza OCTET STRINGine kadar tek bir blok olarak kopyalanır; yani signedAttrsi yeniden hashleyen bir doğrulayıcı, token eklenmeden önce ve sonra aynı baytları görür. Bir doğrulayıcı belgeyi hâlâ çeviriyorsa nedenler genellikle başkadır ve bu PAdES red nedenleri kontrol listesinde kapsanır. Tokenin zamanla ne kazandırdığı ve B-LT ile B-LTA'nın üzerine nasıl kurulduğu, RFC 3161 timestampleri ve DSS anlatımındadır

FPdfAsn1.pasdaki DER okuyucusu ve yazıcısı, FPdfCms.pasdaki CMS kurucusu ve timestamp adımı, Delphi için PDFium Componentle gelen Pascal kaynağıdır. Yeniden kurulan bir SignedData bir doğrulayıcıda ayrıştırılmayı reddettiğinde, her elemanı kesen kodun içinden adım adım gidebilir ve onu atıf yaptığı X.690 maddesiyle karşılaştırabilirsiniz