11
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PythonAdvent Calendar 2018

Day 21

pyca/cryptographyを利用した鍵の動的管理

Last updated at Posted at 2018-12-21

 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(+読み込み)を行います。

crpytography.py
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を使わずとも可能なんじゃないでしょうか。しかし、秘密鍵が永続化される場所は、結局データストアなので、データストアの安全な管理や、鍵が再生成された場合の整合性などには気をつけたいところです。

References

Cryptography documentation

11
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?