スクリプトのお勉強 技術

PythonでPKCS#12を使用して暗号/復号する

投稿日:

1. はじめに

仕事でVPN関係のシステム開発をすることになりました。まずは暗号機能の基本を思い出すため、Pythonで、PKCS#12の公開鍵で暗号、秘密鍵で復号するプログラムを作ってみようと思います。

2. 準備

2.1 PKCS#12ファイル作成

PKCS#12とは、RFC 7292で規定されている、秘密鍵と公開鍵が一体化したデータの ファイルフォーマット規約です。データは、パスワードで暗号しています。

「PKCS#12ファイル」はOpenSSLコマンドでも作成できますが、もっとお気楽にXCAで作成します。

2.2 対象データは「共通鍵暗号」データ

RSA暗号は、暗号/復号するデータは、最大鍵ビット長になります。しかも、暗号/復号する処理が遅いと言われています。

なので、通常の場合RSA暗号で暗号化する対象は「共通鍵暗号」が多いです。

ここでも、「共通鍵暗号」を暗号してみようと思います。アルゴリズムはAESを使用しますので、データ長は256ビット(32バイト)です。

2.3 暗号ライブラリは、pyOpenSSL/PyCryptodome

使用する暗号ライブラリは二つです。

  • pyOpenSSL
    • OpenSSLバインディング
    • ここではPKCS#12の読み込みに使用
  • PyCryptodome
    • 暗号ライブラリ
    • pycrypto の後継
    • 暗号用の機能のみに絞ってる開発している印象
    • ここではAES暗号, 復号に使用
    • 関数仕様

インストールは以下で行っておきます。

### Pythonの仮想環境作成
$ cd encrypt
$ python3 -m venv ./venv
### ライブラリインストール
$ source venv/bin/activate
$ pip install pyopenssl
$ pip install pycryptodomex
$ python -m Cryptodome.SelfTest

2.4 システム構成

システム構成というほど、大きくありませんが、server/clientで通信するサンプルを作成しようと考えました。

サンプルを以下に分けます。

  • PKCS#12ファイル to 公開鍵/秘密鍵抽出(pkcs12.py)
  • RSAによる暗号化/復号(rsa.py)
  • AESによる暗号化/復号(aes.py)
  • server/client 通信サンプル(server.py)

3. PKCS#12ファイル to 公開鍵/秘密鍵抽出

PKCS#12ファイルはXCAで作成し、カレントディレクトリに置いておく前提とします。

以下でPKCS#12ファイルを読み出し、公開鍵(pubkey.pem)と秘密鍵privkey.pem)を抽出します。

$ export P12_PASSWORD='XXXXXXXXXX'
$ pipenv run python3 ./pkcs12.py

PKCS#12ファイルの読み込みサンプル(pkcs12.py)は以下の通りです。

中心部分は、OpenSSLをラップした関数(load_pkcs12)を呼び出しているところです。

from OpenSSL.crypto import load_pkcs12, FILETYPE_PEM
import OpenSSL
import os


class PKCS12:
    # input
    PKCS12_FILENAME = 'server.p12'

    # output
    PUBLIC_KEY_FILENAME = 'pubkey.pem'
    PRIVATE_KEY_FILENAME = 'privkey.pem'

    def __init__(self):
        pass

    def output(self):
        with open(self.PKCS12_FILENAME, 'rb') as f:
            c = f.read()

        p = None
        ### PKCS#12ファイルを読み込む
        try:
            p = load_pkcs12(c, os.environ['P12_PASSWORD'])
        except OpenSSL.crypto.Error as e:
            print(e)
            return

        certificate = p.get_certificate()
        public_key = certificate.get_pubkey()
        private_key = p.get_privatekey()

        type_ = FILETYPE_PEM

        with open(self.PRIVATE_KEY_FILENAME, 'wb') as f:
            f.write(OpenSSL.crypto.dump_privatekey(type_, private_key))

        with open(self.PUBLIC_KEY_FILENAME, 'wb') as f:
            f.write(OpenSSL.crypto.dump_publickey(type_, public_key))


if __name__ == "__main__":
   p = PKCS12().output()

実行結果は以下の通りです。

$ python3 pkcs12.py
encrypt$ ls -l *.pem
-rw-rw-r-- 1 tanino tanino 1704  5月 19 14:26 privkey.pem
-rw-rw-r-- 1 tanino tanino  451  5月 19 14:26 pubkey.pem

