認証局と証明書についての理解を深める(おまけ: PDFに対するタイムスタンプ付き電子署名)

しむどん 2025-08-24

TL;DR

独自のX.509公開鍵基盤を構築したい

文書署名用の証明書が欲しくなったが、PKIで正しく検証できる証明書を取得しようとすると中々に高額だ。しかしちょっとした用途のためだけに、お金をかけたくない。そこで独自のX.509公開鍵基盤を構築する。

基本的なステップとしては以下のようになる。

  1. ルート認証局を作る。
  2. 中間認証局を作る。
  3. 個別の用途に使用する証明書を発行する。
  4. PDFを署名する。(おまけ)

どの場所で何を行っているか分かりやすいように、各認証局は以下のディレクトリに関連するファイルを配置する事にする。本来は、1つの環境で行う事ではないのでその点については注意する。あくまで、どのようなコマンドを実行し、どのようなファイルを作る必要があるのかという事について、僕の理解が進むようにまとめたものだ。

役割 ディレクトリ
作業のルートディレクトリ ${ROOT_DIR}
ルート認証局 ./ca_root/
中間認証局 ./ca_intermediate/
個人用 ./person-A/

ルート認証局を構築する

ルート認証局用のディレクトリを作成し、作業ディレクトリを移動しておく。

cd ${ROOT_DIR}
mkdir -p ./ca_root
cd ./ca_root

私有鍵の作成から自己署名証明書の作成まで

まずは私有鍵を作成する。この情報は漏らしてはいけない。

openssl genrsa -out ./myca.key 4096
私有鍵の作成

設定ファイルも作成する。

[ req ]
default_bits       = 4096
prompt             = no
default_md         = sha256
distinguished_name = dn
x509_extensions    = v3_ca
req_extensions     = v3_intermediate_ca

[ dn ]
CN = Symdon, Co., Ltd.

[ v3_ca ]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer:always
basicConstraints = critical,CA:true
keyUsage = critical, cRLSign, keyCertSign

[ v3_intermediate_ca ]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer:always
basicConstraints = critical,CA:true, pathlen:0
keyUsage = critical, cRLSign, keyCertSign
openssl.cnf

自己署名CA証明書を作成する。この自己署名CA証明書は、公開して信頼してもらう必要がある。

openssl req -x509 -new -key ./myca.key -days 3650 -out ./myca.crt -config ./openssl.cnf

作成したCA証明書の情報を確認する。

openssl x509 -in ./myca.crt -text -noout

正しく設定されていれば成功だ。SubjectにはCommon Nameしか設定しなかったが、本来はもっと詳しく適切な値を設定するべきだろう。

シリアルナンバー用のファイルの作成

証明書を発行する際、シリアルナンバーを採番する。そのシリアルナンバーは -CAserial で指定したファイルの情報を元にして連番を使おうとする。そのデータベースとして ca.srl というファイルを作成しておこう。

echo 1000 > ./ca_root/ca.srl

データベースといっても、単純なテキストファイルだ。この例では 1000 からシリアルナンバーが採番される事になる。

中間認証局を構築する

ルート認証局ができたら、次は中間認証局を構築していこう。中間認証局用のディレクトリを作成し、作業ディレクトリを移動しておく。

cd ${ROOT_DIR}
mkdir -p ./ca_intermediate
cd ./ca_intermediate

私有鍵の作成から証明書署名要求の作成まで

中間認証局用の私有鍵を作成する。

openssl genrsa -out intermediate_ca.key 4096

中間CA用の設定ファイルも作成する。

[ req ]
default_bits       = 4096
prompt             = no
default_md         = sha256
distinguished_name = dn
x509_extensions    = v3_intermediate_ca
req_extensions = v3_req

[ dn ]
CN = Symdon, Co., Ltd.

[ v3_intermediate_ca ]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer:always
basicConstraints = critical,CA:true, pathlen:0
keyUsage = critical, cRLSign, keyCertSign

[ v3_req ]
basicConstraints = critical,CA:FALSE
keyUsage = critical, digitalSignature, nonRepudiation
extendedKeyUsage = critical, codeSigning
openssl.cnf

作成した私有鍵を元にして、証明書署名要求(CSR)を作成する。

openssl req -new -key intermediate_ca.key -out intermediate_ca.csr -config openssl.cnf

コマンドが成功すると証明書署名要求として intermediate_ca.csr というファイルが作られる。証明書署名要求の中の情報を確認しておこう。

