おれさまラボの実験ノート

実際に手を動かして理解を深めるブログ。

秘密鍵のあの文字列ってなんなのって話

はじめに

証明書って本当に難しくないですか。

今回はまず、秘密鍵の中身を理解します。

理解できるかは、今のところ不明です。

一般に、SSL/TLS 通信を行うサーバを作るには以下のステップを踏みますので、順に見ていきましょう。

  1. 鍵を生成する
  2. CSR(Certificate Signing Request)を生成して CA に送る
  3. CA から提供される証明書を自分の Web サーバにインストールする

鍵の生成

まずはキーペア(秘密鍵と公開鍵のペア)を作るところからです。

アルゴリズムの決定

アルゴリズムですが、2020 年時点では RSAデファクトスタンダードです。鍵長は 2048 bit であれば安全と言われています。

アルゴリズム 鍵長(推奨値) 制約
DSA 2048 bit Internet Exproler では 1024 bit までしか対応できないという制約あり。
RSA 2048 bit デファクトスタンダード
ECDSA 256 bit 未対応の CA あり。

秘密鍵の作成

では、実際に秘密鍵を生成してみます。なお、この検証では基本的に Linux 環境で openssl コマンドを使っていきます。

$ openssl genrsa -out p.key 2048
Generating RSA private key, 2048 bit long modulus
........+++
..............+++
e is 65537 (0x010001)

なお、今回は検証なので秘密鍵パスフレーズを設定しませんでしたが、実運用においてはパスフレーズの設定が強く推奨されます。

パスフレーズを設定する場合は AES をアルゴリズムとして使用することが推奨されます。

  • AES-128
  • AES-192
  • AES-256

DES や 3DES、SEED は古いアルゴリズムであり、推奨されません。

秘密鍵パスフレーズを設定する例を以下に示します。

$ openssl genrsa -aes128 -out p-aes128.key 2048
Generating RSA private key, 2048 bit long modulus
.+++
................+++
e is 65537 (0x010001)
Enter pass phrase for p-aes128.key:
Verifying - Enter pass phrase for p-aes128.key:

さて、今回はあまり大きくすると中身を見るのが大変なので 16 bit のパスフレーズなしの秘密鍵を作成してみましょう。

$ openssl genrsa -out p-16bit.key 16
Generating RSA private key, 16 bit long modulus
.+++++++++++++++++++++++++++
.+++++++++++++++++++++++++++
e is 65537 (0x010001)

秘密鍵の中身

生成されたファイルの中身は PEM と呼ばれるテキスト形式で格納されたデータです。-----BEGIN RSA PRIVATE KEY----------END RSA PRIVATE KEY----- で囲まれた部分が PEM 形式に変換された秘密鍵です。

$ cat p-16bit.key
-----BEGIN RSA PRIVATE KEY-----
MCQCAQACAwDBJwIDAQABAgI0oQICAPsCAgDFAgIA3wICAJECAU8=
-----END RSA PRIVATE KEY-----

なお、PEM 形式の場合、パスフレーズを設定していると暗号化されていることが明記されます。

$ openssl genrsa -aes128 -out p-16bit-aes128.key 16
Generating RSA private key, 16 bit long modulus
.+++++++++++++++++++++++++++
.+++++++++++++++++++++++++++
e is 65537 (0x010001)
Enter pass phrase for p-16bit-aes128.key:
Verifying - Enter pass phrase for p-16bit-aes128.key:

$ cat p-16bit-aes128.key
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,3C06EDDC43EB4D460B83C53DB4A0A27E

+s0teteONFo3S9woLxD0Qzy7fr2jf4mPTMu0eqjDvkp/8V3U/QkyumjrbZPoL2xu
-----END RSA PRIVATE KEY-----

PEM の正体

PEM 形式と言っていますがこれってなんでしょうか。

調べてみると、PEM は、「ASN.1 形式で記載されたデータを base64 エンコードを利用して DER を ASCII 形式でエンコードしたもの」だそうです。

よくわからないですね。後方から読み解いていきましょう。

ASCII (アスキー) 形式とはざっくりテキストファイルだと思ってもらって構いません。Wikipedia から引用すると「ASCIIは、7桁の2進数で表すことのできる整数の数値のそれぞれに、大小のラテン文字や数字、英文でよく使われる約物などを割り当てた文字コード」のことです。

