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
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
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
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