PythonでOpenSSLを扱うcryptographyモジュールを使う


はじめに

cryptography とは OpenSSL をラップする Python のモジュールです。高レベルと低レベルの両方のインタフェースを備えていて、OpenSSL でやりたいことすべてを扱うことができます。
https://cryptography.io/en/latest/

Python で公開鍵暗号を扱うコードを見ると、openssl コマンドを subprocess モジュールで呼び出すケースが多いようです。それで不都合はないのですが毎回 fork するコストがかかるので、大量には扱うときはモジュールを使う方が高速にできます。

あと、ラッパーとはいえユーザ側で書くのがすべて Python コードになるのも美しくて素敵なのでは、と。あくまで個人的な感触ですが。

先にサンプルコードから

どれぐらい簡単か、まずサンプルコードを見てみてください。
https://github.com/keioni/qiita-sample/blob/master/cryptography_sample.py

秘密鍵

作成

generate_privkey.py
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa, ec, ed25519

def generate_privkey(key_type = ''):
    if key_type == 'ec' or key_type == 'ecdsa':
        privkey = ec.generate_private_key(
            ec.SECP256R1(),
            default_backend()
        )
    elif key_type == 'ed25519':
        privkey = ed25519.Ed25519PrivateKey.generate()
    else:
        privkey = rsa.generate_private_key(
            public_exponent=65537,
            key_size=2048,
            backend=default_backend()
        )
    return privkey

秘密鍵アルゴリズムによって作り方が変わります。
(生成される鍵オブジェクトも基底クラスが同じではないのでmypy では怒られます)

読み込み

load_privkey.py
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization

def load_privkey(filename):
    with open(filename, 'rb') as fpr:
        privkey = serialization.load_pem_private_key(
            fpr.read(),
            password=None,
            backend=default_backend()
        )
    return privkey

ファイル形式によって関数が変わります。ほとんどの場合 PEM 形式(-----BEGIN .... ----- で始まる形式)だと思います。

鍵のアルゴリズムを意識する必要はありません。

書き込み

save_privkey.py
from cryptography.hazmat.primitives import serialization

def save_privkey(filename, privkey):
    serialized_key = privkey.private_bytes(
            serialization.Encoding.PEM,
            serialization.PrivateFormat.PKCS8,
            serialization.NoEncryption()
    )
    with open(filename, 'wb') as fpw:
        fpw.write(serialized_key)

秘密鍵をバイト配列にシリアライズします。
形式などを指定して、鍵オブジェクトの private_bytes メソッドを呼び出します。

読み込みと同様に、鍵のアルゴリズムを意識する必要はありません。

公開鍵

取得と書き込み

save_pubkey.py
from cryptography.hazmat.primitives import serialization

def save_pubkey(filename, privkey):
    pubkey = privkey.public_key()
    serialized_key = pubkey.public_bytes(
            serialization.Encoding.PEM,
            serialization.PublicFormat.SubjectPublicKeyInfo
    )
    with open(filename, 'wb') as fpw:
        fpw.write(serialized_key)

秘密鍵から公開鍵を取得します。あとは秘密鍵と同じような方法でシリアライズします。メソッドは public_bytes です。

電子証明書

CSRの作成

CSR (Certificate Signing Request) を作ります。
署名に使う秘密鍵 privkey は、前の項で作った鍵オブジェクトを指定します。

make_csr.py
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes

def make_csr(privkey):
    builder = x509.CertificateSigningRequestBuilder()
    builder = builder.subject_name(
        x509.Name([
            x509.NameAttribute(x509.NameOID.COMMON_NAME, 'example.com'),
        ])
    )
    builder = builder.add_extension(
        x509.SubjectAlternativeName([
            x509.DNSName('www.example.com'),
            x509.DNSName('test.example.com')
            ]
        ),
        critical=False
    )
    return builder.sign(privkey, hashes.SHA256(), default_backend())

builder を使うことで楽に作れるようになっています。

利用にあたって気をつける点

どうでしょうか。分かりやすく書けると思います。

では使ってみよう、という時に気をつけないといけない点をいくつか挙げます。

“hazmat”

cryptography は2つのコンポーネントから成り立っています。
ひとつは cryptography.x509cryptography.fernet といった cryptography 直下にあるモジュール群です。

もうひとつは、cryptography.hazmat 以下にあるモジュール群です。
hazmat は “Hazardous Materials” の略で、Hazardous とは「危険な」という意味です。hazmat 以下にあるモジュールは危険、という意味を持ちます。

hazmat のマニュアルの最上部には、

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.

と書いてあります。後段を Google 翻訳で訳すと、

このモジュールは地雷、ドラゴン、レーザー銃を持つ恐竜でいっぱいなので、何をしているかを完全に確信している場合にのみ使用してください。

物騒な話をしていますが、暗号を扱うモジュールだし十分な安全性が確保されているという保証もできないという、"At your own risk" と同じ意味1と捉えていいでしょう。それを「恐竜」と言ったりモジュールの名前にしたりするのは、気の利いた話だなーと思います。

バインディング

冒頭で書いたように cryptography はラッパーなので、中で OpenSSL のライブラリ(libssl.so)を使っています。openssl コマンド自体もこのライブラリを使っているので、openssl コマンドが使えるならこのライブラリが入っていて、問題なく動作しています。

スクリプト言語からこのようなコンパイルされた言語を組み合わせて使うことをバインディングといいます。cryptography を pip で導入しているなら気にする必要はありませんが、特殊な方法で導入している場合2や特殊な環境では問題が起きる可能性があります。
この場合、Python の知識だけで解決するのはなかなか難しいと思います。解決法を探すより openssl コマンドを呼び出した方がいいかもしれません。

動作環境

これもバインディングに関連しますが、OpenSSL が動作しない環境では動作しません。
また pip でインストールできるのは以下の環境に限られます:

  • x86-64 CentOS 7.x
  • x86-64 Fedora (latest)
  • macOS 10.15 Catalina
  • x86-64 Ubuntu 16.04 and rolling
  • x86-64 Debian Stretch (9.x), Buster (10.x), Bullseye (11.x), and Sid (unstable)
  • x86-64 Alpine (latest)
  • 32-bit and 64-bit Python on 64-bit Windows Server 2019

上にない場合、自分で build することもできます。ぱっと見るとそれほど面倒ではないように見えますが、バインディング周りが面倒なんだろうなあ、と思ったり。

まとめ

サンプルコードを読んでもらえたら、それほど難しくないのが分かってもらえると思います。とくに pythonic な抽象化がそこかしこにあるので pythonista にはより扱いやすいのではないか、と思っています。

動作環境をお持ちなら、ぜひ試してみませんか?


  1. 暗号を扱う分、それよりも一段は気をつけて、ぐらいでしょうか 

  2. AWS の Lambda で使うために、手動であれこれしている場合などです