秘密鍵のファイル形式(PEM・PKCS#1・PKCS#8)

「鍵ファイル」の中身は何か

[[暗号化の基礎]] で扱う RSA や楕円曲線の鍵は、数学的にはいくつかの大きな整数の組です。しかしファイルに保存するには、「どの整数がどの役割か」「どのアルゴリズム用か」を決めた入れ物(コンテナ形式)が要ります。

この入れ物が複数あるのが混乱の元です。「秘密鍵は持っているのにライブラリが読んでくれない」というトラブルの大半は、鍵そのものではなく入れ物の不一致です。

PEM と DER — テキストか、バイナリか

まず符号化の違いがあります。

  • DER — 構造をそのままバイナリで書いたもの。.der / .cer などの拡張子で、エディタで開くと文字化けする
  • PEM — DER を Base64 にして、-----BEGIN ...----- と -----END ...----- で挟んだテキスト。設定ファイルや環境変数に貼れるため、Web 開発ではほぼこちら

PEM のヘッダー行のラベルが、中身の構造(次節)を示します。ここを読めばファイルを開いた瞬間に形式が分かります。

PKCS#1 と PKCS#8 — ヘッダー行で見分ける

同じ RSA 秘密鍵に、主に2つの構造があります。

ヘッダー行構造中身
-----BEGIN RSA PRIVATE KEY-----PKCS#1RSA 専用。整数の組がそのまま並ぶ
-----BEGIN PRIVATE KEY-----PKCS#8アルゴリズム識別子を先頭に持ち、その後ろに鍵本体を包む。RSA でも楕円曲線でも同じ外形
-----BEGIN ENCRYPTED PRIVATE KEY-----PKCS#8(暗号化)パスフレーズで保護された PKCS#8
-----BEGIN EC PRIVATE KEY-----SEC1楕円曲線専用の古い形式
-----BEGIN OPENSSH PRIVATE KEY-----OpenSSH 独自ssh-keygen の既定。[[SSH]] 以外ではまず読めない

見分け方は単純で、「RSA」が付いていれば PKCS#1、付いていなければ PKCS#8 です。PKCS#8 は「アルゴリズムは何か」を自分で名乗るため、汎用ライブラリはこちらを標準として扱います。

どこで不一致が起きるか

代表例が [[GitHub App]] の秘密鍵です。GitHub がダウンロードさせる鍵は PKCS#1 ですが、ブラウザや Deno・Cloudflare Workers で使う Web Crypto API の crypto.subtle.importKey は pkcs8 しか受け付けません。そのまま渡すと DataError で止まります。

一方、Node.js の crypto.createSign や OpenSSL は両方を自動判別して読みます。そのため「Node で署名できたから鍵は正しい」と確認しても、Web Crypto を使う本番環境で失敗する、というすれ違いが起きます。

変換と、消す前の確認

PKCS#1 から PKCS#8 への変換は OpenSSL の1コマンドです。

openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in app.pem -out app-pkcs8.pem
head -1 app-pkcs8.pem   # -----BEGIN PRIVATE KEY----- になっていれば成功

-nocrypt を付けないとパスフレーズ付きで書き出され、今度は ENCRYPTED PRIVATE KEY になって読めません。

ここで大事なのは、元の鍵を消す前に、実際に使う経路で読めることを確かめることです。前述のとおり Node の署名は PKCS#1 のままでも成功するため、確認にはなりません。Web Crypto で読む本番なら、同じ importKey('pkcs8', …) を手元で1回通してから元の鍵を処分します。確認せずに消し、変換後の鍵が実は読めなかった、という事故は取り返しがつきません(鍵は再発行できても、その間サービスが止まります)。

取り扱いの作法

形式が合っていても、扱い方を誤れば鍵は漏れます。

  • ファイルの権限は所有者のみ読める設定(chmod 600)にする。SSH クライアントはこれを満たさない鍵を拒否する
  • 秘密鍵をリポジトリに入れない。環境変数やシークレットストアに置く([[シークレット管理]])。PEM は複数行なので、環境変数に入れるときは改行の扱い(\n エスケープか、二重引用符で囲んだ複数行)を確認する
  • チャットやログに貼らない。-----BEGIN で始まる文字列は、秘密情報の検出ツールが最初に探すパターンでもある
  • 秘密鍵から公開鍵は導出できるが、逆はできない。配るのは公開鍵だけ

初学者向けポイント

  • エラーの前にまず head -1 でヘッダー行を見ましょう。形式の不一致はこれだけで9割判定できます
  • 「変換できた」と「使える」は別です。実際に使うライブラリで読み込みテストをしてから古い鍵を消します
  • 拡張子は当てになりません。.pem でも中身が PKCS#1 か PKCS#8 かはヘッダー行でしか分かりません

関連技術とのつながり

  • [[暗号化の基礎]] — 鍵ペアが何を表す整数なのか
  • [[電子署名とハッシュ]] — 秘密鍵の主な用途。形式が合わないと署名処理の入口で止まる
  • [[電子証明書とPKI]] — 証明書も同じ PEM / DER の符号化を使う
  • [[SSH]] — OpenSSH 独自形式と ssh-keygen -m PEM での書き出し
  • [[JWT]] — RS256 署名で秘密鍵を読み込む場面。Web Crypto 実装は PKCS#8 前提
  • [[GitHub App]] — PKCS#1 で配られる鍵を PKCS#8 に変換して使う典型例
  • [[シークレット管理]] — 変換後の鍵の置き場所
Q: -----BEGIN RSA PRIVATE KEY----- で始まる鍵ファイルの構造はどれ?
- [x] PKCS#1
- [ ] PKCS#8
- [ ] OpenSSH 独自形式
解説: ヘッダーに「RSA」が付いていれば PKCS#1、「PRIVATE KEY」だけなら PKCS#8 です。

Q: Web Crypto API の importKey で秘密鍵を読むとき受け付ける形式はどれ?
- [ ] PKCS#1 のみ
- [x] PKCS#8 のみ
- [ ] どちらも自動判別する
解説: Web Crypto は pkcs8 のみです。Node の createSign や OpenSSL は両方を自動判別するため、Node で試して成功しても Web Crypto の検証にはなりません。

Q: 鍵を PKCS#8 に変換した後、元の鍵を消す前にすべきことはどれ?
- [ ] 拡張子を .pem に変える
- [x] 実際に使う経路(たとえば Web Crypto の importKey)で読めることを確かめる
- [ ] 変換後の鍵をチャットに貼って共有する
解説: 変換の成功と、本番の読み込み経路で使えることは別です。確認せずに消すとサービス停止につながります。