謎の拡張子PEMを深掘りしたら沼だった件

こんにちは。
SJC共同開発推進室所属の藤井です。

本日は開発者には馴染み深いSSHについて、
時事ネタ気味ですがOpenSSHのバージョンアップ対策としてのEd25519鍵への移行のお勧めと、
謎の拡張子PEMを深掘りしたら沼だった件について書かせていただきます。
※主にWindowsクライアントからの利用を想定した記事です。

暗号鍵のアルゴリズムにご注意を

最新のLinuxではSSHでRSA暗号鍵が使えない?

OpenSSHは8.8以降、SHA-1(ssh-rsa)がデフォルトで使用不可に変更されました。最近登場したLinuxディストリビューションですと、Amazon Linux 2022・Amazon Linux 2023・Ubuntu 22.04 などで、

がサーバーのSSHサービスのログファイル /var/log/auth.log に残っている場合、ほぼこの変更が原因で認証に失敗しています(筆者も以前しっかり踏み抜きました)。

SSHサーバーへの接続時に鍵のハッシュアルゴリズムでSHA-1が選択できてしまうような古い暗号鍵タイプの場合に、サーバーとクライアントのどちらかが理由でハッシュにSHA-2を自動選択できないと発生します。前世代の業界標準だったRSA鍵がまさに典型です。

サーバーが上述のディストリビューションの場合ですと、SSHクライアント側が古くて自動でSHA-2を選べない、もしくは設定がSHA-1固定になっているケース(クライアントが古いTeraTermやPuTTY、WinSCPを使っている場合や、組み込みOSのLinuxでOpenSSHが古い場合など)と思われます。この場合、対応としては3択になります。

1.クライアント側を最新にするか設定を見直して、SHA-2が選択されるようにする

クライアント側の設定で、SHA-1以外を強制するとうまくいくことがあるようです。RSA鍵自体が無効になったと筆者も最初誤解していましたが、RSA鍵自体が使えなくなったわけではないのです。どうしても鍵を捨てられない場合はこの方法か3の方法を取ることになります。

2.Ed25519 鍵に切り替える

署名の自動選択時にハッシュにSHA-2 しか使われない世代の鍵(RSAなど)から、次世代標準として推奨されている楕円暗号鍵Ed25519へ移行する方法です。

3.サーバー側の設定を変更し、SHA-1を許容する

サーバーのSSHサービスの設定ファイル /etc/ssh/sshd_config に

を追記する方法です。これが後方互換が一番高く楽だと思いますが、それは結局セキュリティ的な負債を引継ぐということでもあるため、お勧めできません。

筆者としては、2のEd25519 を推奨します。これはOpenSSH 9.5で ssh-keygen のデフォルトに採用されており、ほぼ間違いなくポストRSA時代の本命と思われるからなのと、上述の他の方法に比べて鍵だけ変えれば済むため、サーバー・クライアント双方での切り分けに注意する必要がなくなるからです。

というわけで、次節ではEd25519の鍵の作成方法についてご説明します。

Ed25519鍵の作成方法

PuTTYgenを使う

なんだかんだで使うことがあるPuTTYフォーマットの鍵も同時に作成できるので、お勧めです。SFTPクライアントソフトのWinSCPにバンドルされているので、そちらをご紹介します。PuTTYフォーマットについては後述します。パスフレーズ無しだと英語で警告が出ますが、無視して進めても大丈夫です。

PuTTYgen図1
PuTTYgen図2
PuTTYgen図3
PuTTYgen図4
PuTTYgen図5

  • ① OpenSSH フォーマットの秘密鍵
  • ② OpenSSH AUTHORIZED_KEYS フォーマットの公開鍵
  • ③ PuTTY フォーマットの秘密鍵
  • ④ PuTTY フォーマットの公開鍵

生成された OpenSSH AUTHORIZED_KEYS フォーマット公開鍵は、RSA鍵タイプのときの公開鍵と見比べるとかなり短くて不安になるかもしれません。

ssh-keygen での 2048bit RSA の AUTHORIZED_KEYS フォーマットの公開鍵の例

Ed25519 の AUTHORIZED_KEYS フォーマットの公開鍵の例

このように鍵長が短いのに暗号強度が高い点がEd25519の特徴です。

