twitter rss
プライベートCAを使ったクライアント・サーバー間のSSL/TLS通信認証
Jul 24, 2017
8 minutes read

クライアント・サーバー間のSSL/TLS通信認証において、CA(認証局)との関係や証明書認証に対する理解がかなり曖昧だったので、 プライベートCA構築〜自己署名証明書発行〜クライアント・サーバー間のSSL/TLS通信までを実際に全てローカルPC上で行い、理解を深めた。 ここでは自分の脳みそでも理解できるような形でざっくりと以下をまとめる。

  • クライアント、サーバー、CA(認証局)間の関係
  • クライアント、サーバー間の暗号化通信の仕組み
  • プライベートCAの構築
  • サーバー証明書の発行
  • クライアント証明書の発行
  • 証明書発行時のエラー対応
  • クライアント・サーバー間のSSL/TLS通信サンプルプログラム(Golang)
  • 所感

ちなみにここではなぜSSL/TLSで暗号化通信を行う必要があるのかというところは書かない。


クライアント、サーバー、CA(認証局)間の関係

ここではクライアント、サーバー、CAにそれぞれ何が置いてあって、クライアント・サーバーとCA間で何が行われるのかを理解する。 手書きで図をまとめた。

参考

http://d.hatena.ne.jp/ozuma/20130511/1368284304

証明書発行の図

  • まずCAはCA自身の秘密鍵と、そこから発行されたルート証明書を持っている。ルート証明書には公開鍵情報が含まれている。
  • サーバーはサーバー自身の秘密鍵と公開鍵を持っており、そこから証明書署名要求(CSR)を作成する。CAに対してCSRを提出して署名してもらい、CAから認証されたサーバー証明書が発行される。
  • クライアントはクライアント自身の秘密鍵と公開鍵を持っており、そこから証明書署名要求(CSR)を作成する。CAに対してCSRを提出して署名してもらい、CAから認証されたクライアント証明書が発行される。
  • またクライアントとサーバーはそれぞれCAのルート証明書を持つ。

それぞれの証明書がどう使われているのかは、下記のクライアント、サーバー間の暗号化通信の仕組みでまとめる。


クライアント、サーバー間の暗号化通信の仕組み

ここではクライアントとサーバー間の暗号化通信の方法を大雑把に理解する。 手書きで図をまとめた。

参考

https://www.jp.websecurity.symantec.com/welcome/pdf/wp_sslandroot-certificate.pdf https://www.jp.websecurity.symantec.com/welcome/pdf/wp_ssl_negotiation.pdf

クライアント・サーバー間の暗号化通信の図

①クライアントはサーバーへ暗号化通信の接続を要求。暗号化の仕様(暗号方式、鍵長、圧縮方式)を交渉する。

②サーバーはクライアントへサーバー証明書を送る。

③クライアントはCAのルート証明書でサーバー証明書の署名検証を行う。またサーバーの公開鍵を取得する。

④クライアントはサーバーの公開鍵を用いてプリマスタシークレット(共通鍵を生成する基となる乱数データ)を暗号化。

⑤クライアントはサーバーに暗号化されたプリマスタシークレットとクライアント証明書を送付。

⑥CAのルート証明書でクライアント証明書の署名検証を行う。また暗号化されたプリマスタシークレットを復号する。

⑦プリマスタシークレットを元にクライアントとサーバーで同じ共通鍵を生成&共有する。

⑧以降の暗号化通信は共通鍵を用いて暗号化・復号して通信を行う。

これより下記はCAの構築〜サーバー証明書の発行〜クライアント証明書の発行をローカルPC上で実際に試した。


プライベートCAの構築

参考

http://stacktrace.hatenablog.jp/entry/2015/12/12/102945 http://moca.wide.ad.jp/notes/ca_doc/openssl.html http://qiita.com/mitzi2funk/items/602d9c5377f52cb60e54

CA(認証局)は証明書を発行するところ。プライベートCAは第3者ではない立場の方が独自基準で認証局を構築して証明書を発行できるので、 巷ではオレオレ認証局とも言われる。オレオレ認証局によって発行された証明書はオレオレ証明書とも言われる。

CA構築の事前設定

$ sudo vi /private/etc/ssl/openssl_ca.cnf

openssl_ca.cnfの内容

