HotPDFは、リモートのCloud Signature Consortium(CSC)サービスが握る秘密鍵でPDF文書に署名します。THPDFCSCSignatureProviderという署名プロバイダーがCSC APIを駆動します。credential info、認可、signatures/signHash、ポーリング。HTTPトランスポートとOAuthアクセストークンは、あなたのDelphiアプリケーションが供給します。鍵がサービスのHSMを離れることはありません
これが、qualifiedな署名鍵を手に入れる、ますます唯一の方法になっています。トラストサービスプロバイダーが渡すのはCSCエンドポイントとOAuthクライアントであって、PFXファイルやUSBトークンではありません。だからCNGとCAPIを通したWindows証明書ストア署名のように、ローカルの証明書ストアへ読み込むものが存在しません。素朴な統合は予想通りに失敗します。signHash呼び出しがタイムアウトし、リトライが同じ契約書に2回署名する。あるいは40件の請求書バッチが、ハッシュごとに個別に認可されたせいで、40回のワンタイムパスワードを引き起こす。プロバイダーがやっていることの大半は、この2つの失敗への防御です
HotPDFがHTTPをアプリケーションに任せるのはなぜか
トランスポートこそ、デプロイごとに違う場所だからです。プロキシー、TLSピンニング、クライアント証明書、企業のOAuthボールト、ログポリシーは、みなHTTP層に住んでいます。だからTHPDFCSCSignatureProviderはプロトコルの状態を統制し、リクエストごとにTHPDFCSCTransport関数を呼びます。プロバイダーはTHPDFCSCTransportRequestを渡してきます。Method(常にPOST)、ServiceBaseURL+エンドポイントパスから組み立てられた完全なURL、準備済みのAuthorization bearerヘッダー、ContentType、JSONのBody、IdempotencyKey、Attempt番号、MaxResponseBytes。あなたはStatusCode、Body、RetryAfterMSでTHPDFCSCTransportResponseを埋め、ctsSuccess、ctsTemporaryFailure、ctsPermanentFailure、ctsCancelledのどれかを返します
uses
System.Net.HttpClient, System.Net.URLClient, HPDFSignatureProvider,
HPDFCSCSignatureProvider;
function MakeCSCTransport(Client: THTTPClient): THPDFCSCTransport;
begin
Result :=
function(const Request: THPDFCSCTransportRequest;
out Response: THPDFCSCTransportResponse): THPDFCSCTransportStatus
var
Body: TStringStream;
Reply: TMemoryStream;
Headers: TNetHeaders;
HttpResp: IHTTPResponse;
begin
Response := Default(THPDFCSCTransportResponse);
Body := TStringStream.Create(string(Request.Body), TEncoding.UTF8);
Reply := TMemoryStream.Create;
try
Headers := [TNameValuePair.Create('Authorization', string(Request.Authorization)),
TNameValuePair.Create('Content-Type', string(Request.ContentType))];
if Request.IdempotencyKey <> '' then // ヘッダー名はサービスのドキュメントどおりに
Headers := Headers + [TNameValuePair.Create('Idempotency-Key',
string(Request.IdempotencyKey))];
try
HttpResp := Client.Post(Request.URL, Body, Reply, Headers);
except
on ENetHTTPClientException do
Exit(ctsTemporaryFailure); // ソケットやDNSのトラブル:リトライ可能
end;
Response.StatusCode := HttpResp.StatusCode; // 503はそのまま報告し、分類しない
SetLength(Response.Body, Reply.Size);
if Reply.Size > 0 then
Move(Reply.Memory^, Response.Body[1], Reply.Size);
Response.RetryAfterMS := StrToIntDef(HttpResp.HeaderValue['Retry-After'], 0) * 1000;
Result := ctsSuccess;
finally
Reply.Free;
Body.Free;
end;
end;
end;
暗記する価値のある唯一の規則:サーバーが実際に答えたときは、たとえ503でも、ctsSuccessを返す。プロバイダーはステータスコードを自分で分類します。429をctsPermanentFailureに変えるトランスポートは、下で述べるリトライロジックを黙って無効化します。コンストラクターは逆方向に厳格です。トランスポートが欠けている、CredentialIDが空、AccessTokenもトークンコールバックも供給されていない、budgetが範囲外、ServiceBaseURLがHTTPSでない、のときにEHPDFCSCSignatureProviderErrorを上げます。素のhttp://はAllowInsecureHTTPが付いたときだけ受け付けられます。テストリグ専用で、他の場所には出すべきものではありません
SADとは何か、HotPDFが1回の使用後に捨てるのはなぜか
THPDFCSCSignatureProviderはSignature Activation Data(SAD)を1回限りとして扱います。signatures/signHashが受理された瞬間、プロバイダーの状態から消されます。署名そのものが後から非同期ポーリングで届く場合でもです。SADは、署名者がこのハッシュを承認したことの、サービスへの証明です。メモリーに居残るSADは、間違った文書に使われるのを待っている認可です
THPDFCSCOptions.Defaultの既定値、つまりRequireSADとAutoAuthorizeがともにTrueのもとでは、プロバイダーはcredentials/infoを1回読み、THPDFCSCAuthenticationCallbackにauthData値(OTP、PIN、credentialのauthブロックが要求するもの何でも)を尋ね、credentials/authorizeを投稿します。200はSADを直接運び、202はcredentials/authorizeCheckでポーリングされるハンドルを運びます。MaxPollAttempts(60)回まで、PollIntervalMS(250 ms)間隔で。コールバックは最大32値を返せて、各値は256バイトまでの非空IDと4,096バイトまでの値を持ちます。signHashが受理される前にネットワークが切れた場合は、自動取得されたSADは保持されます。署名者に再度尋ねることなく、同じバッチをリトライできるようにするためです
var
Options: THPDFCSCOptions;
Provider: THPDFCSCSignatureProvider;
begin
Options := THPDFCSCOptions.Default; // RequireSAD、AutoAuthorize、非同期モード、リトライ2回
Options.ServiceBaseURL := 'https://csc.example.com/csc/v2';
Options.CredentialID := 'contracts-signing-01';
Options.ClientData := 'invoice-run-2026-09';
Provider := THPDFCSCSignatureProvider.Create(Options, MakeCSCTransport(HttpClient),
function(ForceRefresh: Boolean; const OperationIdentifier: AnsiString;
out AccessToken: AnsiString; out ExpiresAtUTC: TDateTime): THPDFSignatureProviderStatus
begin
// あなたのOAuthクライアント。サービスが401を答えたあとはForceRefreshがTrue
if not TokenVault.Acquire(ForceRefresh, AccessToken, ExpiresAtUTC) then
Exit(spsProviderError);
Result := spsValid;
end,
function(const CredentialID, CredentialInfoJSON, OperationIdentifier: AnsiString;
out Values: THPDFCSCAuthenticationValues): THPDFSignatureProviderStatus
var
Otp: string;
begin
if not AskSignerForOtp(Otp) then // あなたのUI
Exit(spsCancelled);
SetLength(Values, 1);
Values[0].ID := 'otp';
Values[0].Value := AnsiString(Otp);
Result := spsValid;
end);
Options.SADで自分で渡すSADは、別の振る舞いをします。意図的な別の振る舞いです。HotPDFには、それがどのハッシュのために発行されたか分からないので、プロバイダーは事前設定のSADを単一ハッシュのリクエストにしか使いません。AutoAuthorizeを切ったバッチでは、プロバイダーは当て推量の代わりに「CSC SAD is not pinned to the requested hash batch」で失敗します
SignHashBatchは1つの認可で多数の文書にどう署名するか
SignHashBatchは、最大MaxBatchSignatures(64)個のダイジェストに対して、credentials/authorizeを1回とsignatures/signHashを1回送り、両方のボディを同じ配列から組み立てます。つまりnumSignaturesと、hashesの順序と、hashAlgorithmOIDが、2つの呼び出しで同一になります。この一致こそ、CSCのマルチサインモデルが要求するものです。単一ハッシュのSignメソッドを40回ループすれば、認可は40個になります。食い違うauthorizeとsignHashを送れば、サービスはSADを間違ったバッチに対して消費するかもしれません
ネットワークトラフィックの前に、プロバイダーはバッチを検証します。すべてのリクエストは、ダイジェストOIDを持つ、1〜1,024バイトのダイジェスト(sikDigest)でなければならず、すべてのリクエストは1つの署名アルゴリズムOIDと1つのダイジェストOID、そしてRSASSA-PSSでは1つのソルト長を共有しなければなりません。マルチハッシュのバッチはさらにcredentials/infoを読み、credentialのmultisign値がバッチより小さければspsUnsupportedを返します。そしてSADはバッチフィンガープリントに固定されます。バージョンラベル、カウント、リクエストごとのアルゴリズムOID、ダイジェストOID、アルゴリズム、ソルト長、ダイジェストバイト列(それぞれ長さプレフィックス付き)にわたるSHA-256です。2つのハッシュを入れ替えれば、新しい認可を必要とする別のバッチです
var
Requests: THPDFCSCSignatureRequests;
Signatures: THPDFCSCSignatures;
Status: THPDFSignatureProviderStatus;
I: Integer;
begin
SetLength(Requests, Length(Digests)); // Digests:あなたが計算したSHA-256値
for I := 0 to High(Digests) do
begin
Requests[I] := Default(THPDFSignatureProviderRequest);
Requests[I].Algorithm := hsaRSAPKCS1v15; // AlgorithmOIDが空ならsignAlgoを導出
Requests[I].DigestAlgorithmOID := '2.16.840.1.101.3.4.2.1';
Requests[I].InputKind := sikDigest;
Requests[I].Input := Digests[I];
end;
Status := Provider.SignHashBatch(Requests, 'invoices-2026-09-25-a', Signatures);
if Status <> spsValid then
raise Exception.CreateFmt('CSC batch failed (HTTP %d): %s',
[Provider.LastHTTPStatus, Provider.LastError]);
// Signatures[I]はDigests[I]に対応する。カウントはリクエストに対して検証済み
end;
RSASSA-PSSでは、プロバイダーはさらにsignAlgoParamsを送ります。ハッシュアルゴリズム、MGF1、ソルト長を持つ、base64のDER RSASSA-PSS-params構造です。これを組み立てることはOIDのエンコードを意味し、v2.748.5はその一角を直しました。X.690 §8.19.4は最初の2つのアークを1つの値へ折りたたみます(40 × first + second)。そして2ルートの下では、39を超える第2アークがその値を127を超えて押し上げ、base-128のマルチバイト形式が必要になります。旧ビルドはこれを適用していませんでした。SHA-2のOIDはどれも影響を受けません。"2.16"は96へ折りたたまれます。しかし不正なOIDはいまや、EConvertErrorの代わりに、プロバイダー自身のエラーを上げます
リトライされたリクエストが2つ目の署名を作らないのはなぜか
THPDFCSCSignatureProviderは、リトライ可能な呼び出しが決定論的なidempotencyキーを運ぶようにし、完了した結果をキャッシュします。レスポンスを失ったあとのリトライは、HSMに新しい署名を頼む代わりに、元の署名を返します。キーはcsc-に続いて、操作識別子とフェーズのhex SHA-256で、フェーズはauthorizeとsignHashの両方についてバッチフィンガープリントを埋め込みます。切り詰める代わりにハッシュするのが重要です。プレフィックスを共有する2つの長い操作IDは、切り詰めでは衝突します。固定長のコンテンツアドレス型キーなら、試行をまたいで一意かつ安定です
共有リクエスト経路のリトライポリシーは、意図的に狭くしています:
- HTTP 401は、アクセストークンコールバックを通してきっかり1回のトークンリフレッシュを強制し、アクセストークンコールバックが割り当てられていれば、リクエストは1回だけ繰り返されます。2回目の401は最終です
- その他の4xxレスポンスと
ctsPermanentFailureは、spsProviderErrorで呼び出しを終え、サービスのerror_descriptionはLastErrorに入ります - 408、429、5xx、
ctsTemporaryFailureはRetryLimit(既定2)までリトライされ、Retry-AfterかRetryBaseDelayMS× 2attempt(基本100 ms)を待ち、MaxRetryAfterMS(5,000 ms)で頭打ちです - 待ち時間は
Cancelを確認する25 msのスライスで走るので、中止したユーザーが5秒のバックオフを座り続けることはありません signHashがリトライされるのはEnableIdempotencyが有効な間だけです。切ると、送信後のタイムアウトは最終です。キーがすでに使われたかどうか、誰にも分からないからです
非同期署名(operationMode "A"、既定)はもう1つの防御を加えます。responseIDはsignatures/signPollingをポーリングする前に保存されるので、同じ操作識別子での繰り返し呼び出しは、再送する代わりにポーリングを再開します。完了したバッチは、操作識別子、credential、フィンガープリントでキー付けされたキャッシュに座り、MaxOperationCacheEntries(128)で頭打ち、ディープコピーとして返されます。このキャッシュはプロバイダーインスタンスの中に住み、再起動を生き延びません。idempotencyキーは生き延びます。導出されたものであってランダムではないからです。だから操作識別子を再利用する再起動されたプロセスは、同じキーを送ります。サービスがそれで重複排除するかどうかは、サービスの約束であってHotPDFの約束ではありません
CSC署名をPDFへ入れるには
プロバイダーを、GetCertificateChainからのend-entity証明書とともにHPDFCMSSignPDFStreamWithProviderへ渡します。HotPDFがCMS SignedDataを組み立て、プロバイダーがsigned attributesのダイジェストに署名します。入力PDFには、THPDFPage.AddSignedSignatureFieldが書く/ByteRangeと/Contentsプレースホルダーが必要です。HotPDFのPAdES署名ワークフローのとおりです。そしてプロバイダーモデルは、ML-DSAとEdDSA向けのHotPDFプラグ可能署名プロバイダーが扱うのと同じものです
var
Chain: THPDFCSCCertificateChain;
SignOpts: THPDFCMSSignOptions;
Src, Dst: TFileStream;
begin
if Provider.RefreshCredentialInfo <> spsValid then
raise Exception.Create(Provider.LastError);
Chain := Provider.GetCertificateChain; // CSCはend-entity証明書を最初に列挙する
if Length(Chain) = 0 then
raise Exception.Create('Credential returned no certificate');
SignOpts := HPDFCMSDefaultOptions(palBaseline_B_B);
SignOpts.DigestAlgorithm := cmsdaSHA256;
SignOpts.SignatureScheme := cmsRSAPKCS1v15;
Src := TFileStream.Create('contract-unsigned.pdf', fmOpenRead or fmShareDenyWrite);
Dst := TFileStream.Create('contract-signed.pdf', fmCreate);
try
if not HPDFCMSSignPDFStreamWithProvider(Src, Dst, Chain[0], Provider, '', SignOpts) then
raise Exception.Create('PDF signing failed');
finally
Dst.Free;
Src.Free;
end;
end;
/Contentsプレースホルダーのサイズ決定はEstimateSignatureSizeを通ります。EstimatedSignatureBytesを設定していればそれを返し、さもなければcredentialの鍵長からのRSAモジュラスサイズを返します。ECDSAではEstimatedSignatureBytesを自分で設定してください。さもなければ推定はspsUnsupportedを報告します。自動サイズの署名バリアントは、プレースホルダーが小さすぎると判明したときに再署名します。そしてそれは、spcSafeSignRetryを告知するプロバイダーにだけです。"THPDFCSCSignatureProvider"がこれをするのは、EnableIdempotencyが有効な間だけです。PAdES-B-Tのワークフローでは、TimestampDigestが、同じサービスからsignatures/timestamp経由でタイムスタンプトークンを要求します。MaxTimestampBytes(1 MB)で頭打ちです
CSCプロバイダーがやらないこと
メッセージには署名しません。ダイジェストだけです。純モードのEd25519とEd448は、signed-attributesメッセージ全体(sikMessage)をプロバイダーへ渡しますが、バッチバリデーターはそれを不正として拒否します。signHashは定義上ハッシュベースだからです。プロバイダーユニットは、無名メソッドの代わりにプレーンな関数型を使ってFree Pascalでもコンパイルできます。しかしプロバイダー駆動のCMSビルダーは現状FPCではraiseするので、CSC署名をPDFへ埋め込むのはDelphiの経路です
ポリシーも決めません。CredentialInfoはキー状態、証明書状態、認可モード、SCALレベル、マルチサイン上限を報告しますが、プロバイダーは、無効化されたキーやSCAL1のcredentialを、自分から拒否しません。署名者にOTPプロンプトを見せる前に、それらを確認してください。そして1つのプロバイダーインスタンスは1度に1バッチへ署名します。SignHashBatchは内部で直列化されるので、2つのスレッドが1つのSADを取り合うことはありません。つまりスループットは、ワーカースレッドでプロバイダーを共有するのではなく、バッチングから来ます。結果の署名がqualifiedかどうかは、ハッシュを運んだライブラリーではなく、トラストサービスとそのcredential次第です
CSCプロバイダー、CMSとPAdESのビルダー、そしてローカルとPKCS#11のプロバイダーは、HotPDF Delphi PDF componentにすべて同梱されています