WindowsのOpenSSH ssh-keygenを使う

PowerShell などから、

を実行すればEd25519鍵タイプで作成可能です。Windows10から標準でOpenSSHが入っているため、PowerShellやコマンドプロンプトから普通に使うことができます。Windows11でも同様です。わざわざWSLでLinuxを準備する必要すらありません。OpenSSH8系では「-t」で鍵タイプを指定しないとRSA鍵になるのでご注意ください。

WindowsのOpenSSH図1
WindowsのOpenSSH図2

Linux上で ssh-keygen を使う

コマンドラインはWindows版と同じですので省略します。Mac OS(正確にはLinuxではないですが)でも同様です。

TeraTermのSSH鍵生成機能を使う

最近のTeraTermは、それを使ってLinuxサーバーへ接続しなくても、TeraTerm自体でSSH鍵生成が可能です。図はTeraTerm5の例です。

TeraTerm図1
TeraTerm図2
TeraTerm図3

パスフレーズは空欄でも大丈夫です。生成されるのは OpenSSH独自フォーマットの秘密鍵と、OpenSSH AUTHORIZED_KEYSフォーマットの公開鍵です。鍵フォーマットについては次節以降で解説します。

OpenSSHとPuTTYでの鍵の比較

皆様は鍵は何で生成されていますか?SSHの実装系は数あれど、Linux使いはOpenSSH、Windows使いならPuTTYgenで作るのが一般的ではないかと思います(筆者の勝手な推測です)。両者から生成される鍵について比べてみます。

鍵タイプ早見表

表の通り微妙に互換性が無いため、どちらか片方のみで用意してしまうとフォーマットが合わなくてエラーになり困ることがあります。

クライアント:Windows
サーバー:Linux

の構成で、クライアントのWinSCP(PuTTYフォーマットのみ受容する)から
サーバーのOpenSSHサービスを利用するパターンが典型です。

ただしその場合でも、どちらのフォーマットでも良いので秘密鍵が1本あれば、もう一方のフォーマットの秘密鍵へ変換できます。そして、秘密鍵があれば公開鍵も作成可能です。

しかしながら、急ぎのときに鍵変換方法を探すような余計な仕事は避けたいものです。またppk以外は拡張子だけではどのフォーマットか判らないため、筆者の場合、その場限りで鍵を新しく作ってしまうことがよくありました。そしてまた忘れてしまい、同じことを繰り返しているうちに、鍵ファイルがどんどん増えていくことに・・・。

そこで筆者は解決方法として、数年前から以下の自己ルールで作るようにしています。

  • 鍵は常に4本セットで作成する。公開鍵と秘密鍵の鍵ペアはPuTTYgenで作成し、最初からOpenSSHとPuTTYの両フォーマットで鍵ペアを用意する。つまり合計4つの鍵ファイルを作成しておく。
  • 一目で判る鍵ファイル名にする。ファイル名が長くなっても良いので、明示的にどの鍵タイプか入れておく。できれば結びついたアカウント名や、鍵を作った日付、拡張子も入れる。これは後で鍵を見比べる際の手掛かりになる。
    例)

4つの鍵ファイル

物理キーに場所の名前をテプラで貼っておくのと発想的には変わりませんが、これが思ったよりも効果的で、鍵タイプがどれか悩むことが無くなりました!普段は拡張子を非表示にされている方は、拡張子をファイル名に入れるようにしてください。

新人に業務でSSHについて教える機会がある際、上記を草の根的に勧めています。OpenSSHの秘密鍵の拡張子を .pem にしている理由については後述します。

秘密鍵の改行コードにご注意を

鍵ファイルについては改行コードについても注意が必要です。CRLFとLFで試した結果を比較します。

※PuTTY秘密鍵をLinuxで使うことはほぼ無いと思われますので除外しました。
※OpenSSHのauthorized_keyフォーマットについては、.ssh/authorized_keysファイルごと改行コードを変更し、リモートから認証が通るかどうかを試行しました。

鍵ファイルの改行コード比較

