💡 完全な動作例はGitHubで入手可能です:
digital-signing-certificate-validity-dotnet

監査人が指摘するまで誰も気付かないコンプライアンス問題

署名サービスはエラーなく3年間稼働し続けます。文書は送信され、受取人は受領し、ログに問題を示す兆候はありません。ところが、取引先のバリデータがバッチを無効とフラグ付けし、調査の結果、2つの原因が判明しました。署名が SHA-1 で作成されていたこと、そして過去4か月間証明書が期限切れであったことです。

両方の失敗は署名時点では無音でした。これが GroupDocs.Signature 26.9 が変える点です。

証明書有効期限の強制は、.NET デジタル署名の新しいデフォルト動作です。有効期限外の証明書は使用されずに拒否されます。これに加えて、デフォルトの PDF ダイジェストとして SHA-256 が設定され、LogLevel が導入されました。これら3つが、受取人側で発生していた失敗を送信者側に移し、そこで修正できるようにします。

なぜ無音の成功が高コストになるのか

署名は、ミスをした側とそれを発見する側が異なるという点で特殊です。自社システムで不正な請求書が失敗し、数週間後に他社で無効な署名が失敗しても、診断情報が得られません。

この非対称性のため、「API が成功を返した」という保証は意味がありません。従来のデフォルトは呼び出し側を中断させないよう最適化されており、コストは受取人側、そして最終的には数百件の文書を再署名・再送信しなければならない側に転嫁されました。

変更点 1: 期限切れ証明書は拒否される

見出し通りの変更です。Sign は証明書の有効期限が終了している、またはまだ開始されていない場合に GroupDocsSignatureException をスローし、ディスクへの書き込みは行われません。

try
{
    signature.Sign(outputPath, options);
    return true;
}
catch (GroupDocsSignatureException ex)
{
    Console.WriteLine($"   Rejected: {ex.Message}");
    return false;
}

例外メッセージには証明書と、許可すべきプロパティが記載されるため、オペレーターはドキュメントを開かずにログ行だけで対処できます。26.9 にアップグレードして失敗が出たパイプラインでは、ほぼ常にこの理由であり、正しい対応は抑制ではなく更新です。

従来の動作が本当に必要な場合は、次のプロパティを使用します。

var options = new DigitalSignOptions(certificate)
{
    Password = certificatePassword,
    AllowExpired = true
};

文書は署名され、警告がロガーに送られます。バリデータは依然として結果を拒否します。AllowExpired はライブラリが許可するかどうかを制御し、証明書自体の価値を変えるものではありません。AllowNotYetValid フラグはウィンドウのもう一方をカバーし、独立して動作します。期限切れ証明書を許可しても、将来日付の証明書が静かに許可されることはありません。

変更点 2: デフォルトは SHA-256

PDF デジタル署名は、現在のバリデータが期待する adbe.pkcs7.detached 形式で SHA-256 が使用されて書き込まれます。以前のバージョンは SHA-1 を使用していました。

var options = new DigitalSignOptions(certificate)
{
    Password = certificatePassword,
    HashAlgorithm = HashAlgorithm.Sha256,
    Reason = "Approved",
    Location = "Head office"
};

プロパティを明示的に設定する必要があるのは、ポリシーで Sha384 や Sha512 が要求される場合、または Sha1 のみをサポートするバリデータに合わせる場合だけです。タイムスタンプも同じダイジェストを使用します。

同じリリースで検証ロジックも同方向に変更されました。DigitalVerifyOptions に基準が設定されていない場合、以前はほぼ何もしませんでしたが、現在は完全な暗号チェックを実行し、署名後に文書が改ざんされていると無効と報告します。

変更点 3: LogLevel が実際にフィルタリングする

SignatureSettings は長らくロガーを受け入れてきましたが、26.9 以前はレベルが無視され、すべてのメッセージが出力されていました。そのため多くのサービスはトレースに埋もれないようロギングをオフにしていました。

サンプルは、カウントロガーで同一文書を3回署名し、差分を測定します。

