X.509のはなし
かつてのインターネットは「そこに繋がっているという時点で信用に足る人しか居ないはず」という性善説というか会員制クラブみたいな雰囲気があり、暗号化も認証もないSMTPやRSHみたいなプロトコルが普通に使われていました。そんなエデンの園は遥か追憶の過去になり、今のインターネットは暗号化と認証を通さなければ何も信用できない殺伐世界になっています。
IETFはIPsec (AH/ESP)をインターネットのセキュリティ標準に据えたい思惑があったようですが、いろいろあって結局TLSがデファクトスタンダードになりました。TLSはNetscape社とRSA社が共同開発したSSLから発展したもので、その枠組においてX.509デジタル証明書は重要な機能を担っています。
無線通信の世界でも、Wi-Fiエンタープライズ認証ではEAPの上にTLSを載せたTLS系認証プロトコル (EAP-TLS, EAP-TTLS, PEAP, FAST)が使われており、X.509証明書とは浅からぬ縁があります。今回はそのX.509デジタル証明書の話です。
X.500とX.509について
X.509はX.500規格の一部であり、X.500はOSI規格の一部です。OSI (Open System Interconnection)は規格制定開始1977年に遡る「異メーカー・異機種のコンピュータを相互接続する世界規模のネットワーク標準」として、(何かと仲の悪かった)工業規格団体ISOと通信規格団体CCITT(現ITU-T)の国際共同規格ということで普及は必然とみなされていました。現実には後から出てきたインターネット(IPネットワーク)に追い越されて立ち枯れてしまったのですが、まるで「バベルの塔の伝説」のように、その断片はあちこちに生き残っています(※註)。
(※註)たぶんOSI最大の遺産は「7階層モデル」でしょう。今のインターネットは複雑になって実装や運用は必ずしも7階層には当てはまらないのですが、概念としては使われ続けています。
OSIにおいてIPに相当する層はIS-IS (Intermediate System to Intermediate System)・TCPに相当する層はTP4 (Transport Protocol Class 4)と呼ばれ、アプリケーション層はROSE (Remote Operations Service Element)と呼ばれました。ROSEの中にはX.400電子メール (MHS: Message Handling System)やX.500ディレクトリサービス (DAP:Directory Access Protocol)が含まれ、特にDAPは今のWWWに相当する「全世界のデータを相互アクセス可能にする統一プロトコル」になるはずでした。DAPの仕様はのちに簡略化されてTCPに移植されLDAP (Lightweight DAP)となります。
もともとX.500 DAPのセキュリティメカニズムとして制定されたX.509がインターネットのセキュリティに使われるようになったのは、1995年3月にリリースされたWEBブラウザNetscape Navigator v1.1に実装されたSSLv2からで、1996年にはインターネット向けの仕様拡張を追加したX.509 V3仕様がリリースされて定着しました。RFCとしてはRFC2459 (1991/1)が最初にリリースされ、RFC3280 (2002/4)を経てRFC5280 (2008/5)が最新となっています。
X.500 DAPについて
X.509に踏み込む前に、その母体となったX.500 DAPについて少し触れておきます。
DAPは分散データベースシステムです。クライアント (DUA:Directory User Agent)がサーバー (DSA:Directory System Agent)に何かを問い合わせて、それに紐づいたデータが返されるという仕組みです。HTTPで言えば問い合わせる「何か」がURLで、返されるデータがContentということになります。

図1 HTTPにおけるGet動作の例
DAPにおいて問い合わせ対象のフォーマットはDN (Distinguished Name)という仕組みが定義されていました。DNは「XX=YY」の集合で、フルセットのDNは{CN=YS, OU=Dev, O=Silex, L=SNA, S=CA, C=US}のようになります。X.500本来の用途なら、このDNに「本名」「生年月日」「性別」「社員番号」のようなデータが紐づいて読み書きできることになっていました。

図2 DAPにおけるSearch動作の例
OSI規格においてデータフォーマット(プレゼンテーション層)はASN.1 (Abstract Syntax Notation One)という枠組みが使われることになっており、X.500の問い合わせ(Search Request)も返答(Search Result)もASN.1フォーマットです。なので問い合わせに応じて返される「データ」はASN.1で表現可能なすべてのデータ型を取り得ます。ASN.1のデータ型にはBOOLEAN・INTEGER・OCTET STRINGなどの単純型とSEQUENCE(構造体)・SEQUENCE OF(配列)・CHOICE(多様型)などの構造型がありますが、HTTPのContent-Typeのような「静止画」「動画」「音声」などのマルチメディア型が無いのは規格制定された時代を反映しています(1970年代のコンピュータは原則として文字と数字しか扱いませんでした)。
X.509について
X.509はもともとX.500 DAPの一部として、データベースに階層的な証明を与える仕組みとして設計されました。Public Key Infrastructure略してPKIと呼ばれることもあります。そのキモはSubject(対象)とIssuer(発行者)という2つのDNで、Subjectは「それが証明する対象」Issuerは「それを証明する元」で、IssuerはCA (Certificate Authority)とも呼ばれます。CAを証明する上位のCAが連なる場合もあり、これを"Certificate Chain"あるいは"Chain of trust"と呼びます。最上位のCAは「無条件に信頼すべきもの」として扱われ、これをRoot CAと呼びます。

