Webサービスの中で署名や暗号化をする時、鍵をどこかに保存したり、ローテーションするなど管理が必要になります。最近だとVaultやAWS KMSなど外部サービスを利用してストイックに管理する方法もありますが、ここではCryptographyというPythonライブラリを利用して簡単に鍵を生成・管理する方法を紹介したいと思います。
Webサービスで秘密鍵の管理
皆さんはWebサービスで使う秘密鍵はどこに保存していますでしょうか。大抵の場合、DjangoみたいにFrameworkの設定変数にランダムな文字列を入れてそのまま使うと思います。公開鍵暗号方式の非対称鍵の場合はどうでしょうか。非対称鍵といえば、$HOME/.ssh
にある一対となったSSH公開鍵ファイルが思い浮かべますが、どこかに保存された鍵ファイルをアプリから読み込んで使うこともできると思います。
しかし、ScalableなWebアプリの場合、各アプリサーバーに複数の鍵ファイルが同時に存在してしまうと、例えそれが今は同じ鍵であっても鍵のローテーションなどの管理が大変だし、個人的には少々気持ち悪さを感じます。
そこで鍵の存在をRedisなど、データストアの一ヶ所に保存し、必要なときに呼び出したり、管理するために以下の方法を使いました。
pyca/cryptography
名前の通り、暗号化と鍵を扱うためのライブラリです。ここで使うことになるRSA暗号はもちろん、DSA・共通鍵・X.509・2factor認証など、様々な機能をサポートします。
Danger
This is a “Hazardous Materials” module. You should ONLY use it if you’re 100% absolutely sure that you know what you’re doing because this module is full of land mines, dragons, and dinosaurs with laser guns.
下記で扱うRSAなどのパッケージは危険物として上記のような警告文がドキュメントに書いてます(パッケージ名にもhazmat
)。一応自分でもRSAについてはある程度知識と理解があったつもりですが、full of land mines, dragons, and dinosaurs with laser gunsとか言われると更に気をつけておきたいころですね。
このライブラリを使って以下のように、秘密鍵の生成・Serialization(+保存)・Deserialization(+読み込み)を行います。
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
from django.core.cache import cache
CACHE_KEY = __name__ + '.PRIVATE'
class Cryptography:
public_exponent = 65537
key_size = 2048
backend = default_backend()
encoding = serialization.Encoding.PEM
format_ = serialization.PrivateFormat.PKCS8
encryption = serialization.NoEncryption()
password = None
@property
def private_key(self) -> rsa.RSAPrivateKey:
if not hasattr(self, '_private_key'):
self._private_key = self._load_key()
return self._private_key
def reload(self):
self._private_key = self._load_key()
return self
def refresh(self):
self._private_key = self._create_key()
return self
def _load_key(self) -> rsa.RSAPrivateKey:
private_bytes = cache.get(CACHE_KEY)
if private_bytes:
return serialization.load_pem_private_key(private_bytes,
self.password,
self.backend)
return self._create_key()
def _create_key(self) -> rsa.RSAPrivateKey:
key = self._generate_key()
self._store(key)
return key
def _generate_key(self) -> rsa.RSAPrivateKey:
return rsa.generate_private_key(self.public_exponent,
self.key_size, self.backend)
def _store(self, key: rsa.RSAPrivateKey):
private_bytes = key.private_bytes(self.encoding, self.format_,
self.encryption)
cache.set(CACHE_KEY, private_bytes, timeout=None)
cache.persist(CACHE_KEY)
ここではデータストアとして、CACHE_KEY
をキーとしたDjango cache(Redis)を使います。
ざっくりこのクラスでは、private_key
プロパティを呼び出すと、
- 内部のprivateなプロパティのキャッシュ
- データストアから読み込んだ鍵
- 新しく生成した鍵
の優先度で返します。
RSA暗号では秘密鍵から公開鍵を導出することができるので、このクラスで公開鍵は扱いません。
以下でもう少し詳しく説明します。
秘密鍵の動的生成
def _generate_key(self) -> rsa.RSAPrivateKey:
return rsa.generate_private_key(self.public_exponent,
self.key_size, self.backend)
一番最初、おそらくプロパティにも、データストアにも鍵は存在しないので自動で鍵オブジェクトを生成します。
key_size
は最小限2048以上、public_exponent
はわからないのでおとなしく65537という素数にしときましょう。
Serialize/Persistent
def _create_key(self) -> rsa.RSAPrivateKey:
key = self._generate_key()
self._store(key)
return key
def _store(self, key: rsa.RSAPrivateKey):
private_bytes = key.private_bytes(self.encoding, self.format_,
self.encryption)
cache.set(CACHE_KEY, private_bytes, timeout=None)
cache.persist(CACHE_KEY)
鍵が生成されたら、返す前にデータストアに保存します。生成される鍵はオブジェクトなのでserializeが必要です。encoding
はPEM、format
はPKCS8、場合によってencryption
でパスフレーズを指定することもできます。
Fetch/Deserialize
def _load_key(self) -> rsa.RSAPrivateKey:
private_bytes = cache.get(CACHE_KEY)
if private_bytes:
return serialization.load_pem_private_key(private_bytes,
self.password,
self.backend)
return self._create_key()
鍵をロードするときはまずデータストアをチェックして、鍵があればdeserializeしてオブジェクトに変換、なければ生成します。
公開鍵
from cryptography.hazmat.primitives import serialization
crypto = Cryptography()
private_key = crpyto.private_key
private_key = crypto.refresh().private_key # 秘密鍵の再生成
public_key = private_key.public_key()
pem: bytes = public_key.public_bytes(serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo)
上で説明した通り、公開鍵は対になる秘密鍵から簡単に導出できます。導出された鍵はやはりオブジェクトなので、保存や出力の際はserializeが必要です。
最後に
このスクリプトを応用することで、簡単な鍵の管理はVaultやKMSを使わずとも可能なんじゃないでしょうか。しかし、秘密鍵が永続化される場所は、結局データストアなので、データストアの安全な管理や、鍵が再生成された場合の整合性などには気をつけたいところです。