-----BEGIN RSA PRIVATE KEY----------END RSA PRIVATE KEY----- で囲まれた部分は我々人間でも読める英数字で構成されていますね。これが ASCII 形式ということです。

この ASCII 形式で表記された秘密鍵base64 デコードすると DER 形式のデータを取り出すことができます。以下、見ていただけるとわかりますが、文字化けして何のことかさっぱりです。DER はバイナリ形式なので人間が読み取れるような情報は出力されません。

$ cat p-16bit.key | awk /^[^-]/{print} | base64 -d
<バイナリ>

バイナリを人間が識別可能な表記にするには16進数への変換が一般的なので xxd コマンドに渡してビット列を確認してみます。(この行為に特に意味はありません)

$ cat p-16bit.key | awk /^[^-]/{print} | base64 -d | xxd
00000000: 3026 0201 0002 0300 c00b 0203 0100 0102  0&..............
00000010: 0300 acf1 0202 00e9 0202 00d3 0202 00c1  ................
00000020: 0202 00ad 0202 00b4                      ........

さて、DER がバイナリなので普通の人間には読めないとわかったところで、PEM をもとの ASN.1 形式に戻す方法を見ていきましょう。

openssl コマンドを使って ASN.1 形式のデータを表示することができます。

$ openssl asn1parse -inform PEM < p-16bit.key
    0:d=0  hl=2 l=  38 cons: SEQUENCE
    2:d=1  hl=2 l=   1 prim: INTEGER           :00
    5:d=1  hl=2 l=   3 prim: INTEGER           :C00B
   10:d=1  hl=2 l=   3 prim: INTEGER           :010001
   15:d=1  hl=2 l=   3 prim: INTEGER           :ACF1
   20:d=1  hl=2 l=   2 prim: INTEGER           :E9
   24:d=1  hl=2 l=   2 prim: INTEGER           :D3
   28:d=1  hl=2 l=   2 prim: INTEGER           :C1
   32:d=1  hl=2 l=   2 prim: INTEGER           :AD
   36:d=1  hl=2 l=   2 prim: INTEGER           :B4

DER エンコードが必要な理由

少し脱線しますが、DER エンコードが必要な理由を確認しておきましょう。

DER とは、ASN.1 で記載されたデータを転送する際にはビット符号化(バイナリ化)が必要ですが、このときに与えられた値が複数に解釈できてしまうと問題ですね。DER は与えられた値を確実にひととおり(一意)にエンコードするための符号化規則のことです。

暗号、とくにディジタル署名にとって値が常にひととおりにエンコードされるということは、悪意のある他者による悪用を防ぐ目的で重要です。秘密鍵においても ASN.1 の記述を DER 規則でバイナリに変換することで一意性を保ってデータ転送することが可能となります。

なお、今回は DER でしたが、X.509 証明書で使用されるファイル形式は多岐に渡りますのでお忘れなく。それぞれの仕組みは今はお腹いっぱいなので触れません。

ファイル形式 説明
.CER CER で符号化された証明書、ときによっては証明書群の列
.DER DER で符号化された証明書
.PEM Base64 で符号化された証明書で、「-----BEGIN CERTIFICATE-----」と「-----END CERTIFICATE-----」で囲まれている
.P7B .p7c参照
.P7C 被署名データのない PKCS#7 のSignedData構造で、証明書 (群) やCRL (群) がある
.PFX .p12参照
.P12 (公開鍵や、パスワードで保護された) 私有鍵を含む PKCS#12

参考:X.509 - Wikipedia

PEM が用いられる理由

PEM 形式の X.509 証明書が用いられる理由のほとんどが可読性の高さと考えられます。ASCII 形式なので人と共有しやすいです。

秘密鍵の構造

先に記載したとおり、ASN.1 のデータは以下のようなものでした。

$ openssl asn1parse -inform PEM < p-16bit.key
    0:d=0  hl=2 l=  38 cons: SEQUENCE
    2:d=1  hl=2 l=   1 prim: INTEGER           :00
    5:d=1  hl=2 l=   3 prim: INTEGER           :C00B
   10:d=1  hl=2 l=   3 prim: INTEGER           :010001
   15:d=1  hl=2 l=   3 prim: INTEGER           :ACF1
   20:d=1  hl=2 l=   2 prim: INTEGER           :E9
   24:d=1  hl=2 l=   2 prim: INTEGER           :D3
   28:d=1  hl=2 l=   2 prim: INTEGER           :C1
   32:d=1  hl=2 l=   2 prim: INTEGER           :AD
   36:d=1  hl=2 l=   2 prim: INTEGER           :B4

