データの暗号化・復号、署名・検証をPythonで手軽に実現
はじめに:python-gnupgとは? 🤔
python-gnupg は、Pythonプログラムから GnuPG (GNU Privacy Guard) の機能を利用するための強力なラッパーライブラリです。GnuPGは、OpenPGP標準に基づいた公開鍵暗号技術を用いて、データの暗号化、復号、デジタル署名、署名検証、そして鍵管理(生成、リスト表示、削除など)を行うための、広く使われている自由ソフトウェアです。
このライブラリを使うことで、ファイルやメッセージの機密性を高めたり、データの完全性や送信者の真正性を確認したりする処理を、Pythonコードから簡単かつ安全に実装できます。例えば、以下のようなことが可能になります。
- 機密データを安全に保管・送信するための暗号化処理
- 受け取った暗号化データを復号して内容を確認
- ドキュメントやソフトウェアが改ざんされていないことを保証するためのデジタル署名
- 署名されたデータが本物であることを確認するための署名検証
- プログラム内でGnuPGの鍵を管理(インポート、エクスポート、リスト表示など)
python-gnupg
は、GnuPGのコマンドラインインターフェースを内部的に呼び出し、その結果をPythonオブジェクトとして扱えるように抽象化してくれます。これにより、開発者はGnuPGコマンドの詳細なオプションや出力形式を意識することなく、Pythonらしい直感的な方法で暗号化処理を組み込むことができます。
このライブラリは、Python 3.6以上での使用が推奨されていますが、レガシーコード向けにPython 2.7でも動作します。最新版はPyPIで公開されており、pip install python-gnupg
コマンドで簡単にインストールできます。
GnuPG (GNU Privacy Guard、gpgとも呼ばれる) は、PGP (Pretty Good Privacy) の代替として開発された、OpenPGP標準のフリーソフトウェア実装です。公開鍵暗号方式と共通鍵暗号方式の両方に対応し、データの暗号化やデジタル署名を実現します。Windows, macOS, Linuxなど多くのプラットフォームで利用可能です。
GnuPGと公開鍵暗号の基本 🔑
python-gnupg
を効果的に使うためには、GnuPGの基礎となる公開鍵暗号方式の概念を理解しておくことが重要です。
公開鍵暗号方式とは?
公開鍵暗号方式(非対称鍵暗号方式とも呼ばれる)は、暗号化と復号に異なる鍵を使用する方式です。具体的には、「公開鍵」と「秘密鍵」というペアの鍵を使用します。
- 公開鍵 (Public Key): 名前が示す通り、一般に公開される鍵です。誰でもこの鍵を入手して、特定の受信者宛てのデータを暗号化したり、その受信者が作成した署名を検証したりできます。公開しても安全です。
- 秘密鍵 (Private Key / Secret Key): 自分だけが秘密に保管する鍵です。公開鍵で暗号化されたデータを復号したり、データにデジタル署名を作成したりするために使用します。絶対に他人に知られてはいけません。
この方式の流れは以下のようになります。
- 鍵ペアの生成: 受信者(例えばアリス)が、自分専用の公開鍵と秘密鍵のペアを作成します。
- 公開鍵の配布: アリスは自分の公開鍵を、データを送ってほしい人(例えばボブ)や、自分の署名を検証してほしい人に配布します。
- 暗号化(ボブからアリスへ): ボブはアリスの公開鍵を使ってメッセージを暗号化し、アリスに送信します。
- 復号(アリス): アリスは、受け取った暗号化メッセージを、自分の秘密鍵を使って復号します。アリスの秘密鍵を持っていない人は、たとえ暗号化メッセージを傍受しても内容を読むことはできません。
- 署名(アリス): アリスは、メッセージやファイルに対して、自分の秘密鍵を使ってデジタル署名を作成します。
- 署名検証(ボブ): ボブは、アリスの公開鍵を使って、受け取ったメッセージやファイルの署名を検証します。検証に成功すれば、そのデータが確かにアリスによって署名され、改ざんされていないことを確認できます。
この仕組みにより、安全でない通信路上でも、機密性の高い情報のやり取りや、データの真正性の保証が可能になります。
鍵管理の重要性
GnuPGを使用する上で、鍵の管理は非常に重要です。特に秘密鍵は、その名の通り厳重に管理する必要があります。もし秘密鍵が漏洩したり、パスフレーズが破られたりすると、暗号化された情報が解読されたり、なりすましによる署名が行われたりする可能性があります。
GnuPGは、鍵を「キーリング」と呼ばれるファイルに保存して管理します。通常、公開鍵は公開キーリング (pubring.gpg
など)、秘密鍵は秘密キーリング (secring.gpg
など) に保存されます (GnuPG 2.1以降では管理方法が変更されています)。python-gnupg
を使用する際には、これらのキーリングがどこにあるかを適切に指定する必要があります。
gpg-agent
が使われるなど、内部的な仕組みが大きく変わっています。
インストールと準備 ⚙️
python-gnupg
を使用するには、いくつかの準備が必要です。
1. GnuPG本体のインストール
python-gnupg
はGnuPGコマンドのラッパーであるため、まずGnuPG本体がシステムにインストールされている必要があります。
- Linux (Debian/Ubuntu系):
sudo apt-get update && sudo apt-get install gnupg
- Linux (Fedora/RHEL系):
sudo dnf install gnupg2
またはsudo yum install gnupg2
- macOS (Homebrew使用):
brew install gnupg
- Windows: Gpg4win をダウンロードしてインストールするのが一般的です。
python-gnupg
が必要とするのはコマンドラインツールのgpg.exe
です。
インストール後、ターミナル(コマンドプロンプト)で gpg --version
を実行し、バージョン情報が表示されることを確認してください。
gpg.exe
と、それが依存するDLL (iconv.dll
など) を適切な場所(Pythonスクリプトと同じディレクトリや、システムのPATHが通ったディレクトリなど)に配置するだけでも動作する場合があります。
2. python-gnupgライブラリのインストール
次に、Pythonのパッケージインストーラである pip
を使って python-gnupg
をインストールします。
pip install python-gnupg
これで、Pythonスクリプトから import gnupg
としてライブラリを読み込む準備が整いました。
3. GnuPGホームディレクトリの確認 (任意だが重要)
GnuPGは、キーリングや設定ファイルを特定の「ホームディレクトリ」に保存します。デフォルトの場所はOSによって異なりますが(例: Linux/macOSでは ~/.gnupg
)、スクリプト実行時にこの場所を明示的に指定することが推奨されます。これにより、意図しないキーリングが使用されるのを防ぎ、環境による差異を吸収できます。
ホームディレクトリは、後述する gnupg.GPG()
の初期化時に gnupghome
引数で指定します。
import gnupg
import os
# 例: ホームディレクトリを指定してGPGオブジェクトを初期化
gpg_home = os.path.expanduser('~/.gnupg') # デフォルトの場所の例
# gpg_home = '/path/to/your/custom/gpghome' # 特定のディレクトリを指定する場合
gpg = gnupg.GPG(gnupghome=gpg_home)
# gpgオブジェクトを使って様々な操作を行う
# ...
指定したホームディレクトリが存在しない場合、GnuPGが自動的に作成しようとすることがあります。適切な権限があることを確認してください。
基本的な使い方 🛠️
ここでは、python-gnupg
の基本的な機能である、鍵管理、暗号化、復号、署名、検証の方法を見ていきます。
GPGオブジェクトの初期化
すべての操作は gnupg.GPG
クラスのインスタンスを通じて行われます。
import gnupg
# GnuPGホームディレクトリを指定して初期化 (推奨)
gpg = gnupg.GPG(gnupghome='/path/to/gpghome')
# デフォルトのホームディレクトリを使用する場合 (非推奨の場合あり)
# gpg = gnupg.GPG()
# GPG実行ファイルのパスを明示的に指定する場合
# gpg = gnupg.GPG(gpgbinary='/usr/local/bin/gpg')
# 詳細なログを出力する場合 (デバッグ用)
# gpg = gnupg.GPG(gnupghome='/path/to/gpghome', verbose=True)
コンストラクタには他にも、キーリングファイルのパスを指定する keyring
, secret_keyring
や、追加のGPGコマンドラインオプションを指定する options
などの引数があります。
鍵の管理 🔑
鍵のリスト表示
キーリングに登録されている鍵の一覧を取得します。
# 公開鍵の一覧を取得
public_keys = gpg.list_keys()
print("--- Public Keys ---")
for key in public_keys:
print(f"Fingerprint: {key['fingerprint']}")
print(f" Key ID: {key['keyid']}")
print(f" Type: {key['type']}")
print(f" Length: {key['length']}")
print(f" Usage: {key['usage']}")
print(f" Algo: {key['algo']}")
print(f" Ownertrust: {key['ownertrust']}")
print(f" Uids: {key['uids']}")
print(f" Subkeys: {key['subkeys']}")
print("-" * 20)
# 秘密鍵の一覧を取得 (引数に True を指定)
private_keys = gpg.list_keys(True)
print("\n--- Private Keys ---")
for key in private_keys:
# 秘密鍵の場合、表示される情報は限られることが多い
print(f"Fingerprint: {key['fingerprint']}")
print(f" Key ID: {key['keyid']}")
print(f" Type: {key['type']}")
print(f" Trust: {key['trust']}") # 秘密鍵の場合は trust が表示されることがある
print(f" Uids: {key['uids']}")
print("-" * 20)
返されるリストの各要素は辞書型で、鍵に関する詳細情報(フィンガープリント、キーID、ユーザーID、有効期限など)が含まれます。
鍵のインポート
ファイルや文字列から鍵をキーリングにインポートします。主に公開鍵のインポートに使用されます。
# ファイルから公開鍵をインポート
with open('bob_public_key.asc', 'r') as f:
key_data = f.read()
import_result = gpg.import_keys(key_data)
print(f"Imported keys count: {import_result.count}")
print(f"Fingerprints: {import_result.fingerprints}")
# 詳細な結果は import_result.results リスト (辞書) に格納される
for result in import_result.results:
print(f" Action: {result.get('action')}, Fingerprint: {result.get('fingerprint')}")
# 文字列からインポートする場合も同様
# key_string = """-----BEGIN PGP PUBLIC KEY BLOCK-----
# ... (公開鍵データ) ...
# -----END PGP PUBLIC KEY BLOCK-----"""
# import_result = gpg.import_keys(key_string)
import_keys()
メソッドは、インポート結果に関する情報を持つオブジェクトを返します。
import_result
オブジェクトで確認することが重要です。
鍵のエクスポート
キーリングから鍵をエクスポートして、ファイルに保存したり文字列として取得したりします。
# 特定の公開鍵をASCII形式でエクスポート (フィンガープリントで指定)
key_fingerprint = 'YOUR_FINGERPRINT_HERE' # エクスポートしたい鍵のフィンガープリント
ascii_armored_public_key = gpg.export_keys(key_fingerprint)
# ファイルに保存
with open('my_public_key_exported.asc', 'w') as f:
f.write(ascii_armored_public_key)
print("Public key exported to my_public_key_exported.asc")
# 秘密鍵をエクスポート (パスフレーズが必要な場合が多い)
# GnuPG 2.1以降では、秘密鍵のエクスポートにパスフレーズが必須
try:
ascii_armored_private_key = gpg.export_keys(
keyids=key_fingerprint,
secret=True,
passphrase='your_secret_passphrase' # 秘密鍵のパスフレーズ
)
with open('my_private_key_exported.asc', 'w') as f:
f.write(ascii_armored_private_key)
print("Private key exported to my_private_key_exported.asc")
except Exception as e:
print(f"Error exporting private key: {e}")
# 複数の鍵をエクスポートする場合は keyids にリストを渡す
# public_keys_ascii = gpg.export_keys(['FP1', 'FP2'])
secret=True
を指定すると秘密鍵をエクスポートします。この際、通常は passphrase
の指定が必要です。
鍵の生成
新しい鍵ペアを生成します。対話的なプロンプトなしで生成するには、gen_key_input()
でパラメータを設定し、gen_key()
に渡します。
# 鍵生成のためのパラメータを設定
input_data = gpg.gen_key_input(
key_type="RSA", # 鍵の種類 (RSA, DSA, ECCなど)
key_length=2048, # 鍵長 (ビット単位)
name_real="Alice Example", # 氏名
name_comment="Test key", # コメント
name_email="alice@example.com", # メールアドレス
expire_date="2y", # 有効期限 (例: 2年、0は無期限)
passphrase="supers3cr3t" # 秘密鍵のパスフレーズ (GnuPG 2.1以降は必須に近い)
# no_protection=True # パスフレーズなしで生成する場合 (非推奨)
)
# 鍵を生成
key = gpg.gen_key(input_data)
if key:
print(f"Key generated successfully!")
print(f"Fingerprint: {key.fingerprint}")
print(f"Key ID: {key.keyid}")
# key オブジェクトには生成された鍵の情報が含まれる
else:
print("Key generation failed.")
# エラー詳細は gpg オブジェクトのログなどで確認
鍵の種類 (key_type
) や鍵長 (key_length
)、有効期限 (expire_date
) など、様々なパラメータを指定できます。楕円曲線暗号 (ECC) を使う場合は key_type
と key_curve
(例: ‘ed25519’, ‘nistp256’) を指定します (GnuPG 2.1以降が必要)。
no_protection=True
を指定できますが、セキュリティリスクを理解した上で使用してください。
鍵の削除
キーリングから鍵を削除します。注意:一度削除すると元に戻せないため、慎重に実行してください。
fingerprint_to_delete = 'FINGERPRINT_OF_KEY_TO_DELETE'
# まず公開鍵を削除
delete_result_pub = gpg.delete_keys(fingerprint_to_delete)
print(f"Delete public key result: Status={delete_result_pub.status}, Stderr={delete_result_pub.stderr}")
# 次に秘密鍵を削除 (パスフレーズが必要な場合あり)
try:
delete_result_sec = gpg.delete_keys(fingerprint_to_delete, secret=True, passphrase='your_secret_passphrase')
print(f"Delete secret key result: Status={delete_result_sec.status}, Stderr={delete_result_sec.stderr}")
except Exception as e:
print(f"Error deleting secret key: {e}")
# 存在しない鍵を削除しようとするとエラーにはならないが、statusに変化がない場合がある
# delete_result = gpg.delete_keys('NON_EXISTENT_FINGERPRINT')
通常、公開鍵と秘密鍵は別々に削除操作が必要です。secret=True
で秘密鍵を削除します。
データの暗号化 🔒
文字列データやファイルを、指定した受信者の公開鍵を使って暗号化します。
# 暗号化するデータ
plain_text = "これは秘密のメッセージです。🤫"
# 受信者のキーIDまたはフィンガープリント (リストで複数指定可)
recipient_keyid = 'RECIPIENT_KEYID_OR_FINGERPRINT'
# recipients = ['KEYID1', 'KEYID2'] # 複数の受信者
# 文字列データを暗号化
encrypted_data = gpg.encrypt(
plain_text,
recipients=recipient_keyid,
always_trust=True, # キーの信頼性をチェックしない (テスト用など)
# sign='YOUR_SIGNING_KEYID', # 署名を同時に行う場合
# passphrase='your_signing_passphrase' # 署名用パスフレーズ
)
if encrypted_data.ok:
print("Encryption successful!")
encrypted_string = str(encrypted_data) # ASCII Armor形式の暗号文
print("Encrypted data (ASCII Armor):")
print(encrypted_string)
# バイナリ形式で取得したい場合: encrypted_binary = encrypted_data.data
else:
print("Encryption failed.")
print(f"Status: {encrypted_data.status}")
print(f"Stderr: {encrypted_data.stderr}")
# ファイルを暗号化
input_filename = 'plain_document.txt'
output_filename = 'encrypted_document.txt.gpg'
with open(input_filename, 'rb') as f: # バイナリモードで開く
encrypt_status = gpg.encrypt_file(
file=f,
recipients=recipient_keyid,
always_trust=True,
output=output_filename
# armor=False # バイナリ形式で出力する場合
)
if encrypt_status.ok:
print(f"File '{input_filename}' encrypted successfully to '{output_filename}'.")
else:
print(f"File encryption failed.")
print(f"Status: {encrypt_status.status}")
print(f"Stderr: {encrypt_status.stderr}")
encrypt()
は文字列を、encrypt_file()
はファイル(またはファイルライクオブジェクト)を暗号化します。
recipients
: 受信者のキーIDまたはフィンガープリントのリスト。公開鍵がキーリングにインポートされている必要があります。always_trust
:True
にすると、GPGの信頼データベースチェックをスキップします。本番環境では注意が必要です。sign
: 署名に使用する秘密鍵のキーIDを指定すると、暗号化と同時に署名も行います。True
を指定するとデフォルトの秘密鍵が使われます。passphrase
: 署名を行う場合に、署名用秘密鍵のパスフレーズを指定します。output
: (encrypt_file
) 出力ファイル名を指定します。指定しない場合、結果は返り値のオブジェクトに含まれます。armor
: デフォルトはTrue
で、人間が読めるASCII Armor形式で出力します。False
にするとバイナリ形式になります。symmetric
:True
または対称暗号アルゴリズム名(例: ‘AES256’)を指定すると、公開鍵を使わずパスフレーズによる共通鍵暗号を行います。この場合recipients
はNone
にします。extra_args
: 追加のGPGコマンドライン引数をリストで渡せます。例:extra_args=['--set-filename', 'original.txt']
always_trust=True
を設定しない場合、GnuPGは受信者の公開鍵が信頼できるかどうかをチェックします。信頼されていない鍵で暗号化しようとすると、python-gnupg
はエラーを明示的に返さずに失敗することがあります(GPG自体はコンソールに警告を出力しますが、ライブラリ側で検知しにくい場合があります)。事前に鍵の信頼レベルを設定しておくか、always_trust=True
を使用してください。
データの復号 🔓
暗号化されたデータ(文字列またはファイル)を、対応する秘密鍵を使って復号します。秘密鍵のパスフレーズが必要になることが一般的です。
# 暗号化されたASCII Armor形式の文字列
encrypted_string = """-----BEGIN PGP MESSAGE-----
... (暗号化されたデータ) ...
-----END PGP MESSAGE-----"""
# 文字列データを復号
decrypted_data = gpg.decrypt(
encrypted_string,
passphrase='your_secret_passphrase' # 秘密鍵のパスフレーズ
)
if decrypted_data.ok:
print("Decryption successful!")
print(f"Decrypted text: {decrypted_data.data.decode('utf-8')}") # バイト列なのでデコードが必要
print(f"Fingerprint of the key used: {decrypted_data.fingerprint}")
print(f"Key ID used: {decrypted_data.key_id}")
# 署名も含まれている場合
if decrypted_data.signature_id:
print(f"Signature ID: {decrypted_data.signature_id}")
print(f"Signer Fingerprint: {decrypted_data.signer_fingerprint}")
print(f"Signature Valid: {decrypted_data.valid}")
else:
print("Decryption failed.")
print(f"Status: {decrypted_data.status}") # 例: 'decryption failed', 'bad passphrase'
print(f"Stderr: {decrypted_data.stderr}")
# 暗号化されたファイルを復号
encrypted_filename = 'encrypted_document.txt.gpg'
decrypted_filename = 'decrypted_document.txt'
with open(encrypted_filename, 'rb') as f: # バイナリモードで開く
decrypt_status = gpg.decrypt_file(
file=f,
passphrase='your_secret_passphrase',
output=decrypted_filename
)
if decrypt_status.ok:
print(f"File '{encrypted_filename}' decrypted successfully to '{decrypted_filename}'.")
else:
print(f"File decryption failed.")
print(f"Status: {decrypt_status.status}")
print(f"Stderr: {decrypt_status.stderr}")
decrypt()
は文字列を、decrypt_file()
はファイルを復号します。
passphrase
: 復号に使用する秘密鍵のパスフレーズ。GPG Agentなどが設定されていれば不要な場合もあります。output
: (decrypt_file
) 復号結果を書き出すファイル名を指定します。
復号結果のオブジェクト (decrypted_data
, decrypt_status
) には、復号が成功したか (ok
)、ステータスメッセージ (status
)、復号されたデータ (data
、バイト列)、使用された鍵の情報 (fingerprint
, key_id
)、署名が含まれていた場合は署名情報 (signature_id
, signer_fingerprint
, valid
) などが含まれます。
データの署名 ✍️
文字列データやファイルに、自分の秘密鍵を使ってデジタル署名を作成します。これにより、データの送信者が自分であること、そしてデータが改ざんされていないことを証明できます。
# 署名するデータ
message_to_sign = "このドキュメントは私が作成しました。"
# 署名に使用する秘密鍵のキーID (省略するとデフォルトの鍵が使われる)
signing_keyid = 'YOUR_SIGNING_KEYID'
# 文字列データに署名 (クリア署名: 元のテキストが見える形式)
signed_data_clear = gpg.sign(
message_to_sign,
keyid=signing_keyid,
passphrase='your_signing_passphrase',
clearsign=True # デフォルトで True
)
if signed_data_clear: # 成功時は署名データ、失敗時は False など
print("Clear signing successful!")
signed_string_clear = str(signed_data_clear)
print("Signed data (Clear sign):")
print(signed_string_clear)
else:
print(f"Signing failed. Status: {getattr(signed_data_clear, 'status', 'Unknown')}")
print(f"Stderr: {getattr(signed_data_clear, 'stderr', 'Unknown')}")
# 文字列データに署名 (分離署名: 署名データが別になる形式)
signed_data_detached = gpg.sign(
message_to_sign,
keyid=signing_keyid,
passphrase='your_signing_passphrase',
detach=True,
clearsign=False # 分離署名の場合、通常 False にする
# armor=False # バイナリ形式の分離署名にする場合
)
if signed_data_detached:
print("\nDetached signing successful!")
detached_signature = signed_data_detached.data # バイト列の署名データ
print(f"Detached signature (binary, length {len(detached_signature)})")
# 必要であればファイルに保存
# with open('message.sig', 'wb') as sig_file:
# sig_file.write(detached_signature)
else:
print(f"Detached signing failed.")
# ファイルに署名 (ファイルにクリア署名)
input_filename = 'document_to_sign.txt'
output_filename_signed = 'signed_document.txt.asc'
with open(input_filename, 'rb') as f:
sign_status = gpg.sign_file(
file=f,
keyid=signing_keyid,
passphrase='your_signing_passphrase',
output=output_filename_signed,
clearsign=True
)
if sign_status:
print(f"\nFile '{input_filename}' clear signed successfully to '{output_filename_signed}'.")
else:
print(f"File signing failed. Status: {getattr(sign_status, 'status', 'Unknown')}")
sign()
は文字列を、sign_file()
はファイルを署名します。
keyid
: 署名に使用する秘密鍵のキーID。省略するとキーリング内のデフォルト(通常は最初に見つかる)秘密鍵が使われます。passphrase
: 署名用秘密鍵のパスフレーズ。clearsign
:True
(デフォルト) でクリア署名を作成します。元のデータと署名が一体化したテキスト形式で、GPGがない環境でもある程度読めます。detach
:True
にすると分離署名を作成します。元のデータとは別に署名データが生成されます。ファイルなどに添付して送る際に使われます。output
: (sign_file
) 署名結果を書き出すファイル名を指定します。分離署名の場合は署名データのみが書き出されます。armor
: 署名データをASCII Armor形式にするかどうか。
署名が成功すると、署名データを含むオブジェクト(クリア署名の場合)や、ステータス情報を持つオブジェクト(sign_file
の場合)、または署名データそのもの(分離署名の場合)が返されます。
署名の検証 ✅
データに付与された署名が、指定された公開鍵に対応する秘密鍵で作成されたものであり、かつデータが署名後に改ざんされていないことを確認します。
# 検証するクリア署名データ
signed_string_clear = """-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256
このドキュメントは私が作成しました。
-----BEGIN PGP SIGNATURE-----
... (署名データ) ...
-----END PGP SIGNATURE-----"""
# クリア署名を検証
verified_clear = gpg.verify(signed_string_clear)
if verified_clear: # 検証成功時は Verify オブジェクト、失敗時は False など
print("Clear signature verification successful!")
print(f"Signer User ID: {verified_clear.username}")
print(f"Signer Key ID: {verified_clear.key_id}")
print(f"Signer Fingerprint: {verified_clear.fingerprint}")
print(f"Signature ID: {verified_clear.signature_id}")
print(f"Signature Status: {verified_clear.status}") # 例: 'signature valid'
print(f"Timestamp: {verified_clear.sig_timestamp}")
print(f"Trust level: {verified_clear.trust_text} ({verified_clear.trust_level})")
# verified_clear.data には署名部分を除いた元のデータが含まれる (バイト列)
print(f"Original data: {verified_clear.data.decode('utf-8')}")
else:
print("Clear signature verification failed.")
if verified_clear is not None: # Verify オブジェクトだが検証失敗の場合
print(f"Status: {verified_clear.status}") # 例: 'signature bad', 'no public key'
print(f"Stderr: {verified_clear.stderr}")
# 分離署名の検証
original_data_filename = 'original_message.txt'
signature_filename = 'message.sig' # 分離署名ファイル
# ファイルから分離署名を検証
with open(original_data_filename, 'rb') as data_file, open(signature_filename, 'rb') as sig_file:
verified_detached_file = gpg.verify_file(sig_file, data_filename=original_data_filename)
if verified_detached_file:
print("\nDetached signature (file) verification successful!")
print(f"Signer User ID: {verified_detached_file.username}")
# ... (他の情報も同様に表示)
else:
print("\nDetached signature (file) verification failed.")
if verified_detached_file is not None:
print(f"Status: {verified_detached_file.status}")
# メモリ上のデータと分離署名で検証
original_data_bytes = b"This is the original content."
detached_signature_bytes = b"..." # 分離署名データのバイト列
verified_detached_mem = gpg.verify_data(detached_signature_bytes, original_data_bytes)
if verified_detached_mem:
print("\nDetached signature (memory) verification successful!")
print(f"Signer User ID: {verified_detached_mem.username}")
# ...
else:
print("\nDetached signature (memory) verification failed.")
if verified_detached_mem is not None:
print(f"Status: {verified_detached_mem.status}")
署名検証にはいくつかのメソッドがあります。
verify(data)
: 文字列またはバイト列に含まれる署名(クリア署名または通常の署名付きメッセージ)を検証します。verify_file(file, data_filename=None)
: ファイルに含まれる署名を検証します。file
: 署名が含まれるファイル(またはファイルライクオブジェクト)。クリア署名ファイルや、署名付きメッセージファイル、または分離署名ファイル (.sig
) を指定します。data_filename
: 分離署名を検証する場合に、元のデータファイル名を指定します。file
に分離署名ファイルを指定した場合に必須です。
verify_data(sig_data, data)
: メモリ上にある分離署名データ (sig_data
) と元のデータ (data
) を検証します。両方ともバイト列である必要があります。
検証が成功すると、署名者情報、署名の有効性、タイムスタンプなどを含む Verify
オブジェクトが返されます。検証に失敗した場合(署名が不正、対応する公開鍵がない、データが改ざんされているなど)は、False
または失敗情報を含む Verify
オブジェクト(ステータスが ‘signature bad’ など)が返されます。
Verify
オブジェクトの valid
属性は、署名が暗号学的に有効かどうかを示すブール値です(True
/ False
)。ただし、verify*
メソッド自体の返り値が Verify
オブジェクトであるかどうかで、まず検証プロセスが正常に完了したか(公開鍵が見つかったかなど)を確認するのが一般的です。
応用的な使い方 🚀
基本的な機能に加えて、python-gnupg
はより高度なユースケースにも対応します。
ストリーム処理とカスタムデータハンドリング
大きなファイルを扱う場合や、リアルタイムでデータを処理したい場合、GnuPGプロセスとの間でデータをチャンク(塊)ごとにやり取りするストリーム処理が有効です。
python-gnupg
では、GPG
インスタンスの on_data
属性にコールバック関数を設定することで、GPGプロセスから出力されるデータをカスタム処理できます。
import gnupg
import sys
import io
import queue
import threading
gpg = gnupg.GPG(gnupghome='/path/to/gpghome')
# 例1: 復号結果を標準出力にストリーミング
class DataPrinter:
def __call__(self, chunk):
sys.stdout.buffer.write(chunk) # バイト列を標準出力へ
return True # Trueを返すとライブラリ内部でもバッファリングされる
gpg.on_data = DataPrinter()
encrypted_filename = 'large_encrypted_file.gpg'
with open(encrypted_filename, 'rb') as f:
# outputを指定せず、on_dataコールバックで処理
decrypt_status = gpg.decrypt_file(f, passphrase='your_secret_passphrase')
if not decrypt_status.ok:
print(f"\nDecryption failed: {decrypt_status.status}", file=sys.stderr)
# on_data をリセット
gpg.on_data = None
# 例2: データを別スレッドで処理するためにキューに送る
class ChunkForwarder:
def __init__(self, q):
self.queue = q
def __call__(self, chunk):
self.queue.put(chunk)
return False # Falseを返すとライブラリはバッファリングしない
data_queue = queue.Queue()
gpg.on_data = ChunkForwarder(data_queue)
def process_chunks(q):
print("Worker thread started.")
while True:
chunk = q.get()
if chunk is None: # 終了シグナル
q.task_done()
break
# ここでチャンクを処理 (例: ファイルに書き込む、DBに保存するなど)
print(f"Worker received chunk of size {len(chunk)}")
# process_chunk(chunk)
q.task_done()
print("Worker thread finished.")
worker_thread = threading.Thread(target=process_chunks, args=(data_queue,))
worker_thread.start()
# 復号処理を開始 (メインスレッド)
print("Main thread starting decryption...")
with open(encrypted_filename, 'rb') as f:
decrypt_status = gpg.decrypt_file(f, passphrase='your_secret_passphrase')
print(f"Main thread decryption status: ok={decrypt_status.ok}, status='{decrypt_status.status}'")
# 処理終了をワーカーに通知
data_queue.put(None)
# ワーカーの終了を待つ
worker_thread.join()
print("Main thread finished.")
コールバック関数 (on_data
に設定するもの) は、GPGプロセスから受け取ったデータのチャンク(バイト列)を唯一の引数として受け取ります。この関数内で、チャンクをファイルに書き込んだり、ネットワークに送信したり、キューに入れて別スレッドで処理したりできます。
コールバック関数が True
を返すと、python-gnupg
はそのチャンクを内部でも保持し、最終的な結果オブジェクトの data
属性などに含めようとします。False
を返すと、ライブラリはチャンクを保持せず、メモリ使用量を抑えることができます。ストリーム処理でメモリを節約したい場合は False
を返すのが一般的です。
この機能は、復号 (decrypt
, decrypt_file
) だけでなく、暗号化や署名プロセス中の出力に対しても利用できる場合があります(ただし、ユースケースは限定的かもしれません)。
対称暗号(共通鍵暗号)の使用
公開鍵ペアを使わずに、パスフレーズのみで暗号化・復号を行いたい場合は、対称暗号(共通鍵暗号)モードを使用できます。
plain_text = "これはパスフレーズだけで暗号化されたメッセージです。"
password = "very_strong_password"
# 対称暗号で暗号化 (デフォルトは CAST5)
encrypted_data_sym = gpg.encrypt(
plain_text,
recipients=None, # 受信者は指定しない
symmetric=True,
passphrase=password
)
if encrypted_data_sym.ok:
encrypted_string_sym = str(encrypted_data_sym)
print("Symmetric encryption successful (CAST5):")
print(encrypted_string_sym)
else:
print(f"Symmetric encryption failed: {encrypted_data_sym.status}")
# AES256アルゴリズムを指定して対称暗号化
encrypted_data_aes = gpg.encrypt(
plain_text,
recipients=None,
symmetric='AES256', # アルゴリズム名を指定
passphrase=password
)
if encrypted_data_aes.ok:
encrypted_string_aes = str(encrypted_data_aes)
print("\nSymmetric encryption successful (AES256):")
print(encrypted_string_aes)
# 対称暗号化されたデータを復号
decrypted_data_aes = gpg.decrypt(encrypted_string_aes, passphrase=password)
if decrypted_data_aes.ok:
print("\nSymmetric decryption successful (AES256):")
print(f"Decrypted text: {decrypted_data_aes.data.decode('utf-8')}")
else:
print(f"\nSymmetric decryption failed: {decrypted_data_aes.status}")
else:
print(f"\nSymmetric encryption (AES256) failed: {encrypted_data_aes.status}")
encrypt
または encrypt_file
メソッドで recipients=None
とし、symmetric
引数を指定します。
symmetric=True
: GnuPGのデフォルトの対称暗号アルゴリズム(多くの場合CAST5ですが、環境によります)を使用します。symmetric='ALGORITHM_NAME'
: 使用したい対称暗号アルゴリズム名(例: ‘AES’, ‘AES192’, ‘AES256’, ‘TWOFISH’, ‘CAMELLIA256’ など、GnuPGがサポートするもの)を指定します。
復号時も、対応する decrypt
または decrypt_file
メソッドで同じ passphrase
を指定します。
鍵サーバーとの連携
公開鍵を共有・検索するために、鍵サーバーを利用することがあります。python-gnupg
でも基本的な鍵サーバー操作が可能です。
keyserver = 'keys.openpgp.org' # 使用する鍵サーバー
# 鍵サーバーから公開鍵を検索 (メールアドレスなどで)
search_results = gpg.search_keys('user@example.com', keyserver=keyserver)
if search_results:
print(f"Found keys on {keyserver}:")
for key in search_results:
print(f" Key ID: {key['keyid']}, Fingerprint: {key.get('fingerprint')}, Uids: {key['uids']}")
else:
print(f"No keys found for 'user@example.com' on {keyserver}")
# 鍵サーバーから公開鍵を受信 (キーIDで指定)
if search_results:
keyid_to_receive = search_results[0]['keyid'] # 最初の結果のキーIDを使用
receive_result = gpg.recv_keys(keyserver, keyid_to_receive)
if receive_result:
print(f"\nReceived key {keyid_to_receive} from {keyserver}.")
print(f"Imported count: {receive_result.count}")
print(f"Fingerprints: {receive_result.fingerprints}")
else:
print(f"\nFailed to receive key {keyid_to_receive} from {keyserver}. Status: {getattr(receive_result, 'status', 'Unknown')}")
# 自分の公開鍵を鍵サーバーに送信 (キーIDで指定)
my_keyid = 'YOUR_PUBLIC_KEYID'
try:
send_result = gpg.send_keys(keyserver, my_keyid)
if send_result:
print(f"\nSent key {my_keyid} to {keyserver}. Status: {send_result.status}")
else:
print(f"\nFailed to send key {my_keyid} to {keyserver}. Status: {getattr(send_result, 'status', 'Unknown')}")
except Exception as e:
print(f"\nError sending key {my_keyid}: {e}")
search_keys(query, keyserver)
: 指定したクエリ(メールアドレス、名前、キーIDなど)で鍵サーバー上の公開鍵を検索します。recv_keys(keyserver, *keyids)
: 指定したキーIDの公開鍵を鍵サーバーから受信し、ローカルのキーリングにインポートします。send_keys(keyserver, *keyids)
: 指定したキーIDの公開鍵をローカルのキーリングから鍵サーバーに送信(アップロード)します。
これらの操作にはネットワーク接続が必要です。また、使用する鍵サーバーによっては利用ポリシーが異なるため注意が必要です。
注意点とベストプラクティス ⚠️
python-gnupg
を安全かつ効果的に利用するために、以下の点に注意してください。
GnuPG実行ファイルのパス
python-gnupg
は内部で gpg
コマンドを実行します。システムが gpg
コマンドを見つけられない場合、エラーが発生します。
- PATH環境変数:
gpg
(またはgpg.exe
) がインストールされているディレクトリがシステムのPATH環境変数に含まれていることを確認してください。 - 明示的な指定: 環境に依存せず確実に動作させるためには、
gnupg.GPG()
の初期化時にgpgbinary
引数でgpg
実行ファイルのフルパスを指定することを検討してください。gpg = gnupg.GPG(gpgbinary='/usr/bin/gpg', gnupghome='/path/to/gpghome')
GnuPGホームディレクトリの指定
前述の通り、gnupghome
引数でGnuPGのホームディレクトリを明示的に指定することが強く推奨されます。これにより、どのキーリングや設定ファイルが使用されるかを明確に制御でき、予期せぬ動作を防ぐことができます。特に、ウェブアプリケーションなど、複数のユーザーやプロセスが同時にGPG機能を利用する可能性がある環境では必須と言えます。
パスフレーズの管理
秘密鍵を使用する操作(復号、署名、秘密鍵のエクスポートなど)では、多くの場合パスフレーズが必要です。python-gnupg
のメソッドに直接 passphrase
引数で渡すことができますが、コード内にパスフレーズをハードコーディングするのは非常に危険です 🚫。
- 環境変数: 環境変数から読み込む。
- 設定ファイル: 安全な場所に保存された設定ファイルから読み込む(パーミッション設定に注意)。
- Secrets Manager: AWS Secrets ManagerやHashiCorp Vaultなどのシークレット管理サービスを利用する。
- GPG Agent:
gpg-agent
を設定し、パスフレーズを一時的にキャッシュさせる(gnupg.GPG(use_agent=True)
)。これにより、スクリプト実行中に都度パスフレーズを入力する必要がなくなりますが、エージェントの設定が必要です。 - Pinentry: GnuPGは
pinentry
というプログラムを使って安全にパスフレーズ入力を求める仕組みを持っています。python-gnupg
から直接pinentry
を起動・制御するのは複雑ですが、passphrase
を渡さずにgpg-agent
も使わない場合、GnuPGが自動的にpinentry
を呼び出そうとすることがあります(ターミナル環境など)。サーバーサイドのスクリプトでは、これが原因で処理が停止する可能性があるため注意が必要です。
最も安全な方法は、アプリケーションの要件に応じて、シークレット管理サービスや設定ファイル(適切なアクセス制御付き)を利用することです。
エラーハンドリング
GnuPGの操作は様々な理由で失敗する可能性があります(鍵が見つからない、パスフレーズが違う、ファイルアクセス権がない、GPGプロセスとの通信エラーなど)。python-gnupg
の各メソッドの返り値を適切にチェックし、エラー処理を行うことが重要です。
- 多くのメソッドは、成功時にステータス情報を含むオブジェクトを、失敗時には
False
やNone
、あるいはエラー情報を含むオブジェクトを返します。返り値の型と内容をドキュメントで確認してください。 - 返されたオブジェクトの
ok
属性 (ブール値) やstatus
属性 (文字列) で成否や詳細を確認できます。 stderr
属性には、GPGプロセスが標準エラー出力に出力した内容が含まれることがあり、デバッグに役立ちます。- 必要に応じて
try...except
ブロックを使用して、予期せぬ例外(ファイルI/Oエラーなど)も捕捉するようにしてください。
try:
# GPG操作を実行
result = gpg.decrypt(encrypted_data, passphrase=password)
if result.ok:
print("Successfully decrypted.")
# ... 成功時の処理 ...
else:
print(f"Decryption failed. Status: {result.status}")
print(f"Stderr: {result.stderr}")
# ... 失敗時の処理 (パスフレーズ間違いなど) ...
except gnupg.GPGError as e:
print(f"GPGError occurred: {e}")
# ... GPGプロセスとの通信エラーなどの処理 ...
except FileNotFoundError:
print("Input file not found.")
# ... ファイル関連のエラー処理 ...
except Exception as e:
print(f"An unexpected error occurred: {e}")
# ... その他の予期せぬエラー処理 ...
セキュリティに関する考慮事項
- 入力の検証: 外部から受け取ったデータ(ファイル名、キーID、暗号文など)をGPGコマンドに渡す前に、適切に検証・サニタイズしてください。意図しないコマンド実行(シェルインジェクションなど)を防ぐためです。
python-gnupg
は内部でsubprocess
を安全に使用するように努めていますが、入力値の検証は重要です。 - キーの信頼性: 公開鍵をインポートしたり、暗号化に使用したりする際には、その鍵が本当に意図した相手のものであるかを確認するプロセス(フィンガープリントの確認など)を導入することを検討してください。
always_trust=True
は便利ですが、セキュリティリスクを伴います。 - 一時ファイル:
python-gnupg
は内部で一時ファイルを使用することがあります。これらのファイルが安全な場所に作成され、適切に削除されることを確認してください(通常はライブラリが処理します)。 - ログ:
verbose=True
やデバッグログを有効にすると、機密情報(パスフレーズの一部や平文データなど)がログに出力される可能性があります。本番環境ではログレベルを適切に設定してください。
バージョン互換性
- Pythonのバージョン: 推奨されるPythonバージョン (>= 3.6) を使用してください。
- GnuPGのバージョン: 使用しているGnuPGのバージョンによって、利用可能な機能やオプション、デフォルトの動作が異なる場合があります(特に GnuPG 1.x系 と 2.x系)。特定の機能(ECCなど)は新しいバージョンが必要です。
python-gnupg
のドキュメントやGnuPGのドキュメントで互換性を確認してください。 - python-gnupgのバージョン: ライブラリ自体もバージョンアップによって機能追加や変更が行われます。最新の安定版を使用し、変更点に注意してください。
まとめ 🎉
python-gnupg
は、Pythonアプリケーションに強力な暗号化・署名機能を組み込むための非常に便利なライブラリです。GnuPGの複雑なコマンドライン操作を抽象化し、Pythonicなインターフェースを提供してくれます。
この記事では、基本的な使い方から応用的なテクニック、そして利用上の注意点までを解説しました。
- ✅ GPGオブジェクトの初期化と設定
- 🔑 鍵の管理(リスト、インポート、エクスポート、生成、削除)
- 🔒 データの暗号化(公開鍵暗号、対称暗号)
- 🔓 データの復号
- ✍️ データの署名(クリア署名、分離署名)
- ✅ 署名の検証
- 🌊 ストリーム処理とカスタムデータハンドリング
- ☁️ 鍵サーバーとの連携
- ⚠️ 注意点とベストプラクティス(パス、パスフレーズ管理、エラー処理、セキュリティ)
python-gnupg
を活用することで、データの機密性、完全性、否認防止といったセキュリティ要件を満たすアプリケーションを効率的に開発できます。ぜひ、あなたのプロジェクトでこの強力なツールを活用してみてください! 💪
コメント