💡 GitHub で利用できる完全な動作例: pdf-signing-certificate-checks-python
はじめに
サービスは毎晩アップロードされた PDF に署名します。ある朝、使用している証明書が有効期限を過ぎていたことに気づかず、ジョブは実行され、ファイルは書き込まれ、ログも正常に見えました。数週間後、誰かが Acrobat でその文書のひとつを開くと警告バナーが表示されます。期限切れの証明書で作成された署名は「弱い」署名ではなく、検証ツールが無効と報告する署名だからです。承認されたように見える文書は、署名がない文書よりも価値が低くなります。なぜなら、人々がそれを信頼したからです。
その拒否には名前があります。証明書の有効性チェックは、Python 用 GroupDocs.Signature の動作で、証明書の有効期間が終了した、または開始前の場合に署名を拒否します。これはバージョン 26.9 で導入され、同時に 2 つの同様の変更が加わりました:PDF 署名のデフォルトダイジェストが SHA-256 になり、SignatureSettings.log_level が無視されるのではなくフィルタリングされるようになったのです。これらは、以前は黙って起きていた結果を表に出す役割を果たします。
本稿では、Python から .NET 経由で動作するこれら 3 つのコントロールを比較します。各コントロールが出力に与える影響、いつ使うべきか、そしてバインディングの細部で午後を費やした 2 つのポイントを解説します。引用したすべての結果は、1 ページの PDF にサンプルを実行したものです。
バージョンノート以上に重要な理由
この 3 つの変更は、後で発覚する失敗を今すぐに発覚させるという共通の特性を持っています。
- 期限切れ証明書:証明書を更新できる人がいるうちに署名呼び出しが失敗し、配布後に検証が失敗する文書が生成されるのを防ぎます
- ダイジェストのデフォルト:新しい署名は誰も意識せずに SHA-256 を使用するため、弱いオプションは無視できず、明示的な判断が必要になります
- ログレベル:警告のみを設定したサービスは警告だけを受け取り、可読性が向上し、結果として警告が実際に読まれます
最後の点は見た目以上に重要です。期限切れ証明書の警告が価値を持つのは、誰かがそれを見るからです。署名実行ごとに 10 件のトレースメッセージに埋もれた警告は、結局誰にも見られません。
前提条件
開始する前に、以下が揃っていることを確認してください。
- 64 ビットインタプリタ上の Python 3.9 以降 – パッケージはバンドルされた .NET ランタイムを同梱しており、32 ビット用の wheel は提供されていません
- GroupDocs.Signature for Python via .NET 26.10.0、評価制限を解除したい場合は free temporary licence を取得してください
- 署名対象の PDF、そしてサンプルが行っているように使い捨てテスト証明書を作成したい場合は
cryptographyパッケージ
インストール
pip install groupdocs-signature-net cryptography
コントロール 1 - 署名に書き込まれるダイジェスト
DigitalSignOptions の hash_algorithm がダイジェストを選択します。26.9 以降のデフォルトは SHA-256 で、現在の検証ツールが期待する adbe.pkcs7.detached 形式です。それ以前は新しい署名は SHA-1 でした。
with signature.Signature(source_path) as sign:
options = DigitalSignOptions()
options.certificate_stream = io.BytesIO(pfx)
options.password = PASSWORD
options.hash_algorithm = HashAlgorithm.SHA512
options.reason = "Approved"
result = sign.sign(output_path, options)
return len(result.succeeded)
注目すべき点が 2 つあります。証明書はファイルパスではなく io.BytesIO で certificate_stream に渡されます。これはメモリ上で構築した PKCS#12 がディスクに書き込まれることなくライブラリに渡る方法で、サンプルはプライベートキーを一切配布しないようにこの手法に依存しています。また、HashAlgorithm は AUTO, SHA1, SHA256, SHA384, SHA512 を提供し、タイムスタンプを追加した場合は署名が使用したダイジェストがそのまま使われます。
実務では最も触れないコントロールです。デフォルトはすでに正しい選択であり、SHA384 と SHA512 はポリシーで指定されたときに使用し、SHA1 は変更できない検証ツール向けの互換設定です。
コントロール 2 - 有効期限切れの証明書で処理が止まるか
オーバーライドを行わない場合、有効期間が終了している、または開始前の証明書で署名しようとすると GroupDocsSignatureException が発生し、何も書き込まれません。
try:
sign.sign(output_path, options)
return True
except signature.GroupDocsSignatureException as error:
print(f"Rejected: {str(error).splitlines()[0]}")
return False
例外メッセージには証明書名、失効日、サムプリント、そして有効にできるプロパティが含まれます。これだけでアプリケーションはオペレーターに更新対象を伝えられます。Python では最初の 1 行だけを取得することが重要です。例外テキストはバインディングの背後にある .NET スタックトレースが続くため、ユーザーに提示すべきではありません。
どうしても署名が必要なケース(アーカイブ証明書でのテストや、更新作業中でも今夜実行しなければならないバッチなど)では、オーバーライドを呼び出し単位で指定します。
settings = signature.SignatureSettings(ConsoleLogger())
settings.log_level = LogLevel.WARNING | LogLevel.ERROR
with signature.Signature(source_path, settings=settings) as sign:
options.allow_expired = True
result = sign.sign(output_path, options)
allow_not_yet_valid は将来有効になる証明書向けに同様の形で提供され、2 つのフラグは独立しています:期限切れ証明書を許可しても、まだ有効でない証明書は許可されません。前者は通常、マシンの時計がずれていることを示すサインであり、時計がずれているとそのマシンが生成するすべての署名が疑わしくなるため、オーバーライドする前に時計を確認してください。
どちらのオーバーライドも、サイレントに通過させるのではなく警告を出します。これが次のコントロールとつながります。
コントロール 3 - 誰かが検出できるか
SignatureSettings.log_level はフラグ値です。サンプルは同一文書を 3 回署名し、LogLevel.NONE、LogLevel.WARNING | LogLevel.ERROR、LogLevel.ALL の下でそれぞれ何件のメッセージが出たかをカウントします。
logger = CollectingLogger()
settings = signature.SignatureSettings(logger)
settings.log_level = level
with signature.Signature(source_path, settings=settings) as sign:
options.allow_expired = True
sign.sign(output_path, options)
結果は「何もなし」→「警告 1 件」→「警告 + トレース 10 件」の順になります。26.9 以前は 3 行すべて同じ出力で、レベルが受け入れられたものの無視されていたためです。レベルを設定したのに変化が見えず、コードを誤読したと結論付けた経験がある人はこの点を覚えておくと良いでしょう。
バインディングの細部で午後 1 時間を費やした点が 2 つありますので、ここで明示します。SignatureSettings.logger は読み取り専用で、ロガーはコンストラクタ引数として渡す必要があり、後から代入しようとすると AttributeError が発生します。log_level はその後に通常通り設定できます。また、カスタムロガーは groupdocs.signature.logging.ILogger を継承してはいけません。この基底クラスはライブラリが所有するハンドルを必要とするネイティブオブジェクトをラップしているため、継承すると TypeError が発生します。バインディングは以下の 3 つのメソッドを持つ任意のオブジェクトを受け入れます。
class StdlibLogger:
def error(self, message, exception=None):
logging.getLogger("groupdocs").error(message, exc_info=exception)
def warning(self, message, exception=None):
logging.getLogger("groupdocs").warning(message)
def trace(self, message):
logging.getLogger("groupdocs").debug(message)
error と warning にはオプションの exception パラメータを用意してください。ライブラリは常に例外を渡すわけではなく、渡さないメッセージでロガーが例外を必須にしているとエラーになります。
3つの比較: それぞれの使用タイミング
| コントロール | 向いているケース | 主な利点 | 制限事項 |
|---|---|---|---|
hash_algorithm |
ダイジェストを指定したポリシーに準拠したい場合 | 1 回の設定で出力サイズも同一 | 証明書自体が信頼できない場合は意味がない |
| 有効性チェックとオーバーライド | 他者のために署名するすべてのケース | 失敗が修正可能な場所で止まる | オーバーライドはファイルを生成するが、信頼できるものではない |
log_level |
すでにログが多いサービス | 11 件のメッセージが 1 件になる | ログだけをフィルタリングし、例外はフィルタしない |
これらは代替手段ではなく、1 回の署名呼び出しで 3 つすべてが使用されます。考慮すべき順序は「結果の重要度」:有効性チェックがファイルの有無を決め、ダイジェストが内部内容を決め、ログレベルが誰に情報が届くかを決めます。
ログレベルは取得する例外を変えますか?
いいえ。ログレベルはロガーに届くメッセージを決定するだけで、例外の種類には影響しません。LogLevel.NONE でも期限切れ証明書は GroupDocsSignatureException を投げ、allow_expired は LogLevel.ALL でも署名を行います。戻り値と例外はすべてのレベルで同一です。変わるのは、疑わしい署名を説明する警告が実際に人の目に触れるかどうかだけです。
検証も同様の方向へ移行
同リリースのもう片方として言及すべき点です。空の DigitalVerifyOptions を渡した verify は、すべての PDF デジタル署名を暗号的にチェックするようになりました。そのため、署名後に改ざんされた文書は「説明できない」だけでなく「無効」として返ります。
with signature.Signature(signed_path) as sign:
result = sign.verify(DigitalVerifyOptions())
return result.is_valid
2 行のコードで、署名後に保存するパイプラインに組み込む価値があります。True が保証しないことに注意してください:署名が文書と一致していることは示しますが、発行者が信頼できるかは保証しません。サンプルの自己署名証明書はここでは検証に通りますが、PDF リーダーでは依然として拒否されます。信頼性の問題は別途対応が必要です。
ベストプラクティスとヒント
- 拒否をデフォルトに保つ:ユーザーに代わって署名するすべての処理で、グローバルではなく呼び出し単位でオーバーライドしてください。例外はコストが低く、無効な署名が大量に生成されるよりはマシです。
- 警告テキストを記録し、単なるカウンタにしない:証明書名と失効日が含まれるため、オペレーターが対処できる唯一の情報です。
- まだ有効でない証明書を許可する前に時計を確認:証明書は正しくてもマシンの時計がずれていることが多く、複数の署名呼び出しに影響します。
- 本番環境ではトレースを出さない:署名実行ごとに約 10 件のトレースがすぐに蓄積します。診断時だけオンにし、終了後はオフにしてください。
- 署名後に検証を実施:チェックが暗号的になった今、受取側が問題に気付く前に出力の破損を捕捉できます。
結論
3 つのコントロールが 1 回の署名呼び出しで機能し、すべて同じ設計思想に基づいています。リスクのある結果は今すぐ判断が必要になり、安全な結果は何もしなくても済みます。有効性チェックはそのまま残し、allow_expired は呼び出し単位の例外としてログに残す。ポリシーで指示がない限りダイジェストは触らず、警告が読みやすいログレベルを設定してください。
自分の PDF にサンプルを走らせると約 1 分で完了し、各コントロールが何を変えたかが正確に出力されます。署名済みファイルが 6 件、意図的な拒否が 1 件、そして「同じでなくなった」メッセージカウントが 3 行表示されます。