PDFium Component, CMS signedAttrs bloğunu TDerWriter.SetOfSorted üzerinden kurar, çünkü X.690 madde 11.6, bir SET OF'un üyelerinin kendi DER kodlamalarının artan sırasında yayılmasını gerektirir. Onları kurulum sırasına göre dizmek yerine geçersen kendi doğrulayıcının kabul ettiği ama her üçüncü taraf PAdES doğrulayıcısının reddettiği imzalar elde edersin
O son cümle, bu kusurun eksiksiz bir regresyon paketinden sağ çıkmasının tüm nedenidir. İmzalama yolu kapsanmıştı, doğrulama yolu kapsanmıştı, round trip her platformda yeşildi ve çıktı yine de geçerli DER değildi. Kendi yazarınla imzalayıp kendi okuyucunla doğrulayan bir test, kodlama kurallarından sapmayı yakalayamaz, çünkü iki yarım da sapmayı paylaşır
X.690 madde 11.6 aslında ne gerektirir
Bir SET OF değerinin üyeleri sıralanmak zorundadır ve sıralama anahtarı her üyenin eksiksiz DER kodlamasıdır; işaretsiz bir oktet stringi olarak karşılaştırılır ve biri diğerinin öneki olduğunda kısa olan kodlama önce gelir. Bu bir öneri değil kanonik bir kısıttır: DER, verilen bir değerin tam olarak tek bir kodlamasını kabul eder, bu yüzden sıralanmamış bir SET OF aynı değerin farklı sıralanmış bir kodlaması değil, hiçbir DER kodlaması değildir. ASN.1 etiketleri bir TLV'de önce geldiğinden karşılaştırma genellikle ilk iki oktet içinde çözülür ve bir CMS öznitelik listesinin her üyesi 30, yani SEQUENCE etiketiyle başladığından, beraberlik uzunluk oktetinde kırılır. Bu yüzden gereken sıra neredeyse öznitelikleri boyuta göre sıralamak gibi görünür; bir hex dump'a bakıp sıranın neden keyfi göründüğünü merak ederken bunu bilmekte fayda var
Neden minimum öznitelik kümesi bile zaten sıra dışı?
Çünkü doğal kurulum sırası ile kodlanmış sıra, ürettiğin daha ilk imzadan itibaren çelişir ve hiçbir opsiyonel öznitelik işin içinde değildir. FPdfCms.pas içindeki BuildSignedAttrs önce content-type, sonra message-digest, sonra signing-certificate-v2, sonra cms-algorithm-protection ekler. SHA-256 digest'leri ve bir RSA imza algoritmasıyla dört öznitelik TLV'si şu baytlarla başlar
// BuildSignedAttrs'in ürettiği kurulum sırası
// content-type 30 18 (24 content octets)
// message-digest 30 2F (47)
// signing-certificate-v2 30 37 (55)
// cms-algorithm-protection 30 2D (45)
//
// X.690 11.6'nın gerektirdiği DER sırası
// 30 18 content-type
// 30 2D cms-algorithm-protection
// 30 2F message-digest
// 30 37 signing-certificate-v2
Dört öznitelikten ikisi yer değiştirir. 30 2D'deki cms-algorithm-protection, 30 37'deki signing-certificate-v2'den önce gelmek zorundadır; ayrıca 30 2F'deki message-digest'ten de önce gelmek zorundadır. Yani PDFium Component'in yayabildiği en küçük uyumlu PAdES B-B öznitelik kümesi, kimse signing-time, commitment-type-indication ya da signature-policy-identifier eklemeden önce eski kodda zaten yanlış sıralanıyordu. Bug'ın ateşlenmediği tek bir konfigürasyon yoktu
Neden kendi testlerimiz geçerken her harici doğrulayıcı başarısız oluyor?
Çünkü iki doğrulayıcı ailesi mesaj digest'ini farklı baytlar üzerinden hesaplar. RFC 5652 madde 5.4, imza işleminin girdisinin SignedAttributes değerinin DER kodlaması olduğunu söyler; IMPLICIT [0] etiketi SET etiketi 31 ile değiştirilir ve her şeye karar veren ifade DER kodlamasıdır, aldığın oktetler değildir. CryptVerifyDetachedMessageSignature üzerinden ulaşılan Windows platform doğrulayıcısı alınan oktetleri alır, baştaki etiketi takas eder ve onları geldikleri gibi hash'ler; bu yüzden sıra işin içine hiç girmez ve yanlış sıralanmış bir SET OF, imzalayanın hesapladığı şeyin aynısına digest'lenir. Yaygın olarak dağıtılan Avrupa doğrulayıcılarının üzerine kurulduğu OpenSSL ve BouncyCastle ise öznitelik kümesini yapılara ayrıştırır ve digest'lemeden önce yeniden kodlar. Yeniden kodlama sıralamayı uygular, digest artık imzalananla eşleşmez ve imza, yanlış kodlanmış olarak değil bozuk olarak raporlanır; bu da seni bir karşılaştırma fonksiyonunda yaşayan kusur için anahtar materyali ve sertifika zincirlerini didiklemeye yollar
Bu tek bug'ın ötesine genelle, çünkü pahalı olan kısım bu. Aynı kaynaklı bir round trip, kodunun iki yarısının paylaştığı birlikte çalışabilirlik şartnamesinden her sapmaya karşı bağışıktır; yazarının ve okuyucunun birbiriyle anlaştığını kanıtlar, ki kimsenin umursadığı özellik bu değildir. Süreci bir wire format olarak terk eden her şey için tek anlamlı regresyon, baytları şartnameye karşı sabitleyen ya da çıktının üzerinde yabancı bir uygulama çalıştıran regresyondur. OpenSSL CMS doğrulama backend'i burada yerini taşınabilirliğin ötesinde ikinci bir nedenden kazanır: kendi DER'inin gerçekten bağımsız bir okuyucusudur
Düzeltme ve zaten nerede var olduğu
DER yazarında sıralı varyant en başından beri vardı. FPdfAsn1.pas içindeki TDerWriter, önceden birleştirilmiş bir içerik buffer'ını SET etiketiyle sarıp kendisine verilen sırayı ne olursa olsun koruyan SetOf'u ve üyeleri ayrı ayrı bir TDerByteArrays olarak alıp sarmadan önce onları kodlamalarına göre merge-sort yapan SetOfSorted'u açığa çıkarır. FPdfMac.pas içindeki PDF MAC kodu, kimliği doğrulanmış öznitelik kümesini zaten SetOfSorted ile kuruyordu; yalnızca CMS imzalayıcısı kurmuyordu. Üyeleri sarma çağrısına kadar bir dizi olarak tutmak düzeltmenin yapısal yarısıdır, çünkü öznitelikler tek bir buffer'a birleştirildikten sonra sıralama bilgisi kaybolur ve hiçbir aşağı akış özeni onu geri getiremez. SET OF için yalnızca SetOf(Content: TBytes) sunan bir yazar API'si, bu bug'ı davet eden bir API'dir; dizi alan overload, doğru olanı ifade edilebilir kılan şeydir
// Önce: üyeler AddAttr'ın eklediği sırayı korur
Result:= W.SetOf(ConcatMany(AttrList));
// Sonra: X.690 11.6 sıralaması kodlama noktasında uygulanır
Result:= W.SetOfSorted(AttrList);
SET OF sıralanır, SEQUENCE OF sıralanmaz
Bu en çok karıştırılan ayrımdır ve tersine çevirmek, yok saymak kadar zarar vericidir. SEQUENCE OF sıralı bir koleksiyondur: sırası değerin parçasıdır ve onu asla sıralamamalısın. SET OF değer olarak sırasızdır, bu yüzden DER kodlamayı benzersiz kılmak için kanonik bir sıra dayatır. Bir SEQUENCE OF'u sıralamak veriyi bozar; bir SET OF'u sıralanmamış bırakmak DER olmayan bir şey üretir
- SET OF, sıralanmalı:
SignerInfoiçindekiSignedAttributesveUnsignedAttributes,digestAlgorithmskümesi,SignedDataiçindekicertificatesvecrlskümeleri,signerInfoskümesi, her birAttribute'ın içindeki değer kümesi ve bir X.501 Name'in herRelativeDistinguishedName'inin içindeki öznitelik kümesi - SEQUENCE OF, asla sıralanmaz: bir
Name'i oluşturan RDN listesi,SigningCertificateV2'nincertsalanı, extension listeleri ve onların içindeki general-name listeleri
Bir RDN, bir seviye yukarıdaki bir SEQUENCE OF'un içine oturmuş bir SET OF'tur; yani ayırt edici bir ad, iki seviyelik yuvalama içinde her iki kuralı birden barındırır. Bu yerleşim aynı zamanda tek üyeli kümelerin neden bu kadar affedici olduğunu da açıklar: tek öğeli bir küme önemsiz biçimde sıralıdır, bu yüzden BuildSignedAttrs içinde W.SetOf ile kurulan öznitelik değer kümeleri yazıldığı gibi doğruydu ve değişiklik gerektirmiyordu. Bug yalnızca bir kümenin gerçekten iki ya da daha fazla üyesi olduğunda yaşar ve çok değerli RDN'ler, çoğu ad kodlayıcısının aynı hatayla yıllarca kurtulduğu kadar nadirdir
Bunun geri gelmesini nasıl engellersin?
Verdict'ler üzerinde değil, baytlar üzerinde assert et. En ucuz kalıcı kontrol, kendi çıktısının üzerinde yürüyen bir öz denetimdir: yayılan signedAttrs değerini ayrıştır, her üyeyi yazarın kullandığı aynı işaretsiz-oktet-string karşılaştırmasıyla bir öncekiyle karşılaştır ve herhangi bir çift sıra dışıysa build'i başarısız kıl. Mikrosaniyeler içinde çalışır, harici kütüphane gerektirmez ve bir doğrulama round trip'inin aksine şartnamenin gerçekten ifade ettiği özelliği test eder
// Bir SET OF'un üyelerini yürü ve X.690 11.6 sıralamasını doğrula.
// Members, SetOfSorted'a verilen liste ya da yayılan bir
// 31 xx / A0 xx bloğunun yeniden ayrıştırılmasının sonucudur
function SetOfIsSorted(const Members: TDerByteArrays): Boolean;
var
I: Integer;
begin
for I:= 1 to Length(Members)- 1 do
if CompareDerBytes(Members[I- 1], Members[I])> 0 then
Exit(False);
Result:= True;
end;
// İmza testinde "doğrulayıcımız geçerli dedi" noktasında durma.
// İmzalayanın hash'lediği baytları sabitle
Attrs:= BuildSignedAttrs(W, Digest, CertHash, DigestOid, SigOid, nil, Options);
Assert(Attrs[0]= $31, 'signedAttrs must carry the SET tag when hashed');
Assert(SetOfIsSorted(SplitSetOfMembers(Attrs)), 'signedAttrs not in DER order');
Açıkça söylemeye değer iki sınır var. signedAttrs değerini sıralamak tek başına bir imzayı birlikte çalışır hale getirmez, çünkü konteynerdeki diğer her şeyin de ayakta kalması gerekir ve doğrulayıcıların kulağa sağlam gelen PAdES imzalarını reddetme nedenleri kendi tartışmalarını dolduracak kadar çeşitlidir. Ayrıca sıralı bir SET OF, yazdığın şeyin özelliğidir, okuduğun şeyin değil: PDFium Component'in yine de signedAttrs değeri sırasız gelen bir imzayı kabul etmesi gerekir, çünkü bu tür dosyalar gerçek dünyada mevcuttur ve RFC 5652 madde 5.4 bir doğrulayıcıya reddetmesini değil yeniden kodlamasını söyler. Çıktıda katı, girdide hoşgörülü, her zamanki gibi. PAdES B-B walkthrough'unda anlatılan DER yazarı, CMS kurucusu ve imzalama yolu, okunabilir Pascal kaynağı olarak PDFium Delphi component ile birlikte gelir; bir imzalama kütüphanesi için kaynak dağıtmanın tüm amacı da budur: Avrupa'da bir yerdeki bir doğrulayıcı belgenle aynı fikirde olmadığında, kodunun yaydığı tam baytları ve ihlal ettikleri tam maddeyi okuyabilirsin, ticket açıp beklemek yerine