これを理解するのは、RFC 3447 の秘密鍵フォーマットに関する記述を読む必要があります。

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
}

引用:RFC 3447 - Public-Key Cryptography Standards (PKCS) #1: RSA Cryptography Specifications Version 2.1

上から順にそれぞれの INTEGER (任意の長さの整数) が何を表すのか定義されていますので、先の ASN.1 のデータ突き合わせてみましょう。

SEQUENCE
INTEGER           :00       # Version
INTEGER           :C00B     # modulus = 係数[n]
INTEGER           :010001   # publicExponent = 暗号化指数[e]
INTEGER           :ACF1     # privateExponent = 復号化指数[d]
INTEGER           :E9       # prime1 = 素数[p]
INTEGER           :D3       # prime2 = 素数[q]
INTEGER           :C1       # exponent1 = d mod (p - 1)
INTEGER           :AD       # exponent2 = d mod (q - 1)
INTEGER           :B4       # coefficient = (inverse of q) mod p

これを読み解くと以下のとおりです。

  • RSA のバージョンは v1 を使用
  • 係数[n] は 16 進数の C00B つまり 10 進数の 49,163 である
  • 暗号化指数[e] は 16 進数の 010001 つまり 10 進数の 65,537 である
  • 復号化指数[d] は 16 進数の ACF1 つまり 10 進数の 44,273 である
  • 素数[p] は 16 進数の E9 つまり 10 進数の 233 である
  • 素数[q] は 16 進数の D3 つまり 10 進数の 211 である
  • d mod (p - 1) に既出の数値を代入すると 44,273 mod (233 - 1)、よって 16 進数の C1 つまり 10 進数の 193 である
  • d mod (q - 1) に既出の数値を代入すると 44,273 mod (211 - 1)、よって 16 進数の AD つまり 10 進数の 173 である
  • (inverse of q) mod p (は文系のワタシには早すぎたが)、16 進数の B4 つまり 10 進数の 180 である

注1)mod:2つの正の整数である、被除数a および 除数nが与えられる場合、a mod n は a の n による剰余を表し、ユークリッド除法における a を n で除算した余りとなります。(引用:剰余演算 - Wikipedia

これが、openssl コマンドを使って秘密鍵をテキスト出力したときに表示される情報とほぼ一致(Versionだけ含まれない)します。

$ openssl rsa -text -in p-16bit.key
Private-Key: (16 bit)
modulus: 49163 (0xc00b)
publicExponent: 65537 (0x10001)
privateExponent: 44273 (0xacf1)
prime1: 233 (0xe9)
prime2: 211 (0xd3)
exponent1: 193 (0xc1)
exponent2: 173 (0xad)
coefficient: 180 (0xb4)
writing RSA key
-----BEGIN RSA PRIVATE KEY-----
MCYCAQACAwDACwIDAQABAgMArPECAgDpAgIA0wICAMECAgCtAgIAtA==
-----END RSA PRIVATE KEY-----

計算について詳細が知りたい方は暗号技術のすべてに事細かに載っているので読んでみてください。私には理解できませんでしたがみなさんならきっと分かることでしょう。

当たり前ですが補足しておくと、パスフレーズつきで作った秘密鍵p-16bit-aes128.key)を見てみると p-16bit.key とはそれぞれの整数値が異なっていることがわかります。

$ openssl rsa -text -in p-16bit-aes128.key
Enter pass phrase for p-16bit-aes128.key:
Private-Key: (16 bit)
modulus: 45571 (0xb203)
publicExponent: 65537 (0x10001)
privateExponent: 14057 (0x36e9)
prime1: 229 (0xe5)
prime2: 199 (0xc7)
exponent1: 149 (0x95)
exponent2: 197 (0xc5)
coefficient: 145 (0x91)
writing RSA key
-----BEGIN RSA PRIVATE KEY-----
MCUCAQACAwCyAwIDAQABAgI26QICAOUCAgDHAgIAlQICAMUCAgCR
-----END RSA PRIVATE KEY-----

今日はここまで。