RSA鍵のフォーマットについて
元々 https://htlsne.hatenablog.com/entry/2020/09/10/234227 に書いていた記事を、Zennにも転載しておきます。
はじめに
PEMとDERというものがあるがどういう違いがあるのか、ssh-keygenで作成される鍵とopenssl genrsaで作成される鍵は違うのか、など気になったので調査してまとめてみる。
形式について
PKCSという規格群が存在する。
この中でPKCS#1という規格があり、この中でRSA暗号の秘密鍵や公開鍵のフォーマットが規定されている。openssl genrsa
で生成されるのはこちら。
PKCS#8という規格も存在し、RSA暗号に限らない秘密鍵のフォーマットが規定されている。こちらも使用されることがある。例えばJavaの標準クラスで読み込みが可能なのはこちらの形式。
PKCS#1やPKCS#8の中ではASN.1 (Abstract Syntax Notation One) という形式でフォーマットが記述されている。ASN.1は抽象的な形でフォーマットを記述する記法であり、具体的なバイナリ列にするエンコード方法は定義されない。ASN.1は暗号技術の文脈に限らず利用される。
具体的にはこのような形式で記述される。(RFCのAppendix Aを参照。)
RSAPrivateKey ::= SEQUENCE {
version Version,
modulus INTEGER, -- n
publicExponent INTEGER, -- e
privateExponent INTEGER, -- d
prime1 INTEGER, -- p
prime2 INTEGER, -- q
exponent1 INTEGER, -- d mod (p-1)
exponent2 INTEGER, -- d mod (q-1)
coefficient INTEGER, -- (inverse of q) mod p
otherPrimeInfos OtherPrimeInfos OPTIONAL
}
ASN.1で記述されたものをエンコードする方法として、DER (Distinguished Encoding Rules) というものが存在する。他にもBERなどエンコード方法は存在するが、DERは一通りにエンコード方法が定まる点が優れている。DERはテキストとは限らないバイナリ列になる。DERをBASE64化して、改行を入れたりヘッダを入れたりしてテキスト形式にしたのがPEM形式 (Privacy Enhanced Mail) 。テキストの方が扱いやすいためか、PEMの方が見ることが多い。
つまりPKCS#1のPEMとか、PKCS#8のDERとか、これだけで2 * 2 = 4通りある。
ややこしいことに、OpenSSHはバージョン7.8以降から別の形式を採用している。このバージョンから、ssh-keygenで生成される鍵フォーマットがPEM形式から独自形式 (OpenSSH format) に変更になった。これはPKCS#1ともPKCS#8とも異なる形式で、RFC4716で定義されている。見た目はBASE64化されておりPEMに似ているが、厳密にはPEMと異なる形式らしい。
ここまでのまとめ
一旦まとめる。フォーマットとして大きく異なるPKCS#1とPKCS#8がある。PEMやDERはそのエンコード(符号化)方法。全く違う形式としてOpenSSH形式がある。
鍵の中身を見てみる
opensslコマンドで鍵の中身を確認することができる。-noout
オプションでPEM形式の出力を抑制し、-text
オプションで内容をテキスト形式で表示する。PKCS#1でもPKCS#8でも可能だが、以下はPKCS#1の例。なおOpenSSH形式は無理そう。
(長いので...
部分は省略している)
$ openssl rsa -in a.pem -noout -text
RSA Private-Key: (4096 bit, 2 primes)
modulus:
00:b0:e7:d4:47:2c:63:9c:f6:a8:20:b2:8b:f0:3f:
...
publicExponent: 65537 (0x10001)
privateExponent:
31:a8:21:72:09:07:bd:1b:8f:7d:fe:20:41:c3:b8:
...
prime1:
00:eb:94:66:41:e2:e8:42:af:4d:dc:b3:3b:bc:f0:
...
prime2:
00:c0:3d:6d:ff:98:4c:6c:39:e9:5b:fe:47:ab:20:
...
exponent1:
00:b9:96:e9:90:52:60:43:d8:b3:70:81:4b:38:a8:
...
exponent2:
00:b8:b5:cd:8d:3a:ce:a3:66:79:7c:74:b4:74:0e:
...
coefficient:
64:8d:8c:58:fb:fe:8e:43:98:89:c1:2e:16:69:4d:
...
RSA暗号について知識があれば、原理通り主に2つの素数からなっていることが見て取れる。
生成する
PKCS#1
openrsa genrsa
これだと標準出力に出力される。
openrsa genrsa 4096 -out a.pem
鍵の長さを指定したり、-outで出力ファイルを指定したりすることが多い。パスフレーズを指定したい場合は、-aes256
オプションをつければ良い。
ssh-keygenでもオプションをつければ作れる。
ssh-keygen -m PEM
PKCS#8
ssh-keygenを使えば一応可能。
ssh-keygen -m PKCS8 -f openssh.key
OpenSSH形式
バージョン7.8以降のssh-keygenをそのまま使うだけ。-f
は出力ファイル。
ssh-keygen -f openssh.key
形式を変換する
必要そうなものだけにして、網羅はしない。
PKCS#1からPKCS#8
opensslコマンドでできる。
openssl pkcs8 -in a.pem -topk8
パスフレーズが不要な場合は-nocrypt
オプションを付ける。
openssl pkcs8 -in p1.key -topk8 -nocrypt
-outform
オプションでPEMかDERか指定できる。デフォルトはPEM。
openssl pkcs8 -in a.pem -topk8 -nocrypt -outform DER
これだと標準出力に出力されるので、ファイルに保存したい場合はリダイレクトするか-out
オプションで出力ファイルを指定する。
PKCS#8からPKCS#1
openssl rsa
でできる。
openssl rsa -in p8.key
-outform
オプションでPEMかDERか指定できたり、-out
で出力ファイルが指定できるのは同じ。
OpenSSH形式からPKCS#1
素直にはできない。ssh-keygenの、形式を指定する-m
オプションと、パスフレーズを変更する-p
オプションを組み合わせるとできる。ただし、元のファイルを置き換えてしまうので注意。
cp a.key b.key # 置き換えてしまうので一旦コピー
ssh-keygen -f b.pem -m PEM -p
なおこの場合の-f
オプションは入力ファイルを表す。
形式を見分ける
どこまで厳密化はわからないが、PEMのヘッダを見ればある程度わかる。
PKCS#1
-----BEGIN RSA PRIVATE KEY-----
PKCS#8(暗号化なし)
-----BEGIN PRIVATE KEY-----
PKCS#8(暗号化あり)
-----BEGIN ENCRYPTED PRIVATE KEY-----
OpenSSH形式
-----BEGIN OPENSSH PRIVATE KEY-----
Javaで読み込む
標準ライブラリで読めるのはおそらくPKCS#8だけ。DER形式のPKCS#8にすると読める。詳しくは以下を参照。
Bouncy Castleを使えばPKCS#1を直接読める。
参考
-
『プロフェッショナルSSL/TLS』
- TSLについて詳しく解説されている。一部参考にしました。
- 無料で読める『OpenSSLクックブック』もどうぞ。
Discussion
RFC4716に書かれているのは「The Secure Shell (SSH) Public Key File Format」というタイトルのとおり公開鍵ファイルの形式です。Tectia SSH Server等で使われている公開鍵ファイルの形式ですね。
公開鍵ファイルの形式なので、OpenSSH形式の秘密鍵ファイルとは関係ありません。
OpenSSH形式の秘密鍵ファイルのフォーマットは、OpenSSHのソースに含まれているPROTOCOL.keyファイルに書かれています。
細かいところですが、気が付いてしまったので:
openssl genrsa
をopenrsa genrsa
とtypoされているようです。大変よくまとまった記事をありがとうございます!