Advent Calendar 2021:自作メッセージングプロトコル
Day 5: IDへの署名
3日目と4日目でIDの生成と証明書署名要求の発行、その署名要求に署名をするためのCAの準備(鍵ペア・CA証明書の作成)を行いました。今日は、このCAによりIDに対する証明書を発行する部分を実装します。
IDデータベースと証明書シリアル番号の管理
CSRの Common Name
で指定されたIDのユニーク性を担保するためにデータベースで管理します。また、今回はX.509の証明書フォーマットを利用しており、シリアル番号が必要なので、こちらの管理も行います。
なお、IDを大量に発行して、それに対する署名要求をするような Denial of Service (DoS) 攻撃も考えられますが、ここは技術的な解決方法とサービス運用設計(例えば、署名に対していくらかの対価を要求するなど)による解決方法が考えられるので、現時点では一旦棚上げにしておきます。
今回、IDデータベースとシリアル番号の管理はSQLiteを使って実装します。以下のSQLで cert
テーブルを1つ作成します。なお、昨日作成したCAの自己署名証明書のシリアル番号が1であるため、そのデータをダミーで入れます。
create table cert (
serial char(40) primary key not null,
cn char(64) not null,
cert text,
active bool not null
);
create index cert_index on cert (cn, active);
insert into cert (serial, cn, cert, active) values ('0000000000000000000000000000000000000001', '', null, 1);
上の初期化クエリを init.sql
というファイルに保存し、以下のコマンドでデータベースファイル ca/ca.db
を初期化します。
$ sqlite3 -init init.sql ca/ca.db
これでデータベースの初期化が完了しました。
次にシリアル番号の生成関数を実装します。「シリアル」なので連続した値を用いると楽なのですが、連続した値を用いるとシリアル番号からIDの発行数などが推定可能となってしまうため、今回は乱数を割り当てます。ただし、重複が許されないので、IDの重複確認に加えてこちらのシリアル番号の重複確認も行う必要があります。
乱数によるシリアル番号は3日目でIDの生成に使った secrets.randbits()
により行います。SQLiteは160ビットの整数を扱う型がない(と思われる)ため、データベース上では文字列として扱いますが、証明書発行のプログラム内では整数として扱うので、以下の generate_serial()
関数のようにそのまま secrets.randbits()
の返り値を使います。
def generate_serial():
return secrets.randbits(160)
IDのユニーク性の確認するために、PEM形式のCSR req_pem
から Common Name (ID) cn
を取得します。
req = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, req_pem)
cn = req.get_subject().CN
これを以下のようにデータベースへの登録があるかを確認します。
DATABASE_FILE = "ca/ca.db"
# Open the database
db = sqlite3.connect(DATABASE_FILE)
# Check the common name first
cur = db.cursor()
sql = 'select count(*) from cert where cn=? and active=true'
cur.execute(sql, (cn, ))
res = cur.fetchone()
if res[0] > 0:
print("Duplicate common name: {}".format(cn))
db.close()
既にCommon Nameが存在する場合、つまりIDが重複している場合はエラーメッセージを(標準出力に)表示しています。
CAによる署名と証明書の発行
PEM形式のCA証明書 cacert_pem
、CAの秘密鍵 key_pem
はそれぞれ以下のコードで cacert
、key
というOpenSSL (crypto) モジュールのインスタンスにします。
key = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, key_pem)
cacert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cacert_pem)
これを用いてシリアル番号 serial
の署名済み証明書を発行するための関数を以下のように実装します。
VALID_NOT_BEFORE = 0
VALID_NOT_AFTER = 3600 * 24 * 356 * 3 # 3 years
def sign(req, cacert, key, serial):
# Create a self-signed certificate
cert = OpenSSL.crypto.X509()
cert.set_serial_number(serial)
cert.gmtime_adj_notBefore(VALID_NOT_BEFORE)
cert.gmtime_adj_notAfter(VALID_NOT_AFTER)
cert.set_issuer(cacert.get_subject())
cert.set_subject(req.get_subject())
cert.set_pubkey(req.get_pubkey())
cert.sign(key, 'sha256')
return cert
内容としては、4日目に扱った自己署名証明書と同じ手順ですが、Issuer(発行者)にはCAのSubject、Subjectと公開鍵フィールドにはCSRの内容を入れます。
IDと証明書のデータベースへの登録
CAにより署名した証明書をデータベースに登録します。最初にCommon Nameの重複を確認しましたが、証明書への署名をしている間に同一のIDでの登録がされている可能性はゼロではないので、以下のようにトランザクション処理をします。ここで serial
はシリアル番号、cert_pem
は sign()
関数の返り値(署名済み証明書)をPEM形式で出力したものです。
serial_str = '{:0>40x}'.format(serial)
db.execute('begin')
cur = db.cursor()
sql = 'select count(*) from cert where (serial=? or cn=?) and active=true'
cur.execute(sql, (serial_str, cn))
res = cur.fetchone()
if res[0] > 0:
print('Duplicate serial number or common nameerror:\nSerial: {}\nCommon Name: {}'.format(serial_str, cn))
db.close()
return False
sql = 'insert into cert (serial, cn, cert, active) values (?, ?, ?, ?)'
try:
cur.execute(sql, (serial_str, cn, cert_pem.decode('utf-8'), True))
except sqlite3.IntegrityError:
print('Transaction error')
db.close()
return False
db.commit()
db.close()
実装
上記で説明した署名と証明書発行を [github:drpnd/advmsg:sign.py] に実装しました。このプログラムを実行すると以下のように、署名済み証明書を crt.pem
に保存します。なお、証明書のPEMも表示するようにしています。
$ python3 sign.py
Saved the signed certificate to crt.pem
-----BEGIN CERTIFICATE-----
MIIBWjCCAQACAQIwCgYIKoZIzj0EAwIwJzElMCMGA1UEAwwcQWR2ZW50IENhbGVu
ZGFyIENBIFJvb3QgMjAyMTAeFw0yMTEyMDUxMTQ3NTJaFw0yNDExMDcxMTQ3NTJa
MEsxSTBHBgNVBAMMQDYyYzQ3OWQwNTk0MDUyZDZmYzQ0YWM5NWRkNjk4OTkzODRk
NmMwMjhkYjBlMGE4NmEwYzliYjJhYmZiZGJiNGEwWTATBgcqhkjOPQIBBggqhkjO
PQMBBwNCAAThsacVsaF7LbYRgvtl3on3ka6VLkLUkdZpxCG/AnE5siMZrdY41sK5
al9xwj+SMErVb7rPoJgOY3kvjV5oaBsrMAoGCCqGSM49BAMCA0gAMEUCIQCtAAyf
RK2lyITEplzEfKtdxZ5MZldJB71Fy5dSvPuAvwIgd1dDdGr2DvQGdB2vevqkjAEp
k6mO/u1ouJ8Uqlt1G+M=
-----END CERTIFICATE-----
生成された署名済みの証明書は以下のようになっています。
$ openssl x509 -in crt.pem -text
Certificate:
Data:
Version: 1 (0x0)
Serial Number:
a7:27:e7:cc:5d:cf:09:b0:e9:ca:20:76:85:e9:e3:1a:16:d0:a0:b7
Signature Algorithm: ecdsa-with-SHA256
Issuer: CN=Advent Calendar CA Root 2021
Validity
Not Before: Dec 5 11:56:46 2021 GMT
Not After : Nov 7 11:56:46 2024 GMT
Subject: CN=62c479d0594052d6fc44ac95dd69899384d6c028db0e0a86a0c9bb2abfbdbb4a
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:e1:b1:a7:15:b1:a1:7b:2d:b6:11:82:fb:65:de:
89:f7:91:ae:95:2e:42:d4:91:d6:69:c4:21:bf:02:
71:39:b2:23:19:ad:d6:38:d6:c2:b9:6a:5f:71:c2:
3f:92:30:4a:d5:6f:ba:cf:a0:98:0e:63:79:2f:8d:
5e:68:68:1b:2b
ASN1 OID: prime256v1
NIST CURVE: P-256
Signature Algorithm: ecdsa-with-SHA256
30:46:02:21:00:e6:92:b1:f3:ea:b4:a3:f5:28:53:c6:8a:a4:
34:f0:f7:4c:34:0c:e3:95:c7:c8:da:a9:22:39:bd:c7:02:27:
02:02:21:00:fb:2d:5e:9a:f4:04:ad:97:a5:c3:25:2d:42:d6:
f0:20:71:0c:94:d4:ae:e4:79:ed:a0:12:4d:61:1c:ed:8e:70
-----BEGIN CERTIFICATE-----
MIIBbzCCARQCFQCnJ+fMXc8JsOnKIHaF6eMaFtCgtzAKBggqhkjOPQQDAjAnMSUw
IwYDVQQDDBxBZHZlbnQgQ2FsZW5kYXIgQ0EgUm9vdCAyMDIxMB4XDTIxMTIwNTEx
NTY0NloXDTI0MTEwNzExNTY0NlowSzFJMEcGA1UEAwxANjJjNDc5ZDA1OTQwNTJk
NmZjNDRhYzk1ZGQ2OTg5OTM4NGQ2YzAyOGRiMGUwYTg2YTBjOWJiMmFiZmJkYmI0
YTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOGxpxWxoXstthGC+2XeifeRrpUu
QtSR1mnEIb8CcTmyIxmt1jjWwrlqX3HCP5IwStVvus+gmA5jeS+NXmhoGyswCgYI
KoZIzj0EAwIDSQAwRgIhAOaSsfPqtKP1KFPGiqQ08PdMNAzjlcfI2qkiOb3HAicC
AiEA+y1emvQErZelwyUtQtbwIHEMlNSu5HntoBJNYRztjnA=
-----END CERTIFICATE-----
まとめと明日の予定
今日は、証明書署名要求で要求されたIDのCAによるユニーク性の検証と証明書への署名を実装しました。明日はこのID・証明書を管理するP2Pネットワークを実装しようと思います。