[ req ]
#default_bits           = 2048
#default_md             = sha256
#default_keyfile        = privkey.pem
distinguished_name      = req_distinguished_name
attributes              = req_attributes

[ req_distinguished_name ]
countryName                     = Country Name (2 letter code)
countryName_min                 = 2
countryName_max                 = 2
stateOrProvinceName             = State or Province Name (full name)
localityName                    = Locality Name (eg, city)
0.organizationName              = Organization Name (eg, company)
organizationalUnitName          = Organizational Unit Name (eg, section)
commonName                      = Common Name (eg, fully qualified host name)
commonName_max                  = 64
emailAddress                    = Email Address
emailAddress_max                = 64

[ req_attributes ]
challengePassword               = A challenge password
challengePassword_min           = 4
challengePassword_max           = 20

[ ca ]
default_ca = CA_default # The default ca section

[ CA_default ]
dir = /etc/ssl/myCA
certs = $dir/certs
crl_dir = $dir/crl
database = $dir/index.txt
new_certs_dir = $dir/newcerts

certificate = $dir/cacert.pem
serial = $dir/serial
crl = $dir/crl.pem
private_key = $dir/private/cakey.pem

default_days    = 365                   # how long to certify for
default_crl_days= 30                    # how long before next CRL
default_md      = sha1                  # which md to use.
preserve        = no                    # keep passed DN ordering

# A few difference way of specifying how similar the request should look
# For type CA, the listed attributes must be the same, and the optional
# and supplied fields are just that :-)
policy          = policy_match

# For the CA policy
[ policy_match ]
countryName             = optional
stateOrProvinceName     = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

# For the 'anything' policy
# At this point in time, you must list all acceptable 'object'
# types.
[ policy_anything ]
countryName             = optional
stateOrProvinceName     = optional
localityName            = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ usr_cert ]
basicConstraints = CA:TRUE

[ v3_ca ]
nsCertType = sslCA, emailCA
// 必要なディレクトリ作成
$ sudo mkdir -p /etc/ssl/myCA/certs
$ sudo mkdir -p /etc/ssl/myCA/private
$ sudo mkdir -p /etc/ssl/myCA/crl
$ sudo mkdir -p /etc/ssl/myCA/newcerts

// 秘密鍵が置かれるディレクトリのアクセス制限
$ sudo chmod 700 /etc/ssl/myCA/private

// 証明書のデータベースを初期化
$ sudo touch /etc/ssl/myCA/index.txt

// CAが発行する証明書のシリアル番号を保存
$ sudo sh -c "echo '01' > /etc/ssl/myCA/serial"

CAの秘密鍵とルート証明書の生成

// CAの秘密鍵生成とルート証明書(自己署名証明書)の生成
$ cd /etc/ssl/myCA
$ sudo openssl req -config /private/etc/ssl/openssl_ca.cnf -new -x509 -newkey rsa:2048 -out cacert.pem -keyout private/cakey.pem -days 365
Generating a 2048 bit RSA private key
................+++
............................................................................................+++
writing new private key to 'private/cakey.pem'
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----
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) []:JP
State or Province Name (full name) []:Tokyo
Locality Name (eg, city) []:Minato-ku
Organization Name (eg, company) []:keita
Organizational Unit Name (eg, section) []:keita
Common Name (eg, fully qualified host name) []:myCA
Email Address []:

// 証明書ができたかどうかの確認
$ openssl x509 -in /etc/ssl/myCA/cacert.pem -text

サーバー証明書の発行

事前設定

$ sudo vi /private/etc/ssl/openssl_server.cnf

openssl_server.cnfの内容

[ req ]
#default_bits           = 2048
#default_md             = sha256
#default_keyfile        = privkey.pem
distinguished_name      = req_distinguished_name
attributes              = req_attributes

[ req_distinguished_name ]      
countryName                     = Country Name (2 letter code)
countryName_min                 = 2
countryName_max                 = 2
stateOrProvinceName             = State or Province Name (full name)
localityName                    = Locality Name (eg, city) 
0.organizationName              = Organization Name (eg, company)
organizationalUnitName          = Organizational Unit Name (eg, section)
commonName                      = Common Name (eg, fully qualified host name)
commonName_max                  = 64
emailAddress                    = Email Address
emailAddress_max                = 64

[ req_attributes ]
challengePassword               = A challenge password
challengePassword_min           = 4
challengePassword_max           = 20