openssl req -in intermediate_ca.csr -text -noout

正しく設定されていたら、ルート認証局のディレクトリに、証明書署名要求をコピーする。

cp intermediate_ca.csr ../ca_root/

通常これは、正式な認証局に対して証明書署名要求を送る操作に相当するが、今回はルート認証局も中間認証局も同じコンピュータ上にあるため、便宜上コピーで代用する。

証明書署名要求を署名し証明書を発行する

作成した証明書署名要求を、ルート認証局で署名し、中間認証局の証明書を発行する。

ここからはルート認証局での作業であるため、作業ディレクトリを移動しておく。

cd ${ROOT_DIR}
cd ./ca_root/

証明書署名要求を署名し、中間認証局の証明書を発行する。

openssl x509 -req \
  -in ./intermediate_ca.csr \
  -CA ./myca.crt \
  -CAkey ./myca.key \
  -CAserial ./ca.srl \
  -out ./ca_intermediate.crt \
  -days 3650 \
  -extensions v3_intermediate_ca \
  -extfile ./openssl.cnf

成功すると ./ca_intermediate.crt が作成される。これが中間認証局の証明書となる。そして証明書が発行されたため、 ca.srl はインクリメントされる。

以下のコマンドで情報を確認する。

openssl x509 -in ./ca_intermediate.crt -text -noout

情報に問題がなければ、このファイルを中間認証局用のディレクトリにコピーする。

cp ./ca_intermediate.crt ../ca_intermediate/

ここまでの操作で、ルート認証局と中間認証局の準備が整った。

シリアルナンバー用のファイルの作成

ルート認証局で発行した証明書のシリアルナンバーを管理するためのファイルを作成した。ただしそのファイルは、あくまでルート認証局のためのものだ。

中間認証局でも同様に、発行した証明書のシリアルナンバーを管理するためのファイルが必要になる。そこで ca.srl というファイルを作成しておく

cd ${ROOT_DIR}
cd ./ca_intermediate

echo 1000 > ./ca.srl

個別の証明書を発行する

ここまでルート認証局と中間認証局を構築してきたが、これらの証明書を直接使って、PDFなどの文書に署名をするわけではない。文書に対する署名には、中間認証局から発行された証明書を使う事にする。

証明書を発行する単位は運用次第ではあるけれど、人に紐付いた形で発行すると分かりやすいため person-A というAさん用のディレクトリを作り、そこで作業を行う事にする。

cd ${ROOT_DIR}
mkdir -p ./person-A/
cd ./person-A/

私有鍵の作成から証明書署名要求の作成まで

まずは私有鍵を作成する。

openssl genrsa -out ./person-A-20250824.key 4096
私有鍵の作成

続いて設定ファイルを作成する。

[ req ]
default_bits       = 2048
prompt             = no
default_md         = sha256
distinguished_name = dn
req_extensions     = v3_ext

[ dn ]
CN = person-A

[ v3_ext ]
basicConstraints = critical,CA:FALSE
keyUsage = critical, digitalSignature, nonRepudiation
extendedKeyUsage = critical, codeSigning
openssl.cnf

これらのファイルを元に、証明書署名要求を作成する。

openssl req -new \
    -key ./person-A-20250824.key \
    -out ./person-A-20250824.csr \
    -config openssl.cnf
証明書署名要求の作成

コマンドが成功すると、証明書署名要求が作成される。設定されている情報を確認しておこう。

openssl req -in ./person-A-20250824.csr -text -noout
証明書署名要求の内容を確認

情報に問題がなければ、この証明書署名要求を中間認証局にコピーする。

cp ./person-A-20250824.csr ./ca_intermediate/

証明書署名要求を署名し証明書を発行する

中間認証局を使って、作成した証明書署名要求を署名し、証明書を作成する。

ここからは中間認証局での作業となるため、作業ディレクトリを移動しておく。

cd ${ROOT_DIR}
cd ./ca_intermediate

証明書署名要求を署名し証明書を発行する。

openssl x509 -req \
  -in ./person-A-20250824.csr \
  -CA ./ca_intermediate.crt \
  -CAkey ./intermediate_ca.key \
  -CAserial ./ca.srl \
  -out ./person-A-20250824.crt \
  -days 365 \
  -extensions v3_req \
  -extfile ./openssl.cnf
証明書を発行

コマンドが成功すると person-A-20250824.crt という証明書が作成される。これが個別の証明書となる。設定された情報を確認しよう。

