これは「TeX & LaTeX Advent Calendar 2020」の 13 日目の記事です。12 日目はabenoriさんの「\futurelet」14 日目はCareleSmith9さんです。
本記事では PDF/A に電子署名してみます。「LuaLaTeX で pdfx パッケージを使い PDF/A に準拠した PDF を作る」では、長期保存を目的とした PDF である PDF/A を作りました。それに電子署名してみようというわけです。
なお、電子署名用の証明書としてCAcert.orgのものを使用していますが、私は保証ポイントを持っていないため名前を入れることができませんでした。CAcert.org の保証人で私にポイントを下さる方がいらっしゃるようでしたらご連絡いただけると幸いです。
はじめに
「LuaLaTeX で pdfx パッケージを使い PDF/A に準拠した PDF を作る」で説明したとおり、PDF/A は長期保存を目的とした PDF の規格です。PDF/A に準拠した PDF は、現在の PDF ビュアー(たとえば Adobe Acrobat Reader DC 2020)だけでなく、長期間保存後に将来の未知の PDF ビュアーにおいても、同じように表示したり、同じように取り扱ったりできることが期待できます。
そうすると、保存された PDF/A について、
- いつ作成されたものか確かめたい
- 誰が作成したものか確かめたい
- 作成後に改変されていないことを確かめたい
という要望が出てくるかもしれません。OSS 的には GnuPG で署名するという方法もあるとは思いますが、それだと「いつ」を証明することができません。そこで、PDF/A にタイムスタンプを付けて電子署名することによって、これらを証明できる(後から確かめることができる)ようにします。
PDF の電子署名
PDF の電子署名については、参考になるサイトがいろいろあるので探してみると良いと思います。また、PDF 1.7 (ISO 32000-1:2008) 規格は無料で PDF が入手でき、電子署名の記述もありますので、かなり参考になります。ここでは簡単に端折って説明をします。
まず、PDF の電子署名には見た目の分類として
- 不可視署名
- PDF コンテンツの表示や印刷には現れない署名
- 可視署名
- PDF コンテンツの表示や印刷に現れる署名
の 2 種類があります。可視署名だと PDF コンテンツの表示や印刷に現れますが、だからとって、その表示や印刷を見ただけで署名の検証ができるわけではありません。可視署名とまったく同じ見た目のコンテンツを用意して貼り付けておくことは簡単にできますので、表示や印刷だけでは署名の確認をしたことにはなりません。署名を検証したければ、署名のプロパティなどを確認する必要があります。
次に、目的の分類として
- 通常の署名
- 主に文書の内容を承認したという意味で署名する
- 複数の署名が可能
- 改変防止の署名 (DocMDP: Document Modification Detection and Prevention)
- 文章の内容が改変されていないことを保証するために署名する
- 署名できるのは 1 つだけ
などがあります(他にもあるようです)。
稟議書のようなものを PDF で電子化して運用するのであれば、見た目の分類では可視署名にしてハンコやサインのように見えるようにし、目的の分類では通常の署名として承認したことを示し、これを順番に承認者が次々に電子署名していき、最後に全員の署名が揃ったら完了、といような形が想定されているものと思います。各署名にタイムスタンプサーバを用いたタイムスタンプを付けておけば、いつ誰が承認したのかがわかることになります。
一方で、最初に挙げさせていただいた「作成後に改変されていないことを確かめたい」の場合は、見た目の分類は可視でも不可視でもよいですが、目的の分類としては改変防止の署名 (DocMDP) にすべきということになります。また「誰が作成したものか確かめたい」についても、PDF のメタデータやコンテンツの中に作成者名を入れておけば、それらも改変防止の対象にできます。作成者名が改変されていないことを保証できるので、後から誰が作成した者か確かめることができます。もちろん「いつ作成されたものか確かめたい」は、タイムスタンプサーバを用いたタイムスタンプを付けておくことで、いつ作成されたものか確かめる1ことができます。
PDF 向け電子署名ツール
PDF へ電子署名できるツールには、いろいろなものがあります。実は無料で使える Acrobat Reader でも電子署名することができます2。そこで、PDF/A に準拠した PDF を用意して Acrobat Reader で電子署名し、veraPDF で検証してみたところ、見事に PDF/A 非準拠になってしまいました…。また、PDF へ電子署名できる OSS をいくつか見付けることができたので、試してみたのですが、残念ながら PDF/A を維持したまま署名できるツールを見つけることはできませんでした3。
PDF 1.7 規格には電子署名について記述があります。一方で PDF/A-2 は PDF 1.7 をベースとした規格ですが、電子署名については禁止されていないようなので4、本来は両立できると考えられます。私が試した電子署名ツールは、何か PDF/A で禁止されていることをしてしまって非準拠の PDF を出力したわけです。これは単純に電子署名を付与したからというわけではなさそうで、準拠のまま電子署名を付与することも可能ではないかと考えました。というわけで自分で PDF/A を維持したまま電子署名できるツールを作ってみることにしました。
PDF 電子署名の構造
PDF に電子署名するには、元の PDF に署名辞書 (Signature dictionary) などの電子署名関連のデータを追加することになります。では、何をどのように追加していけばよいのか。まずは、PDF/A 非準拠ではあるものの、いくつかの PDF 電子署名ツールが使えるので、これらの出力した PDF と、PDF 1.7 規格を突き合わせるとともに、veraPDF で PDF/A としてはどこがいけないのか確かめることから始めました。
PDF の中身を覗く方法はいくつかあると思いますが、今回はQPDFというツールを使って、QDF 形式に変換してテキストエディタで見ていくことにしました5。その結果、PDF の電子署名としては、複数の形式があるようでしたが、PDF 1.7 規格の「12.8.3.3 PKCS#7 Signatures as used in ISO 32000」にある、adbe.pkcs7.detached
の形式として、PKCS#7 SignedData 形式を Signature dictionary (署名辞書)の /Contents の中に入れる方法が主流であることがわかりました。これは Acrobat Reader DC 2020 のデフォルト設定で電子署名した場合でも使われる形式です6。
可視署名だと署名の見た目が必要になるので、よりシンプルな不可視署名を実現することを考えます。まずは署名用のアノテーション辞書を追加して、PDF の /Root
から以下の 2 つのルートでたどれるようにします。
-
/Root
→/AcroForm
→/Fields
→署名用アノテーション辞書 -
/Root
→/Pages
→/Kids
→(最初のページ)→/Annots
→署名用アノテーション辞書
署名用アノテーション辞書は以下のような感じになります。/P
にはこのアノテーションが存在するページ(つまり最初のページ)のページオブジェクトを指定します。そして /V
に PDF 電子署名でキモとなる署名辞書のオブジェクトを作成して追加します。
<<
/Type /Annot
/Subtype /Widget
/FT /Sig
/T (Signature)
/Rect [ 0 0 0 0 ]
/F 132
/P 40 0 R
/V 65 0 R
>>
署名辞書は以下のような感じになります7。/Filter
と /SubFilter
で電子署名の形式を指定しており、この場合は先ほど述べた adbe.pkcs7.detached
の形式を意味しています。
<<
/Type /Sig
/Filter /Adobe.PPKLite
/SubFilter /adbe.pkcs7.detached
/ByteRange [
0
1217
9411
17188
]
/Contents <30820e...
...(省略)...
...000000>
>>
adbe.pkcs7.detached
の場合は、/ByteRange
で、この PDF の署名対象範囲を指定し、/Contents
で、署名対象範囲に対する署名を PKCS#7 SignedData 形式で格納します。
/ByteRange
配列の最初の数字 0
は、PDF のオフセット 0 つまりファイルの最初を意味しています。次の数字 1217
は、長さを意味しています。これはファイルの最初から 1,217 バイトが署名対象範囲ということになります。これはどこまでの範囲かというと、ちょうと署名辞書の /Contents
の <
直前までが範囲に含まれるようになっています。その次の 9411
と 17188
も同じ意味で、ファイルのオフセット 9,411 から 17,188 バイトが署名対象範囲ということになります。こちらの範囲は、ちょうど署名辞書の /Contents
の最後の >
の次から、ファイルの最後までが含まれるようになっています。逆に言うと、範囲外なのは、署名辞書の /Contents
の中身のバイナリ表現部分、<30820e...(省略)...000000>
のところだけです8。
PDF に電子署名する
PDF に電子署名する手順は以下のようになります。
- PDF に署名用アノテーション辞書や署名辞書を追加して、
/Root
から辿れるようにする- 署名辞書の
/Contents
の中身はゼロで埋めたダミーを入れておく- 後で置き換える PKCS#7 SignedData のサイズより大きくなければなりません
- 署名辞書の
/ByteRange
も一旦ダミーの数字を入れてファイル全体を作成し、/Contents
の中身のオフセットが決まったら正しい数字に置き換える
- 署名辞書の
- ファイル全体から署名辞書の
/ByteRange
で指定された範囲のみ取り出して電子署名し PKCS#7 SignedData 形式を作る -
/Contents
の中身のダミーを、電子署名した PKCS#7 SignedData 形式で置き換える- 余った部分は最初に入れたダミーのままにしておく
というわけで、これができるツールを作ってみました。Experiment PDF sign toolです。 PDF 操作ライブラリとしてQPDFをつかっており、ビルドするには QPDF 9.1.0 以降が必要です9。
これをビルドすると、 experiment-pdf-sign-prepare
と experiment-pdf-sign-finalize
の 2 つの実行ファイルができます。
先ほどの手順「1.」は experiment-pdf-sign-prepare
を使い、入力となる PDF と、条件等を指定すると、中間 PDF 、署名対象バイナリ、オフセットファイルの 3 つを出力します。中間 PDF は、/Contents
の中身がダミーのゼロで埋まっている PDF です。署名対象バイナリは、中間 PDF のうち /ByteRange
で指定した範囲のみ取り出して連結したファイルです。オフセットファイルは、/Contents
の中身が始まるオフセットを記述したファイルです。以下のように実行します。
$ ./experiment-pdf-sign-prepare --input 入力.pdf \
--intermediate 中間.pdf \
--to-be-signed 署名対象.bin \
--offsetfile オフセット.txt
手順「2.」は PKCS#7 SignedData 形式が作れればなんでも構いません。例えば OpenSSL を使って以下のようにします。OpenSSL で署名する場合、署名に使う証明書は秘密鍵を含めた .pem
に変換しておく必要があります10。
$ openssl smime --sign -binary -noattr \
-in 署名対象.bin \
-out 署名.p7s \
-outform DER \
-signer 証明書.pem
そして手順「3.」は experiment-pdf-sign-finalize
を使い、以下のようにすると、署名された PDF ができます。
$ ./experiment-pdf-sign-finalize `cat オフセット.txt` \
中間.pdf \
署名.p7s \
署名された.pdf
この「1.」~「3.」の一連の動作は、シェルスクリプト experiment-pdf-sign.sh
でできるようにしてあります。詳しくは Experiment PDF sign tool の README.md をご覧ください。
電子署名した PDF/A
「LuaLaTeX で pdfx パッケージを使い PDF/A に準拠した PDF を作る」で作った PDF/A-2u に準拠している simple17fix.pdf
に電子署名をして simple17fix-signed.pdf
を作ってみました。証明書には CAcert.org で発行した私のクライアント証明書を使いました。まず、veraPDF で確認したところ、PDF/A-2u 準拠が維持できていることが確認できました。では Acrobat Reader DC 2020 で開いてみましょう。
署名パネルを開いてみると、ちゃんと電子署名できていることが確認できます11。署名者の名前が「CAcert WoT User」になってしまっていますが、これは私が CAcert.org の保証ポイントを持っていなくて名前を入れられなかったからです。また、「署名時刻は使用できません」と表示されていますが、これは署名にタイムスタンプが無いためです。「署名は LTV 対応ではなく、…を過ぎると有効期限が切れます」と表示されているのは、LTV (Long Term Validation) に対応していないため、今回使った証明書の有効期限が過ぎると署名の有効期限も切れてしまうという意味です。
いくつか問題はありますが、これで PDF/A と電子署名を両立させることができました。
PKCS#7 SignedData
先ほどの手順「2.」では PKCS#7 SignedData を作るために、OpenSSL を使いました。しかし、PKCS#12 形式の証明書を .pem
変換しておかなければならない(しかも単純な変換ではダメでテキストエディタで編集する必要がある場合もある)ため、少々面倒です。また、署名の内容についてある程度制御しようとすると OpenSSL のコマンドではあまり自由度がなさそうに見えます。そこで、自前の PKCS#7 署名ツールを作ってみました。GnuTLS PKCS#7 classです。名前の通りビルドには GnuTLS が必要です。このツールを手順「2.」で使えば、PKCS#12 形式の証明書を変換せずにそのまま使えます。
$ ./pkcs7_sign --md=sha256 \
--time \
--in 署名対象.bin \
--out 署名.p7s \
--cert 証明書.p12
タイムスタンプ対応
タイムスタンプは PDF 1.7 規格を読むと、RFC 3161 を使って、PKCS#7 SignedData 形式の中に unsigned attribute として埋め込むように書かれています。このためには、さきほどの手順「2.」で作った PKCS#7 SignedData 形式を元にタイムスタンプリクエストを作って、タイムスタンプサーバへ投げ、タイムスタンプサーバから返ってきたタイムスタンプレスポンスを受け取り、レスポンスの中身からタイムスタンプを取り出し、タイムスタンプを PKCS#7 SignedData に埋め込み、そして手順「3.」へ進むことになります。
これらの動作を行うことができるツールも作ってみました。Time stamp (RFC 3161) toolsです。本ツールもビルドには GnuTLS が必要です。ビルドすると、いくつかの実行ファイルができます。
pkcs7_ts_req
が PKCS#7 SignedData 形式を元にタイムスタンプリクエストを作るツールです。
$ ./pkcs7_ts_req sha256 \
署名.p7s \
タイムスタンプリクエスト.bin
タイムスタンプリクエストは、例えば以下のように curl を使ってタイムスタンプサーバに投げると、タイムスタンプレスポンスが得られます。ここではタイムスタンプサーバに freeTSA.orgを使っています。
$ curl -H "Content-Type: application/timestamp-query" \
--data-binary @タイムスタンプリクエスト.bin \
-o タイムスタンプレスポンス.bin \
"https://freetsa.org/tsr"
ts_reqp
がタイムスタンプレスポンスからタイムスタンプを取り出すツールです。
$ ./ts_resp タイムスタンプリクエスト.bin \
タイムスタンプレスポンス.bin \
タイムスタンプ.p7s
merge_sign
が元の PKCS#7 SignedData 形式にタイムスタンプを埋め込んだ、新しい PKCS#7 SignedData を作るツールです。
$ ./merge_sign 署名.p7s \
タイムスタンプ.p7s \
タイムスタンプ付き署名.p7s
そしてこの、タイムスタンプ付き署名を、手順「3.」で PDF に組み込みます。
これらの一連の操作をシェルスクリプトにまとめたものをPDF sign scriptsに置いてあります。
Experiment PDF sign tool、GnuTLS PKCS#7 class、Time stamp (RFC 3161) toolsをすべてビルドした上で、実行ファイルを同じディレクトリに置き、PDF sign scriptsのスクリプトも同じディレクトリに置いて、以下のようにするとタイムスタンプ付きの署名ができます。タイムスタンプサーバを変更したい場合は変数 TIMESTAMP_SERVER_URL
を変更してください。
$ ./signpdf-with-timestamp.sh 入力.pdf 出力.pdf 証明書.p12
さて、これでタイムスタンプ付きの署名をした PDF を作ってみました。veraPDF で検証してみると、見事に PDF/A-2u に準拠していることがわかりました。では Acrobat Reader DC 2020 で開いてみましょう。
署名パネルを開いてみると、タイムスタンプが無かったときに「署名時刻は使用できません」になっていたところが「埋め込みタイムスタンプが署名に含まれています」に変わりました。一番上の「バージョン1: CAcert WoT User <…> により署名済み」のところを右クリックして「署名のプロパティ」を表示してみます。
タイムスタンプの時刻が表示されました12。
LTV 対応
タイムスタンプに対応にしても「署名は LTV 対応ではなく、…を過ぎると有効期限が切れます」が残りました。これに対応するにはどうしたらいいでしょうか。LTV については「長期署名」とか「PAdES」とかで検索すると、参考になりそうな記事が見つかります。ここでは簡単な説明を試みます。電子署名に使った証明書は失効することがあります。証明書の有効期限内であれば、CRL や OCSP を使って、証明書が失効しているか否かを検証することができます。もし、署名のタイムスタンプ時刻よりも前に証明書が失効していたら、その署名は無効ということになります。そこで問題になるのは、長期保存後で証明書の有効期限が既に切れてしまっていた場合です。証明書の有効期限よりも署名のタイムスタンプ時刻の方が前であれば、有効期限内に署名されたものであることは分かります。ですが、それだけでは、署名時に証明書が失効していたか否かがわかりません。そこで、署名後の一定時間経過後13に、検証に必要な CRL や OCSP などの情報、そしてさらにそれらの検証に必要な証明書などをすべて集めて PDF に追加しておきます。そうすれば、後からでも、署名のタイムスタンプ時刻に証明書が有効であったか失効していたかを検証することができる、というわけです。
この LTV 対応に必要な情報の追加で、せっかく署名した PDF を壊してしまったら意味がありません。そこで PDF の Incremental Save (増分更新)という仕組みを使って追加します。ただ、残念ながら QPDF は Incremental Save に対応していないので、LTV に対応させるツールを作ることができません…。
ですが、実は Acrobat Reader で LTV 対応にすることができます。署名パネルを開いて一番上の「バージョン1: CAcert WoT User <…> により署名済み」のところを右クリックして「検証情報の追加」を選びます。検証情報が正しく追加された旨のダイアログが出たら、メニューから「ファイル」→「名前を付けて保存」をして PDF を書き出すと LTV 対応になってくれます。ただ、これだけでファイルサイズが 38 KB から 3,100 KB へと、一気に 3MB 近く増えてしまいましたが…。さて、この PDF を改めて Acrobat Reader で開いてみます。
署名パネルを開いてみると「署名は LTV 対応です」に変わってくれました。しかも、これを veraPDF で検証してみると、ちゃんと PDF/A-2u 準拠になっています。
おわりに
本記事では PDF/A に電子署名してみました。「LuaLaTeX で pdfx パッケージを使い PDF/A に準拠した PDF を作る」で作った、長期保存を目的とした PDF である PDF/A に対して、電子署名してなおかつ PDF/A を維持することに成功しました。
今回はとりあえず通常の署名になっていますが、長期保存に伴う改変防止を考えると DocMDP 署名にする必要があるだろうと思っています。また、最後に示した LTV 対応は一応できましたが、それだけでファイルサイズがかなり大きくなってしまいました。これは CAcert.org の CRL が大きくて、それを取り込んでしまっているからというのがあると思っていますが、OCSP で小さくできないかなぁと思っています14。QPDF は Incremental Save 非対応ですが、OSS なら例えば Apache PDFBox ならできそうです15。
なお、PDF/A は veraPDF のような検証ツールがあるので、正しいか否か正確に判定できますが、電子署名は残念ながらそのような検証ツールがなさそうです。現在の Acrobat Reader DC 2020 でどのようになるか確かめていますが、もしかしたら穴があるかもしれません。
最後に繰り返しになりますが、電子署名用の証明書としてCAcert.orgのものを使用していますが、私は保証ポイントを持っていないため名前を入れることができませんでした。CAcert.org の保証人で私にポイントを下さる方がいらっしゃるようでしたらご連絡いただけると幸いです。
-
少なくともタイムスタンプの日時より前に作成されたコンテンツであることを示すことができます。もちろん、そのタイムスタンプが信用できるものかどうか、検証する必要はあります。 ↩
-
不可視署名ができない、DocMDP 署名ができないなど、できることに制限はあるようです。 ↩
-
商用の署名ツールには PDF/A を維持したまま署名できるものがあるのかもしれません。 ↩
-
PDF/A-2 (ISO 19005-2) 規格は有料なので読んでいませんが、veraPDF を使うと PDF が PDF/A に準拠しているか否か、準拠していないなら何がいけないのかを示してくれます。また、veraPDF のドキュメントも大変参考になります。 ↩
-
QDF は、ほぼほぼ PDF と同じ形式ですが、テキストエディタで覗いたりいじったりしやすくなっています。圧縮は解除され、オブジェクトストリームはバラバラにされ、さらにインデントもされて、一部にはコメントも入っていて、かなりわかりやすいです。 ↩
-
PDF 1.7 規格では
adbe.pkcs7.detached
の他にadbe.pkcs.sha1
やadbe.x509.rsa.sha1
などの形式が記載されています。PDF 1.7 規格ではなく ETSI 規格によるETSI.CAdES.detached
というものもあるようですが、PDF/A-2 は PDF 1.7 ベースなので使えないのではないかと思います。 ↩ -
署名辞書は後で示す作成の都合などから圧縮や暗号化の対象外です。 ↩
-
範囲の指定が全然違ったり、少しでもズレていたりすると、Acrobat Reader が正しい電子署名として認識してくれないようです。これは範囲指定を故意に小さくするなどの悪用をすることで、電子署名されているように見えるが、対象範囲が限られていてほとんど保護されていない、というような状態を防ぐためと思われます。 ↩
-
もともと QPDF にはこの署名ツールを実現するために必要な機能がなかったので、いくつか Pull request して受け入れていただきました。そのため QPDF 9.1.0 のリリースノートに私の名前が載ってます。 ↩
-
CAcert.orgのクライアント証明書がブラウザに入っているのであれば「エクスポート」や「バックアップ」といった機能で秘密鍵とともにファイルへ書き出すと PKCS#12 形式のファイル(拡張子は
.pfx
または.p12
)ができます。これを.pem
にするには、例えば OpenSSL を使って$ openssl pkcs12 -in JohnDoe.pfx -out JohnDoe.pem
のようにすればできます。ただ、.pem
が、秘密鍵・ルート証明書・クライアント証明書の順番だと OpenSSL がエラーを起こすようです。その時はテキストエディタで、秘密鍵・クライアント証明書・ルート証明書の順番になるように入れ替えればエラー回避できるようになると思います。 ↩ -
CAcert.org のルート証明書を信用する設定にしたので、このような表示になっています。Acrobat Reader DC 2020 のデフォルトでは CAcert.org の証明書は信用されません。 ↩
-
署名時刻とタイムスタンプ時刻が数秒ズレていますが、これは署名時刻は最初の PKCS#7 SignedData を作った時刻なのに対して、タイムスタンプ時刻は、それを元にタイムスタンプサーバが生成したタイムスタンプレスポンスの時刻になっており、少し後になるためです。 ↩
-
署名直後にやりたくなりますが、それだとあまりよろしくないようです。もちろん、すべての証明書の有効期限内にする必要がありますが、少なくとも 1 日以上経過してからやった方がよいようです。これは、証明書の失効手続きにかかるタイムラグを見込む必要があるかららしいです。また、検証に CRL が必要で、その発行周期が長い場合は、1 日以上経過した後、さらに次の CRL が発行されるまで待った方がよいかもしれません。 ↩
-
ただ、CAcert.org のクライアント証明書は OCSP で失効確認できるので CRL 不要なんですが、その OCSP レスポンスに署名している証明書の失効確認に結局 CRL が必要な感じなので、Acrobat Reader が CRL を付けるのは正しい動作なのかもしれません…。 ↩
-
というか、そもそも PDF 署名の機能もあるようです。Java 用のライブラリなので、Java で書かなければなりませんが…。そういえば、OSS の PDF 署名ツールって、ほとんどが Java で動作するものなんですよね…。みんな PDFBox の PDF 署名 API を使っているということでしょうか…。 ↩