[ ca ]
default_ca = CA_default # The default ca section

[ CA_default ]
dir = /etc/ssl/myCA
certs = $dir/certs
crl_dir = $dir/crl
database = $dir/index.txt
new_certs_dir = $dir/newcerts           

certificate = $dir/cacert.pem
serial = $dir/serial
crl = $dir/crl.pem
private_key = $dir/private/cakey.pem    

default_days    = 365                   # how long to certify for
default_crl_days= 30                    # how long before next CRL
default_md      = sha1                  # which md to use.
preserve        = no                    # keep passed DN ordering

# A few difference way of specifying how similar the request should look
# For type CA, the listed attributes must be the same, and the optional
# and supplied fields are just that :-)
policy          = policy_match

# For the CA policy
[ policy_match ]
countryName             = optional
stateOrProvinceName     = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

# For the 'anything' policy
# At this point in time, you must list all acceptable 'object'
# types.
[ policy_anything ]
countryName             = optional
stateOrProvinceName     = optional
localityName            = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ usr_cert ]
basicConstraints=CA:FALSE
nsCertType = server

サーバー証明書の発行

// サーバーの秘密鍵生成
$ sudo openssl genrsa -out server_private.pem 2048
// サーバーの秘密鍵を元にCSR生成
$ sudo openssl req -new -key server_private.pem -out server_csr.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]:JP
State or Province Name (full name) [Some-State]:Tokyo
Locality Name (eg, city) []:Minato-ku
Organization Name (eg, company) [Internet Widgits Pty Ltd]:keita
Organizational Unit Name (eg, section) []:keita
Common Name (e.g. server FQDN or YOUR name) []:localhost
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

// CAでの署名とサーバ証明書の生成
$ sudo openssl ca -config /private/etc/ssl/openssl_server.cnf -in server_csr.pem -out server_crt.pem

※ Common NameはサーバーのIPまたはドメインを設定する


クライアント証明書の発行

事前設定

$ sudo vi /private/etc/ssl/openssl_client.cnf

openssl_client.cnfの内容

[ req ]
#default_bits           = 2048
#default_md             = sha256
#default_keyfile        = privkey.pem
distinguished_name      = req_distinguished_name
attributes              = req_attributes

[ req_distinguished_name ]
countryName                     = Country Name (2 letter code)
countryName_min                 = 2
countryName_max                 = 2
stateOrProvinceName             = State or Province Name (full name)
localityName                    = Locality Name (eg, city)
0.organizationName              = Organization Name (eg, company)
organizationalUnitName          = Organizational Unit Name (eg, section)
commonName                      = Common Name (eg, fully qualified host name)
commonName_max                  = 64
emailAddress                    = Email Address
emailAddress_max                = 64

[ req_attributes ]
challengePassword               = A challenge password
challengePassword_min           = 4
challengePassword_max           = 20

[ ca ]
default_ca = CA_default # The default ca section

[ CA_default ]
dir = /etc/ssl/myCA
certs = $dir/certs
crl_dir = $dir/crl
database = $dir/index.txt
new_certs_dir = $dir/newcerts

certificate = $dir/cacert.pem
serial = $dir/serial
crl = $dir/crl.pem
private_key = $dir/private/cakey.pem

default_days    = 365                   # how long to certify for
default_crl_days= 30                    # how long before next CRL
default_md      = sha1                  # which md to use.
preserve        = no                    # keep passed DN ordering

# A few difference way of specifying how similar the request should look
# For type CA, the listed attributes must be the same, and the optional
# and supplied fields are just that :-)
policy          = policy_match

# For the CA policy
[ policy_match ]
countryName             = optional
stateOrProvinceName     = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

# For the 'anything' policy
# At this point in time, you must list all acceptable 'object'
# types.
[ policy_anything ]
countryName             = optional
stateOrProvinceName     = optional
localityName            = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ usr_cert ]
basicConstraints=CA:FALSE
nsCertType = client, email, objsign

クライアント証明書の発行

// クライアントの秘密鍵生成
$ openssl genrsa -out client_private.pem 2048
// クライアントの秘密鍵を元にCSR生成
$ openssl req -new -key client_private.pem -out client_csr.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]:JP
State or Province Name (full name) [Some-State]:Tokyo
Locality Name (eg, city) []:Minato-ku
Organization Name (eg, company) [Internet Widgits Pty Ltd]:keita
Organizational Unit Name (eg, section) []:keita
Common Name (e.g. server FQDN or YOUR name) []:localhost
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