openssl x509 -in ./person-A-20250824.crt -text -noout

情報に問題がなければ、 Aさんにこの証明書を渡すため、ファイルをコピーする。

cp ./person-A-20250824.crt ../person-A/

これによりAさんは、中間認証局から発行された証明書を受け取った事になる。

文書に署名する

ルート認証局、中間認証局、そして個人であるAさんという3者にそれぞれ、私有鍵と証明書が行き渡った。基本的にはこれでPKIが構築されたと考える事ができる。続いて実際にPDFへの署名を行う方法を確認する。

作業しやすいように、作業ディレクトリを移動しておく。

cd ${ROOT_DIR}
cd ./person-A/

PKCS12形式のファイルを作成する

現時点でAさんは、私有鍵と証明書を持っているため、PDFに署名できるはずだ。ただしPDFに署名する際には、秘密鍵と証明書の形式をひとまとまりにしたPKCS12形式にした方が都合がいい。そこでPKCS12形式のファイルを作成する。

openssl pkcs12 -export \
    -out ./person-A-20250824.p12 \
    -inkey ./person-A-20250824.key \
    -in ./person-A-20250824.crt \
    -name "testing"

コマンドが成功すると、 person-A-20250824.p12 というファイルが作成される。

PyHankoのインストール

PDFへ署名する方法はいくつか考えられるが、ここではPyHankoというPython製のライブラリとコマンドを使い署名を行う。まずはPyHankoのCLI用パッケージをインストールする。

pip3 install pyhanko-cli

署名用フィールドの追加

PDFに署名を挿入するためのフィールドを追加する。

pyhanko sign addfields \
   ./example.plain.pdf \
   ./example.field.pdf \
   --field 1/450,400,500,450/siga

第一引数に指定されたファイルを入力にして、 --field オプションで指定した値を元にフィールドを追加し、第二引数で指定したファイルに出力される。

ここでは --field オプションに 1/450,400,500,450/siga を指定している。これは1ページ、 Xmin=450,Ymin=400,Xmax=500,Ymax=450 の位置に、 siga という名前でフィールドする事を表している。

タイムスタンプ付きの署名を行う

追加したフィールドに対してタイムスタンプ付きの署名を行う。タイムスタンプサーバは無料で利用可能な freetsa.org を使用する。

pyhanko sign addsig \
  --timestamp-url https://freetsa.org/tsr \
  pkcs12 \
  ./example.field.pdf \
  ./example.signed.pdf \
  ./person-A-20250824.p12

コマンドが成功すると、PDFはPKCS12ファイルを使って署名される。

電子帳簿保存法対応のタイムスタンプとしてfreetsa.orgは使用できない

日本の電子帳簿保存法では、文書を保存する際にタイムスタンプを付与する必要がある。しかしその時に使用するタイムスタンプサーバとして freetsa.org を使う事は妥当ではない。電子帳簿保存法で使用可能なタイムスタンプサーバは、総務省が認定したタイムスタンプサーバしか認められない。そのタイムスタンプサーバは数社しかなく、しかもいずれも非常に高額だ。

なぜ、こんな状況になっているのか分からないが、明らかにおかしな状態となっている。おそらくこの状況では、電子帳簿保存法対応としてタイムスタンプが広く使用される事はないだろう。

署名を確認する

PDFに付与されたタイムスタンプと電子署名を検証してみよう。 pyhanko sign validate で署名を検証する。これらを検証するには、署名で使用された証明書を信頼する必要がある。つまり今回構築したルート認証局の自己署名証明書と、freetsa.orgの証明書を信頼する。freetsa.orgの証明書については、Webサイトから取得し、=freetsa.org/tsa.crt= に配置した。

pyhanko sign validate \
  ./example.signed.pdf \
  --trust ../ca_root/myca.crt \
  --trust ../freetsa.org/tsa.crt \
  --pretty-print

今回作成した認証局用の証明書

ルート認証局