var levels = new Dictionary<string, LogLevel>
{
    ["None"] = LogLevel.None,
    ["Warning | Error"] = LogLevel.Warning | LogLevel.Error,
    ["All"] = LogLevel.All
};

None はメッセージをゼロにし、Warning | Error は期限切れ証明書が許可されていない場合に出る単一の警告だけを残し、All はステップごとにトレースを追加します。カウントロガー自体が自社スタックとの統合ポイントです。

public void Warning(string message)
{
    Warnings++;
    WarningMessages.Add(message);
}

この3つのメソッドを Serilog、NLog、または Application Insights に実装すれば、ライブラリの診断情報はサービス全体のログに流れ込みます。

ログレベルの変更で例外が変わりますか?

いいえ。二つは見た目が似ているだけです。LogLevel は ILogger に届く情報をフィルタリングしますが、例外はコードに対して常にスローされます。AllowExpired が無い期限切れ証明書は LogLevel.None でも例外を投げ、キャッチブロックの挙動は同じです。診断情報と制御フローは別チャンネルであり、Warning | Error で本番運用しても安全です。

拒否は見た目以上に安価

ハードストップへの抵抗は運用上の問題です。夜間バッチが 02:00 に失敗し、誰かがページャで呼び出されます。これは実際のコストですが、依然として小さい方です。拒否されたバッチはアラート1件、更新1件、再実行1件で済みます。期限切れ証明書で署名されたバッチは受取人側で発覚し、サポートスレッド、影響を受けたすべての文書の再発行、そして「どれだけ長く続いていたか」という微妙な会話が必要になります。

サンプルは失敗を理論ではなく具体的に示します。期限切れ証明書で意図的に署名し、例外を捕捉してメッセージを出力するので、アップグレード前にログに何が出るか確認できます。バージョンアップを計画する前に、ぜひご自身の証明書ストアでこのメソッドを実行してください。

アップグレード前にやるべきこと

失敗しやすい順に3つのチェックを行います。

  1. すべての署名パスで証明書の有効期限を確認します。月次・四半期ごとに実行されるものが、期限切れ証明書が最も長く潜んでいる場所です。
  2. HashAlgorithm を検索します。設定が無い場合、アップグレード時にダイジェストが SHA-1 から SHA-256 に変わります。これはリリースノートに記載すべき改善です。
  3. ログレベルを意図的に決めます。サービスの正直なデフォルトは Warning | Error、All は特定問題の再現用、None は免除された署名が作成されたことを示す唯一のシグナルを放棄することを意味します。

同じ方向への検証変更

見落としがちですが、呼び出し側コードの変更は不要です。DigitalVerifyOptions に基準が設定されていない場合、以前はほぼ何もしませんでしたが、26.9 以降は同呼び出しで全 PDF デジタル署名の暗号チェックを実行します。

受信文書を検証するサービスにとって、これは「署名が存在する」から「この署名がこの内容と一致する」への無音アップグレードです。先月は検証に合格した文書が失敗し始めたとき、実は文書が改ざんされていたことが分かります。古いチェックは単に見ていなかっただけです。

サンプルに含まれる証明書

コードではなく、サンプルが持つべき重要な点は「プライベートキーが含まれていない」ことです。TestCertificates.cs は実行時にメモリ上で 3 つの自己署名 PFX を生成します(有効、昨年期限切れ、来年から有効)。そのため、今日の日付に関係なくデモが動作し、リポジトリに機密情報は残りません。

このパターンは自前のテストスイートでも採用すべきです。コミットされたテスト証明書はやがて期限切れになり、失敗がちょうどこのリリースで表面化するバグと同じ形になります。

結論

3 つの変更が同一方向に向かいます。受取人側で現れた失敗が送信者側に移ります。AllowExpired に頼らず証明書を更新し、デフォルトで SHA-256 を使用し、受信文書を暗号的に検証し、必要になる前に適切なログレベルを選択してください。サンプルは 6 つの動作すべてを一度に実行し、拒否も含めて数分でアップグレードをリハーサルできます。

追加リソース