ちなみにパスワードを間違えると、例外を処理しているので、以下になります。

[('PKCS12 routines', 'PKCS12_parse', 'mac verify failure')]

4. RSAによる暗号化/復号

RSAによる暗号化/復号のサンプル(rsa.py)は以下の通りです。

from Cryptodome.Cipher import PKCS1_OAEP
from Cryptodome.PublicKey import RSA

from pkcs12 import PKCS12

from aes import generate_random_secret_key


class RSACipher:

    def __init__(self):
        self._pubKey = None
        self._privKey = None

    def import_pubkey(self, public_key):
        """ public_key: PEM
            暗号用
        """
        self._pubKey = RSA.importKey(public_key)

    def import_privkey(self, private_key):
        """ private_key: PEM
            復号用
        """
        self._privKey = RSA.importKey(private_key)

    def encrypt(self, byte_data):
        if not self._pubKey:
            print("pubKey is not loaded")
            return
        pkcs1Cipher=PKCS1_OAEP.new(self._pubKey)
        encryptedString=pkcs1Cipher.encrypt(byte_data)
        return encryptedString

    def decrypt(self, encryptedString):
        if not self._privKey:
            print("privKey is not loaded")
            return
        pkcs1Cipher=PKCS1_OAEP.new(self._privKey)
        decryptedString=pkcs1Cipher.decrypt(encryptedString)
        return decryptedString


if __name__ == "__main__":
    # 簡易テスト
    rsa=RSACipher()
    with open(PKCS12.PUBLIC_KEY_FILENAME) as f:
        rsa.import_pubkey(f.read())

    with open(PKCS12.PRIVATE_KEY_FILENAME) as f:
        rsa.import_privkey(f.read())

    # 公開鍵で暗号化
    key =  generate_random_secret_key()
    print("encrypted key: {}".format(key))
    encryptedString = rsa.encrypt(key)
    # 秘密鍵で復号
    decryptedString = rsa.decrypt(encryptedString)
    print("decrypted key: {}".format(decryptedString))

実行する際は、上記のpkcs12.pyを同じディレクトリに格納してください。

実行結果は以下の通りです。

$ python3 rsa.py
encrypted key: b'e\xc7\x9a\xceJ\x99A\x7f\xdd\xba\xd4\xc5c\x8c\xca\x10d*eP\xef\xa8;\x0c\xbe\xa8\xfe\x18?\x9e.\x97'
decrypted key: b'e\xc7\x9a\xceJ\x99A\x7f\xdd\xba\xd4\xc5c\x8c\xca\x10d*eP\xef\xa8;\x0c\xbe\xa8\xfe\x18?\x9e.\x97'

5. おわりに

思ったより長くなったので、ここで切ります。
次は、AES暗号化から通信する部分までを実装します。

-スクリプトのお勉強, 技術

執筆者:

関連記事

リモート実家帰りしてみる

このご時世、実家には直接帰れないけど、1月には一応実家帰り的な感じでリモート実家帰りをしようかと思いました。 リモートは大変。。 一応実家には自分で設置したインターネットや無線LANがあるので、Zoo …

Nuxt.js – CRUDアプリケーションのフォーム/一覧を作成する

前回で作ったAPIのフロントエンドアプリケーションを作ろうと思います。 どういうアプリ? サンプルとして作ったAPIが住所録的だったので、住所録を作りました。 以下の機能があります。 登録(確認付き) …

Pythonパッケージ管理の歴史

歴史っても、あまり過去に興味がないので、、 Pythonのパッケージ管理の歴史は、常に流動的で、そもそもからして、とてもじゃないがまとめて説明できるようなものではないです。 はっきり言って昔からよく分 …

CentOS7 + Django2.2でSQlite3を使用する方法

そのままだとエラーになる 素のCentOS7で、SQLite3を使用して、Djangoアプリを起動すると以下のエラーになります。 File “/opt/webapps/django_upload/.v …

seaborn + Pandas + Python によるグラフ描画(その2: グラフ描画編)

前回の続き 前回の続きです。 折れ線グラフ まずは折れ線グラフを描画したいと思います。 描画するのは以下です。 運用商品(4つ)日経平均 以下で起動します。引数(dataset-2017-201908 …