-----BEGIN CERTIFICATE-----
MIIFgTCCA2mgAwIBAgIURuBtjowgop06aaom9D4KVM2z1YYwDQYJKoZIhvcNAQEL
BQAwJjEkMCIGA1UEAwwbU3hpbWFkYSBDcmVhdGl2ZSwgQ28uLCBMdGQuMB4XDTI1
MDgyNDAwMTA0M1oXDTM1MDgyMjAwMTA0M1owJjEkMCIGA1UEAwwbU3hpbWFkYSBD
cmVhdGl2ZSwgQ28uLCBMdGQuMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC
AgEAy5UysCeceS46zV/a4X1rxIzXo2BZKLto8RsiE8f2VeWk3A1K2tv5gGsAwaiO
eDrGgsDSqSw+KHyr1tXgzuCOY5ES2iTNBRHpnKiS6jT2mLoC+JVXMZSWtgqwE/mc
L6ROsdt4LWHwowl39+/OU7qzilj1lUc6vMTXNTYnd/rOz3eu8TRaAzSSIH23x1ph
0gS0f5BRiu2yRleDSi0syUpJGPyt43wXW6Xjt0Z+QhQam8rz8flhIECjKlDE7F1/
usFbjuHsTknFBW4UzkH/amFMP4v9sDXU+DBP7YGo/agS0VR65irgjHTIpkBmsyt2
7hqkNdOSz0+bL9YQalgfxTqFz9WS8fp+16k4zWEKY3Cy4bxZZsEVFPHyGaXAOhcS
KmWatPWmjPKi9JSLuU+Lf0zpY5t8dbxnNeCAnsrRZdXgLfnMwj9Wd3bBXAEELcmO
/QwTez0BQHMbXOEAivlHQFVI6+ieRYAzN5fbuhhE3dhqkBsACqLX1lv7mbz+etAY
bqDMmV3n+kkMosKr201FlWWSPL4DXG5maroOsTCMDwL7/luMhc1Pl4rdzMexb+RH
ESlz62IyCKXJ48xvjSYPI3DvrkL19TQMTj29wGZ+bBHZ1ZLvj+VAsBQ8wtbPZTKk
IT0pn3Y0pWQdqDN7D8XCUt3tYZfk7dqFBs5nmntlHfGRKRECAwEAAaOBpjCBozAd
BgNVHQ4EFgQU7q14p8hF0hufh1CS2T1CDP/nRcIwYQYDVR0jBFowWIAU7q14p8hF
0hufh1CS2T1CDP/nRcKhKqQoMCYxJDAiBgNVBAMMG1N4aW1hZGEgQ3JlYXRpdmUs
IENvLiwgTHRkLoIURuBtjowgop06aaom9D4KVM2z1YYwDwYDVR0TAQH/BAUwAwEB
/zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHvTghfxzyu8iVlA
znzZuKRYxnBcuHE2jgYeBQCL54OsLnrX57GW5+5PmmCCgfUGHeVHEFgrtyqbrScB
2GnjE2Ur5GzfrUqgHPeW/ZmTpAWavO2VGtP2VVZdJ7moJrV4I2mLc6sob2nBzH0e
InrSDRpXAlWagIeBjPxQecb383aKNHPB558Z6js6rLahtypFDGuzhC/6i+choBdj
Ywn9JUZeYHtsisEQGwWcbU9Vr4k4zSBPNTeHEvY249cSSVqqYJuZUeuxhYqnaIVe
wCcLAKOl6DOWF1+zxh7YDhM+5AEojydcoV/sFYJHKo16D5+Y8V+zMc3+9fs8xKVq
7vednouLhH7vm8diajdR2lhcEFidzNgXlFWMXT+4oHGIt+MT08L1hv0W61i2VUBQ
Yn1AETw1Qmy7CnpEbLMVwZ5qnuNR0ewywRbYh9ZWJ80byd+TxicrK8+c9m1c8K59
CTXUrg87yXmk1fD7TJvZru0G6QlPoYceNYxgODc9T+rRInGiEfEGXxsnDMkBWQ+g
CbpmALS/hUpQdSP9YFnSgehxSeMn1sFp7MgSJTPrp3HtKH0kklRVgj6nfcjmDSRd
xTi3L20rWrTMm/yXKC0el7PN3rIXsZ5IAThLGXIdCa6Yd793KZ17eZ8vkT9pcs1V
1kjkQvGYAPE56N3bOfcootitoB74
-----END CERTIFICATE-----

中間認証局

