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

## 秘密鍵

### 作成
```python: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` では怒られます)


### 読み込み

```python: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 .... -----` で始まる形式)だと思います。

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

### 書き込み

```python: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` メソッドを呼び出します。

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

## 公開鍵

### 取得と書き込み

```python: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` は、前の項で作った鍵オブジェクトを指定します。

```python: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.x509` や `cryptography.fernet` といった `cryptography` 直下にあるモジュール群です。

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

[hazmat のマニュアル](https://cryptography.io/en/latest/hazmat/primitives/)の最上部には、

>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" と同じ意味[^at_your_own_risk]と捉えていいでしょう。それを「恐竜」と言ったりモジュールの名前にしたりするのは、気の利いた話だなーと思います。

[^at_your_own_risk]: 暗号を扱う分、それよりも一段は気をつけて、ぐらいでしょうか

## バインディング

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

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

[^lambda]: AWS の Lambda で使うために、手動であれこれしている場合などです

## 動作環境

これもバインディングに関連しますが、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 することもできます](https://cryptography.io/en/latest/installation/#build-on-linux)。ぱっと見るとそれほど面倒ではないように見えますが、バインディング周りが面倒なんだろうなあ、と思ったり。

# まとめ

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

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