はじめに
背景
おお! angelよ。
なんと OpenSSLでも ed25519の
かぎが つくれるのじゃ。
べんりな よのなかに なったもの
よのう。
という啓示を受けましたので ( 今更 )、折角なので鍵の相互変換ができないものか考えてみたネタになります。
環境
Ubuntu 18.04(Win10+WSL1), OpenSSH 7.6p1, OpenSSL 1.1.1 です。
OpenSSL 1.1.1 以降で ed25519 を扱えるようになっています。
鍵としては、RSA, ECDSA, ed25519 の3種を試そうと思います。
公開鍵⇔公開鍵変換は面倒なので、秘密鍵⇒秘密鍵,公開鍵の変換のみです。また、パスフレーズによる保護は考えないものとします。
楽なやつ(RSA/ECDSA)
ではまず楽な方から、ということでRSAとECDSAです。
なお、ECDSA(EC鍵)について、OpenSSLでは多数種類が選べるのですが、OpenSSHで使えるのは P-256(prime256v1), P-384(secp384r1), P-521(secp521r1)の3種類のみですのでご注意ください。
OpenSSH旧型式秘密鍵
OpenSSHの秘密鍵には、旧型式・新形式の2通りがあります。OpenSSH 7.8以降は新形式がデフォルト、それまでは旧型式がデフォルトです。
明示的に旧型式で作成する場合は ssh-keygen -m pem
、新形式で作成する場合は ssh-keygen -o
を指定します。
で、この旧型式の方はOpenSSLの秘密鍵フォーマットをそのまま流用しています。なので、変換するまでもなく同じ形式です。念のため、公開鍵の作成方法を挙げておきます。
- OpenSSH公開鍵作成
ssh-keygen -y -f 秘密鍵ファイル > 公開鍵保存先
- OpenSSL公開鍵作成
openssl pkey -in 秘密鍵ファイル -pubout -out 公開鍵保存先
OpenSSH新型式秘密鍵
OpenSSHの新形式の秘密鍵はOpenSSLとは互換性がありません。
しかし、ssh-keygen で新旧相互変換が可能です。旧型式はOpenSSL形式そのものですので、これでOpenSSL形式に変換をかけたことになります。
- 新→旧変換
ssh-keygen -p -N "" -m pem -f 秘密鍵ファイル
- 旧→新変換
ssh-keygen -p -N "" -o -f 秘密鍵ファイル
いずれも、既存のファイルを上書きする形での変換となりますのでご注意ください。
楽じゃないやつ(ed25519)
では本命のed25519です。ただこちらは形式に全く互換性がないため、標準的なツールで変換することはできません。ただ、ed25519 の鍵データは固定長のパラメータが埋め込まれているだけなので、バイナリデータを切り貼りすればなんとかなります。
OpenSSH秘密鍵の構造
OpenSSHでのed25519秘密鍵は、-t ed25519
を指定することで作成することができます。
※公開鍵生成は ssh-keygen -y
で他と変わらないので割愛します。
以下、ssh-keygen -t ed25519 -C "" -N "" -f 秘密鍵ファイル名
で作成した、パスフレーズ保護・コメントなしの秘密鍵の、base64部分をデコードしたバイナリ構造です。なお、ed25519 では必ず新形式のフォーマットになることにご注意ください。
※The OpenSSH Private Key Formatを参考にしています。
ということで、秘密鍵データを得るには、図中の赤網掛け部分 ( 162バイト目~193バイト目 ) の32バイトを抽出すれば済みますし、逆に秘密鍵ファイルを作るには、秘密鍵データ32バイトと公開鍵データ32バイトをこの構造に埋め込んで ( 一部乱数を作って埋め込んで )、base64変換してあげれば済みます。
OpenSSL鍵ファイルの構造
では続いてOpenSSLでの、秘密鍵・公開鍵の構造についてです。
まず、ed25519の秘密鍵は genpkey サブコマンドで作ります。一旦秘密鍵を作った後は、pkey サブコマンドで、公開鍵生成等の操作が行えます。
- 秘密鍵作成
openssl genpkey -algorithm ed25519 -out 秘密鍵ファイル名
- 公開鍵作成
openssl pkey -in 秘密鍵ファイル名 -pubout -out 公開鍵ファイル名
ed25519の鍵は、他の鍵データとは異なり、秘密鍵データ・公開鍵データが完全に分離された形になっています。
※RSAやECの場合、秘密鍵ファイルは公開鍵データも同梱しています。
バイナリ(DER形式)としての各データはASN1という構造になっていますが、図にすると次のようになります。
結局のところ、秘密鍵データ、公開鍵データは末尾の32バイトで、それ以外は固定となりますので、データ生成もデータ抽出も非常に楽にできます。
変換ツール
ということで、以下のような変換ツールでOpenSSHのed25519秘密鍵と、OpenSSLのed25519秘密鍵を相互に変換できます。いずれも変換前の秘密鍵ファイル名を引数に取り、標準出力へ変換後の鍵データを出力します。
#!/bin/bash
pkey="$1"
(
echo -ne '\x30\x2e\x02\x01\0\x30\x05\x06\x03\x2b\x65\x70\x04\x22\x04\x20'
grep -v ^- "$pkey" | base64 -d | head -c 193 | tail -c 32
) | openssl pkey -inform der
#!/bin/bash
pkey="$1"
read rwhex < <( head -c 4 /dev/urandom | xxd -p )
echo '-----BEGIN OPENSSH PRIVATE KEY-----'
(
echo -ne 'openssh-key-v1\0'
echo -ne '\0\0\0\x04none'
echo -ne '\0\0\0\x04none'
echo -ne '\0\0\0\0'
echo -ne '\0\0\0\x01'
echo -ne '\0\0\0\x33'
echo -ne '\0\0\0\x0bssh-ed25519'
echo -ne '\0\0\0\x20'
openssl pkey -outform der -pubout -in "$pkey" | tail -c 32
echo -ne '\0\0\0\x88'
echo "$rwhex$rwhex" | xxd -r -p
echo -ne '\0\0\0\x0bssh-ed25519'
echo -ne '\0\0\0\x20'
openssl pkey -outform der -pubout -in "$pkey" | tail -c 32
echo -ne '\0\0\0\x40'
openssl pkey -outform der -in "$pkey" | tail -c 32
openssl pkey -outform der -pubout -in "$pkey" | tail -c 32
echo -ne '\0\0\0\0'
echo -ne '\x01\x02\x03\x04\x05'
) | base64 -w 70
echo '-----END OPENSSH PRIVATE KEY-----'
実際にツールを使って変換を試した実行例になります。
$ umask 066
$ ssh-keygen -t ed25519 -C "" -N "" -f ssh-ed
Generating public/private ed25519 key pair.
Your identification has been saved in ssh-ed.
Your public key has been saved in ssh-ed.pub.
The key fingerprint is:
SHA256:qDnKs9s+m6mfJkNgbMBjbRzFrYizEEYJvLMt9hBCKfU
(略)
$ ./sshkey2pem.sh ssh-ed > ed.key
$ ./pem2sshkey.sh ed.key > ssh-ed2
$ more ed.key
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIKTZKMLPNdLcmGlb6HjIkkELK7cMK5TMyWhDt7y/j9Mz
-----END PRIVATE KEY-----
$ openssl pkey -text -noout -in ed.key
ED25519 Private-Key:
priv:
a4:d9:28:c2:cf:35:d2:dc:98:69:5b:e8:78:c8:92:
41:0b:2b:b7:0c:2b:94:cc:c9:68:43:b7:bc:bf:8f:
d3:33
pub:
4c:6e:8b:55:55:a0:8a:0c:04:32:66:c8:21:b7:da:
30:98:35:7a:30:a1:60:1a:fd:2f:ad:fb:5a:1b:d8:
0d:78
$ more ssh-ed2
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBMbotVVaCKDAQyZsght9owmDV6MKFgGv0vrftaG9gNeAAAAIhb4MEqW+DB
KgAAAAtzc2gtZWQyNTUxOQAAACBMbotVVaCKDAQyZsght9owmDV6MKFgGv0vrftaG9gNeA
AAAECk2SjCzzXS3JhpW+h4yJJBCyu3DCuUzMloQ7e8v4/TM0xui1VVoIoMBDJmyCG32jCY
NXowoWAa/S+t+1ob2A14AAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----
$ ssh-keygen -l -f ssh-ed2
256 SHA256:qDnKs9s+m6mfJkNgbMBjbRzFrYizEEYJvLMt9hBCKfU (ED25519)
ツールでSSH秘密鍵に逆変換した時のフィンガープリントが元の鍵のフィンガープリント SHA256:qDnKs9s+m6mfJkNgbMBjbRzFrYizEEYJvLMt9hBCKfU
に一致していることから、変換に特に問題が出ていないことが分かります。
おまけ
OpenSSLでRSA/ECDSA秘密鍵を作成する際の違い
opensslコマンドで鍵を作る場合、以前は RSA なら genrsa、ECDSA(EC鍵)なら ecparam といったサブコマンドを使うものでしたが、バージョン1.0.0からは統一的な genpkey というサブコマンドが用意されています。これは ed25519鍵を作る時と同じものです。
P-256(prime256v1)でのECDSA秘密鍵(EC鍵)をそれぞれで作るコマンドの違いは次のようになります。
- ecparamサブコマンド
openssl ecparam -name P-256 -genkey -noout -out 保存先ファイル名
- genpkeyサブコマンド
openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:P-256 -out 保存先ファイル名
なお、それぞれのサブコマンドで生成した秘密鍵はフォーマットが違います。後者の方は先頭行に"EC"というアルゴリズムを示す情報が無い分、鍵データの中に含めるようになっています。その分、若干容量が増えています。
※RSAの方でも genrsa と pkey サブコマンドで、似たような違いが出ます
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIGkioiJ7fuMcJ3OY1GIp5rEr247NRFV5W0Btcd/vGAZdoAoGCCqGSM49
AwEHoUQDQgAEMvL6HXWSNeYovboMEwTHN+47rMDj+koeQn/Iup00vMui8Sai2ahH
fsVzmAGTWlzLtX0SFI5TjCdRY27Qbal7Pg==
-----END EC PRIVATE KEY-----
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgaSKiInt+4xwnc5jU
YinmsSvbjs1EVXlbQG1x3+8YBl2hRANCAAQy8voddZI15ii9ugwTBMc37juswOP6
Sh5Cf8i6nTS8y6LxJqLZqEd+xXOYAZNaXMu1fRIUjlOMJ1FjbtBtqXs+
-----END PRIVATE KEY-----
なお、この2種類のフォーマットは ec サブコマンドおよび pkey サブコマンドで相互変換可能です。が、特に変換しなくてもどちらの形式でも OpenSSHの秘密鍵として用いることができます。
- "EC PRIVATE KEY"→"PRIVATE KEY"変換
openssl pkey -in 変換前ファイル -out 変換後ファイル
- "PRIVATE KEY"→"EC PRIVATE KEY"変換
openssl ec -in 変換前ファイル -out 変換後ファイル
RSAの場合も似たようなものです。コマンドのみ紹介します。
- "RSA PRIVATE KEY"→"PRIVATE KEY"変換
openssl pkey -in 変換前ファイル -out 変換後ファイル
- "PRIVATE KEY"→"RSA PRIVATE KEY"変換
openssl rsa -in 変換前ファイル -out 変換後ファイル
ssh-keygenで新旧変換した場合の違い
ssh-keygen で旧型式のECDSA秘密鍵作成 → ssh-keygen で新形式に変換 → ssh-keygen で旧型式に再変換、とした場合、鍵の内容に変化はないはずなのに、容量がずいぶんと膨れ上がります。
$ ssh-keygen -t ecdsa -b 256 -C "" -N "" -f ssh-ec.key
(略)
$ more ssh-ec.key
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIP46vNIVSWYgTQDdn1MKyGkl2M92b58x7ASz4ozuIG99oAoGCCqGSM49
AwEHoUQDQgAEfYJBCbU4YnmVV66NNOUrnhhNSyf7VEY5XjkS9wZP5KT589hs3ebL
cEZwzTARZ8N8Bq//JQ98GXmKnu3IwrKgKw==
-----END EC PRIVATE KEY-----
$ ssh-keygen -p -N "" -o -f ssh-ec.key
Your identification has been saved with the new passphrase.
$ ssh-keygen -p -N "" -m pem -f ssh-ec.key
Key has comment ''
Your identification has been saved with the new passphrase.
$ more ssh-ec.key
-----BEGIN EC PRIVATE KEY-----
MIIBaAIBAQQg/jq80hVJZiBNAN2fUwrIaSXYz3ZvnzHsBLPijO4gb32ggfowgfcC
AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA////////////////
MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr
vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE
axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W
K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8
YyVRAgEBoUQDQgAEfYJBCbU4YnmVV66NNOUrnhhNSyf7VEY5XjkS9wZP5KT589hs
3ebLcEZwzTARZ8N8Bq//JQ98GXmKnu3IwrKgKw==
-----END EC PRIVATE KEY-----
理由についてはよく分かりませんが、最初に鍵を作った時、鍵に使う楕円曲線の情報が「曲線名」として省容量化されているところ、変換を掛けると曲線のパラメータを省略せずに入れるようにしているためのようです。
openssl ec -text -noout -in 秘密鍵ファイル
見てみると違いが分かります。
$ openssl ec -text -noout -in ssh-ec.key
read EC key
Private-Key: (256 bit)
priv:
fe:3a:bc:…(略)
pub:
04:7d:82:…(略)
ASN1 OID: prime256v1
NIST CURVE: P-256
$ openssl ec -text -noout -in ssh-ec.key
read EC key
Private-Key: (256 bit)
priv:
fe:3a:bc:…(略)
pub:
04:7d:82:…(略)
Field Type: prime-field
Prime:
00:ff:ff:…(略)
A:
00:ff:ff:…(略)
B:
5a:c6:35:…(略)
Generator (uncompressed):
04:6b:17:…(略)
Order:
00:ff:ff:…(略)
Cofactor: 1 (0x1)
Seed:
c4:9d:36:…(略)
このように前者では、P-256 (prime256v1) という名前でまとめられていたものが、具体的なパラメータ p,a,b ( 曲線の方程式 $y^2\equiv x^3+ax+b~mod~p$ ), G,n ( ベースポイントおよびその位数 $nG=O$ ) ,h(cofactor, $h=|E|/n$ ), S(seed...各パラメータをランダムに決めるための元になった値) が全部具体的な数値として入っているため容量が大きくなるということです。
各パラメータの詳細に興味がある方は、例えば SEC 2: Recommended Elliptic Curve Domain Parameters か、draft NIST SP 800-186 あたりをご覧になるのが良いのではないかと思います。
好みの文字列を入れた秘密鍵
流石に公開鍵に好みの文字列を入れるのは容易ではありませんが、秘密鍵データは単なる32バイトのランダム文字列なので、好みの文字列を仕込むことができます。
次のように、秘密鍵データとしてfefc779abde9ff85e969b3fb61a2c7bfc21a3f85e969fed85e9ac7a5bdeb3ff8
を仕込んだOpenSSL秘密鍵を用意します。( 作り方は割愛します )
$ more heaven.pem
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIP78d5q96f+F6Wmz+2Gix7/CGj+F6Wn+2F6ax6W96z/4
-----END PRIVATE KEY-----
$ openssl pkey -text -noout -in heaven.pem
ED25519 Private-Key:
priv:
fe:fc:77:9a:bd:e9:ff:85:e9:69:b3:fb:61:a2:c7:
bf:c2:1a:3f:85:e9:69:fe:d8:5e:9a:c7:a5:bd:eb:
3f:f8
pub:
bd:49:44:84:f9:75:78:78:4e:0b:91:00:2b:0f:28:
66:05:92:44:25:f2:62:5d:6b:58:85:a0:08:4b:92:
c5:28
これ自体は特に何か仕込んであるようには見えません。しかし、前述のツールでOpenSSH秘密鍵に変換すると、仕込んでいた英語の一節が現れていることが分かります。
$ ./pem2sshkey.sh heaven.pem
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACC9SUSE+XV4eE4LkQArDyhmBZJEJfJiXWtYhaAIS5LFKAAAAIgj/oKxI/6C
sQAAAAtzc2gtZWQyNTUxOQAAACC9SUSE+XV4eE4LkQArDyhmBZJEJfJiXWtYhaAIS5LFKA
AAAED+/Heaven/helps/those/who/help/themselves/+L1JRIT5dXh4TguRACsPKGYF
kkQl8mJda1iFoAhLksUoAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----
鍵データのランダム性を損なうことになるのでお薦めできるものではないですが、気分転換にそういう鍵を作ってみるのも良いかも知れません。
終わりに
ということで、ed25519を主として、鍵データの変換について取り組んでみました。
意外と構造は難しくないので、自力で読み解くのも面白いのではないかと思います。