Delphi向けPDFium Componentは、ECDSAとEdDSAのPDF署名すべてをISO/TS 32002のアルゴリズムプロファイルに照らして検査し、判定をTPadesSignatureValidation.AlgorithmPolicyStatusで報告します。合格できるのはP-256、P-384、P-521、3つのBrainpool r1曲線、Ed25519、Ed448だけで、それぞれ一致するダイジェストが必要です。不一致はppeiSignatureAlgorithmMismatchを立ち上げます。このポリシーは聞こえ以上に重要です。brainpoolP160r1上の署名や、SHA-512ダイジェストに署名するP-256鍵は、数学レベルでは完璧に検証できます。だからWindows CryptoAPIは署名値を良好と報告するのに、厳格なPDF 2.0バリデーターはファイルを拒否します。ポリシー検査はその隙間を閉じます。しかも意図的に、署名バイトが暗号学的に正しいかという問いとは分けてあります
ISO/TS 32002は楕円曲線署名に実際何を許すのか
ISO/TS 32002がPDF署名に許すのは、ちょうど6つのECDSA曲線と2つのEdDSAスキームで、各曲線は運んでよいダイジェストサイズに結び付いています。PDFium Componentはその表をPadesCurveDigestAllowedに符号化し、署名者証明書の曲線OIDで引きます。NIST曲線は厳格です。ダイジェストは曲線と同じビット幅でなければならず、SHA-2かSHA-3かのどちらかです。Brainpool曲線は緩く、自分の幅か、それより広いものなら何でも受け付けます
- P-256(
1.2.840.10045.3.1.7):SHA-256またはSHA3-256のみ - P-384(
1.3.132.0.34):SHA-384またはSHA3-384のみ - P-521(
1.3.132.0.35):SHA-512またはSHA3-512のみ - brainpoolP256r1(
1.3.36.3.3.2.8.1.1.7):256から512ビットの任意のSHA-2 / SHA-3ダイジェスト - brainpoolP384r1(
1.3.36.3.3.2.8.1.1.11):384ビットまたは512ビットのSHA-2 / SHA-3 - brainpoolP512r1(
1.3.36.3.3.2.8.1.1.13):SHA-512またはSHA3-512のみ
EdDSAには曲線の選択もダイジェストの選択もありません。だからこそ規則は強度ではなく符号化についてです。RFC 8419によれば、Ed25519のSignerInfoはパラメータなしでSHA-512をdigestAlgorithmと宣言しなければならず、PAdESが常に使うsigned-attributes経路のEd448 SignerInfoは、ちょうど512のINTEGERパラメータ付きでid-shake256-len(2.16.840.1.101.3.4.2.18)を宣言しなければなりません。両スキームとも、署名のAlgorithmIdentifierと証明書公開鍵のAlgorithmIdentifierは、パラメータを一切運んではいけません。ここにNULLを書くプロデューサー、RSAエンコーダーが多くのASN.1ライブラリに染み込ませた癖です、は鍵も署名値も正常なまま、不適合の署名を作り出します
PDFium ComponentがアルゴリズムのトリプルをCMSから取り出す方法
PDFium自身にはこの問いに答えられません。公開署名APIは署名辞書を読みますが、CMSを検証せず、署名者証明書の曲線も露出しないからです。そこでPDFiumの上に築かれたPAdES検査レイヤーが、CMS SignedData(RFC 5652)を自力でパースします。InspectPadesSignatureAlgorithmは最初のSignerInfoのdigestAlgorithmとsignatureAlgorithmを読み、それから署名者証明書を探して、SubjectPublicKeyInfoから鍵アルゴリズムと曲線を読みます。証明書の探索は意図的に有界です。CMSのcertificates集合からは最大64証明書までしか調べず、マッチはissuerAndSerialNumberからの発行者とシリアル番号の完全なバイト比較であり、「そこにある唯一の証明書」へのフォールバックは、集合がパース可能な証明書をちょうど1つだけ持つときに限られます。順序なし集合から最初のEC証明書を拾うのは簡単ですし、それでCA証明書が署名者の使ったはずの曲線を決めてしまうことになります
uses
PDFium, FPdfPades;
const
StatusNames: array[TPadesCryptoStatus] of string =
('not checked', 'valid', 'invalid', 'unsupported', 'indeterminate');
var
Pdf: TPdf;
R: TPadesValidationResult;
A: TPadesSignatureAlgorithmInfo;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'invoice-ecdsa-signed.pdf';
Pdf.Active := True;
R := Pdf.ValidatePades;
for I := 0 to Length(R.Signatures) - 1 do
begin
A := R.Signatures[I].AlgorithmInfo;
Writeln('Signature ', I);
Writeln(' digestAlgorithm : ', string(A.DigestAlgorithmOid));
Writeln(' signatureAlgorithm : ', string(A.SignatureAlgorithmOid));
Writeln(' public key / curve : ', string(A.PublicKeyAlgorithmOid),
' / ', string(A.CurveOid));
Writeln(' policy : ',
StatusNames[R.Signatures[I].AlgorithmPolicyStatus]);
end;
if ppeiSignatureAlgorithmMismatch in R.Issues then
Writeln('At least one signature violates the ISO/TS 32002 profile');
finally
Pdf.Free;
end;
end;
TPdf.ValidatePadesは適合パスの一部としてポリシーを適用します。出発点はCMS内部で見つけた証明書です。TPdf.ValidatePadesTrustは、Windows CryptoAPIが検証に実際に使った署名者証明書に対して再実行します。つまり最終判断権はCryptoAPIが報告した証明書にあります。生の入力はすべてTPadesSignatureAlgorithmInfoに着地します。DigestParametersPresent、DigestParameterBits、SignatureParametersPresent、PublicKeyParametersAreNamedCurveも含まれるので、拒否はいつでもログ行ではなくレコードから説明できます
SHA3-256のP-256署名がポリシーに落ちる理由
P-256署名がPDFium Componentのポリシーに落ちるのは、CMSのdigestAlgorithmと、ECDSAのsignatureAlgorithmが含意するダイジェストが食い違うときです。両方が曲線に対して個別に許容可能でもです。EvaluatePadesSignatureAlgorithmはまずecdsa-with-SHA256、ecdsa-with-SHA3-256とその兄弟をダイジェストへマップし、宣言されたdigestAlgorithmと比較して、曲線表を引く前に違いがあればpcsInvalidを返します。このケースは実在します。署名ツールがハッシュをSHA3-256へ切り替えながら、ハードコードしたecdsa-with-SHA256識別子をそのままにする。結果は、適合するバリデーターのどれも一貫して解釈できないファイルです。関数は公開されているので、マトリクスはPDFを組み立てずにユニットテストで固定できます
var
Info: TPadesSignatureAlgorithmInfo;
begin
Info := Default(TPadesSignatureAlgorithmInfo);
Info.Family := psafEcdsa;
Info.PublicKeyAlgorithmOid := '1.2.840.10045.2.1'; // id-ecPublicKey
Info.PublicKeyParametersPresent := True;
Info.PublicKeyParametersAreNamedCurve := True;
Info.CurveOid := '1.2.840.10045.3.1.7'; // P-256
Info.DigestAlgorithmOid := '2.16.840.1.101.3.4.2.8'; // SHA3-256
Info.SignatureAlgorithmOid := '2.16.840.1.101.3.4.3.10'; // ecdsa-with-SHA3-256
Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsValid);
Info.SignatureAlgorithmOid := '1.2.840.10045.4.3.2'; // ecdsa-with-SHA256
Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsInvalid); // ダイジェスト不一致
Info.CurveOid := '1.3.36.3.3.2.8.1.1.1'; // brainpoolP160r1
Info.DigestAlgorithmOid := '2.16.840.1.101.3.4.2.1'; // SHA-256。いまは一致
Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsUnsupported); // プロファイルにない曲線
end;
曲線の符号化にも同じ厳格さが適用されます。RFC 5480 §2.1.1はECParametersを名前付き曲線OID、暗黙曲線(NULL)、完全な明示的パラメータ集合のいずれかに許し、PKIXプロファイルは名前付きを要求します。PDFium Componentは、id-ecPublicKey証明書がパラメータなし、暗黙パラメータ、明示パラメータのいずれかを運ぶときpcsInvalidを返します。明示パラメータは、攻撃者に標準曲線に「似ただけ」の曲線を記述させてしまうからです。ISO/TS 32002のリストに単に載っていない、正しく名前付きの曲線、先のbrainpoolP160r1やsecp256k1には、代わりにpcsUnsupportedが付きます
無効、未サポート、不定:ステータスを正直に読む
AlgorithmPolicyStatusの3つの非validステータスは、それぞれ意味が違い、1つの「失敗」バケツにまとめると監査人に必要な情報が捨てられます。pcsInvalidは、認識されたアルゴリズムの組み合わせが奇形か不一致であることを意味します。TPadesValidationResult.IssuesへppeiSignatureAlgorithmMismatchを加え、集計のIntegrityStatusをpcsInvalidへ押し下げるので、CMS署名値が合格していてもIsCryptographicallyValidはFalseを返します。pcsUnsupportedは、曲線かダイジェストがプロファイルの名指す範囲の外であること、つまり能力の結果であって改ざんの証明ではありません。pcsIndeterminateは、署名者証明書を特定できなかったことです。たいていは候補を複数持つCertificateSetでissuerAndSerialNumberの完全一致がなく、コードは曲線の推測を拒みます。v3.124.0以降は、SHA-1やSHA-224のような112ビットダイジェスト上のRSA署名も印を付けます。現行の検証ではもはや合意されていないからです。同じ分かれ道は、CryptoAPIがEd25519やEd448を検証できないマシン上のEdDSAにも当てはまります。符号化は正しくて、欠けていたのは検証器だけだったので、CmsSignatureStatusはpcsUnsupportedのまま、AlgorithmPolicyStatusはpcsValidでいられます。AdobeやDSS系バリデーターの拒否を追いかけているなら、バリデーターがPAdES署名を拒否する理由のガイドが他のよくある原因を扱っています
var
Pdf: TPdf;
Options: TPadesTrustValidationOptions;
R: TPadesValidationResult;
S: TPadesSignatureValidation;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'invoice-ecdsa-signed.pdf';
Pdf.Active := True;
Options := TPadesTrustValidationOptions.Default; // オフライン、失効確認なし
R := Pdf.ValidatePadesTrust(Options);
for I := 0 to Length(R.Signatures) - 1 do
begin
S := R.Signatures[I];
if S.IsDocumentTimeStamp then
Continue;
case S.AlgorithmPolicyStatus of
pcsInvalid: Writeln(I, ': reject, algorithm combination is invalid');
pcsUnsupported: Writeln(I, ': manual review, curve or digest outside the profile');
pcsIndeterminate: Writeln(I, ': manual review, signer not identified or digest no longer agreed');
pcsValid:
if S.AlgorithmInfo.Family = psafRsa then
Writeln(I, ': RSA digest suite accepted, check key size yourself')
else
Writeln(I, ': EC/EdDSA profile satisfied');
end;
end;
Writeln('Cryptographically valid: ', R.IsCryptographicallyValid);
finally
Pdf.Free;
end;
end;
pcsValidが保証しないもの
AlgorithmPolicyStatus = pcsValidが証明するのは、ECDSAかEdDSAの署名が承認曲線と、一致し正しく符号化されたダイジェストを使っていること、RSA署名が現行スイートのダイジェストを使っていることだけです。署名値が正しいかについては何も言いません。v3.124.0より前、EvaluatePadesSignatureAlgorithmのRSA分岐は意図的に広く、PKCS #1アーク1.2.840.113549.1.1.*の下のsignatureAlgorithmは何でもpcsValidを返しました。レガシーのsha1WithRSAEncryptionも含めてです。PDFiumPas v3.124.0以降、RSA分岐はETSI TS 119 312の署名スイートを適用します。MD2、MD4、MD5ダイジェストはpcsInvalidです。SHA-1やSHA-224のような112ビットダイジェストはpcsIndeterminateなので、SHA-1署名は全体の整合性の結果を保ち、拒否ではなく要審査の印を付きます。署名アルゴリズムが固定するダイジェストと違うdigestAlgorithm、SHA-1ダイジェスト上のsha256WithRSAEncryptionや、非RSA署名者鍵へのRSA署名アルゴリズムはpcsInvalidで、ppeiSignatureAlgorithmMismatchとして報告され、整合性に落ちます。見つからない署名者証明書は、ECDSAでそうだったようにpcsIndeterminateを返し、認識できないダイジェストや非署名のRSA OIDはpcsUnsupportedです。モジュラス長は依然として検査せず、PSSパラメータはここでは検証しません(署名側での符号化はRFC 4055のRSASSA-PSS-params記事が扱います)。さらにSHA-1やMD5ダイジェストは、別個のppeiBadDigestAlgorithm問題も立ち上げます。同様に、署名の数学、証明書チェーン、失効は、Windows CryptoAPIが供給するCmsSignatureStatus、CertificateTrustStatus、RevocationStatusの仕事のままです。pcsValidは「アルゴリズムプロファイルが成立している」と読み、「この鍵は十分強い」とは決して読まないことです
署名付きPDFの請求書、契約書、アーカイブパッケージを受け付けるDelphiアプリケーションにとって、実用的な設定は短く済みます。ValidatePadesTrustを回し、ppeiSignatureAlgorithmMismatchでは拒否し、pcsUnsupportedとpcsIndeterminateは人間へ回し、RSA鍵サイズの下限はポリシーが見ないぶん自分で強制する。DelphiとLazarus向けPDFium ComponentにはPAdESバリデーター、エビデンスレポートビルダー、署名パイプラインが搭載されているので、同じライブラリで署名の生成から末端までの検査までこなせます