技術記事

eIDAS 信頼リストと合格 PDF 署名

PDF 署名が eIDAS の下で合格であると判断するとは、暗号とは何の関係もない問いに答えることです。署名がなされた時点で、その証明書は、加盟国が合格と列挙した信頼サービスによって発行されたのか。答えは信頼リスト、つまり領土ごとに公開される XML 文書の中にあり、その文書の価値のすべては真正性に依存します。したがって PDFium コンポーネントは、誰かが保証するまで、中を見ようとしません。TPdfEuropeanTrustedList.ParseAuthenticated は、1 つのサービスを解析する前に、完全な生のバイトを呼び出し側供給の IPdfTrustedListAuthenticator へ渡し、その認証器が明示的に合格した場合にのみスナップショットを作成します

Delphi における欧州信頼リストの認証前解析フロー。新規取得またはキャッシュスナップショットからの生 XML が、TPdfEuropeanTrustedList が何かを解析する前に IPdfTrustedListAuthenticator を通る。
新鮮なリストもキャッシュされたリストも同じ認証器を通ります。スナップショットが存在するのは、合格した後だけです

この順序こそが設計です。この機能の他のすべて、不便に見える部分を含めて、ここから従います

解析できたことは、信頼されたことではない

きれいに解析できる信頼リストが教えるのは、XML が整形式であることだけです。誰が書いたかは何も教えません。リストは合格ステータスの判断全体が依りかとするものなので、解析できたという理由で受け入れると、判断は無意味になります。リストをすり替えられる攻撃者は、自分の認証局を合格と宣言できます

同じ論理がキャッシュにも当てはまり、これが名指す価値のある罠です。スナップショットキャッシュは元の XML を SHA-256 ダイジェストとともに保存します。ロード時に一致するダイジェストを、リストが本物である証拠として扱いがちです。違います。ファイルを保存したのと同じプロセスが計算した、鍵の関与しないダイジェストが検証するのは、バイトが書き込まれてから変わっていないことだけです。リストがキャッシュされた時点で偽物だったなら、ダイジェストは同じ偽物のリストであることを確認するだけです。したがってキャッシュスナップショットのロードは、新規の解析と同じ認証器を通ります。完全性と真正性は別の性質であり、鍵を必要とするのは片方だけです

uses
  FPdfTrustedList;

type
  TListAuthenticator = class(TInterfacedObject, IPdfTrustedListAuthenticator)
  public
    function Authenticate(const XmlData: TBytes;
      out AuthenticationDetails: string): Boolean;
  end;

function TListAuthenticator.Authenticate(const XmlData: TBytes;
  out AuthenticationDetails: string): Boolean;
begin
  // あなたのポリシーはここに住む。帯域外で固定したリスト署名証明書に
  // 対して XMLDSIG の enveloped 署名を検証し、監査証跡のために
  // 何を確認したかを記述する
  Result := VerifyEnvelopedXmlSignature(XmlData, FPinnedListSigner);
  if Result then
    AuthenticationDetails := 'XMLDSIG verified against pinned LOTL signer';
end;

var
  List: TPdfEuropeanTrustedList;
  Cache: TFileStream;
begin
  List := TPdfEuropeanTrustedList.ParseAuthenticated(RawXml,
    TListAuthenticator.Create, TPdfTrustedListOptions.Default);
  // スナップショットが存在するのは、認証器が yes と言ったからだけ
  Cache := TFileStream.Create('tl-de.snapshot', fmCreate);
  try
    List.SaveCache(Cache);
  finally
    Cache.Free;
  end;
end;

バリデータはネットワークポリシーを所有しない

PAdES バリデータが決めるべきではないのは、信頼リストのリストへの到達方法、プロキシを通るかどうか、どのくらいの頻度でリトライするか、領土に到達できないとき何をするかです。それらはアプリケーションと配備の判断であり、規制された環境では頻繁に監査されます。したがって更新は IPdfTrustedListSource を通じて届きます。URI とバイト上限を渡され、バイトを返します

コンポーネントが強制するのは、更新を更新たらしめ、すり替えと区別する不変量です。Update は、領土が変更されていないこと、シーケンス番号が厳密に増加すること、発行時刻が後戻りしないことを要求します。この 3 つの確認が、最も明白なダウングレード攻撃を打ち負かします。撤回されたはずのサービスをまだ列挙している古いリストの再生、または信頼するつもりのなかった別の領土のリストへの差し替えです

PDFium 信頼リストコンポーネントが強制する更新の不変量。変更されない領土、厳密に増加するシーケンス番号、後戻りしない発行時刻。合わせてダウングレード攻撃を防ぐ。
3 つの単調な確認が、本物の更新を、再生されたリストや差し替えられたリストから分けます

パーサーの上限と、DTD の完全排除

TPdfTrustedListOptions は、XML サイズ、トークン数、ネストの深さ、サービス数、証明書数、個々の証明書のサイズに上限を設けます。使用可能な値を供給する Default クラス関数が付きます。信頼リストは予測可能なサイズの公開文書であるため、上限の設定は安価であり、それを超える必要のある正当なリストは存在しません