Linux上のOpenSSHで秘密鍵の改行コードがCRLFの場合はエラーになります。ただ、OpenSSH秘密鍵の生成時はOpenSSHでもPuTTYgenでも常にLFですので、考えられるシナリオとしては、

  • Linuxサーバー上でOpenSSHの ssh-keygen で鍵ペアを生成
  • Windowsからパスワード認証でそのサーバーに入り、SFTPでテキストモードで秘密鍵をダウンロード(ここで改行コードが自動変換されてしまう)
  • PowerShellからではなく、WSLにコピーするか参照で、Linux版のOpenSSH経由でCRLFな秘密鍵でログインを試すとエラーになる

のようなケースでしょうか。その場合は切り分け調査のためにPowerShellで試すとログインできてしまうため、かなり悩むことになるでしょう。

メモ:SSL証明書も改行コードにご注意を

WebサービスのHTTPS通信などで利用されている、いわゆるSSL証明書(TLS証明書)でも、改行コードが原因で思わぬエラーになることがあります。開始マーカー「—–BEGIN CERTIFICATE—–」と終了マーカー「—–END CERTIFICATE—–」の行は、余計なものを含まずに改行されていなければなりません。

なまじ開発経験者の場合、この開始終了マーカーの文字列がプログラミング言語のコメントに見えて冗長なものに感じるかもしれません。ですが開始終了マーカーは、ハイフンの数やスペース、文字と行頭行末まで完全一致する必要がある、いわば定数的な文字列です。

そのため、例えば更新する証明書を証明書販売サービサーのWebサイトの管理画面などから手動でコピペしてファイルにしたときに、誤ってファイル最終行を改行を含めないで「—–END CERTIFICATE—–」で終えてしまうと、中間者証明書と連結してサイト証明書とする際に注意しないと

という行が生まれてしまいます。こうした場合規格から外れるので、大抵は正しい証明書とみなされなくなります。

このあたりの開始と終了の「—–BEGIN △△—–」「—–END △△—–」の文字列や改行の考え方がSSHの鍵フォーマットと似ているのは、両者とも共通の祖先としてPEM(RFC1421)から枝分かれしたからです。

SSL証明書はLet’s Encrypt やAWS ACMなどでの自動更新が普及しつつありますが、レガシーサービスの保守作業でいまだに手動更新が必要なことも多く、もし読者の方でも作業する機会がある場合はお気をつけください。

(OpenSSHとOpenSSLって1文字違いなので一度は混同しますよね?)

OpenSSH秘密鍵=PEM ?

ssh-keygen や PuTTYgen で鍵を生成する際、デフォルトでは拡張子がつきません(デフォルトのRSAフォーマットなら id_rsa というファイル名で自動生成されます)。ですが筆者の経験上、拡張子「.pem」で秘密鍵を授受されるケースが往々にしてあります。

どうして標準では拡張子が無いのでしょうか。気になってRFCをいくつか調べたところ、下記が判りました。開始終了マーカーに着目して比べてみます。

RFC比較

さあ混乱してきました…。PEMが複数ありますね。さらに混乱することに、OpenSSH8.9のオンラインマニュアルには以下の記載がありました。

-m key_format
Specify a key format for key generation, the -i (import), -e (export) conversion options,
and the -p change passphrase operation.
The latter may be used to convert between OpenSSH private key and PEM private key formats.
The supported key formats are:
“RFC4716” (RFC 4716/SSH2 public or private key),
“PKCS8” (PKCS8 public or privatekey) or
“PEM” (PEM public key).
By default OpenSSH will write newly-generated private keys in its own format,
but when converting public keys for export the default format is “RFC4716”.
Setting a format of “PEM” when generating or updating a supported private key type will
cause the key to be stored in the legacy PEM private key format.

生成キー、-i(インポート)、-e(エクスポート)変換オプションにキーフォーマットを指定します、
および -p パスフレーズ変更の操作を指定します。
後者は、OpenSSH 秘密鍵形式と PEM 秘密鍵形式の間で変換するために使用します。
対応しているキーフォーマットは以下のとおりです:
「RFC4716」(RFC 4716/SSH2公開鍵または秘密鍵)、
「PKCS8」 (PKCS8 公開鍵または秘密鍵) または
「PEM」(PEM公開鍵)です。
もちろん、OpenSSHは新たに生成された秘密鍵を独自のフォーマットで書き込みます、
しかし公開鍵をエクスポート用に変換するときは、私の書式は「RFC4716」になります。
サポートされている秘密鍵の種類を生成したり更新したりするときに「PEM」フォーマットを設定すると
を設定すると、鍵は従来のPEM秘密鍵形式で保存される。

