RSA秘密鍵について

PEMとDERというものがあるがどういう違いがあるのか、ssh-keygenで作成される鍵とopenssl genrsaで作成される鍵は違うのか、など気になったので調査してまとめてみる。

形式について

PKCSという規格群が存在する。

PKCS - Wikipedia

この中でPKCS#1という規格があり、この中でRSA暗号秘密鍵や公開鍵のフォーマットが規定されている。 openssl genrsaで生成されるのはこちら。

RFC 8017 - PKCS #1: RSA Cryptography Specifications Version 2.2

PKCS#8という規格も存在し、RSA暗号に限らない秘密鍵のフォーマットが規定されている。こちらも使用されることがある。例えばJavaの標準クラスで読み込みが可能なのはこちらの形式。

PKCS8EncodedKeySpec (Java Platform SE 8)
RFC 5958 - Asymmetric Key Packages

PKCS#1やPKCS#8の中ではASN.1 (Abstract Syntax Notation One) という形式でフォーマットが記述されている。 ASN.1は抽象的な形でフォーマットを記述する記法であり、具体的なバイナリ列にするエンコード方法は定義されない。 ASN.1は暗号技術の文脈に限らず利用される。

Abstract Syntax Notation One - Wikipedia
RFC 6025 - ASN.1 Translation

具体的にはこのような形式で記述される。 (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と異なる形式らしい。

https://www.openssh.com/txt/release-7.8
https://tools.ietf.org/html/rfc4716#section-3.5

ここまでのまとめ

一旦まとめる。 フォーマットとして大きく異なる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にすると読める。 詳しくは以下を参照。

https://stackoverflow.com/a/19387517

Bouncy Castleを使えばPKCS#1を直接読める。

https://www.bouncycastle.org/

参考