はじめに
この記事を書いている2018年9月現在、OpenSSHでは、鍵に使用する公開鍵暗号(電子署名)アルゴリズムとして、楕円曲線暗号であるECDSAを選択することができます。
そして、公開鍵暗号の常ですが、秘密鍵の中で本当に秘密にすべき情報というのは実はそれほど大きなデータではありません。
であれば、その核となるデータから鍵を復元できるのではないかと思い立ちテストしたため、その備忘録として記します。
なお、環境は Windows10 WSL/Ubuntu 16.04.5 LTS + OpenSSL 1.0.2g + OpenSSH 7.2p2、使用する楕円曲線は prime256v1 とします。
ECDSA鍵について
OpenSSHのECDSA鍵は、ssh-keygenコマンドで、-t ecdsa
オプションを指定することで作成できます。
例えば次は、256bit ECDSA ( prime256v1 ) の鍵を、id_ecdsa(秘密鍵), id_ecdsa.pub(公開鍵) に作成・保存する例です。
$ ssh-keygen -t ecdsa -b 256 -P "" -C "" -f id_ecdsa
Generating public/private ecdsa key pair.
Your identification has been saved in id_ecdsa.
Your public key has been saved in id_ecdsa.pub.
The key fingerprint is:
SHA256:+EC2TlpJR9lFwBxdpVkN2PdhZtLAxeskwY+a/RZPXCM
The key's randomart image is:
+---[ECDSA 256]---+
| .=o*==B++|
| .. + o=.@o|
| + . @.+|
| + = E =o|
| B S + =.o|
| = o o . oo|
| . . . ..o|
| o.|
| . |
+----[SHA256]-----+
特に形式を指定せず、またパスフレーズを空で指定すれば、秘密鍵は暗号化なしのPEM形式で保存されます。これでファイルサイズは227バイトです。
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEICv2fo3vC3lOeuGtRzLwrdeOLyIbZ7xyEgGkPB4rO3RvoAoGCCqGSM49
AwEHoUQDQgAE7W5UuuFIumTq9w7ufxOgrFOlvOUaaUD8EHlIb+nWTzgCtFBN560O
aWs+eOl0MH4PT80UeALizOJjyXUfeT+Upw==
-----END EC PRIVATE KEY-----
しかし、opensslコマンドで情報を見てみると、本当に秘密にすべき情報は(以下のコマンドの出力にある通り)256bit、base64エンコードした状態でも44バイトに過ぎないことが分かります。
$ openssl ec -text -noout -in id_ecdsa
read EC key
Private-Key: (256 bit)
priv:
2b:f6:7e:8d:ef:0b:79:4e:7a:e1:ad:47:32:f0:ad:
d7:8e:2f:22:1b:67:bc:72:12:01:a4:3c:1e:2b:3b:
74:6f
pub:
04:ed:6e:54:ba:e1:48:ba:64:ea:f7:0e:ee:7f:13:
a0:ac:53:a5:bc:e5:1a:69:40:fc:10:79:48:6f:e9:
d6:4f:38:02:b4:50:4d:e7:ad:0e:69:6b:3e:78:e9:
74:30:7e:0f:4f:cd:14:78:02:e2:cc:e2:63:c9:75:
1f:79:3f:94:a7
ASN1 OID: prime256v1
NIST CURVE: P-256
実際に対応するのは秘密鍵の次の強調部分です。
-----BEGIN EC PRIVATE KEY-----
MHcCAQEE ICv2fo3vC3lOeuGtRzLwrdeOLyIbZ7xyEgGkPB4rO3Rv oAoGCCqGSM49
次のように、base64デコードして16進ダンプを見てみると、丁度"priv"の部分の256bit(+先頭に余分な8bit)に対応していることが分かります。
$ base64 -d <<< ICv2fo3vC3lOeuGtRzLwrdeOLyIbZ7xyEgGkPB4rO3Rv | od -tx1 -An
20 2b f6 7e 8d ef 0b 79 4e 7a e1 ad 47 32 f0 ad
d7 8e 2f 22 1b 67 bc 72 12 01 a4 3c 1e 2b 3b 74
6f
ECDSA鍵の復元
ということで、今回、上記の44バイト部分をパラメータとして与えて、元のECDSA鍵を復元するテストプログラムecrecover-p256
を作りました。ただし、使用する楕円曲線はprime256v1限定です。
実行すると次のように、標準出力に秘密鍵を出力します。これはちゃんと元の鍵と一致するものです。
$ ./ecrecover-p256 ICv2fo3vC3lOeuGtRzLwrdeOLyIbZ7xyEgGkPB4rO3Rv
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEICv2fo3vC3lOeuGtRzLwrdeOLyIbZ7xyEgGkPB4rO3RvoAoGCCqGSM49
AwEHoUQDQgAE7W5UuuFIumTq9w7ufxOgrFOlvOUaaUD8EHlIb+nWTzgCtFBN560O
aWs+eOl0MH4PT80UeALizOJjyXUfeT+Upw==
-----END EC PRIVATE KEY-----
$ ./ecrecover-p256 ICv2fo3vC3lOeuGtRzLwrdeOLyIbZ7xyEgGkPB4rO3Rv | diff -s id_ecdsa -
Files id_ecdsa and - are identical
ということで、( 鍵に使う楕円曲線がprime256v1限定ではありますが ) 核となる秘密情報のみから、鍵本体を復元できることが分かりました。
ソースコード
以下、ecrecover-p256
のソース(C99)です。
gcc -o ecrecover-p256 ecrecover-p256.c -lcrypto
というコマンドでビルドできます。なお、OpenSSLの開発用パッケージ(Ubuntuならlibssl-dev)が必要です。
中身はやっつけなので、特に解説はしません。リソース解法もさぼってます。
※OpenSSL1.0.2だとヘッダーにPEM_write_ECPrivateKey
関数が見当たらなかったため、自分でプロトタイプをでっちあげました。関数本体はライブラリにあるのでちゃんと使うことができます。
#include <stdio.h>
#include <string.h>
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/obj_mac.h>
#include <openssl/ec.h>
extern int PEM_write_ECPrivateKey(FILE *,EC_KEY *,void*,unsigned char*,int,void*,void*);
void b64decode(char* b64str, unsigned char* buffer, size_t* length) {
BIO *bio, *b64;
bio=BIO_new_mem_buf(b64str,-1);
b64=BIO_new(BIO_f_base64());
bio=BIO_push(b64,bio);
BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
*length = BIO_read(bio, buffer, strlen(b64str));
BIO_free_all(bio);
}
int main(int argc,char *argv[]) {
if ( argc<2 ) {
fprintf(stderr,"Usage: %s b64string\n",argv[0]);
return 1;
}
size_t b64len=strlen(argv[1]),decodelen;
char buffer[b64len];
b64decode(argv[1],buffer,&decodelen);
BIGNUM *priv=BN_bin2bn(&buffer[1],decodelen-1,NULL);
EC_GROUP *curve=EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1);
EC_POINT *pub=EC_POINT_new(curve);
BN_CTX *ctx=BN_CTX_new();
EC_POINT_mul(curve,pub,priv,NULL,NULL,ctx);
EC_KEY *key=EC_KEY_new();
EC_KEY_set_group(key,curve);
EC_KEY_set_asn1_flag(key,OPENSSL_EC_NAMED_CURVE);
EC_KEY_set_private_key(key,priv);
EC_KEY_set_public_key(key,pub);
PEM_write_ECPrivateKey(stdout,key,NULL,NULL,0,NULL,NULL);
}
終わりに
テストと備忘録なので、今回は特になしです。