PDFium Component sürüm 3.114.20, RSASSA-PSS-params kodlamasını üç PAdES imzalama arka ucunda da düzeltiyor: Windows CNG, macOS Keychain ve PKCS#11. RFC 4055 §3.1, RSASSA-PSS-params'ın her alanına [0] ile [3] arasında explicit context-specific bir tag verir; arka uçlar ise saltLength değerini çıplak bir universal INTEGER olarak yazıyor ve varsayılanına eşit bir trailerField çıkarıyordu. İmza baytları baştan sona doğruydu. Onları tarif eden AlgorithmIdentifier doğru değildi ve bu tek başına bir doğrulayıcının imzayı reddetmesine yeter
Sinir bozucu olan kısım, hatanın saklandığı yer. Bir CMS imzasının iki yarısı vardır: kriptografik işlem ve doğrulayıcıya işlemin nasıl yapıldığını söyleyen ASN.1. Birincisini doğru, ikincisini yanlış yaparsanız ortaya çıkan şey, spesifikasyona uyan hiçbir aracın sahteden ayırt edemeyeceği bir belgedir. Bu makale yalnızca o ikinci yarıyla ilgili: RSASSA-PSS-params nasıl tag'lenmek zorunda, üç arka uç bunu nasıl aynı biçimde yanlış yaptı ve düzeltilmiş DER TDerWriter terimleriyle nasıl görünüyor
Bir doğrulayıcı baytları doğru olan bir RSASSA-PSS imzasını neden reddediyor?
Çünkü RSASSA-PSS, doğrulayıcının parametreleri imzanın kendisinden kurtaramadığı tek RSA şemasıdır. PKCS#1 v1.5 dolgusu sha256WithRSAEncryption OID'i tarafından tamamen belirlenir, bu yüzden parametreleri çıplak bir NULL'dur ve yanlış yapılacak bir şey yoktur. PSS ise bir hash fonksiyonu, kendi hash'i olan bir mask generation function ve bir salt uzunluğu ile parametrelenir; RFC 8017 §A.2.3 üçünü de açık bırakır. Bunları imzalayan seçer, AlgorithmIdentifier taşır ve doğrulayıcı EMSA-PSS-VERIFY başlamadan önce onları birebir yeniden üretmek zorundadır
Yani PDFium Component SHA-256, SHA-256 üzerinden MGF1 ve 32 baytlık bir salt ile imzaladığında bu üç olgu burada DER kodlamasından ve başka bir uygulamada DER çözümlemesinden sağ çıkmak zorundadır. Doğrulayıcının ayrıştıramadığı bir parametre bloğu, herhangi bir modüler üs alma işlemi olmadan doğrulamayı bitirir. Ayrıştırdığı ama farklı ayrıştırdığı bir blok daha kötüdür, çünkü RFC 4055 §3.1 saltLength için 20 varsayılanını verir. Tanımadığı bir alanı atlayan bir çözücü o varsayılana düşer, 32 ile hesaplanmış bir imzaya karşı 20 baytlık salt ile EMSA-PSS-VERIFY çalıştırır ve sorunun anahtarda değil metadatada olduğuna dair hiçbir ipucu vermeden kötü imza bildirir. Her iki sonuç da 3.114.19 kodlamasının ürettiği şeydir; hangisinin çıkacağı doğrulayıcının ne kadar katı olduğuna bağlıdır ve hiçbiri AlgorithmIdentifier'ı işaret etmez
RFC 4055 §3.1 RSASSA-PSS-params için aslında neyi şart koşuyor
RFC 4055 §3.1, RSASSA-PSS-params'ı her biri explicit context-specific bir tag ve bir DEFAULT değer taşıyan dört alanlı bir SEQUENCE olarak tanımlar:
// RSASSA-PSS-params ::= SEQUENCE {
// hashAlgorithm [0] HashAlgorithm DEFAULT sha1,
// maskGenAlgorithm [1] MaskGenAlgorithm DEFAULT mgf1SHA1,
// saltLength [2] INTEGER DEFAULT 20,
// trailerField [3] TrailerField DEFAULT trailerFieldBC
// }
DER'de explicit tagging, her alanın kurulmuş bir context-specific TLV içine sarmalanması demektir: [0] için A0, [1] için A1, [2] için A2 ve [3] için A3; değerin universal kodlaması da içine yerleştirilir. Her alanın tag'lenmesinin nedeni, her alanın varsayılanı sayesinde isteğe bağlı olmasıdır. Tag'ler olmasa bir çözücü, tek bir AlgorithmIdentifier tutan bir SEQUENCE'in hashAlgorithm mı yoksa maskGenAlgorithm mı taşıdığını söyleyemezdi, çünkü ikisi de SEQUENCE tipidir; tag'lerle birlikte ise hangi komşuların bulunduğundan bağımsız olarak alanı tag numarası tanımlar. PDFium Component'in yazdığı değerler ETSI TS 119 312 §7'deki profile uyar — SHA-256, SHA-256 ile MGF1 ve digest uzunluğuna eşit salt — ve her platform imzalama çağrısına söyleneni birebir yansıtır: NCryptSignHash için cbSalt değeri 32 olan bir BCRYPT_PSS_PADDING_INFO, PKCS#11 mekanizması için sLen değeri 32 olan bir CK_RSA_PKCS_PSS_PARAMS ve Security framework içindeki SHA-256 digest-signing PSS algoritması
Üç arka uç aynı hatayı nasıl yaptı
3.114.19 kodlaması ilk iki alanı tag'leyip son ikisini çıplak bırakıyordu ve bunu TWinCmsSigner, TKeychainCmsSigner ile TPkcs11CmsSigner içinde birebir aynı biçimde yapıyordu. Bu simetri rastlantı değil: üçü de FPdfCms.pas içindeki ICmsSigner arayüzünü uygular ve GetSignatureAlgorithmParams gövdeleri tek bir şablona göre yazılmıştı. Şablon şöyleydi:
// 3.114.20 öncesi: [0] ve [1] tag'li, [2] ve [3] değil
Result := W.Sequence(Concat4(
W.ContextSpecific(0, W.AlgId(OID_SHA256), True),
W.ContextSpecific(1, W.Sequence(ConcatBytes(
W.OID(OID_MGF1), W.AlgId(OID_SHA256))), True),
W.IntegerOf(32), // [2] EXPLICIT gereken yerde çıplak INTEGER
W.IntegerOf(1))); // trailerField, DEFAULT'a eşit, hiç bulunmamalı
O SEQUENCE üzerinde yürüyen bir çözücü A0 görüp hash algoritmasını okur, A1 görüp mask generation function'ı okur ve ardından 02 01 20 ile karşılaşır. Bu bir universal INTEGER'dır ve RSASSA-PSS-params'ın hiçbir yerinde tag'siz bir INTEGER üyesi yoktur. Katı bir çözücü orada durur. Hoşgörülü olan tanımadığı öğeyi atlar, hiçbir zaman bir A2 bulamaz, saltLength değerine 20 varsayılanını atar, sonra ikinci bir başıboş INTEGER olan 02 01 01 ile karşılaşır ve aynı sorunu yeniden yaşar. Hiçbir yol 32 baytlık bir salt değerine ulaşmaz. Paylaşılan bir şablon doğru olduğunda verimlidir, olmadığında ise üç kez yanlış olmanın aynı derecede verimli bir yoludur; düzeltmenin tek commit'te üç unit'e birden girmesinin ve üç method gövdesinin sonrasında yapısal olarak özdeş kalmasının nedeni budur. Gelecekteki bir arka uç bu bloğu yeniden türetmek yerine bunlardan birinden kopyalamalıdır, çünkü hata tam da türetme sırasında yapıldı
trailerField neden [3] olarak tag'lenmiyor da tamamen atlanıyor?
Çünkü X.690 §11.5, bir DER encoder'ının değeri DEFAULT'una eşit olan bir bileşeni kodlamayacağını söyler ve trailerFieldın DEFAULT'u, yani 1 tam sayısı olan trailerFieldBCdir. Eski koddaki bariz düzeltme, çıplak W.IntegerOf(1) ifadesini W.ContextSpecific(3, W.IntegerOf(1), True) ile değiştirmek, izin veren bir BER çözücüsünün kabul ettiği ama katı bir DER çözücüsünün reddetmeye hakkı olan bir blok üretir. Değer yanlış değil. Varlığı yanlış. Diğer üç alanın neden bulunduğunun nedeni de aynı kural: SHA-256 varsayılan sha1 değil, SHA-256 ile MGF1 varsayılan mgf1SHA1 değil ve 32 varsayılan 20 değil. Arka uç SHA-1 ve 20 baytlık bir salt ile imzalıyor olsaydı RFC 4055 §3.1 parametreleri boş bir SEQUENCE'e, 30 00a indirgerdi ve bir doğrulayıcının beklediği NULL değil o boş SEQUENCE olurdu. PDFium Component bu biçimi hiç üretmiyor çünkü o değerlerle hiç imzalamıyor; ama parametre yok her zaman 05 00 diye yazılır varsayımını yakalayan durum budur
Bu, DER ile BER arasındaki ayrımın özellikle imzalar için önem taşıyan kısmıdır. BER bir encoder'ın varsayılan değerli bir bileşeni eklemesine izin verir; DER yasaklar, çünkü DER bir değerin tam olarak tek bir kodlaması olması için vardır ve iki geçerli kodlaması bulunan bir yapı üzerindeki imza, üzerinde tartışılabilecek bir imzadır. CMS signedAttrs içindeki her şey bu yüzden DER'dir ve parametre bloğu hem dıştaki signatureAlgorithm içinde hem de signedAttrs içindeki cmsAlgorithmProtection özniteliği üzerinden signedAttrs'ın içinde yol alır, yani hiçbir muafiyet almaz
Düzeltilmiş TDerWriter kodlaması
PDFium Component artık parametreleri FPdfAsn1.pas içindeki TDerWriter.ContextSpecific çağrılarıyla, varsayılan olmayan alan başına bir tane olacak biçimde kuruyor; her çağrıda explicit tag sarmalayıcısını üretmek için Constructed argümanı True ve trailer alanı için hiç satır yok. Aşağıda OID'ler açık yazılmış hâliyle TWinCmsSigner.GetSignatureAlgorithmParams gövdesi var; Keychain ve PKCS#11 unit'leri aynı değerleri OID_SHA256, OID_MGF1 ve OID_RSASSA_PSS olarak yazıyor:
function TWinCmsSigner.GetSignatureAlgorithmParams: TBytes;
var
W: TDerWriter;
begin
if FPaddingScheme = psRsaPss then
begin
W := TDerWriter.Create;
try
// RFC 4055 3.1 dört alanın hepsini tag'ler. saltLength [2]'dir; burada
// çıplak bir INTEGER başka bir alanın başlangıcı olarak okunur. trailerField
// DEFAULT'u 1 olan [3]'tür ve X.690 11.5 varsayılana eşit bir değerin
// kodlanmasını yasaklar, bu yüzden tamamen atlanır
Result := W.Sequence(Concat3(
W.ContextSpecific(0, W.AlgId('2.16.840.1.101.3.4.2.1'), True),
W.ContextSpecific(1, W.Sequence(ConcatBytes(
W.OID('1.2.840.113549.1.1.8'), // id-mgf1
W.AlgId('2.16.840.1.101.3.4.2.1'))), True),
W.ContextSpecific(2, W.IntegerOf(32), True)));
finally
W.Free;
end;
end
else
Result := nil; // PKCS#1 v1.5 ve ECDSA: AlgIdWithParams NULL yazar
end;
Çevredeki mekanizmanın iki ayrıntısı önemli. TDerWriter.AlgId, parametreleri NULL olan bir AlgorithmIdentifier üretir; RFC 4055 §2.1'in encoder'lara iç içe hashAlgorithm ve MGF1 iç hash'i için üretmelerini söylediği budur. FPdfCms.pas içindeki CMS kurucusu da imza OID'ini bu baytlarla TDerWriter.AlgIdWithParams üzerinden eşleştirir; bu fonksiyon parametreler nil olduğunda NULL koyar. psRsaPkcs1v15 ile psEcdsanın basitçe nil dönmesinin ve hiç etkilenmemesinin, üç imza OID'i arasında gerçek bir parametre bloğu taşıyan tek tanesinin 1.2.840.113549.1.1.10, yani id-RSASSA-PSS olmasının nedeni budur. SHA-256 profili için ortaya çıkan baytlar sabittir ve gözle kontrol edilebilecek kadar kısadır: 15 baytlık SHA-256 AlgorithmIdentifier'ı çevreleyen A0 0F ile 28 baytlık MGF1 AlgorithmIdentifier'ını — ki onun kendi parametreleri yine aynı SHA-256 AlgorithmIdentifier'ıdır — çevreleyen A1 1C ve salt için A2 03 02 01 20 tutan dıştaki bir 30 34 SEQUENCE. signatureAlgorithm dökümünüz parametre SEQUENCE'inin en üst düzeyinde, bir A2 içinde değil de 02 01 20 gösteriyorsa baktığınız şey 3.114.19 kodlamasıdır
Test takımı bozuk bir AlgorithmIdentifier'ı neden yakalamadı?
Çünkü PAdES testleri CMS kurucusunu, sha256WithRSAEncryption bildiren ve GetSignatureAlgorithmParams fonksiyonundan nil dönen sahte bir imzalayıcı üzerinden sürüyor, yani PSS parametre bloğu testlerde hiç kurulmadı. Bu, sertifika deposu, Keychain ya da token olmadan çalışması gereken testler için makul bir tasarım ve tam olarak şu biçimde bir kör noktası var: yalnızca gerçek bir arka ucun ürettiği şeyleri yalnızca gerçek bir arka uç çalıştırır. İkinci katman daha ilginç. PDFium Component ayrıca imza AlgorithmIdentifier'ını, parametreleri dahil, RFC 6211'deki cmsAlgorithmProtection signed attribute'ının içine koyuyor ve bir doğrulayıcı bu kopyayı dıştaki signatureAlgorithm ile karşılaştırıyor. İki kopya da aynı çağrıdan geldiği için mükemmel eşleşiyordu ve her iç tutarlılık kontrolü geçti. Kodlama kendi kendisiyle tutarlı ve yanlıştı; bir yapıyı kendisiyle karşılaştırmanın asla ortaya çıkaramayacağı hata sınıfı bu. Aynı ders farklı bir yapıyla CMS signedAttrs ve DER SET OF sıralaması yazısında anlatılıyor; orada bir sırayla hash'lenip başka bir sırayla yazılan bir SET, yabancı bir doğrulayıcı hash'i yeniden hesaplayana kadar sorunsuz görünüyordu
Bu sınıf hatayı yakalayan şey, encoder'ı yazanın yazmadığı bir çözücünün gerçek arka ucun gerçek çıktısı üzerinde çalıştırılmasıdır. PDFium Component'teki Windows doğrulama yolu kütüphanenin kendi reader'ı yerine CryptoAPI'den geçiyor ve orada reddedilen bir PSS imzası, bizi parametrelere geri götüren şey oldu. Bir uygulamanın başka uygulamaların okuması için ürettiği her ASN.1, kontrol etmediği bir çözücüden en az bir round-trip geçmeyi hak eder; yapının varsayılanı ve tag'i ne kadar çoksa o round-trip o kadar değerlidir
Bunun PSS hikâyesinin geri kalanındaki yeri
Bu düzeltme, PSS'in bir PAdES imzasında yanlış gidebileceği diğer iki yerden bağımsızdır ve ikisini ayrı tutmak hata ayıklamayı kısaltır. macOS arka ucu belirli bir anahtarın ya da eski bir sistemin PSS'i reddettiğini görüp PKCS#1 v1.5'e düşebilir ve AlgorithmIdentifier bu düşüşü izlemek zorundadır; bu bir yetenek sorusudur ve macOS Keychain kimliğiyle PAdES imzalama yazısında ele alınıyor. PKCS#11 arka ucu tokene, bir tam sayı genişliği uyuşmazlığı yüzünden tokenin farklı okuduğu bir düzende CK_RSA_PKCS_PSS_PARAMS verebilir; bu bir ABI sorusudur ve CK_ULONG ve PKCS#11 packing tuzağı yazısında ele alınıyor. Bu makale ise üçüncü arızayla ilgili: anahtarın istekli olduğu, tokenin doğru baytları hesapladığı ve sonucu tarif eden DER'in RFC 4055 §3.1'e uymadığı durum
PSS bildiren bir imzalayıcı, v1.5 imzalayıcının hiç taşımadığı bir yükümlülük alır: kendi parametrelerini, başka bir uygulamanın aynı üç değere çözeceği bir biçimde tarif etmek. RFC 4055 §3.1 tag'leri, X.690 §11.5 hangi alanların görünebileceğini, ETSI TS 119 312 §7 ise seçilmeye değer değerleri sabitler. PDFium Delphi componentin üç arka ucu da kaynak olarak geliyor, yani yukarıdaki GetSignatureAlgorithmParams gövdesi, güvenip geçmek yerine okuyabileceğiniz, dökümünü alabileceğiniz ve kendi doğrulayıcınızla karşılaştırabileceğiniz gövdedir