図3 証明書の連鎖
X.509における「証明」は公開鍵暗号を使った署名操作(Public Key Signing)で、証明書は必ず公開鍵と秘密鍵のペアが伴います。公開鍵暗号については以前にも何度か紹介していますが、暗号化に用いる鍵と復号化に用いる鍵が異なり、片方からもう片方を算出することが極めて困難な数学的操作です(「落とし戸つき一方向関数」と呼びます)。X.509ではまず証明書本体部分をハッシュ関数にかけ、算出されたハッシュ値を上位(CA)証明書の秘密鍵で暗号化したものを「署名」として証明書末尾にくっつけます。
証明書の正当性を検証する場合、証明書に添付された署名を(CA)証明書の公開鍵を使って復号した結果と、証明書本体のハッシュ値が一致することをもって「正当な証明書である(偽造・変造されていない)」ことが確認できます。

図4 証明書の証明(実際のハッシュ値はもっと多くのフィールドを含みますが、図では簡略化しています)
この検証をすり抜けるためには(A)無意味データの付加などでハッシュ値が既存証明書と一致する偽造データを作る・(B)逆算によってCA秘密鍵を求めてハッシュが一致する署名を作り直す・(C)本物と同じDNを持つが鍵情報の異なる偽のCA証明書を作り、何らかの方法でそれを「信用に足る」ものとして登録させる、などが必要になります。(A)(B)は「理論上不可能ではない」もののその必要計算量が膨大で(※註)、事実上偽造はできないと考えられています。
(C)はシステムのCA証明書の追加登録を管理者権限に限定することで、一般ユーザーに偽証明書を登録させないような運用が取られます。
(※註)数年前に騒がれた量子演算器は、量子状態重ね合わせ演算によって既存コンピュータとは桁違いの組み合わせ探索が可能になり、従来は百万年単位が必要だったはずの公開鍵暗号の逆算が実用時間内に達成できてしまう可能性が語られていました。現実には危惧されたほどには量子演算器の能力が伸びていないのでまだ安全ですが、既存の公開鍵暗号が低コストで解読可能になるのは時間の問題と考えられています。ただそれが3年後か10年後かを予言するのは難しいです。
現実問題としては、証明書発行機関のミス(ハッキングや人為的持ち出し)でCA秘密鍵が漏洩して「信用に足るはずのCAで検証可能な偽証明書」が作れてしまうことがあり、その場合は「もうこの証明書は信用できない」とする失効処理が行われます。これをCertificate Revocationと呼び、「現在までにわかっている失効証明書の一覧」をCRL (Certificate Revocation List)と呼びます。
X.509とSubject CNについて
X.509本来の使われ方ならば「証明すべき対象」はSubject DNがそのままX.500の名前として使われるのですが、インターネット(SSL/TLS)ではDNを使っているわけではないので不整合が生じます(※註)。これを解決するためにX.509 V3仕様では「subjectAltName」という拡張アトリビュートが追加され、X.500 DN以外の「別名」を含められるようになりました。仕様上はrfc822Name, dNSName, x400Address, directoryName, ediPartyName, uniformResourceIdentifier, iPAddress, registeredIDが定義されていますが、インターネット上ではほぼdnsNameが使われます。
(※註) RFC2247(1998/1)でDC(Domain Component)というアトリビュートを拡張して、DNS名をwww.google.com→DC=www, DC=google, DC=comのように分解してDN名に対応することが提案されましたが、これは全く根付きませんでした。
RFC2459では「subjectAltNameにDNS名を含め、Subject DN=空っぽ」という証明書フォーマットも使用可能とされていますが、インターネットでは慣例的にSubject DNのCN (Common Name)にDNS名を含めます。CN以外のC(国)やO(企業/組織)を残すか・何を入れるかは解釈が分かれますが、最近では「CNのみ」の証明書が多いです。例えばAmazonのサイト証明書にはCN=www.amazon.comしか入っていません。またGoogleの証明書にはCN=*.google.comのワイルドカードになっているものがありますが、これはRFC2818 (HTTP over TLS, 2000/5)で定義された拡張仕様(※註) です。
(※註) RFC2818では下記のように定義されています。
Matching is performed using the matching rules specified by [RFC2459]. If more than one identity of a given type is present in the certificate (e.g., more than one dNSName name, a match in any one of the set is considered acceptable.) Names may contain the wildcard character * which is considered to match any single domain name component or component fragment. E.g., *.a.com matches foo.a.com but not bar.foo.a.com. f*.com matches foo.com but not bar.com.
OpenSSLを使った自己署名証明書の作成
まず、最も単純な自己署名証明書 (Self-Signed Certificate)を作ってみます。自己署名というのはSubject=Issuerで、その証明書の正当性を証明する上位CA証明書を持たない証明書のことです。エンジニアの俗語では「オレオレ証明書」「野良証明書」という言い方をすることもあります。以下の解説ではUbuntu 20.04上のOpenSSL 1.1.1fを使っていますが、Windowsも含む大抵のプラットホーム上で同等の環境は実現できるはずです。

