本文へスキップ
ecdsa.com

age と GPG の比較|2026年のファイルとシークレットの暗号化

age の役割は 1 つだけで、短い公開鍵またはパスフレーズを宛先としてファイルを暗号化します。GnuPG は署名、鍵管理、メールを含む OpenPGP のすべてを実装しており、それに伴う複雑さも抱えています。

age と GPG のどちらを使うかは、小さな単一用途の暗号化ツールと、OpenPGP の完全な実装のどちらを選ぶかという問題です。age(形式は age-encryption.org/v1、C2SP で仕様化)は、1 つ以上の受信者に宛ててファイルを暗号化します。各受信者は age1… のような短い文字列、SSH 公開鍵、またはポスト量子の age1pq1… 鍵です。鍵のカプセル化には X25519 またはハイブリッドの ML-KEM を、ペイロードには ChaCha20-Poly1305 を使います。GnuPG は OpenPGP を実装しており、暗号化、署名、鍵の証明、キーリング、鍵サーバー、スマートカード、メールとの連携を備え、数十年にわたる互換性の積み重ねがあります。

比較表

ageGnuPG(OpenPGP)
範囲ファイルの暗号化のみ暗号化、署名、鍵の証明、鍵管理
仕様age v1(C2SP)。形式は 1 つで、ネゴシエーションするバージョンはなしRFC 9580(2024年)と RFC 9980(2026年)。GnuPG は LibrePGP に準拠
公開鍵62 文字(age1…)。ポスト量子では 1,959 文字(age1pq1…)ユーザー ID と自己署名を含むバイナリ形式の鍵。Ed25519 と Cv25519 で 473 バイト(実測)
アルゴリズムX25519、ML-KEM-768 + X25519 ハイブリッド、パスフレーズ用の scrypt、ChaCha20-Poly1305、HKDF と HMAC-SHA-256多数:RSA、ECC、AES、Camellia、古いデータ用のレガシー暗号。GnuPG 2.5 では ML-KEM-768 + X25519 も
設定なし優先設定、信用モデル、アルゴリズムと圧縮の設定
26 バイトのファイルの暗号文(実測)226 バイト(X25519)、1,685 バイト(ポスト量子)203 バイト(Cv25519)、1,288 バイト(ML-KEM-768 + X25519 の副鍵)
SSH キーを受信者に使用対応(ssh-ed25519、ssh-rsa)非対応
ハードウェアキープラグイン:YubiKey PIV、TPM、Secure EnclaveOpenPGP スマートカードとセキュリティキー
署名設計上なしあり
鍵の検索なし。受信者は直接やり取りする鍵サーバー、WKD、キーリング
大きなファイル64 KiB の認証付きチャンク単位でストリーム処理。1.3 以降はシーク可能な復号 API ありストリーム処理。チャンク分割された 1 つの AEAD パケット

同じファイルを両方の方法で暗号化する

以下のスクリプトは、26 バイトのシークレットを、age の X25519 受信者、age のポスト量子受信者、GnuPG の Cv25519 副鍵(subkey)、GnuPG の ML-KEM-768 + X25519 副鍵のそれぞれに宛てて暗号化し、それぞれを復号してサイズを比較します(age 1.3.2、GnuPG 2.5.21)。処理はすべて一時ディレクトリ内で行います。

age と GnuPG、従来方式とポスト量子
#!/bin/sh
dir=$(mktemp -d /tmp/agegpg.XXXXXX)   # short path: gpg-agent sockets need one
cd "$dir" || exit 1
export GNUPGHOME="$dir/g"
mkdir -m 700 g
printf 'db_password=correct-horse\n' > secret.env    # 26 bytes

echo "== age"
age-keygen -o key.txt 2> /dev/null
recipient=$(age-keygen -y key.txt)
echo "recipient: ${#recipient} characters"
age -r "$recipient" -o secret.env.age secret.env
age -d -i key.txt secret.env.age | cmp -s - secret.env && echo "decrypted: identical"
echo "ciphertext: $(wc -c < secret.env.age | tr -d ' ') bytes"

echo "== age, post-quantum recipient"
age-keygen -pq -o pq.txt 2> /dev/null
pqrecipient=$(age-keygen -y pq.txt)
echo "recipient: ${#pqrecipient} characters"
age -r "$pqrecipient" -o secret.env.pq.age secret.env
age -d -i pq.txt secret.env.pq.age | cmp -s - secret.env && echo "decrypted: identical"
echo "ciphertext: $(wc -c < secret.env.pq.age | tr -d ' ') bytes"

echo "== GnuPG"
gpg --batch --quiet --passphrase '' --quick-gen-key "Ops <ops@example.org>" ed25519 cert 1y
fpr=$(gpg --list-keys --with-colons ops@example.org 2> /dev/null | awk -F: '/^fpr/ { print $10; exit }')
gpg --batch --quiet --passphrase '' --quick-add-key "$fpr" cv25519 encr 1y
gpg --batch --quiet --trust-model always -r ops@example.org -o secret.env.gpg -e secret.env
gpg --batch --quiet -d secret.env.gpg 2> /dev/null | cmp -s - secret.env && echo "decrypted: identical"
echo "ciphertext: $(wc -c < secret.env.gpg | tr -d ' ') bytes"
echo "public key export: $(gpg --export ops@example.org | wc -c | tr -d ' ') bytes binary"

