純規の暇人趣味ブログ

首を突っ込んで足を洗う

[スクリプト有]Let's EncryptにECDSA(ECC)の鍵を作成してもらう

      2017/04/19    HimaJyun

このブログ、つい最近(秘密裏に)サーバのアップデートを実行し、HTTP2を利用したかったため、SSLに対応させました。

その際、せっかくなのでRSAより負荷が少ない(と言われる)ECDSA(ECC:楕円曲線暗号)を採用してみる事にしました。

同様の投稿は2,3件ほど見受けられますが、最新のコマンドと微妙に引数が合わなかったりしたので、スクリプトと合わせて紹介しようと思います。

Let's ECDSA

このページをご覧になっている時点で、ECDSAがどうこう、暗号化どうこうと言うのは理解しているかと思われます。

Let's Encryptはいつの間にやら正式版となって、しかもECDSAの鍵が発行して頂けると言うではありませんか……

ただ、現状発行されるのはRSA2048bit(s)、ECDSAは面倒な手順を踏まないと行けない様なので、頑張って解説してみます。

鍵は/etc/letsencrypt/ecdsa/以下に置いて行くとします。(普通にやった時の挙動を真似て、/etc/letsencrypt/ecdsa/archiveに作成して/etc/letsencrypt/ecdsa/liveにシンボリックを張るとします。)

