Teknik Makale

PDFium Delphi'de RFC 4055'e Göre RSASSA-PSS-params Kodlama

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:

AlanEtiket (DER baytı)DEFAULTETSI TS 119 312 §7 değeriKodlanır mı?
hashAlgorithm[0] (A0)sha1SHA-256Evet
maskGenAlgorithm[1] (A1)mgf1SHA1SHA-256 ile MGF1Evet
saltLength[2] (A2)2032 (özet uzunluğu)Evet
trailerField[3] (A3)trailerFieldBC (1)1Hayı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

RFC 4055'e göre RSASSA-PSS-params'ın PDFium Component şeması: hashAlgorithm A0, maskGenAlgorithm A1 ve saltLength A2, DEFAULT değerleriyle açık bağlam etiketleri taşır; ETSI profili SHA-256, SHA-256 ile MGF1 ve 32 salt yayar; trailerField A3, trailerFieldBC'ye eşittir ve DER onu tümüyle atlar
Her alan tam olarak kendi varsayılanı üzerinden isteğe bağlı olduğu için etiketlidir; böylece etiket numarası, kodlayıcının komşulardan hangilerini bıraktığına bakmaksızın alanı belirler

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

RSASSA-PSS-params etiketleme tuzağı şeması: A0 ve A1'in ardından yanlış etiketli blok, A2 açık etiketinin yerine çıplak bir 02 01 20 ile karşılaşır; katı decoder durur, hoşgörülü olan varsayılan 20 baytlık salt'a döner; doğru kodlama ise 32 baytlık saltı A2 içine sarar
RSASSA-PSS-params'ın etiketsiz bir INTEGER üyesi yoktur; oradaki başıboş baytlar ya bir ayrıştırma hatasıdır ya da varsayılan salt uzunluğuna sessiz bir dönüş ve iki yol da imzalayıcının 32'sine ulaşmaz

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

PSS parametrelerinde X.690 11.5 kuralının PDFium Component şeması: DEFAULT'ı trailerFieldBC'ye eşit olan trailerField yok kalmalıdır; çünkü etiketli bir A3 sarmalayıcısı, katı DER'in reddettiği kodlamadır; buna karşın saltLength 32, varsayılan 20'den farklıdır ve A2 olarak bulunmak zorundadır
DER, bir değerin tam olarak bir kodlaması olması için vardır ve varsayılanına eşit bir bileşenin zaten var olan en kısa kodlaması, hiç görünmemektir

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