echo "== GnuPG, ML-KEM-768 + X25519 subkey"
gpg --batch --quiet --passphrase '' --quick-gen-key "Ops PQ <ops-pq@example.org>" ed25519 cert 1y
fpr=$(gpg --list-keys --with-colons ops-pq@example.org 2> /dev/null | awk -F: '/^fpr/ { print $10; exit }')
gpg --batch --quiet --passphrase '' --quick-add-key "$fpr" ky768_cv25519 encr 1y
gpg --batch --quiet --trust-model always -r ops-pq@example.org -o secret.env.pq.gpg -e secret.env
gpg --batch --quiet -d secret.env.pq.gpg 2> /dev/null | cmp -s - secret.env && echo "decrypted: identical"
echo "ciphertext: $(wc -c < secret.env.pq.gpg | tr -d ' ') bytes"
gpgconf --kill gpg-agent
cd / && rm -rf "$dir"
出力
== age
recipient: 62 characters
decrypted: identical
ciphertext: 226 bytes
== age, post-quantum recipient
recipient: 1959 characters
decrypted: identical
ciphertext: 1685 bytes
== GnuPG
decrypted: identical
ciphertext: 203 bytes
public key export: 473 bytes binary
== GnuPG, ML-KEM-768 + X25519 subkey
decrypted: identical
ciphertext: 1288 bytes

どちらも受信者ごとに一定のヘッダーと、わずかな認証用のオーバーヘッドが加わるだけなので、実際のファイルでは差は無視できます。ポスト量子のオーバーヘッドは、各受信者スタンザに含まれる ML-KEM-768 の暗号文(1,088 バイト)です。age のオーバーヘッドのほうが大きいのは主に、ヘッダーがテキスト形式で、バイナリ値を base64 で格納しているためです。スクリプトで --trust-model always を使っているのは、鍵を 1 秒前に作成したからにすぎません。実際の運用では、受信者のフィンガープリントを検証します。

相互運用性で押さえておくべき詳細

上のファイルに gpg --list-packets を実行すると、知っておくべき点が 2 つわかります。Cv25519 鍵に対して GnuPG 2.5 が書き出したのは、RFC 9580 が定義するバージョン 2 の暗号化データパケットではなく、LibrePGP 形式である OCB の「AEAD encrypted packet」でした。GnuPG 自身の鍵がこの形式への対応を表明しているためです。RFC 9580 だけに準拠した実装では、このようなファイルを読めない可能性があります。ポスト量子の副鍵に対して GnuPG が使ったのは公開鍵アルゴリズム 8、つまり LibrePGP における GnuPG の Kyber 複合方式で、RFC 9980 は ML-KEM-768 + X25519 に 35 を割り当てています。そのため、GnuPG のポスト量子鍵と RFC 9980 の鍵には相互運用性がありません。age の形式は 1 つだけで、ポスト量子の構成も 1 つだけです(X-Wing とも呼ばれる ML-KEM-768 と X25519 のハイブリッドを使う HPKE)。テストベクトルも公開されています。

age があえて小さく作られている理由

age は、OpenPGP を安全に使いにくくしていた要素をすべて省いています。アルゴリズムのネゴシエーションも、圧縮(平文に関する情報が漏洩するおそれがあります)も、認証なしのモードも、鍵サーバーも、信用の輪(web of trust)もありません。受信者は設定ファイルに貼り付けられる文字列で、複数の受信者に宛てて暗号化したファイルには、受信者ごとに 1 つのスタンザがあります。その代償は機能範囲の狭さです。age は署名できないため、誰がファイルを暗号化したかを証明できず、鍵の管理や配布も行いません。こうした不足は周辺のツールが補います。SOPS は YAML や JSON の値を age の受信者に宛てて暗号化し、プラグインを使えば identity(秘密鍵)を YubiKey、TPM、macOS の Secure Enclave に保管できます。

OpenPGP が共通言語になっている場面では、GnuPG の幅広さがそのまま価値になります。既存のやり取り相手との署名付き・暗号化メール、パッケージリポジトリ、下流の検証者がすでにチェックしているリリース署名、そして署名用・暗号化用・認証用の鍵をまとめて保持する OpenPGP スマートカードです。

どちらを選ぶべきか

  • Git 内のシークレット(SOPS)、CI の変数、バックアップ、dotfile:age を使います。受信者は人またはマシンごとに 1 つとし、ハードウェアまたはオフラインの復旧用受信者を加えます。
  • 手元にある相手の SSH キーに宛てた暗号化:age を使います。
  • 長期保管する機密アーカイブ:harvest now, decrypt later(今収集し、後で解読)に耐えられるよう、ポスト量子の age1pq1 受信者を指定した age を使います。
  • 暗号化メール:OpenPGP を使います(または S/MIME。S/MIME と OpenPGP の比較を参照してください)。
  • OpenPGP を使う受信者に向けて暗号化と署名を一度に行う場合:GnuPG を使います。
  • リリースとパッケージの署名:暗号化ツールとしてはどちらも該当しません。Sigstore と GPG の比較を参照してください。

移行時の注意点

  1. 人またはマシンごとに age の identity を生成し(age-keygen。暗号化や復号を行う全員が age 1.3 以降を使うようになったら age-keygen -pq)、復旧用の identity をオフラインで保管します。
  2. 変換ではなく再暗号化する:各 GPG ファイルを古い鍵で復号し、新しい受信者に宛てて暗号化します。形式を変換する手段はありません。SOPS では、.sops.yaml の作成ルールに age の受信者を追加し、各ファイルに対して sops updatekeys を実行します。後で PGP 鍵を削除するときは、続けて sops rotate を実行し、データ鍵も変更します。
  3. すべての古いファイルとバックアップが再暗号化されるか期限切れになるまで、GPG 鍵を保持します。
  4. GPG 署名は別途置き換えます。age は署名を提供しないため、SSH 署名、minisign、Sigstore を使います。
  5. age 暗号化ツールでサイズとツールを確認し、OpenPGP インスペクターで古いファイルを解析して、どの鍵とアルゴリズムが使われているかを調べます。

参考資料

すべての比較