Certbot(Let's Encrypt)をインストールする

letsencrypt-autoは僕の知らない間に「certbot-auto」と言う物に変貌を遂げています。

と、言う事で、Certbotをインストールします、今回は後からスクリプトで自動更新する事も考えて「/usr/local/bin/」にインストールします。

# ディレクトリ移動
cd /usr/local/bin/
# git clone
sudo git clone https://github.com/certbot/certbot
cd certbot
# rootユーザに切り替え(sudo付けて実行するのとrootになって作業するのとでは違う(sudo付ける方だと失敗する))
sudo su
# Certbotの初期設定
./certbot-auto

CSRを作成する

CSRを作成しましょう、CSRってのはCertificate Signing Requestの事らしいです、要は「俺はスーパーサ○ヤ人のベ○ータ様だ!!署名しやがれクソッタレ!!」ってなファイルです。

# /etc/letsencrypt/ecdsa/ に置いて行くので移動
sudo mkdir -p /etc/letsencrypt/ecdsa/archive
cd /etc/letsencrypt/ecdsa/archive
# 秘密鍵を作成する(このprime256v1ってのがECDSAの指定的な奴)
sudo openssl ecparam -out privkey.pem -name prime256v1 -genkey
# 一時ファイル
cat << '__EOL__' > /tmp/lets_ecdsa
[req]
distinguished_name = dn
[dn]
[SAN]
subjectAltName=DNS:example.com
__EOL__
# derとか言う代物を作成
sudo openssl req -new -key privkey.pem -sha256 -nodes -outform der -out csr.der -subj "/CN=example.com" -reqexts SAN -config /tmp/lets_ecdsa
rm /tmp/lets_ecdsa

"/CN=example.com"の所、example.comを自分のドメインにして実行しましょう(例:jyn.jp)

subjectAltName=DNS:example.comの所も同じです、この時、SANとしてサブドメインとかを複数指定する事でマルチドメインに対応した証明書を発行する事が出来ます。

こんな感じ

subjectAltName=DNS:example.com, DNS:hoge.example.jp

署名してもらう

「さぁ、署名しやがれ!!」

cd /etc/letsencrypt/ecdsa/archive
sudo su
# 署名してもらう
/usr/local/bin/certbot/certbot-auto certonly --webroot --csr csr.der -m 自分のメアド@example.com --renew-by-default --agree-tos -w /var/www/main -d example.com -w /var/www/hoge -d hoge.example.com

サブドメインがある場合はサブドメインの分だけ「-w /Webのルートパス/ -d ドメイン.example.com」を指定しましょう。

証明書の取得が上手く行くとカレントディレクトリに以下の3つのファイルが作成されます。

  • 0000_cert.pem(サーバ証明書)
  • 0000_chain.pem(中間証明書)
  • 0001_chain.pem(サーバ証明書と中間証明書が1つになったモノ)

それぞれ分かりやすい名前にリネームしておきましょうか。

sudo mv 0000_cert.pem cert1.pem
sudo mv 0000_chain.pem chain1.pem
sudo mv 0001_chain.pem fullchain1.pem
sudo mv privkey.pem privkey1.pem

普通に取得した時の挙動も真似てliveディレクトリにシンボリックリンクを張っておきましょう。

sudo mkdir -p /etc/letsencrypt/ecdsa/live
sudo ln -s /etc/letsencrypt/ecdsa/archive/cert1.pem /etc/letsencrypt/ecdsa/live/cert.pem
sudo ln -s /etc/letsencrypt/ecdsa/archive/chain1.pem /etc/letsencrypt/ecdsa/live/chain.pem
sudo ln -s /etc/letsencrypt/ecdsa/archive/fullchain1.pem /etc/letsencrypt/ecdsa/live/fullchain.pem
sudo ln -s /etc/letsencrypt/ecdsa/archive/privkey1.pem /etc/letsencrypt/ecdsa/live/privkey.pem

サーバの設定

さて、さっそく作成した鍵ファイルを利用してApacheやnginxをセットアップしてみましょう。

細かい暗号化の設定は別として、鍵ファイル周りをご紹介します。

nginx

鍵ファイルの設定は以下の様になります。

ssl_certificate /etc/letsencrypt/ecdsa/live/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/ecdsa/live/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/ecdsa/live/fullchain.pem;

鍵ファイルがSANとして複数のドメインに対応している場合は全てのサーバの設定に同じファイルを指定すればよろし

Apache(2.4.8~)

Apacheはバージョンが2.4.8以上か否かで設定が変わってきます。

まずは2.4.8以上の場合

SSLCertificateFile /etc/letsencrypt/ecdsa/live/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/ecdsa/live/privkey.pem

Apache(~2.4.8)

バージョンが2.4.8未満の場合は以下の様に設定します。

SSLCertificateFile /etc/letsencrypt/ecdsa/live/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/ecdsa/live/privkey.pem
SSLCertificateChainFile /etc/letsencrypt/ecdsa/live/chain.pem

暗号スイートの選択

みなさんの迷い所、ssl_ciphersです。

人によっては凄く色々と羅列して悦に浸っているかも知れませんが、長ったらしく説明する意味はありません。(と言うかMozzilaのジェネレータがそう言う奴を吐き出す)

ECDSAで使える暗号スイートは少しだけです、他は指定しても無意味になります。

  • ECDHE-ECDSA-AES256-GCM-SHA384
  • ECDHE-ECDSA-AES256-SHA384
  • ECDHE-ECDSA-AES128-GCM-SHA256
  • ECDHE-ECDSA-AES128-SHA256
  • ECDHE-ECDSA-AES256-SHA
  • ECDHE-ECDSA-AES128-SHA

しかも、これらは「HIGH」を指定する事で自動的に暗号に128bit以上を使う物を指定します、すなわち上に上げた6つも含まれます。

と言うより「HIGH」指定の方が良いらしいです、「HIGH」指定を利用すると新しい暗号スイートが利用出来る様になった、脆弱性がある暗号スイートが推奨されなくなった時に自動的に(opensslなどのバージョンアップなどによって)選択される物が変わります。(出展:どこかのQiitaのコメントでみかけた情報だけどどこだか忘れた……)

と言う訳で、僕は以下の様に設定してみました。

ssl_ciphers 'AESGCM:HIGH:!aNULL:!eNull:!EXPORT:!DES:!3DES:!MD5:!DSS:!RC4:!PSK';

とっても短くて良いですね!!(こんなに色々除外する物を設定する必要はないかもですが)

「AESGCM:HIGH」としてAESGCMの優先度を上げているのは、こうしないとHTTP/2利用時にFireFoxなどでネゴシエーションに失敗するためです。

自動更新

先程ご紹介したECDSAの鍵ファイルを作成するコマンドをまとめてスクリプトにしてみました。

完全に確認している訳では無いので実行結果とかは/dev/nullに捨てずにメール送信した方が良いかもしれません。

このスクリプトはrootを要求するので、「crontab -e」ではなく「sudo crontab -e」でrootのcrontabとして実行して下さい。

#!/bin/bash

# Let's Encrypt用ECDSA鍵自動更新スクリプト

# ==== 設定 ====
# Webサーバの種類(nginx/apache2)
WEBSERVER='nginx'
# Let's Encryptコマンド
LETS_PATH='/usr/local/bin/certbot/certbot-auto'
# 鍵ファイルの場所
KEY_PATH='/etc/letsencrypt/ecdsa'
# メアド(設定済みなら無くても良い)
EMAIL='webmaster@example.com'
# メインのドメイン
DOMAIN_NAME='example.com'
# ドメインとルートのマップ
declare -A DOMAINS
DOMAINS=(
	['example.com']='/var/www/site'
	['blog.example.com']='/var/www/blog'
)
# ===========

# デストラクタ
cleanup=()
function __destructor () {
	for entry in "${cleanup[@]}";do
		eval "${entry}"
	done
}
trap "__destructor" EXIT

# 言語を戻す処理
cleanup+=("LANG=$LANG")
# 英語に
LANG=C
# 開始しましたよ通知
date
echo 'Update the certificate file...'
# rootで実行されている事を確認、違えば終わらせる
if [ "$(whoami)" != "root" ];then
  echo 'This script require root!!'
  echo 'Update faild...'
  # 終了
  exit 1
fi
# カレントディレクトリに移動
cd $(dirname $0)

# OpenSSLの存在確認
if [ -z "$(which openssl)" ];then
	echo 'OpenSSL not found!!'
	echo 'Update faild...'
	exit 1
fi
# Let's Encryptの存在確認
if [ ! -e ${LETS_PATH} ];then
	echo "Let's Encrypt(${LETS_PATH}) not found!!"
	echo 'Update faild...'
fi

# 秘密鍵の作成
echo 'Create new private key...'
# 一時ディレクトリ作成
path=$(mktemp -d)
cleanup+=("rm -r ${path}")
# 秘密鍵作成
openssl ecparam -out "${path}/privkey.pem" -name prime256v1 -genkey

# CSRの発行
# 一時ファイル作成
tmp=$(mktemp)
cleanup+=("rm ${tmp}")
cat << '__EOL__' > "${tmp}"
[req]
distinguished_name = dn
[dn]
[SAN]
__EOL__
san=''
for entry in "${!DOMAINS[@]}";do
	san="${san}, DNS:${entry}"
done
# 先頭の「, 」を消す
san=${san:2}
echo "subjectAltName=${san}" >> "${tmp}"
openssl req -new -key "${path}/privkey.pem" -sha256  -nodes  -outform der -out "${path}/csr.der" -subj "/CN=${DOMAIN_NAME}"  -reqexts SAN  -config "${tmp}"

# 鍵の発行
maps=''
for entry in "${!DOMAINS[@]}";do
	maps="${maps}, \"${entry}\" : \"${DOMAINS[${entry}]}\""
done
maps=${maps:2}
cd "${path}"
if [ -n "${EMAIL}" ];then
	EMAIL="-m ${EMAIL}"
fi
${LETS_PATH} certonly --webroot --webroot-map "{${maps}}" --csr "${path}/csr.der" -t -n --redirect --agree-tos --renew-by-default "${EMAIL}"

# 鍵が存在しない(失敗)
if [ ! -e 0000_cert.pem ];then
	echo 'Key file not found?!'
	echo 'Update faild...'
	exit 1
fi

# 鍵ファイルの移動処理
# 一応、鍵ファイルを入れる場所を作成
mkdir -p ${KEY_PATH}/archive
mkdir -p ${KEY_PATH}/live
# 現在の世代確認
version=$(ls ${KEY_PATH}/archive | grep -o '[0-9]*' | sort -r | head -1)
if [ -z "${version}" ];then
	# 空っぽなら第1に
	version='1'
else
	# 違えば加算
	version=$((${version}+1))
fi

# 移動
mv 0000_cert.pem "${KEY_PATH}/archive/cert${version}.pem"
mv 0000_chain.pem "${KEY_PATH}/archive/chain${version}.pem"
mv 0001_chain.pem "${KEY_PATH}/archive/fullchain${version}.pem"
mv "${path}/privkey.pem" "${KEY_PATH}/archive/privkey${version}.pem"
# 権限変更
chmod 600 "${KEY_PATH}/archive/cert${version}.pem"
chmod 600 "${KEY_PATH}/archive/chain${version}.pem"
chmod 600 "${KEY_PATH}/archive/fullchain${version}.pem"
chmod 600 "${KEY_PATH}/archive/privkey${version}.pem"

# シンボリックリンク更新
find ${KEY_PATH}/live -type l -print0 | xargs -0 -n1 unlink
ln -s "${KEY_PATH}/archive/cert${version}.pem" "${KEY_PATH}/live/cert.pem"
ln -s "${KEY_PATH}/archive/chain${version}.pem" "${KEY_PATH}/live/chain.pem"
ln -s "${KEY_PATH}/archive/fullchain${version}.pem" "${KEY_PATH}/live/fullchain.pem"
ln -s "${KEY_PATH}/archive/privkey${version}.pem" "${KEY_PATH}/live/privkey.pem"

echo 'Update success!!'
echo "${WEBSERVER} restart now!!"
systemctl restart ${WEBSERVER}
if [ $? -eq 0 ];then
	echo 'All success!!'
else
	echo "${WEBSERVER} restart error?!"
fi

参考

 - サーバ運営