PDFium Component version 3.114.20は、3つすべてのPAdES署名バックエンド——Windows CNG、macOS Keychain、PKCS#11——でRSASSA-PSS-paramsの符号化を修正しました。RFC 4055 §3.1はRSASSA-PSS-paramsのすべてのフィールドに明示的なコンテキスト固有タグ[0]から[3]を与えており、バックエンドは既定値に等しいtrailerFieldを書き出しながら、saltLengthを裸のユニバーサルINTEGERとして出力していました。署名バイトは最初から正しいままでした。それを記述するAlgorithmIdentifierが正しくなく、それだけで検証器が署名を拒否するのに十分です
腹立たしいのは不具合の隠れ場所です。CMS署名には2つの半身があります。暗号演算と、その演算がどう行われたかを検証器に伝えるASN.1です。前者を正しく後者を誤れば、仕様準拠のツールには偽造と区別がつかない文書ができあがります。この記事はその後半だけを扱います。RSASSA-PSS-paramsをどうタグ付けしなければならないか、3つのバックエンドがどう同じ間違いをしたか、そして修正されたDERがTDerWriterの言葉でどう見えるかです
バイト列が正しいRSASSA-PSS署名を検証器が拒否する理由
RSASSA-PSSが、検証器が署名自体からパラメーターを復元できない唯一のRSA方式だからです。PKCS#1 v1.5のパディングはOID sha256WithRSAEncryptionによって完全に決まるため、そのパラメーターは裸のNULLで、間違えようがありません。PSSはハッシュ関数、それ自身のハッシュを持つマスク生成関数、そしてソルト長でパラメーター化され、RFC 8017 §A.2.3はその3つをすべて開いたままにしています。署名者がそれらを選び、AlgorithmIdentifierがそれらを運び、検証器はEMSA-PSS-VERIFYを始める前にそれらを正確に再現しなければなりません
したがってPDFium ComponentがSHA-256、SHA-256上のMGF1、32バイトのソルトで署名するとき、その3つの事実はここでのDER符号化と、別の実装でのDER復号を生き延びる必要があります。検証器が解析できないパラメーターブロックは、モジュラー冪乗が一度も走る前に検証を終わらせます。それを別の形で解析するブロックはもっと厄介です。RFC 4055 §3.1がsaltLengthに既定値20を与えているからです。認識しないフィールドを読み飛ばすデコーダーはその既定値に着地し、32で計算された署名に対して20バイトのソルトでEMSA-PSS-VERIFYを走らせ、問題が鍵ではなくメタデータにあるというヒントもなしに不正な署名を報告します。どちらの結果も3.114.19の符号化が生み出したもので、検証器がどれだけ厳格かによって決まり、どちらもAlgorithmIdentifierを指しません
RFC 4055 §3.1がRSASSA-PSS-paramsに実際に要求するもの
RFC 4055 §3.1はRSASSA-PSS-paramsを4つのフィールドのSEQUENCEとして定義し、それぞれが明示的なコンテキスト固有タグとDEFAULT値を持ちます
// 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における明示的タグ付けとは、各フィールドが構造型のコンテキスト固有TLV——[0]にはA0、[1]にはA1、[2]にはA2、[3]にはA3——で包まれ、その中に値のユニバーサル符号化が入れ子になることを意味します。すべてのフィールドがタグ付けされるのは、すべてのフィールドが既定値によって任意だからです。タグがなければ、デコーダーは単一のAlgorithmIdentifierを保持するSEQUENCEがhashAlgorithmを運んでいるのかmaskGenAlgorithmを運んでいるのか区別できません。どちらもSEQUENCE型だからです。タグがあれば、どの隣接フィールドが存在していても、タグ番号がフィールドを特定します。PDFium Componentが出力する値はETSI TS 119 312 §7のプロファイルに従い、SHA-256、SHA-256を使うMGF1、ダイジェスト長に等しいソルトで、各プラットフォームの署名呼び出しに伝えられる内容をそのまま写しています。NCryptSignHashにはcbSaltが32のBCRYPT_PSS_PADDING_INFO、PKCS#11メカニズムにはsLenが32のCK_RSA_PKCS_PSS_PARAMS、SecurityフレームワークにはSHA-256ダイジェスト署名PSSアルゴリズムです
3つのバックエンドが同じ間違いをした方法
3.114.19の符号化は最初の2つのフィールドにタグを付け、最後の2つを裸のままにしていました。TWinCmsSigner、TKeychainCmsSigner、TPkcs11CmsSignerでまったく同じです。この対称性は偶然ではありません。3つともFPdfCms.pasのICmsSignerインターフェースを実装しており、それぞれのGetSignatureAlgorithmParamsの本体が1つのテンプレートどおりに書かれていました。テンプレートはこうでした
// 3.114.20より前:[0]と[1]はタグ付き、[2]と[3]はタグなし
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が必要な場所に裸のINTEGER
W.IntegerOf(1))); // DEFAULTに等しいtrailerFieldは省略しなければならない
そのSEQUENCEをたどるデコーダーはA0を見てハッシュアルゴリズムを読み、A1を見てマスク生成関数を読み、次に02 01 20に出会います。それはユニバーサルINTEGERであり、RSASSA-PSS-paramsにはタグなしのINTEGERメンバーがどこにもありません。厳格なデコーダーはそこで止まります。寛容なデコーダーは認識しない要素を読み飛ばし、A2を決して見つけず、saltLengthに既定値20を割り当て、次に2つ目のはぐれたINTEGER02 01 01に当たり、また同じ問題を抱えます。どちらの経路も32バイトのソルトには到達しません。共有テンプレートは正しいときには効率的で、正しくないときには3回間違える同じくらい効率的な方法です。修正が1つのコミットで3つのユニットすべてに入り、修正後も3つのメソッド本体が構造的に同一のままなのはそのためです。将来のバックエンドはこれを自分で導出するのではなく、これらのどれかからブロックをコピーすべきです。間違いが起きたのはまさにその導出だからです
trailerFieldが[3]としてタグ付けされず省略される理由
X.690 §11.5が、値がDEFAULTに等しいコンポーネントをDERエンコーダーは符号化してはならないと述べており、trailerFieldのDEFAULTはtrailerFieldBC、つまり整数1だからです。古いコードへの明白な修正——裸のW.IntegerOf(1)をW.ContextSpecific(3, W.IntegerOf(1), True)に置き換える——は、寛容なBERデコーダーは受け入れるが厳格なDERデコーダーは拒否する権利のあるブロックを生みます。値が間違っているのではありません。存在していることが問題なのです。同じルールが、なぜ他の3つのフィールドは存在するのかの理由でもあります。SHA-256は既定のsha1ではなく、SHA-256を使うMGF1は既定のmgf1SHA1ではなく、32は既定の20ではありません。もしバックエンドがSHA-1と20バイトのソルトで署名していたなら、RFC 4055 §3.1はパラメーターを空のSEQUENCE、30 00に畳み込み、検証器が期待するのはNULLではなくその空のSEQUENCEです。PDFium Componentがその形を出力することはありません。それらの値で署名しないからですが、「パラメーターなし」が常に05 00だと仮定する人を捕まえるのはこのケースです
これが署名にとって特に重要なDERとBERの区別です。BERはエンコーダーが既定値のコンポーネントを含めることを許しますが、DERは禁じます。DERは1つの値がちょうど1つの符号化を持つために存在し、2つの合法な符号化を持つ構造への署名は、議論の余地のある署名になるからです。CMSのsignedAttrs内部がすべてDERであるのはそのためで、パラメーターブロックは外側のsignatureAlgorithmだけでなくcmsAlgorithmProtection属性を通してもsignedAttrsの中を旅するため、免除はありません
修正されたTDerWriterの符号化
PDFium Componentは今、パラメーターをFPdfAsn1.pasのTDerWriter.ContextSpecificへの3回の呼び出しで構築します。非既定のフィールドごとに1回で、それぞれConstructed引数をTrueにして明示タグのラッパーを生成し、trailerフィールドには行が一切ありません。以下はOIDを書き下したTWinCmsSigner.GetSignatureAlgorithmParamsの本体です。KeychainとPKCS#11のユニットは同じ値をOID_SHA256、OID_MGF1、OID_RSASSA_PSSとして綴っています
function TWinCmsSigner.GetSignatureAlgorithmParams: TBytes;
var
W: TDerWriter;
begin
if FPaddingScheme = psRsaPss then
begin
W := TDerWriter.Create;
try
// RFC 4055 3.1は4つのフィールドすべてにタグを付ける。saltLengthは[2]で、
// ここに裸のINTEGERがあると別のフィールドの開始として読まれる。trailerFieldは
// DEFAULT 1を持つ[3]で、X.690 11.5は既定値に等しい値の符号化を禁じるため、
// 完全に省略される
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とECDSA:AlgIdWithParamsがNULLを書く
end;
周辺の仕組みの2つの詳細が重要です。TDerWriter.AlgIdはNULLパラメーターを持つAlgorithmIdentifierを生成します。RFC 4055 §2.1が入れ子のhashAlgorithmとMGF1の内側のハッシュに対してエンコーダーが生成すべきと述べているとおりです。そしてFPdfCms.pasのCMSビルダーは、署名OIDとこれらのバイトをTDerWriter.AlgIdWithParamsを通して組み合わせ、パラメーターがnilのときはNULLを代入します。だからこそpsRsaPkcs1v15とpsEcdsaは単にnilを返し、影響を受けなかったのであり、1.2.840.113549.1.1.10、id-RSASSA-PSSが3つのうち実際のパラメーターブロックを運ぶ唯一の署名OIDなのです。SHA-256プロファイルの結果のバイト列は固定で、目で確認できるほど短いものです。外側の30 34 SEQUENCEが15バイトのSHA-256 AlgorithmIdentifierを包むA0 0Fを保持し、A1 1Cが28バイトのMGF1 AlgorithmIdentifier——そのパラメーターは同じSHA-256 AlgorithmIdentifierです——を包み、ソルトにはA2 03 02 01 20が付きます。あなたのsignatureAlgorithmのダンプで、paramsのSEQUENCEのトップレベルに02 01 20がありA2の内側にないなら、それは3.114.19の符号化を見ています
テストスイートが不正なAlgorithmIdentifierを捕まえなかった理由
PAdESのテストが、sha256WithRSAEncryptionを報告しGetSignatureAlgorithmParamsからnilを返す偽の署名者を通してCMSビルダーを動かしており、PSSのパラメーターブロックがテストで一度も構築されなかったからです。証明書ストア、Keychain、トークンなしで走らなければならないテストとしては妥当な設計で、そこには正確な形の盲点があります。本物のバックエンドだけが生成するものは、本物のバックエンドでしか行使されません。2つ目の層のほうが興味深い。PDFium Componentは署名のAlgorithmIdentifierをパラメーターごとRFC 6211のcmsAlgorithmProtection署名属性の中にも置き、検証器はそのコピーを外側のsignatureAlgorithmと比較します。どちらのコピーも同じ呼び出しから来ているため完全に一致し、内部の整合性チェックはすべて通りました。その符号化は自己整合的で誤っており、構造をそれ自身と比較しても決して明かせない類の不具合です。同じ教訓を別の構造で語っているのがCMS signedAttrsとDER SET OFのソートで、ある順序でハッシュし別の順序で出力されたSETは、外部の検証器がハッシュを計算し直すまで問題なく見えました
この類の不具合を実際に捕まえるのは、エンコーダーの作者が書いていないデコーダーを、実際のバックエンドの実際の出力に対して走らせることです。PDFium ComponentのWindows検証経路はライブラリ自身のリーダーではなくCryptoAPIを通り、そこで拒否されたPSS署名がパラメーターへとたどり着くきっかけでした。実装が他の実装に読ませるために出力するASN.1は、自分が制御しないデコーダーを少なくとも1回は通るべきです。そして構造が持つ既定値とタグが多いほど、その往復の価値は高くなります
PSSの話の残りとの位置関係
この修正は、PAdES署名でPSSが誤り得る他の2つの箇所とは独立しており、それらを分けておくとデバッグが短くなります。macOSバックエンドは、特定の鍵や古いシステムがPSSを拒否することを見つけてPKCS#1 v1.5へダウングレードすることがあり、AlgorithmIdentifierはそのダウングレードに追随しなければなりません。これは能力の問題で、macOS KeychainのIDでPAdESに署名するで扱っています。PKCS#11バックエンドは、整数幅の不一致によりトークンが別の形で読むCK_RSA_PKCS_PSS_PARAMSをトークンに渡すことがあります。これはABIの問題で、CK_ULONGとPKCS#11のパッキングの罠で扱っています。この記事は3つ目の失敗、つまり鍵が応じ、トークンが正しいバイトを計算し、結果を記述するDERがRFC 4055 §3.1に準拠していなかったケースについてです
PSSを宣言する署名者は、v1.5の署名者が決して負わなかった義務を引き受けます。自分のパラメーターを、別の実装が同じ3つの値に復号できる形で記述することです。RFC 4055 §3.1がタグを定め、X.690 §11.5がどのフィールドが現れてよいかを定め、ETSI TS 119 312 §7が選ぶ価値のある値を定めています。PDFium Delphiコンポーネントの3つのバックエンドはすべてソースとして出荷されるため、上のGetSignatureAlgorithmParamsの本体は、信用するのではなく、読んでダンプし、自分の検証器と比較できるものです