※DeepL.com(無料版)で翻訳しました。

これを見ると一見、OpenSSHのデフォルトフォーマットはRFC4716に見えるかもしれませんがそんなことはなく、OpenSSHのssh-keygen のデフォルトは秘密鍵も公開鍵もOpenSSH独自フォーマットです(オンラインマニュアルの別な箇所に明記されています)。そしてまたこの記述だとPEMフォーマットもサポートしているようにも見えますが、これが曲者です。

厳密にはRFC4716には、表の通り公開鍵ファイルのマーカーしか定義がないので、RFC 4716のprivate keyという記載がまず怪しいです。また、表の通りかっちりしたPEMとはRFC1421で、秘密鍵公開鍵は登場しません。ですのでこの PEM public key も謎の規格に見えます(おそらくRFC7468を指している可能性が高いです)。

さらに、さらに混乱することに、「legacy PEM private key format」とある通り、「-m PEM」を指定すれば、RSA鍵タイプの場合はOpenSSHの言うところのPEMフォーマット(「—–BEGIN RSA PRIVATE KEY—–」)で秘密鍵が生成されますが、公開鍵は OpenSSH AUTHORIZED_KEYS フォーマットのままで変わりません。公開鍵をOpenSSHの言うところのPEMで得るには、一旦 RSA鍵を生成してから、

などで再度エクスポートする必要があります。そして鍵タイプ -t ed25519 で

と指定した場合は、RSAではないためか、秘密鍵は独自フォーマット「—–BEGIN OPENSSH …」のまま特にエラーや警告も無く、しれっと生成されます。RFC7468的にはPEMの系譜だからセーフ!ということなのかもしれません。その一方で、

とEd25519ではエラーにするので、一方ではRFC1421のPEMをRFC7468で広げて使って独自フォーマット「—–BEGIN OPENSSH …」を表現しているのに、もう一方では拡大解釈せずに絞る(謎のマーカー「—–BEGIN RSA PRIVATE KEY—–」しか認めない)という、いったい何を信じたら良いのか判らなくなる挙動になっています。

ある程度 m オプションでコントロール可能ですが、利用者がOpenSSHの上記の複雑な仕様と、非常に似通ったこれらPEMの親戚の各フォーマットを理解していないと、生成されるファイルフォーマットの区別はまったくつかない状態です。

そうした混乱が背景にあり、OpenSSH、PuTTYともに、デフォルトでOpenSSH秘密鍵の拡張子は無しとしているのではないかと筆者は考えます。拡張子 .pem が PKCS#8 以外でも使われ始めた経緯はひょっとしたら以下だったのではないでしょうか。

・ssh-keygen のデフォルトが公開鍵を .pub にするため、PuTTY利用者も公開鍵の拡張子を.pubにするようになった
⇒ PuTTYの秘密鍵は ppk のため、OpenSSH秘密鍵だけ拡張子が無い形になった
⇒ 拡張子が無いと、鍵が増えてきたときに区別が付きにくくなるし、見た目も寂しいので何か付けたい…
⇒「—–BEGIN △△—–」「—–END △△—–」のスタイルはPEMフォーマットっぽい
⇒ その結果、OpenSSH秘密鍵を .pem とするのが流行った???

長くなりましたが結論として、OpenSSHフォーマットの秘密鍵の拡張子を.pem とするのは、完全には間違いではないものの、上記の混乱に巻き込まれる可能性がありますので十分お気をつけてお使いください。と書きつつも、筆者は今後も使い続ける予定です!

記事のまとめ

  • 暗号鍵はEd25519フォーマットがお勧めです
  • 鍵は常に4本セットで作っておくと少しだけ楽になるでしょう(OpenSSHフォーマットの鍵ペアと、PuTTYフォーマットの鍵ペア)
  • 鍵ファイル転送時は改行コードに気を付けましょう
  • 拡張子.pemは人によって解釈が違う恐れがあるので気を付けましょう