スクリプトのお勉強 技術

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暗号化から通信する部分までを実装します。

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

執筆者:

関連記事

小ネタ: Ansible , with_itemsをloopに変える方法

とっても小さい小ネタです。 Ansible 2.4(今現在) -> Ansible 2.9に変えたい Ansibleを使用していますが、だいたい2.4ぐらいを使っています。流石にバージョンアップしない …

最近の故障/修正物

近年で故障し、しょうがないから修理、というか購入したもの、及び顛末を書いてみます。 ハードディスク・レコーダー ちょっと前から調子が悪く、電源がつかなかったり、ついたと思ったら、録画物のリストがおかし …

pipenv + Apache + Django起動設定

仕事でwebアプリケーションを作成しています。 Djangoで作成し、webサーバをApache、環境をpipenvで設定したpython3環境上で動かす予定です。 Apacheが起動するまでに、苦労 …

FastAPIでAPIを作ってみる(その1)

勉強として、FastAPIを使用して、実際にAPIを作成し、起動しようと思います。目標としては、本番運用用の設定まで行いたいと思います。 FastAPIとは FastAPIとは、Pythonによる、W …

CentOS7 + Django2.2 + uwsgi + nginx 連携方法

Djangoとnginx連携方法 以前はApache経由で連携しましたが、今回はnginxと連携する方法を記述します。 環境 設定した環境は以下です。 OS: CentOS7Python3.6.8 ( …