💡 完全に動作するサンプルは GitHub で入手できます: vault-transit-pdf-signing-python
自動署名がキー管理レビューで失敗する理由
コンプライアンスや財務オペレーションチームは、何千件もの文書に署名します。ローン契約書、ステートメント、ポリシーパック、監査レターなどです。そのほとんどは、人がボタンをクリックして署名するわけではありません。サービスがスケジュールに従って署名し、文書に添付された証明書には組織名が記載されます。これにより、署名キーは企業が所有する最も機密性の高いものの一つとなり、通常はそれを使用するアプリケーションの隣にある PFX ファイルに保存されます。
外部署名は、.NET 経由で Python 用の GroupDocs.Signature が提供する機能で、アプリケーションが保持するプライベートキーで署名する代わりに、文書のダイジェストをユーザーが提供するコードに渡します。これは、PFX の配置が監査に耐えられないために存在します。ファイルはコピーであり、コピーはコンテナイメージ、デプロイパイプライン、バックアップ、昨春に本番問題をデバッグした開発者のラップトップへと広がります。そのコピーとパスワードを持つ誰でも、証明書が取り消されるまであなたの名前で署名できます。
これらのチームが本当に必要としているのは別のファイル形式ではなく、キーが正確に一箇所に存在し、使用されたたびに監査トレイルが残り、キー自体を信頼せずに署名を要求できる署名サービスです。HashiCorp Vault のようなキー管理ツールがその役割を果たし、文書ライブラリが暗号処理を委任できる場合にのみ有効です。
パスワード保護された証明書ファイルだけでは不十分な理由
本能的には PFX をロックしようとします。ファイル権限を制限し、パスワードをシークレットマネージャに保存し、ボリュームを暗号化します。各ステップは助けになりますが、根本的な事実は変わりません。署名時にプライベートキーがプロセスにロードされるため、メモリダンプ、コンテナエスケープ、あるいは不注意なログ出力でもキーが露出する可能性があります。求められる制御は「読み取れないキー」であり、「読み取りにくいキー」ではありません。
Vault の Transit エンジンはそれを提供します。暗号処理をサービスとして実行し、値を送信するとキーで署名または暗号化し、結果を返します。内部で作成されたキーはエクスポート不可としてマークでき、受け取ったデータは保存しません。GroupDocs.Signature は残りの部分をカバーします。DigitalSignOptions.custom_sign_hash により、独自の署名者を PDF 署名パイプラインに組み込むことができます。
セットアップ: Vault Transit を使用した Python 用 GroupDocs.Signature
GroupDocs.Signature は暗号処理までをすべて行います。PDF を開き、署名がカバーするバイト範囲を決定し、その範囲をハッシュ化し、ダイジェストを署名者に渡します。署名者はダイジェストを Vault に送信し、Vault が生成したバイト列を返します。ライブラリはそれらを証明書と共に埋め込み、通常の署名済み PDF を書き出します。
必要なのは Python 3.11 または 3.13、groupdocs-signature-net と asn1crypto パッケージ、そして開発モードの Vault コンテナ用 Docker です。GroupDocs のライセンスは任意で、ライセンスがなくても評価モードでは最大 2 ページまでの文書を受け付けます。サンプルリポジトリは 2 つのコマンドでエンドツーエンドに実行でき、環境変数を 2 つ変更するだけで本番 Vault を指すようになります。
手順を段階的に構築する
ステップ 1 - Vault から署名キーを読み取る
アプリケーションは使用許可されたキーを Vault に問い合わせ、バージョン番号と公開鍵を取得します。プライベート側は決して回答に含まれません。
key = vault.read_key(KEY_NAME)
if key is None:
# First run: enable Transit and create an RSA-2048 key.
vault.enable_transit()
vault.create_key(KEY_NAME, "rsa-2048")
key = vault.read_key(KEY_NAME)
print(f"Signing key: '{KEY_NAME}', version {key.version}")
バージョンを一度だけ読むことがこのステップの目的です。証明書は 1 つの公開鍵を保持するため、証明書とすべての署名は同じバージョンから生成されなければなりません。
ステップ 2 - 誰もエクスポートできないキー用の証明書を作成する
PDF 署名は X.509 証明書を伴い、検証者は公開鍵を取得できます。エクスポート不可キーの場合、証明書は公開側だけで組み立てられ、Vault が署名します。
tbs_certificate = x509.TbsCertificate({...}) # built around the Vault public key
# Hash locally, then have Vault sign the certificate's own digest.
digest = hashlib.sha256(tbs_certificate.dump()).digest()
signature = vault.sign_hash(key_name, key.version, digest, "sha2-256")
certificate = x509.Certificate({
"tbs_certificate": tbs_certificate,
"signature_algorithm": algorithm,
"signature_value": signature,
})
証明書は意図的に現在より 1 日早い日付で開始されます。PDF リーダーは署名時刻と開始日を比較し、UTC より 2 時間東のマシンでは、署名直前に作成された証明書が Adobe Acrobat に「期限切れ」または「まだ有効ではない」と報告されました。
ステップ 3 - 外部署名者を介して PDF に署名する
certificate_object = DigitalVerifyOptions(
certificate_stream=io.BytesIO(certificate)).certificate
digital_signature = DigitalSignature(certificate=certificate_object)
digital_signature.sign_time = datetime.now().astimezone()
options = DigitalSignOptions()
options.signature = digital_signature
options.custom_sign_hash = VaultSigner(vault, KEY_NAME, key.version)
options.hash_algorithm = HashAlgorithm.SHA256
options.reason = "Document signed with HashiCorp Vault Transit"
with gs.Signature(str(input_pdf)) as signature:
signature.sign(str(output_pdf), options)
Python 固有の 2 行です。バインディングに X.509 型がないため、証明書オブジェクトは DigitalVerifyOptions から取得します。DigitalSignOptions にパスを渡すと「Specified certificate doesn’t contain private key」というエラーが発生します。また sign_time は意図的にタイムゾーン情報付きにしており、オフセット付きの値が PDF に書き込まれます。
署名者内部では、prehashed = true と pkcs1v15 を指定してダイジェストを Vault に送ります。この 2 つのフィールドが署名の有効性を決定し、最初のフィールドが欠けているとサイレントに失敗します。
ステップ 4 - サービスから出る前に検証する
options = DigitalVerifyOptions(certificate_stream=io.BytesIO(certificate))
with gs.Signature(str(signed_pdf)) as signature:
is_valid = signature.verify(options).is_valid
同一実行内で署名と検証を行うことで、サイレント失敗モードを即時失敗に変換します。文書パイプラインにおいてこれはゲートウェイとなり、検証に失敗したファイルはアーカイブや取引先、顧客ポータルに届きません。
ビジネスへの影響: 監査で何が変わるか
| 懸念事項 | アプリケーションホスト上の PFX | Vault Transit を介した署名 |
|---|---|---|
| プライベートキーのコピー | ホスト、イメージ、バックアップ、ラップトップごとに 1 つ | Vault 内に 1 つ |
| 署名アプリが必要とする権限 | キー ファイルへのフルアクセス | キー読み取り、transit/sign 呼び出し |
| 文書が署名された証拠 | アプリケーションログ(誰かが追加した場合) | 署名ごとに 1 つの Vault 監査エントリ |
| 疑わしい漏洩への対応 | 取り消し、再発行、全てに再デプロイ | Vault でキーをローテーションし、1 回再発行 |
| あなたの名前で署名できる人 | ファイルとパスワードを持つ全員 | 有効な Vault トークンを保持する全員 |
最後の行が正直な答えです。この設計はシークレットを削除するのではなく移動させます。盗まれた Vault トークンでも署名要求は可能です。ただしトークンは短命で、1 カ所で取り消し可能、2 つの操作に限定され、すべての使用がログに記録されます。これは、コピーが数え切れないファイルを管理するよりも、レビューで防御しやすい点です。
実際のワークフロー: サービスによって署名されたローン契約書
融資プラットフォームは、申請が承認されると契約書 PDF を生成し署名して、顧客の文書ボールトに格納します。この変更前は、署名サービスが会社の証明書を保持し、デプロイ手順書に PFX を新しいホストにコピーするステップがありました。変更後は、サービスが 2 つの権限を持つ AppRole トークンを保持し、証明書のプライベートキーは Vault 以外に存在しません。
監査人が「前四半期に何件の文書が署名されたか、誰が署名したか」を尋ねると、答えは Vault の監査デバイスから取得され、アプリケーションログを再構築する必要はありません。チームがキーをローテーションするときは、1 カ所でローテーションし、新バージョン用に新しい証明書を発行します。
最初に正しくすべきことは何か?
ダイジェストの取り扱いです。アルゴリズム名が間違っていると Vault が 500 crypto/rsa: input must be hashed message と loudly に返し、数分の遅延になります。prehashed フラグが欠けているとエラーは全く出ず、Vault がダイジェストを再度ハッシュし、署名は誤った値をカバーし、PDF は完成したように見えます。署名するすべてのファイルを検証し、失敗が見えなくならないようにしてください。
この設計が適用できる他の場所
フックは背後に何があるかを認識しないため、同じ 4 ステップは HSM、クラウド KMS、スマートカードでも機能します。.NET の兄弟からこの実装を移植した際、署名者が再実装を必要とすると思っていましたが、実際に変更されたのは証明書の受け渡しだけでした。Vault のリクエストボディとバージョン固定は同一です。医療、政府、保険のチームがこの手法を採用する理由は、財務部門と同様に「キーが何に使われたかを証明できる場所」にある必要があるからです。
はじめに
- 無料で開始: トライアル ライセンスを取得、フル API アクセス
- 統合ガイドを読む: Python で Vault Transit キーを使用した PDF 署名
- コードを見る: vault-transit-pdf-signing-python
- 実装に関する質問は コミュニティ フォーラム で