// CAでの署名とクライアント証明書の生成
$ sudo openssl ca -config /private/etc/ssl/openssl_client.cnf -in client_csr.pem -out client_crt.pem

証明書発行時のエラー対応

TXT_DB error number 2 エラーが出た時の対応

// newcerts配下のシリアル番号が一番大きいpemに対してrevokeする
$ sudo ls /private/etc/ssl/myCA/newcerts/
$ sudo openssl ca -config /private/etc/ssl/openssl.cnf -revoke /private/etc/ssl/myCA/newcerts/02.pem
Using configuration from /private/etc/ssl/openssl.cnf
Enter pass phrase for /etc/ssl/myCA/private/cakey.pem:
Revoking Certificate 02.
Data Base Updated

The stateOrProvinceName field needed to be the same in the CA certificate (Tokyo) and the request (Tokyo)が出た時の対応

根本的な解決方法はよくわからなかった。今回はとりあえず以下の方法で回避。

// -policy policy_anythingオプション付けてpolicyのmatch条件を回避する
$ sudo openssl ca -config /private/etc/ssl/openssl_client.cnf -policy policy_anything ...

もしくはopensslのcnfファイルのpolicy_match部分を全部optionalにする


クライアント・サーバー間のSSL/TLS通信サンプルプログラム

サンプルプログラムはGolangで書いてます

サーバー側実装

構成

oreore-serverディレクトリ内にサーバ証明書と秘密鍵を置く

main.goの内容

package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
)

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello World")
}

func main() {
	path, err := os.Getwd()
	if err != nil {
		log.Fatal(err)
	}
	http.HandleFunc("/", handler)
	if err := http.ListenAndServeTLS(":8080", path+"/server_crt.pem", path+"/server_private.pem", nil); err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}

サーバー起動

$ cd $GOPATH/src/oreore-server
$ go run main.go

https://localhost:8080 を開くとブラウザにこの接続ではプライバシーが保護されませんと表示されます。

クライアント側実装

構成

oreore-server-clientディレクトリ内にクライアント証明書と秘密鍵を置く

main.goの内容

package main

import (
	"crypto/tls"
	"log"
	"net/http"
	"os"
)

func main() {
	req, err := http.NewRequest("GET", "https://localhost:8080", nil)
	if err != nil {
		log.Fatal(err)
	}

	path, err := os.Getwd()
	if err != nil {
		log.Fatal(err)
	}

	cert, err := tls.LoadX509KeyPair(path+"/client_crt.pem", path+"/client_private.pem")
	if err != nil {
		log.Fatal(err)
	}
	tlsConfig := &tls.Config{
		Certificates: []tls.Certificate{cert},
	}
	tlsConfig.BuildNameToCertificate()
	transport := &http.Transport{TLSClientConfig: tlsConfig}

	client := &http.Client{Transport: transport}

	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	log.Println(resp.StatusCode)
}

クライアント起動

$ cd $GOPATH/src/oreore-server-client
$ go run main.go

クライアントを起動すると、以下のようなエラーが出る

2017/07/25 02:33:40 Get https://localhost:8080: x509: certificate signed by unknown authority
exit status 1

証明書が不明な機関によって署名されたというエラーなので、CA(認証局)を信頼してあげる必要がある。以下、Macでの動作手順。

  • CAの証明書(/etc/ssl/myCA/cacert.pem)をクリックすると、キーチェーンアクセスが開く。
  • 証明書を以下のように常に信頼に設定

証明書の設定

こうするとクライアント起動時のエラーが消える。


所感

SSL/TLS暗号化通信の資料はインターネットや参考書でたくさん出回っているが、CA、認証局、自己署名証明書、ルート証明書、公開鍵証明書、 サーバー証明書、SSLサーバ証明書、クライアント証明書、オレオレ証明書など、同じであろう意味に対して異なる呼び方が使われていることが多く、 理解が難しい。。 そして、クライアント、サーバー、CAに置いてある各証明書は何か、それらが暗号化通信でどう作用するか、わかりやすくまとまっている資料が意外と少ない。


Back to posts