-----BEGIN CERTIFICATE-----
MIIFcjCCA1qgAwIBAgICEAUwDQYJKoZIhvcNAQELBQAwJjEkMCIGA1UEAwwbU3hp
bWFkYSBDcmVhdGl2ZSwgQ28uLCBMdGQuMB4XDTI1MDgyNDA2MjU1OFoXDTM1MDgy
MjA2MjU1OFowJjEkMCIGA1UEAwwbU3hpbWFkYSBDcmVhdGl2ZSwgQ28uLCBMdGQu
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwAzfAUAtlqWkakZwihFR
I8KBvDSTrqUQoyWzJxv0H0GXLDFyumH1GsJ1gRz21qzjG9H8jxBqUiPCOw9ssHFE
0WHyaVAs0mBE8FNqYFACJkUJ71yos0OmYX8ALvUcULAPQzbztL73F0vk5SU0Rp5X
ndw2/qHFWfSc3SkRFsayw9QTLYyPysSCtxAXCLim2Ksz52vM6SwRcWtWE5EJXovF
Ow/XIqr3llC2CsKs9s1GkGCGp9kSRMMHCfHUGOhGsS2d9w/aADm/Wl9FMrMXGUlt
MgJplbj1yRondS6VezeuKnTSwkLpTMqnYEZLbRRIkxmfUU/zngCSvnpHyH2NKp1A
QWXyRydQv5IgnISRV0kVXd1BOnCc6kkRysCvVAs01/z2sn7txsxq2BHIZbi18HSu
CWeMtmtPDCVuv3CY8IC1Awwse8hXAWnqU8OHW6N0ScR/KiDeeEV9i1bdrgyl2XfC
3++SoRpFj/SaB4E1q31EVW7HnSV9jmf053CPSVxDLUVlpN/tsnuTEZgtdbTZSxhT
sWxnuJuGuZrMSV/NkmG8zg/MX5/6Pd4L6HCAUy9eb//u2wn+2J2dL5FnpkwJaOeP
CYYOPE/h/EfFfl5fp19CrueULR4MD0eO0Io5LuHSdV89zGtpR1ljJ5lfY6Jz0snI
LYGIp+Mm7Szx8179qBONzmcCAwEAAaOBqTCBpjAdBgNVHQ4EFgQUfe1/buJofe6M
rQnyMt4PdmoKn0cwYQYDVR0jBFowWIAU7q14p8hF0hufh1CS2T1CDP/nRcKhKqQo
MCYxJDAiBgNVBAMMG1N4aW1hZGEgQ3JlYXRpdmUsIENvLiwgTHRkLoIURuBtjowg
op06aaom9D4KVM2z1YYwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMC
AQYwDQYJKoZIhvcNAQELBQADggIBAB1i+zkuP5XQAAbqjTkfetarsoYqyGscKVso
u72rCQy62O80sH31Kpt2H1AHy1dOSv2JyGTuRaEB0yzScLNeAoS/J9pZTw5bGyZS
VU+yNZ/Ubso6lIhlm03cTWa3b2VjKjTv0f8f65VnlUL3ea5eEkXmepId9bcNXd+N
Hfi4/9ckYoRmKvWM/HmJBkIrebEcLeGpw8+CuYxKQ/gR04n8nNosut0JxnXEsovG
HwhPgI3urP/0B1vK3nJKeJUnlWtC7a4qhZn8wRMANg9IN3Lk2ds69+rtXOpxp2UM
GWnebu0SSaiG7cTib8QsQjG1ZbSJWoAmWm3c+g6J9tSjtn2Pyr3DajhhTBqqxWSQ
kfFV/hTZakzgJy0FWhsE2+GTBKXvovZByDkht4MNj2bB/wgrlmBETI7uX6ntmrWg
rVwIBjyJoB89AUEm7aEsTGwIdfCbZBJIhULRtJXuWKKlTRaqcSWyHHiwl3eK5VZy
Q3m+W3i+EY+smOdBP/nq6aHqvdJVRnbhhwZTDwhPx+7r0OPc7jQ7si+xzK73fjCF
dR114yhPrybH6C4Ik6WCNefPpxrpzjdDXzkFFYuBvfagdjG6z3rVLAIL+hks7+To
7MA5NvcTf62Ov8SazYP6xSPo3zpqt4JangXn/IoX1kN8opw/irfpOjVz5kZAXgpv
h8AXbZB0
-----END CERTIFICATE-----

まとめ

組織内向けの独自公開鍵基盤を作るために、ルート認証局、中間認証局を整備し、個別の証明書を発行した。また発行した証明書を使いPDFに対する署名と、その署名の検証の手順を確認した。

かなり手順は煩雑だが、一度通しでやると、どのような仕組みになっているのかという事の理解が深める事ができた。