RSA暗号とPEM/DERの構造
cryptoRSA暗号とは
c ≡ m^e (mod n) で暗号化し、m ≡ c^d (mod n) で複合する暗号。 e と n が公開鍵で d が秘密鍵。n はとても大きく近くない素数 p,q の積で、 これを公開しても p * q に素因数分解できないのがこの暗号の前提になっている。 768bit(10進数で232桁)では既に解読されているので、少なくとも1024bit以上にする。
eはEuler totient function(1~nまでの整数でnと互いに素なものの個数 φ(n)=(p-1)(q-1))未満で互いに素な正の整数で、小さすぎても大きすぎても良くない。2^16 + 1 = 65537 がよく使われる。
d は ed ≡ 1 (mod φ(n)) を満たす値にする。
例
例として (p,q) = (193,709) とすると各変数は次のようになる。
- n = p * q = 136837
- φ(n) = (p-1)(q-1) = 135936
- e = 65537 < φ(n)
秘密鍵 d は 65537d ≡ 1 (mod 135936) の式を変形した 65537d - 135936*x = gcd(65537,135936) = 1 を、次のように 135936 と 65537を残しながら展開していく、拡張されたユークリッドの互除法で求める。
これをコードに表したのがこれ。
const d = (phi,e) => {
let history = {[phi]: [1,0], [e]:[0,1]} // [x,y] => φ * x + e * y
let x = phi
let y = e
while(y > 1){
const nextY = x % y
history[x % y] = history[x].map((vx,index) => vx - history[y][index] * Math.floor(x/y))
x = y
y = nextY;
}
return history;
}
const phi = (193-1) * (709-1)
const e = 65537
const result = d(phi,e);
console.log(result);
console.log(`1 = ${phi}*${result[1][0]} + ${e}*${result[1][1]}`);
{ '1': [ 9503, -19711 ],
'6': [ -8519, 17670 ],
'7': [ 984, -2041 ],
'62': [ -647, 1342 ],
'69': [ 337, -699 ],
'131': [ -310, 643 ],
'200': [ 27, -56 ],
'2331': [ -13, 27 ],
'4862': [ 1, -2 ],
'65537': [ 0, 1 ],
'135936': [ 1, 0 ] }
1 = 135936*9503 + 65537*-19711
1 = 135936*-65537 + 65537135936 = 135936(9503-65537) + 65537*(-19711 + 135936) なので d=116225 となる。
平文が 12345 のとき、公開鍵 e,n で暗号化すると (12345 ** 65537) % 136837 = 6964 になり、 これを秘密鍵 d で復号することで (6964 ** 116225) % 136837 = 12345 のように元の平文が得られる。
鍵ファイルの生成
ssh-keygenで生成されるようなPEMファイルを作る。
用語
DER(Distinguished Encoding Rules)は ASN.1(Abstract Syntax Notation One)記法で定義されたデータを エンコードするルールの一つ。1a(INTEGER) 0b(byte) 68 65 6c 6c 6f 20 77 6f 72 6c 64(“hello world”)) のようなtype-length-valueで表す。
PEM(Privacy-enhanced Electronic Mail)は 公開鍵のフォーマットの定義X.509で 定められている拡張子で、Base64でエンコードされたDER。
PKCS(Public-Key Cryptography Standards)は公開鍵暗号における標準仕様を定めたもの。PKCS#1(RFC2313)にはRSA暗号の方式やASN.1表現などが含まれている。
RFC2313に書かれているASN.1を見ながらPEMの内容を確認する。
RSAPrivateKey
n,e,dに加えて生成に使った素数まで含んでいる。
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 }
Version ::= INTEGER
pemを生成し、Base64デコードしてDERにする。
$ openssl genrsa 1024 > secret.pem
$ cat secret.pem
-----BEGIN RSA PRIVATE KEY-----
(内容)
-----END RSA PRIVATE KEY-----
$ echo (内容) | base64 -D | xxd
00000000: 3082 025c 0201 0002 8181 .... .... ....
00000080: .... .... .... .... .... ..02 0301 0001
00000090: 0281 80..
00000110: .... ..02 41..
00000150: .... .... .... 0241 ....
00000190: .... .... .... .... ..02 40..
000001d0: .... .... .... .... .... ..02 41.. ....
00000210: .... .... .... .... .... .... .... 0240
整理するとこんな感じ。
[0000] 30 82 02 5c
(30->Type=SEQUENCE 82->先頭bitが1なのでlengthに使うバイト数(2bytes) 025c->Length=605bytes)
[0000] 02 01 00
(version: 02->Type=INTEGER, 01->先頭が0なのでそのままLength=3bytes, Value=0)
[0000] 02 81 81 ..
(modulus: Type=INTEGER, Length=129bytes)
[0080] 02 03 01 00 01
(publicExponent: Type=INTEGER, Length=3bytes, Value=65537)
[0090] 02 81 80 ..
(privateExponent: Type=INTEGER, Length=128bytes)
[0110] 02 41 ..
(prime1: Type=INTEGER, Length=65bytes)
[0150] 02 41 ..
(prime2: Type=INTEGER, Length=65bytes)
[0190] 02 40 ..
(exponent1: Type=INTEGER, Length=64bytes)
[01d0] 02 41 ..
(exponent2: Type=INTEGER, Length=65bytes)
[0210] 02 40 ..
(coefficient: Type=INTEGER, Length=64bytes)
RSAPublicKey
nとeだけ。
RSAPublicKey ::= SEQUENCE {
modulus INTEGER, -- n
publicExponent INTEGER -- e }
秘密鍵から公開鍵を生成して同様にDERにする。
$ openssl rsa -pubout < secret.pem > public.pem
$ cat public.pem
-----BEGIN PUBLIC KEY-----
(内容)
-----END PUBLIC KEY-----
$ echo (内容) | base64 -D | xxd
00000000: 3081 9f30 0d06 092a 8648 86f7 0d01 0101
00000010: 0500 0381 8d00 3081 8902 8181 .... ....
00000090: .... .... .... .... .... .... ..02 0301
000000a0: 0001
[0000] 30 81 9f
(Type=SEQUENCE, Length=159bytes)
[0000] 30 0d
(Type=SEQUENCE, Length=13bytes)
[0000] 06 09 2a 86 48 86 f7 0d 01 01 01
(Type=OBJECT IDENTIFIER, Length=9bytes,
value=http://www.oid-info.com/get/1.2.840.113549.1.1.1
最上位ビットは区切りのサイン
2a ((0)010 1010) => 40 * 1 + 2 => 1(iso) 2(member-body)
86 48 ((1)000 0110 (0)100 1000) => 840(us)
86 f7 0d ((1)000 0110 (1)111 0111 (0)000 1101) => 113549(rsadsi)
01 ((0)000 0001) => 1(pkcs)
01 ((0)000 0001) => 1(pcks-1)
01 ((0)000 0001) => 1(rsaEncryption)
[0010] 05 00
(アルゴリズムパラメータ: Type=Null 0byte)
[0010] 03 81 8d 00
(Type=BIT STRING, 141bytes, 最終byteの切り捨て0bit)
[0010] 30 81 89
(Type=SEQUENCE, 137bytes)
[0010] 02 81 81 ..
(modulus: Type=INTEGER, Length=129bytes)
[0090] 02 03 01 00 01
(publicExponent: Type=INTEGER, Length=3bytes, Value=65537)
digestInfo
署名に使う。
digestInfo ::= SEQUENCE {
digestAlgorithm DigestAlgorithmIdentifier,
digest Digest
}
Digest ::= OCTET STRING
digestAlgorithmでデータをハッシュ化したものをdigestInfoに詰め、秘密鍵で暗号化したものを署名とし、 公開鍵で複合してdigestと実際のハッシュ値が一致することを確認する。 RSASSA-PKCS1-v1_5では暗号化の前に 00 01 ff ff ff .. 00 のパディングを加える。
参考
A Method for Obtaining Digital Signatures and Public-Key Cryptosystems
RSA 秘密鍵/公開鍵ファイルのフォーマット - bearmini’s blog
openssl - How is OID 2a 86 48 86 f7 0d parsed as 1.2.840.113549? - Cryptography Stack Exchange