図5 自己署名証明書
まずは公開鍵/秘密鍵のペアを作ります。2048bit RSAで128bit AESの暗号化を掛けたフォーマットにします。パスワードは忘れないようにしてください。
$ openssl genrsa -out priv_key.pem -aes128 2048
Generating RSA private key, 2048 bit long modulus (2 primes)
........
e is 65537 (0x010001)
Enter pass phrase for priv_key.pem:
Verifying - Enter pass phrase for priv_key.pem:
次に、この秘密鍵に基づいた自己署名証明書を作ります。-days 3650で有効期限を10年に設定しています。
$ openssl req -x509 -new -days 3650 -key priv_key.pem -out self_cert.pem
opensslは秘密鍵ファイルのパスワードを訊ねたあと、DNに含める一連の質問をしてきます。何も入力せず[Enter]で応えるとC = AU, ST = Some-State, O = Internet Widgits Pty LtdというDNが生成されてしまいます。
Enter pass phrase for priv_key.pem:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:CA
Locality Name (eg, city) []:SNA
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Silex
Organizational Unit Name (eg, section) []:Dev
Common Name (e.g. server FQDN or YOUR name) []:localhost
Email Address []:
出来たファイルの中身は下記のコマンドで見ることができます。
$ openssl x509 -in self_cert.pem -noout -text
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
2e:e6:f5:87:56:4f:06:7c:71:d9:c7:aa:4a:45:13:9f:8e:a2:03:1c
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = US, ST = CA, L = SNA, O = Silex, OU = Dev, CN = localhost
Validity
Not Before: Jun 23 17:15:55 2025 GMT
Not After : Jun 21 17:15:55 2035 GMT
Subject: C = US, ST = CA, L = SNA, O = Silex, OU = Dev, CN = localhost
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
00:cc:41:7c:49:de:fc:39:21:9e:99:a2:71:9d:3e:
:
84:d9
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Key Identifier:
D5:DC:7C:BB:34:05:23:11:33:BB:F9:02:92:07:0F:7E:E1:F5:7E:ED
X509v3 Authority Key Identifier:
keyid:D5:DC:7C:BB:34:05:23:11:33:BB:F9:02:92:07:0F:7E:E1:F5:7E:ED
X509v3 Basic Constraints: critical
CA:TRUE
Signature Algorithm: sha256WithRSAEncryption
5d:55:0b:53:0d:c0:80:2c:e0:7b:ad:bc:2a:96:98:2b:06:6d:
:
56:e1:b5:bf
Subject Public Key InfoのModulusフィールドは上で作ったpriv_key.pemのmodulusフィールド、ExponentフィールドはpublicExponentフィールドと全く同じで、これがRSAアルゴリズムの公開鍵情報になります。Issuer / SubjectのDNが同じになっている=自己署名証明書であることと、Not Before / Not Afterの有効期限日付に注目してください。X.509V3拡張については"Subject Key Identifier" "X509v3 Authority Key Identifier" "Basic Constraints"が入っていますが"subjectAltName"は入っていません。これがOpenSSL x509コマンドで作る自己署名証明書のデフォルトです。
もう少し「X509V3風」にするには、次のようなコマンドを使います。-subjでSubjectフィールドのDNを指定し、ここではCN=localhostだけを指定しています。--addextでX509V3拡張フィールドを追加しており、subjectAltName=DNS:localhostを追加しています。
$ openssl req -x509 -new -days 3650 -key priv_key.pem -out self_cert.pem -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost"
$ openssl x509 -in selfcertv3.pem -noout -text
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
60:f6:3e:6d:d8:55:e8:7a:a9:3f:8b:31:94:74:46:23:ea:ff:2c:fc
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN = localhost
Validity
Not Before: Jun 23 17:37:12 2025 GMT
Not After : Sep 9 17:37:12 2033 GMT
Subject: CN = localhost
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
00:cc:41:7c:49:de:fc:39:21:9e:99:a2:71:9d:3e:
:
84:d9
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Key Identifier:
D5:DC:7C:BB:34:05:23:11:33:BB:F9:02:92:07:0F:7E:E1:F5:7E:ED
X509v3 Authority Key Identifier:
keyid:D5:DC:7C:BB:34:05:23:11:33:BB:F9:02:92:07:0F:7E:E1:F5:7E:ED
X509v3 Basic Constraints: critical
CA:TRUE
X509v3 Subject Alternative Name:
DNS:localhost
Signature Algorithm: sha256WithRSAEncryption
47:4c:ae:a2:48:13:ac:b4:25:5c:9e:7c:23:07:60:e4:88:77:
:
65:bb:42:ef
自己署名証明書は中間介在者攻撃(偽造証明書との差し替え)への耐性が皆無なので、普通はテスト以上の用途には使いません。EAP認証ではPEAPやTTLSのような片道認証(AP→STAには証明書を送るが、STA→APは暗号トンネル確立後のパスワード認証を行う)の場合はwpa_supplicant.confに"ca_cert="を指定しないことで「証明書のCA検証を行わない」が指定できますが、EAP-TLSは双方向認証 (Mutual Authentication)でSTA→APに送った証明書をRADIUSサーバで検証するので、RADIUSサーバを「自己署名証明書も受け付ける」ように設定しなければ使えず、そして少なくともFreeRADIUSにはそんな設定はありません。

図6 双方向認証の原理。TLSネゴシエーションは実際より簡易化しています。

