PDFiumPasは、PAdES署名を2つの呼び出しに分割し、秘密鍵が自分のプロセス内に存在する必要を一切なくします。PreparePadesRemoteSignatureは、空の固定幅/Contentsプレースホルダーを伴う増分更新を書き込み、SHA-256のドキュメントダイジェスト、正確なByteRange、準備済みファイルのフィンガープリントを運ぶリクエストレコードを返します。CompletePadesRemoteSignatureは、署名サービスが返してきたデタッチCMSを受け取り、その予約済みスロットへ収めます
この2つの呼び出しの間には、数分から数時間が経過することもあれば、プロセスが再起動することも、作業が別のマシンに移ることもあります。このAPIがこのような形をしている理由は、まさにその間隔にあります
リモート鍵で通常の署名呼び出しが使えないのはなぜか
なぜなら、SignPadesBytesは署名操作が呼び出しの内部で完結することを前提としているからです。この関数は、増分更新を構築し、ByteRangeに対するダイジェストを計算し、署名し、結果を書き込む、というすべてを戻る前に行います。鍵がWindows証明書ストアや自分が読み込んだPKCS#12ファイルの中に存在する場合には、これはまさに正しい動作です
しかし、鍵がネットワークHSM、トラストサービスプロバイダーが運用する適格署名生成装置、あるいはユーザーがスマートフォンでの確認を必要とするクラウド署名APIの中にある場合、これは不可能です。そうしたケースでは、この一連の流れは関数呼び出しではなく、対話です。自分はダイジェストを送信し、何か別のものが人間を認証し、CMSは後になって返ってきます。同期APIは、第二要素を必要とするかもしれない操作にスレッドをブロックさせることなく「後で」を表現することはできません
2段階プロトコル
フェーズ1はドキュメントを準備します。PDFiumPasは署名フィールドと値辞書を追加し、/Contentsに16進エンコードされたContentsSizeバイト分の領域を予約し、その予約領域の周囲にByteRangeを計算し、FormatVersion、PreparedFingerprint、DocumentDigest、4要素のByteRange、ContentsHexOffset、ContentsSizeを含むTPadesRemoteSigningRequestを生成します
自分の署名サービスが必要とする値はDocumentDigestだけです。これは、返されるCAdES SignedDataがメッセージダイジェストとして運ばなければならないSHA-256です。このレコード内の他のすべては、フェーズ2が、完成させようとしているファイルが、そのダイジェストの計算元となったファイルと同一であることを証明できるようにするために存在しています
uses
FPdfPades;
var
Options: TPadesRemoteSignOptions;
Request: TPadesRemoteSigningRequest;
Source, Prepared, Session: TFileStream;
begin
Options := TPadesRemoteSignOptions.Default;
Options.Reason := 'Approved by finance';
Options.Location := 'Lisbon';
Options.Name := 'A. Moreira';
Options.SigningTimeUtc := NowUtc;
Options.ContentsSize := 16384; // CMS用に予約された16進バイト数
Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
Prepared := TFileStream.Create('contract.prepared.pdf', fmCreate);
try
PreparePadesRemoteSignature(Source, Prepared, Options, Request);
finally
Prepared.Free;
Source.Free;
end;
// 後で別の実行から、あるいは別のマシンから完了できるよう、セッションを永続化する
Session := TFileStream.Create('contract.signreq', fmCreate);
try
SavePadesRemoteSigningRequest(Session, Request);
finally
Session.Free;
end;
SendDigestToSigningService(Request.DocumentDigest);
end;
Completeは何を拒否するのか、そして各チェックが存在する理由
完了処理こそが、リモート署名の設計が通常うまくいかなくなる場所であり、そのため検証は意図的に容赦のないものになっています。CompletePadesRemoteSignatureは、フィンガープリントがもはやリクエストと一致しない準備済みPDF、記録されたプレースホルダー座標と一致しないByteRange、変更された/Contentsデリミタ、もはや空ではないプレースホルダー、予約領域より大きいCMS、正確に1つのDER値ではないCMS、非対応のSignedData形状、signing-certificate-v2属性の欠落、そしてメッセージダイジェストが準備済みドキュメントダイジェストと一致しないCMSを拒否します
これらはそれぞれ、実際の障害に対応しています。フィンガープリントとByteRangeのチェックは、フェーズの間に誰かが準備済みファイルを再生成してしまったケースを捕捉します。これを見逃すと、誰も持っていないバイト列に対して検証が通ってしまう署名が生まれてしまいます。空プレースホルダーのチェックは二重完了、つまり、すでに存在する署名の上に2つ目のCMSが書き込まれてしまうケースを捕捉します。メッセージダイジェストのチェックは、最も危険なケース、つまり別のドキュメントに対して正しく署名されたCMSを捕捉します。これは、キューが2つの並行する署名セッションを取り違えたときに起こることです。このチェックがなければ、署名されているように見えてどこでも検証に失敗するファイル、あるいはもっと悪いことに、他人の承認を運んでしまうファイルが出来上がってしまいます
signing-certificate-v2の要件は、整合性の問題というよりもPAdES準拠の問題です。ETSI EN 319 142は、署名証明書が署名属性の中に結びつけられていることを要求しており、その属性を欠くCMSは、たとえ暗号学的に検証できたとしても、PAdES署名ではありません。完了時にこれを拒否するということは、顧客からの検証ツールの報告書ではなく、ここでそれに気づけるということです。この話題は検証ツールがPAdES署名を拒否する理由でさらに掘り下げています
var
Request: TPadesRemoteSigningRequest;
Session, Prepared, Dest: TFileStream;
CmsDer: TBytes;
begin
Session := TFileStream.Create('contract.signreq', fmOpenRead);
try
Request := LoadPadesRemoteSigningRequest(Session);
finally
Session.Free;
end;
CmsDer := FetchDetachedCmsFromService; // HSMまたはTSPから返される
Prepared := TFileStream.Create('contract.prepared.pdf', fmOpenRead);
Dest := TFileStream.Create('contract.signed.pdf', fmCreate);
try
try
CompletePadesRemoteSignature(Prepared, Dest, Request, CmsDer);
except
on E: EPadesCrypto do
// すべての拒否には具体的な理由が伴う。そのままログに残す
FailSession(E.Message);
end;
finally
Dest.Free;
Prepared.Free;
end;
end;
プロセスとマシンの境界を越える
SavePadesRemoteSigningRequestとLoadPadesRemoteSigningRequestは、セッションを安定したバージョン管理付きのバイナリ形式でシリアライズし、これによってこの設計は単に正しいだけでなく実用的なものになっています。Webアプリケーションは、あるリクエストの中でドキュメントを準備し、準備済みPDFとセッションのバイナリデータを保存し、スマートカード署名のためにダイジェストをブラウザへ返し、まったく別のリクエストハンドラーでファイルを完成させることができます
FormatVersionフィールドこそが、アップグレードをまたいでもこれを安全に保っています。古いビルドで書かれ、新しいビルドで読み込まれたセッションは、形の違うレコードとして誤読されるのではなく、明示的に認識または拒否されます。自分のキューがセッションを何日も保持できるなら、このフォーマットバージョンを、単なる実装の詳細ではなくログに残す価値のある運用上の事実として扱ってください
プレースホルダーのサイズ決定
ContentsSizeは、CMSが存在するより前に固定されるため、真剣に考えておかなければならない唯一のパラメータです。これは16進エンコードされた予約領域を数えるものであるため、6KBのDER CMSには少なくとも12KBの領域が必要であり、実装はこの予約を64MiBに制限しています
予約を少なくしすぎると、署名サービスがすでに作業を終えた後になってから、完了処理が「CMSが大きすぎる」というエラーで失敗し、従量制の適格署名サービスでは無駄な操作になってしまいます。予約を多くしすぎると、署名済みのすべてのドキュメントが、そのパディングを永久に運び続けることになります。賢明なアプローチは実測です。実際の証明書チェーンで1つのドキュメントに署名し、DERの長さを確認し、16進化のために2倍にし、T-レベルの署名へアップグレードする予定があるならタイムスタンプトークン用にたっぷりと余裕を追加してください。複数の中間証明書と長いOCSPレスポンスを持つチェーンは、人が思うよりも速く大きくなります
署名の後に来るもの
完了したリモート署名はPAdES B-Bです。長期検証にはタイムスタンプと検証用素材が必要であり、これはDSSと署名ごとのVRI辞書を追加する別の増分更新であり、RFC 3161タイムスタンプとDSSによる長期署名で説明しています。このステップはローカルで完結します。追加するのは証明書、OCSPレスポンス、CRLであり、そのいずれも秘密鍵を必要としません
出荷する前に、依拠当事者が使うのと同じコード経路で、自分が生成したものを検証してください。これはデジタル署名とPAdESレベルの検査で扱っています。署名と検証は別のコードであり、リモート署名パイプラインは、外部の検証ツールが指摘するまで誰も気づかないまま、その2つが乖離してしまうまさにその場所です
PDFiumPasは、PDFiumエンジンを取り巻くDelphi・Lazarus向けコンポーネントであり、ネイティブなPascal製PAdESスタックを備えているため、署名、タイムスタンプ、検証は外部のコマンドラインツールなしで動作します。完全なAPIドキュメントと評価版はPDFium Delphiコンポーネントページにあります