別個に、かつ無条件に、パーサーは DTD とエンティティ宣言を拒否します。これが 1 つの拒否で、エンティティ展開のサービス拒否と外部エンティティの情報漏えい経路の両方を閉じます。信頼リストはエンティティを使わないため、代償はゼロです。信頼できない入力から到達可能な XML パーサーはすべて、このように設定されるべきです。ここでの違いは、拒否が設定可能ではないことで、善意のあるオプション変更によってオフにできません

合格ステータスはチェーン信頼の隣に記録される。併合はされない

評価側は意図的に別です。TPadesTrustValidationOptions.QualifiedTrustEvaluatorIPdfQualifiedTrustEvaluator を受け取り、信頼リストスナップショットがこれを実装します。検証の間、評価器はリーフ証明書、チェーン、検証時刻を受け取り、署名者とチェーンに対して正確な DER 比較でサービス証明書を突き合わせ、その時点でのサービスステータス、サービスタイプ識別子、修飾子 URI を組み合わせ、評価レコードを返します

結果は各署名の 2 か所に着地します。大まかなステータスとしての QualifiedTrustStatus と、領土、プロバイダー名、サービス名、タイプ識別子、ステータス、ステータス開始時刻を伴う完全な評価としての QualifiedTrust です。しないのは、CertificateTrustStatus を変えることです。システムのチェーン信頼と合格ステータスは異なる問いに答えます。それらを潰した報告は、「信頼されるが合格ではない」と「合格だがチェーンが検証されない」を区別できません。どちらも実在し、どちらも別の処理を必要とします

Delphi における PAdES 合格信頼の評価。IPdfQualifiedTrustEvaluator が正確な DER 比較でサービス証明書を突き合わせ、QualifiedTrustStatus と QualifiedTrust を埋め、CertificateTrustStatus は無変更のまま。
チェーン信頼と eIDAS 合格ステータスは並んで記録されます。両方の発見が見えるままであるために
var
  Options: TPadesTrustValidationOptions;
  Report: TPadesValidationResult;
  I: Integer;
begin
  Options := TPadesTrustValidationOptions.Default;
  Options.CheckRevocation := True;
  Options.QualifiedTrustEvaluator := List;      // 認証済みスナップショット
  Options.QualifiedValidationTime := SigningTime; // Now ではない
  Report := Pdf.ValidatePadesTrust(Options);

  for I := 0 to High(Report.Signatures) do
    if Report.Signatures[I].QualifiedTrustStatus = pcsValid then
      Writeln(Format('signature %d qualified by %s / %s (%s)',
        [I, Report.Signatures[I].QualifiedTrust.Territory,
         Report.Signatures[I].QualifiedTrust.ProviderName,
         Report.Signatures[I].QualifiedTrust.ServiceName]))
    else if Report.Signatures[I].QualifiedTrustStatus = pcsIndeterminate then
      // 一致するサービスがない、またはスナップショットがこの時刻に答えられない
      Writeln(Format('signature %d: qualified status undetermined', [I]));
end;

検証時刻が now ではない理由

合格は一瞬の性質だからです。信頼サービスは合格ステータスを付与され、後に撤回され、さらに後に復活し得ます。そしてそれぞれの遷移は、リストの中に開始時刻を運びます。サービスが合格であった間になされた署名は、その後も合格のままです。付与より前になされた署名は、遡って合格にはなりません。したがって現在時刻に対して評価すると、両方向で誤った答えが得られます

リストはこのために必要なものを運びます。各サービスレコードにはステータス開始時刻があり、履歴エントリと現在のエントリを区別するフラグがあり、評価器は供給された時刻に対してそれらを組み合わせます。実際にはその時刻は、CMS が主張する署名時刻からではなく、署名上の信頼されたタイムスタンプから来ます。ポリシーの照合に見える問いであっても、長期検証素材が重要である理由です。タイムスタンプと DSS の側面は、長期署名の記事で扱っています

それでも自分で作らなければならないもの

3 つあり、どれも PDF ライブラリに属しません。認証器です。信頼する経路を通って入手したリスト署名証明書に対する実際の XMLDSIG 検証を意味します。取得ポリシーです。どう、どのくらいの頻度で更新するか、更新が失敗したときアプリケーションが何をするかを意味します。そして領土スコープです。そもそもどのリストを運ぶかであり、取引先がどの加盟国で署名するかについてのビジネス判断です

コンポーネントから得られるのは、微妙に誤りやすい部分です。認証前解析の順序、上限付きかつエンティティフリーの XML 解析、単調な更新不変量、正確な DER によるサービス突き合わせ、履歴ステータス評価、そして通常のチェーン信頼とは別であり続ける結果です。当面の問題がもっと基本的なもの、つまり正しいはずの署名をバリデータが拒否するものであれば、よくある原因はバリデータが PAdES 署名を拒否する理由に分類されており、署名の検査面は署名と PAdES レベルの検査に述べています。コンポーネントの機能は、PDFium Delphi component の製品ページに一覧があります