図7 片道認証の原理。実際のInner Authenticationは生パスワードが
送られるわけではなく、チャレンジ認証のかたちを取ります。
OpenSSLを使ったCAつき証明書の作成
CAつき証明書はより本格的なPKI運用に使われます。EAP-TLSであればCA証明書・AP証明書・STA証明書の3つを作って、APにはCA証明書とAP証明書、STAにはCA証明書とSTA証明書をインストールして運用します。CA証明書に外部の証明書発行機関(RSA,VeriSign,Microsoft,Google,Amazonなど)の署名を持たせるか・自己署名で完結させるかは運用条件によります。

図8 CAつき証明書
まずCA証明書用の公開鍵/秘密鍵のペアを作り、次にCA証明書を作ります。作り方は自己署名証明書と同じです。ここではDNをいちいち問答形式で入力するかわりに-subjオプションで一括指定しています。
$ openssl genrsa -out ca_key.pem -aes128 2048
$ openssl req -x509 -new -days 3650 -key ca_key.pem -out ca_cert.pem -subj "/C=US/ST=CA/L=SNA/O=Silex/CN=test_ca"
次に署名要求Certificate Signing Request (CSR)というものを作ります。これは「証明書のサナギ」みたいなもので、証明書に必要な情報が一通り入っているものの、まだ署名の付いていない状態のファイルで、PKCS#10というファイルフォーマットが使われます。なお秘密鍵/公開鍵のペアは上で自己署名証明書のときに作ったものを流用しています。
$ openssl req -new -out test_cert.csr -key priv_key.pem -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost"
こうして出来たCSRファイルに、CA証明書とCA秘密鍵を使って署名を付けます。"-CAcreateserial"は最初の1回だけ必要になり、test_ca.srlというファイルが作られたあとは自動的に更新されてゆきます。
$ openssl x509 -req -in test_cert.csr -out test_cert.pem -days 3650 -CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial
生成された証明書の中身を見ると次のようになっています。自己署名証明書と異なり、IssuerとSubjectのDNが異なっていることに注目してください。またversion=0x00となっていますが、OpenSSLのデフォルトではCSRから生成される証明書はX.509V1フォーマットになります。これをV3仕様にする方法は後述します。
$ openssl x509 -in test_cert.pem -noout -text
Certificate:
Data:
Version: 1 (0x0)
Serial Number:
5f:5d:4d:2a:b5:b7:be:c0:6e:41:05:3a:94:4f:06:e9:30:8e:f2:60
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = US, ST = CA, L = SNA, O = Silex, CN = test_ca
Validity
Not Before: Jun 25 22:15:21 2025 GMT
Not After : Jun 23 22:15:21 2035 GMT
Subject: CN = localhost
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
00:cc:41:7c:49:de:fc:39:21:9e:99:a2:71:9d:3e:
:
84:d9
Exponent: 65537 (0x10001)
Signature Algorithm: sha256WithRSAEncryption
95:3f:21:f5:c0:ee:00:20:a1:d8:f1:f2:97:11:02:50:ef:68:
:
20:ff:27:7b
生成されたtest_certがca_certで証明可能かどうか、下記のコマンドで調べることができます。
$ openssl verify -CAfile ca_cert.pem test_cert.pem
test_cert.pem: OK
EAPの証明書としてはV1でも大抵の場合は使えますが、V3拡張を明示することでX.509V3フォーマットを生成することも可能です。
まずV3拡張の内容を指示するテキストファイルを作ります。ここではv3ext.txtというファイル名にしています。
authorityKeyIdentifier = keyid
subjectKeyIdentifier = hash
basicConstraints = CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment
extendedKeyUsage = serverAuth, clientAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
個々のフィールドはざっくり次のような意味です。
authorityKeyIdentifier = keyid
AKID拡張フィールドにCA公開鍵のハッシュ値を含めることを示します。
subjectKeyIdentifier = hash
SKID拡張フィールドにピア公開鍵のハッシュ値を含めることを示します。
basicConstraints = CA:FALSE
これがCA証明書ではない(ピア証明書である)ことを示しています。
keyUsage = digitalSignature, nonRepudiation, keyEncipherment
この証明書の用途がデジタル署名・取り消し不可・鍵の暗号化にあることを示しています。
extendedKeyUsage = serverAuth, clientAuth
この証明書の拡張用途がTLSのサーバおよびクライアント認証にあることを示しています。
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
証明書に含めるsubjectAltNameのフィールドにDNS:localhostを含めることを指示しています。
opensslのコマンドラインに-extfile <filename>でV3拡張を指示してCSRを実行します(※註)。
(※註)自己証明書の作成時に-extfileを指定することはできません。openssl reqには-configという別のオプションで設定ファイルを渡すことができますが、configファイルとextファイルのフォーマットは互換ではありません。めんどくさいですね。
$ openssl x509 -req -in test_cert.csr -out test_cert.pem -days 3650 -CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial -extfile v3ext.txt
こうして出来た証明書は確かにX.509V3フォーマットになっています。Versionフィールドが0x02になっていること、X509v3 extensionsフィールドが付いていることに注目してください。証明書のシリアルは上で作ったV1証明書と上位桁が同じで最下位が0x60→0x61に上がっていますが、これを管理しているのが-CAcreateserialで作られたファイルca_cert.srlです。
$ openssl x509 -in test_cert.pem -noout -text
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
5f:5d:4d:2a:b5:b7:be:c0:6e:41:05:3a:94:4f:06:e9:30:8e:f2:61
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = US, ST = CA, L = SNA, O = Silex, CN = test_ca
Validity
Not Before: Jun 25 22:16:28 2025 GMT
Not After : Jun 23 22:16:28 2035 GMT
Subject: CN = localhost
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
00:cc:41:7c:49:de:fc:39:21:9e:99:a2:71:9d:3e:
:
84:d9
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Authority Key Identifier:
keyid:C1:D4:96:40:D3:8B:19:9E:36:60:AD:8E:0D:52:CB:1C:EF:D0:41:E9
X509v3 Subject Key Identifier:
D5:DC:7C:BB:34:05:23:11:33:BB:F9:02:92:07:0F:7E:E1:F5:7E:ED
X509v3 Basic Constraints:
CA:FALSE
X509v3 Key Usage:
Digital Signature, Non Repudiation, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication, TLS Web Client Authentication
X509v3 Subject Alternative Name:
DNS:localhost
Signature Algorithm: sha256WithRSAEncryption
88:32:78:64:a0:bf:8a:8e:17:52:4e:6b:e1:6f:cb:58:d7:41:
:
ec:c9:d1:ef
APとSTAに別々の証明書を作るとか、STA毎に別々の証明書を作る場合は秘密鍵生成・CSR作成・署名の手順を繰り返します。めんどくさいですね(※註)。これがTLS双方向認証がウザがられる主な理由で、このウザさゆえに「STA証明書が必要ない、ユーザ名+パスワードで認証できる」LEAP・TTLS・PEAP・FASTのような方式が乱立することになってしまいました。
(※註)CSR作成時に-newkey rsa:2048 -keyout <filename>を指定してCSRと秘密鍵を同時に生成することも可能ですが、出来たファイルを管理しなければならないことに違いはありません。どのSTAにどの証明書が入り秘密鍵パスワードは何だったのかを何処かにまとめておかないと簡単にカオス化します。
DSA証明書の場合
DSA (Digital Signature Algorithm)は離散対数演算を使った公開鍵暗号で、まだRSAアルゴリズムの特許が有効だった1990年代に「特許に縛られない公開鍵暗号標準」としてIETFが普及させようとしていました。しかしRSAよりも演算量や鍵データが大きくなる不利があり、そうこうしている間に2000年9月にRSAアルゴリズムの特許が失効したこともあって結局あまり普及しませんでした。いまどきDSA証明書を使う実用的意味は少ないですが、これを知っておけばECDSAの理解が早まるかも知れません。
OpenSSLでDSA秘密鍵を作るのは"dsaparam"と"gendsa"の2段階の操作になります。
$ openssl dsaparam -out dsa_param.pem 2048
Generating DSA parameters, 2048 bit long prime
This could take some time
........
$ openssl gendsa -aes128 -out dsa_privkey.pem dsa_param.pem
Generating DSA key, 2048 bits
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
dsaparamでは特定の条件を満たす素数P,Qと整数Gを選択し、gendsaではこのP,Q,Gから一方向性のある公開鍵Xと秘密鍵Yを算出します。DSA法における公開鍵にはXだけでなくP,Q,Gも添付されるので、同等鍵長のRSAより鍵データが大きくなってしまいます。
生成したdsaparamの中身は
$ openssl dsaparam -in dsa_param.pem -noout -text
P:
00:e9:e0:71:5a:1b:05:b0:ef:b0:32:01:1f:e5:48:
:
2c:11
Q:
00:85:c2:92:87:61:47:5c:82:e6:d7:25:0a:e6:8f:
:
9b:76:59
G:
01:25:2f:6d:72:52:b4:96:19:11:50:67:0f:aa:96:
:
ac:53:8e:4e:94:ad:b6:c3:b4:77:e9:ed:a4:e1:ac
dsa_privkey.pemの中身は
$ openssl dsa -in dsa_privkey.pem -noout -text
read DSA key
Enter pass phrase for dsa_privkey.pem:
Private-Key: (2048 bit)
priv:
06:53:5e:19:ce:f3:1e:35:df:1d:b8:d8:36:35:9f:
:
e8:b0
pub:
00:85:39:5c:2c:8f:fb:04:c4:13:fb:c4:ce:1e:52:
:
c2:79
P:
00:e9:e0:71:5a:1b:05:b0:ef:b0:32:01:1f:e5:48:
:
2c:11
Q:
00:85:c2:92:87:61:47:5c:82:e6:d7:25:0a:e6:8f:
:
9b:76:59
G:
01:25:2f:6d:72:52:b4:96:19:11:50:67:0f:aa:96:
:
ac:53:8e:4e:94:ad:b6:c3:b4:77:e9:ed:a4:e1:ac
で見ることができます。dsaparamで生成したP,Q,G値がそっくりそのままdsapriv_keyに入っていることがわかります。
いちど秘密鍵ができてしまえば、そこから後はRSAと同じ操作で証明書を作ることができます。自己署名証明書の場合は
$ openssl req -x509 -new -days 3650 -key dsa_privkey.pem -out dsacert.pem -subj "/C=US/ST=CA/L=SNA/O=Silex/CN=test_ca"
CAつき証明書の場合はRSA同様に、まずDSAのCA秘密鍵とCA証明書を作り、
$ openssl dsaparam -out dsa_caparam.pem 2048
$ openssl gendsa -aes128 -out dsa_cakey.pem dsa_caparam.pem
$ openssl req -x509 -new -days 3650 -key dsa_cakey.pem -out dsa_cacert.pem -subj "/C=US/ST=CA/L=SNA/O=Silex/CN=test_ca"
次にDSA秘密鍵を使ってピア証明書のCSRを作り、CA証明書とCA秘密鍵を使って署名します。
$ openssl req -new -out dsa_test.csr -key dsa_privkey.pem -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost"
$ openssl x509 -req -in dsa_test.csr -out dsa_cert.pem -days 3650 -CA dsa_cacert.pem -CAkey dsa_cakey.pem -CAcreateserial -extfile v3ext.txt
ECDSA証明書の場合
ECDSAは楕円曲線 (Elliptic Curve)という数学空間上の演算で実装された公開鍵暗号で、RSAやDSAと同等の強度をより短い鍵長で扱うことができ(※註)、鍵長が2048bitを越えた頃からRSAを置き換えつつあります。
(※註)256bit ECDA は3072bitRSA に、 384bit ECDSAは7680bit RSAに匹敵するとされています。
OpenSSLでECDSA秘密鍵を作るのは"ecparam -genkey"で行えます。-nameで指定しているのは定義済みの楕円曲線パラメータで、openssl ecparam -list_curvesで一覧が出てきます(かなりの数があります)。この「定義済み曲線」がDSA法におけるdsaparamのP,Q,G値に相当するので、ECDSAの鍵生成は1段階で済みます。
$ openssl ecparam -genkey -name secp384r1 -out ecdsa_privkey.pem
生成した鍵の中身は例によって"-noout -text"で見ることができます。DSA鍵におけるP,Q,G値の代わりに"sec384r1"という定義済み曲線のIDが入っています。鍵長じたいが短いだけでなく、dsaparam値をいちいち明示しなくて済むようになったことがECDSAの利点です。
$ openssl ec -in ecdsa_privkey.pem -noout -text
read EC key
Private-Key: (384 bit)
priv:
98:1a:1c:ad:ee:6d:d0:1e:53:b2:6d:b1:d2:71:94:
:
91:bf:d9
pub:
04:2a:d9:c5:f2:24:a6:e1:13:c1:8a:80:c2:da:b3:
:
3b:b6:8e:5a:e5:c9:5d
ASN1 OID: secp384r1
NIST CURVE: P-384
OpenSSLには(どういう訳か)ECDSA秘密鍵生成時に暗号化するオプションがありません。秘密鍵を暗号化したい場合、"ec"コマンドに暗号化オプションを渡して暗号化します。
$ openssl ec -in ecdsa_privkey.pem -out ecdsa_privkey_s.pem -aes128
秘密鍵ができれば、それを元にした証明書の作成手順はRSAやDSAと同じです。
$ openssl req -x509 -new -days 3650 -key ecdsa_privkey.pem -out ecdsa_cert.pem -subj "/C=US/ST=CA/L=SNA/O=Silex/CN=test_ca"
参考までに、RSA, DSA, ECDSAで作った自己証明書のサイズをDERフォーマットで比較すると次のようになります。ECDSAの鍵の短さがかなり効いていることがわかりますね。
self_cert.der (2048bit RSA) 921byte
dsa_cert.der (2048bit DSA) 1219byte
ecdsa_cert.der (384bit ECDSA) 553byte
デジタル証明書がらみのフォーマット
デジタル証明書が「難しい」と思われる理由の1つには、同じことをするのに幾つも違う方法が併存している現状があります。証明書をファイルとして扱うフォーマットが多いこともその1つで、ここでは主用される証明書がらみのフォーマットについて簡単に解説します。
PEM
Privacy-Enhanced Mailの略で、拡張子.pemがよく使われます。もともとはEメールのマルチメディア拡張仕様のMIME (RFC1341)を応用したセキュアEメールの仕様 (RFC1421~1424)で、1行64文字で改行したBASE64エンコーディングにヘッダ・トレイラーを付けたものです。X.509証明書のヘッダタイプは"CERTIFICATE"で、こんな感じになります。
-----BEGIN CERTIFICATE-----MIIDXDCCAkSgAwIBAgIUdg+NSGlvouQLtCZdnbT6H2jWNTEwDQYJKoZIhvcNAQEL
BQAwSjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQwwCgYDVQQHDANTTkExDjAM
:
8yp0rVGkqiaG4YdFXu3A8S+BdAon/CyLaqxSpjf8hMzvgtqVGTZsVtecJvV1fg0A
-----END CERTIFICATE-----
秘密鍵は"PRIVATE KEY"で、RSA秘密鍵(暗号化無し)ならこんな感じ、
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAprwwdiUZVidOhhjtKCmXi6MAGJhA553kCZZ8C1yqvWXOeyeP
E0rzxoZ3P5KT0y1smFUHXIfyNpMBKKWtEto2fk1opweh7S00E+s3Mr9chG4w6nu8
:
BMz354N/xA105aQLFbHRs++C5TI1tBfCGkGd0UciFe0AGsbuPbw=
-----END RSA PRIVATE KEY-----
暗号化されていた場合は、ヘッダの後ろに暗号化を示す追加ヘッダが付けられます。
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,BE926F64AB651AC283717D3F79BEF478
MEZV2ELZ1vPeQQNW4Hi2yD0+8kGEiv6YXP/F3x6IgqxGCQmPLK6WMVpf+i0vqmZk
qjnYVjfy9YRKd00o5as/xWKltJRHVK02md56/zAFqlhNZrmdcsRsRYPD7JvXEQdL
:
nUJNAkOUgXUz/Asj+y3z3S1AYITG25NfXsbSyH0hLLu5NjJ2sYHbbyYHVzhckHYP
-----END RSA PRIVATE KEY-----
PEMはOpenSSLの標準フォーマットで、OpenSSLをセキュリティ実装の基盤としているUnix系OS (Linux)でもデファクトスタンダードになっています。原型がMIMEなので1つのファイルに複数のPEM (証明書+秘密鍵+CRLなど)をくっつけて1にまとめたり、テキストエディタを使ってパートを分離することも可能です。
PEMは秘密鍵・証明書・CRLなど複数の異なるデータを同じMIMEの枠組みで扱えるので、しばしば証明書を.cer, 秘密鍵を.keyなど、フォーマットではなく中身を拡張子で示すこともあります。このような「OS・組織・個人によって慣例が異なる」ことも混乱を招く一因になっています。
DER
Distinguished Encoded Ruleの略で、X.509のASN.1バイナリ形式そのままです。拡張子は.derがよく使われます。ASN.1はもともとソースコード形式からコンパイルしてバイナリに落とせる仕様になっており、8bit単位のエンコード規則をBER (Basic Encoding Rule)としていました(※註)。DERは原型のBER仕様にあった曖昧さ・柔軟さの幾つかを「必ず~とすべきである」と厳格化したサブセット仕様ですが、普通に使っている分にはBERとDERの区別はあまり意識しません。
(※註)可変ビット長エンコードを用いるPER (Packed Encoding Rule)も制定されましたが、それによって実現できるサイズ削減効果にくらべて実装がめんどくさいので殆ど使われていません。まだコンピュータが高価で「メモリの1bitは血の一滴」だった1970年代の遺産みたいなものです。
DERはファイルサイズが最も小さいですが、バイナリデータなので扱い方しだいで化ける「ことがある(※註)」とか、HEXダンプしないと中身がわからない・ダンプしてもわかるとは限らないなどの使い難さも伴います。
(※註)証明書ファイル転送を古典的FTPで行うとき、バイナリモード(TYPE I)を明示的に指定しないとテキストモード(TYPE A)が適用されてCR(0x0d)/LF(0x0a)が化けることがあります。GUI FTPクライアントだと転送モードの設定がGUI設定の奥に隠れていてわかりにくくトラブルの元になったりします。
OpenSSLでPEMからDERを作るには次のコマンドを使います。
$ openssl x509 -in test_cert.pem -outform der -out test_cert.der
古いWEBブラウザだと、「ページの情報を見る」からそのページに紐づいた証明書を表示して「ダウンロード」を選択しても、保存フォーマットにPEMが選べない(DERしか無い)ものがありました。DERからPEMへの変換は次のコマンドを使います。
$ openssl x509 -in test_cert.der -inform DER -out test_cert.pem -outform PEM
PKCS#7
PKCS (Public-Key Cryptography Standards)はかつてRSA Laboratories社が自主的に発表していたセキュリティ関連の標準提案で、PKCS#7は「署名付き封筒 (Signed Envelope)」という規格でした。デジタル証明書の世界では、ピア証明書と上位のCA証明書をまとめて1つのファイルとして扱う場合に多用されます。拡張子はCA証明書を同梱する場合は.p7b、証明書単体の場合は.p7cを使う慣習があるようです。
上で作ったピア証明書test_cert.pemとCA証明書ca_cert.pemをまとめたPKCS#7ファイルを作るには、次のようなOpenSSLコマンドを使います。"-nocrl"は、失効証明書リスト(CRL)を伴わないことを指示しています。これを付けない場合、標準入力あるいは-in <filename>でCRLファイルを渡す必要があります。
$ openssl crl2pkcs7 -nocrl -certfile test_cert.pem -certfile ca_cert.pem -out test_cert.p7b
OpenSSLのcrl2pkcs7コマンドはデフォルトでPEMエンコードを吐き、"BEGIN PKCS7"~"END PKCS7"のヘッダで囲まれます。あえてPEMではなくDERで出力したい場合には"-outform DER"を付加しますが、PKCS#7のDERエンコードはあまり使われないようです。
PKCS#7の中身を見る場合は、-textではなく-printを指定します。
$ openssl pkcs7 -in test_cert.p7b -noout -print
-print_certsオプションを指定すると、PKCS#7に含まれる証明書が分解されたマルチパートのPEMとして出力されます。
$ openssl pkcs7 -in test_cert.p7b -print_certs
PKCS#8
PKCS#8は秘密鍵のフォーマットです。今では使われることは少ないかもしれません。拡張子も特に慣例は無いようで、.pk8や.priが使われるようです。上で作ったpriv_key.pemをPKCS#8に変換するには次のようなOpenSSLコマンドを使います。
$ openssl pkcs8 -topk8 -in priv_key.pem -inform PEM -out priv_key.pk8
"BEGIN ENCRYPTED PRIVATE KEY"~"END ENCRYPTED PRIVATE KEY"で囲まれたPEMが出力されます。普通は使いませんが"-nocrypt"を付加すれば暗号化なしのフォーマットにもでき、この場合は"BEGIN PRIVATE KEY"~"END PRIVATE KEY"で囲まれます。PKCS#7同様、あえてPEMではなくDERで出力したい場合には"-outform DER"を付加します。
PKCS#8からPEMへの変換は"-traditional"オプションを指定します。ただし暗号化は掛からないので、一度吐き出した「生のPEM秘密鍵」を改めて暗号化するには2段階の操作が必要になります。
$ openssl pkcs8 -in priv_key.pk8 -traditional -out priv_key_raw.pem
$ openssl rsa -in priv_key_raw.pem -out priv_key_enc.pem -aes128
PKCS#12
PKCS#12はArchive Formatとされており、複数の異種データを暗号化・署名つきでまとめて扱えるフォーマットです。デジタル証明書の世界では、証明書と秘密鍵をまとめて1つのファイルとして扱う場合に多用されます。拡張子は.p12あるいは.pfxが用いられます。
上で作ったピア証明書test_cert.pemと秘密鍵priv_key.pemまとめたPKCS#12ファイルを作るには次のようなOpenSSLコマンドを使います。
$ openssl pkcs12 -export -in test_cert.pem -inkey priv_key.pem -out test_cert.pfx
PKCS#12はDERフォーマットのまま扱うのが普通なようで、openssl pkcs12コマンドに"-outform"オプションはありません。またPKCS#8と違って暗号化なしにもできません。
PKCS#12を証明書と秘密鍵に分解するのは、ただ-inを指定すればマルチパートのPEMとして出力されます。秘密はPEMではなくPKCS#8で出力されるので、これをPEMに変換するのは上で解説した操作が必要になります。
$ openssl pkcs12 -in test_cert.pfx
デジタル証明書とY2*問題
X.509証明書はnotBefore/notAfterという2つの日付情報で有効期間が示されます。組み込み製品ではバッテリバックアップつきの時計(RTC)を持たず、インターネット経由での時刻情報(NTP)ができる保証のないことも多いので、AP証明書の有効期間を無視する実装を選択する場合もあります。エンタープライズ無線LANのEAP認証の場合は、wpa_supplicant.confにphase1="tls_disable_time_checks=1"を設定することで有効期限を無視させることができます。
country=US
ctrl_interface=/var/run/wpa_supplicant
network={
ssid="eap-tls-test"
auth_alg=OPEN
proto=RSN
ieee80211w=2
key_mgmt=WPA-EAP
eap=TLS
identity="XXXXX"
password="YYYYY"
ca_cert="/tmp/ca_cert.pem"
client_cert="/tmp/test_cert.pem"
private_key="/tmp/priv_key.pem"
private_key_passwd="ZZZZZ"
phase1="tls_disable_time_checks=1"}
p2p_disabled=1
ignore_old_scan_res=1
AP側でSTA証明書の有効期限を無視するか否かはRADIUSサーバの設定によりますが、普通はそんな設定はできないはずです。少なくともFreeRADIUSにはありません。
ASN.1の日付情報はUTCTimeという型で定義されていましたが、これは西暦が2桁しかありませんでした。なのでUTCTimeで表現可能な範囲は1950~2049年とされ、2050年以後は4桁に拡張されたGeneralizedTimeという型を使うと規定されています。いまどきGeneralizedTimeに対応していない実装系があると思いたくはないですが、昔のWEBブラウザにはGeneralizedTimeを含んだ証明書を問答無用でエラー扱いにするものもありました。これをY2050問題と呼びます。
もうひとつ、古いOSでは時刻情報を1970/1/1 00:00:00UTCからの経過秒数として扱うものがあり、これが符号付き32bit整数型だと2038/1/19 03:14:07 UTCを過ぎるとマイナスになってしまうY2038問題というのもありました。これもいまどきのコンパイラではtime_tを64bit整数型に割り当てているはずですが、古い実装系では2038年以後の日付がまともに扱えないものがあるかも知れません。
このような潜在的問題があるので、テスト用の証明書は古い実装に引っかからないよう意図的に期限を2037年以前にしたり、あるいは逆に意図的に2050年以後にしてY2038やY2050問題がクリアできていることを確認したりします。
まとめ
以上、
・OSI X.500に遡るX.509の歴史
・DNの概念、Subject/Issuerの連鎖関係
・OpenSSLを使ったRSA, DSA, ECDSA証明書作成の実例
・デジタル証明書がらみのファイルフォーマットの解説
・証明書の期限にからむY2*問題
についてざっと解説しました。何かと「難しい」「わかりにくい」とされるEAP認証と証明書についての理解の助けになれば幸いです。