RSASSA-PSS'li bir signatureAlgorithm'ın A0 0F … A1 1C … 02 01 20 02 01 01 ile biten hex dökümü tamam görünür: SHA-256, MGF1, 32 baytlık bir salt ve 1'lik bir trailer. Katı bir decoder o parametre bloğunu tümüyle reddeder; hoşgörülü olan ise salt uzunluğunu 20 okur. Hangisi olursa olsun, RSA aritmetiği doğru olan bir imza geçersiz döner. RFC 4055 §3.1 nedenini açıklar: RSASSA-PSS-params'ın her alanı açık bir context tag taşır; dolayısıyla salt A2'nin içinde olmalıdır ve DER altında, değerine eşit olan bir trailerField hiç görünmemelidir
Bu, bir CMS imzasında birbirinden bağımsız bozulan iki yarı demektir. Biri kriptografik işlemdir; öbürü, işlemin nasıl yapıldığını doğrulayıcıya anlatan ASN.1'dir. Birinciyi doğru, ikinciyi yanlış yaparsanız, spec'e uyan bir doğrulayıcının belgenizi bir sahteden ayırmasının bir yolu kalmaz. ASN.1 yarısı, bu makalenin konusudur
Bir doğrulayıcı, baytları doğru olan bir RSASSA-PSS imzasını neden reddeder?
Çünkü RSASSA-PSS, doğrulayıcının parametreleri algoritma OID'sinden çıkaramadığı tek RSA şemasıdır. PKCS#1 v1.5, sha256WithRSAEncryption ile baştan sona sabitlenmiştir; parametreleri çıplak bir NULL'dur. PSS ise bir ileti hash'i, kendi hash'ine sahip bir maske üretim işlevi ve bir salt uzunluğuyla parametrelendirilir ve RFC 8017 §A.2.3, üçünü de imzalayıcıya bırakır. AlgorithmIdentifier, imzalayıcının seçimlerini taşır ve doğrulayıcı, EMSA-PSS-VERIFY çalışabilmeden önce onları harfiyen yeniden üretmek zorundadır
Doğrulayıcının ayrıştıramadığı bir blok, herhangi bir modüler üs alma işlemi başlamadan doğrulamayı bitirir. Farklı ayrıştırdığı bir blok daha kötüdür. RFC 4055 §3.1, saltLength'e 20 varsayılanı verir; salt alanını hiç bulamayan bir decoder, 32 ile hesaplanmış bir imzaya karşı EMSA-PSS-VERIFY'i 20 baytlık saltla çalıştırır. Sonuç kötü imza olarak raporlanır ve hiçbir şey metadata'yı işaret etmez. Hangi başarısızlığı göreceğinize doğrulayıcının katılığı karar verir; iki hata iletisi de AlgorithmIdentifier'i anmaz
RSASSA-PSS-params'ın dört alanı ve etiketleri
RFC 4055 §3.1, RSASSA-PSS-params'ı dört alandan oluşan bir SEQUENCE olarak tanımlar; her biri açık bir bağlama özgü etiket ve bir DEFAULT değeri taşır. DER'de açık etiketleme, her alanı kurulu bir bağlam TLV'sine sarar — A0'dan A3'e — alanın kendi evrensel kodlaması da içine yuvalanmıştır:
| Alan | Etiket (DER baytı) | DEFAULT | ETSI TS 119 312 §7 değeri | Kodlanır mı? |
|---|---|---|---|---|
hashAlgorithm | [0] (A0) | sha1 | SHA-256 | Evet |
maskGenAlgorithm | [1] (A1) | mgf1SHA1 | SHA-256 ile MGF1 | Evet |
saltLength | [2] (A2) | 20 | 32 (özet uzunluğu) | Evet |
trailerField | [3] (A3) | trailerFieldBC (1) | 1 | Hayır, varsayılana eşit |
Etiketler, her alan kendi varsayılanı üzerinden isteğe bağlı olduğu için oradadır. hashAlgorithm ile maskGenAlgorithm ikisi de AlgorithmIdentifier SEQUENCE'idir; etiket olmadan, tek bir SEQUENCE tutan bir parametre bloğuna bakan bir decoder, ikisinden hangisi olduğunu söyleyemez. Etiketlerle birlikte etiket numarası alanı adlandırır; kodlayıcının komşulardan hangilerini bıraktığı fark etmez
Saltı özet uzunluğuna eşit seçin ve orada bırakın. Bazı API'ler "azami salt" ya da "otomatik salt uzunluğu" sunar; bu, anahtar boyutuna bağlı bir değer verir. RFC 8017 altında yasaldır ama o zaman AlgorithmIdentifier, anahtardan anahtara değişen bir sayı bildirmek zorunda kalır; üstelik SHA-256 için 32 baytlık salt, ETSI TS 119 312 §7'nin zaten istediği şeydir. PDFium Component'in yerleşik imzalama arka uçlarının tümü onu 32'de sabitler ve aynı sayıyı platform çağrısına geçirir: Windows'ta NCryptSignHash'e verilen BCRYPT_PSS_PADDING_INFO içindeki cbSalt; PKCS#11 token için CK_RSA_PKCS_PSS_PARAMS içindeki sLen; OpenSSL altında EVP_PKEY_CTX_set_rsa_pss_saltlen; macOS'ta kSecKeyAlgorithmRSASignatureDigestPSSSHA256'ın ima ettiği özet uzunluğundaki salt
Kendi ICmsSigner'ınız için GetSignatureAlgorithmParams yazmak
PDFium Component'te her imzalama arka ucu, FPdfCms.pas'tan ICmsSigner arayüzünü uygular ve parametre bloğu tek bir yöntemden, GetSignatureAlgorithmParams'tan gelir. CMS kurucusu, sonucu GetSignatureAlgorithmOid'den gelen OID ile TDerWriter.AlgIdWithParams üzerinden eşler. Bu yöntemle, yerleşik arka uçların kapsamadığı bir imzalayıcıyı — ağ HSM'i, bir imzalama servisi, akıllı kart sürücüsü — ConfigurePadesSignerProvider üzerinden takarken tanışırsınız. İnsanların ilk yazdığı kodlama, sarmalanmaya açıkça ihtiyaçları olduğu için iki AlgorithmIdentifier'i etiketler ve iki tamsayıyı çıplak bırakır:
// Sık görülen ilk deneme: [0] ve [1] etiketli, [2] ve [3] değil
Result := W.Sequence(Concat4(
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'),
W.AlgId('2.16.840.1.101.3.4.2.1'))), True),
W.IntegerOf(32), // [2] EXPLICIT gereken yerde çıplak INTEGER
W.IntegerOf(1))); // DEFAULT'ına eşit trailerField: hiç bulunmamalı
Blok, bir decoder gibi yürüyerek inceleyin. A0'ı görür ve hash'i okur; A1'i görür ve maske üretim işlevini okur; sonra 02 01 20 ile, evrensel bir INTEGER ile karşılaşır. RSASSA-PSS-params'ın hiçbir yerde etiketsiz bir INTEGER üyesi yoktur; katı bir decoder durur. Hoşgörülü olan bilinmeyen ögeyi atlar, hiçbir zaman bir A2 bulamaz, varsayılan 20 salt'ı atar ve ardından ikinci başıboş INTEGER olan 02 01 01'e takılır. İki yol da 32'de bitmez
Kurallara uyan sürüm, FPdfAsn1.pas'tan TDerWriter.ContextSpecific'e üç çağrı yapar; varsayılanından sapan alan başına bir tane, her biri sonucun açık bir sarmalayıcı olması için Constructed True ile. Trailer alanı için hiçbir satır yoktur. Windows CNG, macOS Keychain, PKCS#11 ve OpenSSL arka uçlarının yaydığı blok budur; burada özel bir imzalayıcının taşıyacağı biçimde yazılmıştır:
uses
System.SysUtils, FPdfSha256, FPdfAsn1, FPdfCms;
type
TMyHsmSigner = class(TInterfacedObject, ICmsSigner)
private
FUsePss: Boolean;
public
function GetDigestAlgorithmOid: AnsiString;
function GetSignatureAlgorithmOid: AnsiString;
function GetSignatureAlgorithmParams: TBytes;
function GetSignerId: TCmsSignerId;
function SignSignedAttrsDigest(const Digest: TSHA256Digest): TBytes;
end;
// Kendi sınıfınız; öbür dört yöntem HSM'nizle konuşur, burada gösterilmiyor
function TMyHsmSigner.GetSignatureAlgorithmParams: TBytes;
var
W: TDerWriter;
begin
if not FUsePss then
Exit(nil); // v1.5 ve ECDSA: AlgIdWithParams, nil için NULL yazar
W := TDerWriter.Create;
try
// RFC 4055 3.1: [0] hash, [1] kendi hash'iyle MGF1, [2] salt.
// [3] trailerField = 1, DEFAULT'ına eşittir; X.690 11.5 onu atlar
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;
TDerWriter.AlgId, NULL parametreli bir AlgorithmIdentifier yazar; RFC 4055 §2.1, kodlayıcılara iç içe SHA-256 tanımlayıcıları için tam olarak bunu üretmelerini söyler. Ortaya çıkan baytlar gözle denetlenecek kadar kısadır: dışta bir 30 34, 15 baytlık SHA-256 AlgorithmIdentifier'ının çevresinde A0 0F, 28 baytlık MGF1 AlgorithmIdentifier'ının çevresinde A1 1C ve salt için A2 03 02 01 20. GetSignatureAlgorithmParams içinde yazdığınız salt, HSM'inizin gerçekten kullandığıyla eşleşmek zorundadır; servis yalnızca "PSS with SHA-256" sunuyor ve salt uzunluğunu söylemiyorsa, gemiye vermeden önce öğrenin; çünkü AlgorithmIdentifier onunla ilgili bir iddiadır
trailerField neden [3] olarak etiketlenmek yerine atlanıyor?
X.690 §11.5, bir DER kodlayıcının değeri DEFAULT'ına eşit olan bileşeni kodlamaması gerektiğini söyler. trailerField'ın DEFAULT'ı, 1 tamsayısı olan trailerFieldBC'dir ve RFC 8017'nin tanımladığı hâliyle PSS yalnızca o trailer'ı kullanır. İlk denemenin apaçık onarımı — W.IntegerOf(1)'i W.ContextSpecific(3, W.IntegerOf(1), True) ile değiştirmek — sonuçta A3 03 02 01 01 üretir; buna hoşgörülü bir BER decoder kabul eder, katı bir DER decoder reddetme hakkına sahiptir. Değer doğrudur. Varlığı doğru değildir
Aynı kural, öbür üç alanın neden göründüğünü de açıklar: SHA-256, sha1 değildir; SHA-256 ile MGF1, mgf1SHA1 değildir; 32, 20 değildir. 20 baytlık saltla SHA-1 kullanan bir imzalayıcı tüm bloğu boş bir SEQUENCE'e, 30 00'a indirgerdi. Bu biçim, "parametre yok"un her zaman 05 00 yazıldığını varsayan herkesi şaşırtır; CMS'te id-RSASSA-PSS için parametre alanı zorunludur, boş bir SEQUENCE "tüm varsayılanlar" demektir ve NULL geçerli bir RSASSA-PSS-params değeri değildir
BER, bir kodlayıcının varsayılan değerli bileşeni içermesine izin verir; DER yasaklar, çünkü DER bir değerin tam olarak tek bir kodlaması olsun diye vardır ve iki yasal kodlaması olan bir yapı üzerine atılan imza tartışmaya açıktır. Parametre bloğu burada DER'den kaçamaz. SignerInfo'nun dış signatureAlgorithm'ında durur ve RFC 6211'den cmsAlgorithmProtection özniteliği aracılığıyla imzalı özniteliklerin içine kopyalanır; her bayt imzanın kapsamındadır
Kendi kurmadığınız bir CMS'i denetlemek
cmsAlgorithmProtection içindeki kopya, yanlış etiketlenmiş bir bloğu yakalamaz. Öznitelik ile dış signatureAlgorithm aynı çağrıdan gelir; her zaman eşleşirler ve yapıyı kendisiyle karşılaştıran bir denetim geçer. Sorunu yakalayan şey, parametre alanını alan alan okuyan bir decoder'dır. PDFium Component'in FPdfPades.pas içindeki InspectPadesSignatureAlgorithm'ı tam olarak bunu yapar: yalnızca A0 ile A3 arasını, her birini en fazla bir kez ve artan sırada kabul eder; olmayanlar için RFC 4055 varsayılanlarını doldurur ve etkin değerleri TPadesRsaPssParameters içinde raporlar. Bloktaki çıplak bir INTEGER, SignaturePss.Valid'i False bırakır; EvaluatePadesSignatureAlgorithm da PolicyStatus'i pcsInvalid yapar. SignerInfo özet algoritmasından farklı bir PSS hash'ini de reddeder; hoşgörülü bir sha1 düşüşünün üreteceği sonuç tam olarak budur
Bu, özellikle uzaktan imzada önemlidir; harici bir servis hazır bir CMS döndürür ve CompletePadesRemoteSignature onu hazırlanmış PDF'in içine gömer. Blobu gömerken denetleyin. O noktada reddetmek bir API çağrısına mal olur; bir müşterinin doğrulayıcısından öğrenmek ise servisi işleten kişiyle bir yeniden imza turuna mal olur:
uses
System.SysUtils, FPdfPades;
// CompletePadesRemoteSignature öncesinde kendinizin ekleyeceği önerilen denetim
function CheckRemotePssCms(const CmsDer: TBytes; out Why: string): Boolean;
const
OID_RSASSA_PSS = '1.2.840.113549.1.1.10';
var
Info: TPadesSignatureAlgorithmInfo;
Values: string;
begin
Result := False;
Why := '';
// nil: imza sertifikasını CMS içinde bulmayı denetçiye bırakır
if not InspectPadesSignatureAlgorithm(CmsDer, nil, Info) then
begin
Why := 'CMS SignerInfo could not be parsed';
Exit;
end;
if Info.SignatureAlgorithmOid = OID_RSASSA_PSS then
begin
if not Info.SignaturePss.Valid then
begin
Why := 'RSASSA-PSS-params does not decode as RFC 4055 3.1';
Exit;
end;
// Etkin değerler, varsayılanlar dahil: eksik bir A2 burada 20 görünür
Why := Format('hash %s, MGF hash %s, salt %d', [
string(Info.SignaturePss.HashAlgorithmOid),
string(Info.SignaturePss.MaskGenHashAlgorithmOid),
Info.SignaturePss.SaltLength]);
end;
if Info.PolicyStatus = pcsInvalid then
Exit; // Why, günlük için çözülmüş değerleri hâlâ tutuyor
Values := Why;
Result := VerifyCmsAlgorithmProtection(CmsDer, Why);
if Result then
Why := Values; // koruma denetimi, başarı hâlinde Why'yi temizler
end;
Why dizgesini başarı durumunda da günlüğe yazın. SHA-256 hash'inin yanında bir 20 salt, A2'si kaybolmuş bir parametre bloğunun parmak izidir; servisin kendi günlükleri ne derse desin. Kodsuz hızlı bir bakış için openssl asn1parse -inform DER -in signature.p7s ağacı basar; id-RSASSA-PSS OID'sinin altında, 20 hex'lik bir INTEGER'ı saran bir cont [ 2 ] görmek ve hiçbir cont [ 3 ] görmemek istersiniz. O SEQUENCE'in içindeki üst düzey bir INTEGER, ilk kod bloğundaki yanlış etiketli kodlamadır
Burasının PSS öyküsünün geri kalanıyla ilişkisi
Parametre kodlaması, PSS'in bir PAdES imzasında yanlış gidebileceği üç ayrı yoldan biridir ve üçünü ayrı tutmak hata ayıklamayı kısaltır. macOS'ta belirli bir anahtar ya da eski bir sistem PSS sunmayabilir; arka uç o zaman PKCS#1 v1.5 ile imzalar ve bunu karşılamak üzere sha256WithRSAEncryption bildirir; o yetenek sorunu, macOS Keychain kimliğiyle PAdES imzalama içinde ele alınır. Bir PKCS#11 token, CK_RSA_PKCS_PSS_PARAMS'i çağıranın yazdığından farklı bir tamsayı genişliğinde okuyabilir; CK_ULONG ve PKCS#11 paketleme tuzağı içindeki ABI sorunudur. Üçüncüsü yukarıdaki sorundur: anahtar isteklidir, token doğru baytları hesaplar ama onları tanımlayan DER, RFC 4055 §3.1'i izlemez
İmzalı özniteliklerin SET'i de kendi öz-tutarlılık körlüğü sürümüne sahiptir; bir sırada hash'lenip başka bir sırada yayılan SET, yabancı bir doğrulayıcı hash'i yeniden hesaplayana dek sorunsuz görünür; bkz. CMS signedAttrs ve DER SET OF sıralaması. Yukarıdaki denetimin takıldığı hazırla-uzaktan imzala-tamamla döngüsü için HSM ile uzaktan PAdES imzalama, iki aşamalı protokolü ve tamamlama adımının neleri reddettiğini anlatır
PDFium Delphi component, imzalama arka uçlarını kaynak olarak dağıtır; Windows, macOS, PKCS#11 ve OpenSSL birimlerindeki GetSignatureAlgorithmParams gövdeleri, kendi imzalayıcınızın çıktısının yanına bayt bayt okumak ve dökmek için oradadır