OpenSSLのコマンド覚えられない。
2020/3/4 Makefile 修正 (Linuxでのバグfix)
内部ネットワークで(試験的に)SSL/TLSを用いるサービスを立ち上げるようとすると、まずはopenssl
での自己署名証明書を作成することになる。openssl
で叩くコマンドは長いので、なんどもやってるはずなのに覚えられない。Web検索するとopenssl
による自己署名証明書の作成方法のメモや解説は山のようにヒットするので、私だけでなく覚えられない人がたくさんいる、ということなのだろうと想像している。
また、作業手順に(そのための引数をつけずにopenssl
を実行すると)、国名や組織名などプロンプトにしたがって入力するところがあり、入力ミスをしてしまうとBackspaceで修正できず、一からやり直しになってしまう、というのがストレスの溜まる原因となる。また、最近では証明書を作成するのに「SAN(subjectAltName)を適切に設定すべし」など、さらに設定方法が複雑になってきている。以前書いた記事(iOS13からのメールサーバへの接続にかんするメモ)では、「LibreSSL
をインストールして、その設定ファイルを書き換えて,... 」といったことをしていたが、もっと汎用的に使えるな方法を考えることにした。
make による自動化
以前書いた記事(iOS13からのメールサーバへの接続にかんするメモ)で書いた部分できるだけ再利用するため、同様にmake
により自動化することとした。macOS
とlinux(CentOS)
での利用を想定している。「一般ユーザー権限でインストールされている標準のopenssl
を利用すること」と、「'Makefile'の冒頭(もしくはmake
のコマンドライン引数で)の変数を適切に設定すればopenssl
コマンドに対して対話的な入力は必要としないこと」の二点に留意した。
使い方
Makefile
は長くなってしまったのであとまわしにして、さきに実行例を記載する。
> make
(opensslのoutputは省略)
> ls
Makefile service.crt service.enc.key service.passphrase localhost_ca.cnf localhost_ca.csr localhost_ca.key localhost_ca.srl
service.cnf service.csr service.key service.pem localhost_ca.crt localhost_ca.enc.key localhost_ca.passphrase localhost_ca_certs.pem
この例ではservice.pem
(service.crt
)が自己署名証明書で,service.key
が暗号化されていない鍵ファイルで、service.enc.key
がservice.passphrase
に記載されたパスフレーズで暗号化された鍵ファイルである。`localhost_'で始まるファイル名(実際には実行ホスト名ではじまるファイル名となる)は自己認証局に関係するファイルで自己署名証明書を複数作成する場合には再利用できる。また、ホスト名やIPアドレスを自動的に取得して、SAN(Subject Alternative Name)に記載するようにしている。鍵の暗号化に用いるパスフレーズは、「自動生成」もしくは「プロンプトにしたがって手動入力」ができる。
また、make distclean
と叩けば生成したファイルを全て消去して、一からやり直しできる。適切なmake変数設定を引数につけて叩けば、生成するファイル名や証明書に記載される内容を変えることができるが、基本的には最初の一回だけMakefile
の先頭にあるmake変数設定の行を書き換えて使うことを想定している。
> make usage
[usage] make [TARGET=xxx.pem] [options]
[options] PASSPHRASE_READ=[0|1]
[examples]
% make TARGET=pop3s.pem INFO_C=JP INFO_ST='My Prefecture' INFO_L='My City' INFO_O='My organization name' INFO_OU='My branch name'
Makefileの中身
あまり気にせず書いたのでGNU makeの拡張を使ってしまっていると思います。先頭にある変数を適切に設定して使うことを想定してます。パスフレーズの自動生成の仕方は、こちらの記事を参考にしました。作成中にわかったTipsとして、「openssl version
に引数を与えることで、openssl
のコンパイル時の設定依存のデフォルト設定が採れる」というのがあります。
#
# Openssl command path
#
OPENSSL ?= openssl
#
# Name of self-signed cert file output
#
TARGET ?= service.pem
#
# pass-phrase option
#
# PASSPHRASE_READ == 0 --> Automatic random generation
# PASSPHRASE_READ != 0 --> Manual input
#
PASSPHRASE_READ ?= 0
#
# Subject names for cert
#
INFO_C ?= JP
INFO_ST ?= My Prefecture
INFO_L ?= My City
INFO_O ?= My organization name
INFO_OU ?= My branch name
# INFO_CN ?= "sample.sampleTLD"
# <-- if it is not defined here, hostname will be used later.
INFO_EM ?=
#
# Key configurations
#
KEY_NBITS ?= 4096
KEY_ENCOPT ?= -camellia256
#KEY_ENCOPT ?= -aes256
CSR_ENCOPT ?= -sha256
CRT_ENCOPT ?= -sha256
CRT_EXTSEC ?= x509v3_san_attr
CRT_EXPIREDAYS ?= 824
PASSPHRASE_LENGTH ?= 16
PASSPHRASE_CHARS ?= 'a-zA-Z0-9#$\%&@+\-*/_<>,.;:'
CA_CRT_SERIALNO ?= 0
#
# Hostname and IP address
#
HOSTNAME ?= $(shell hostname -f)
HOSTNAME_S ?= $(shell hostname -s)
SED ?= "sed"
SEDOPT ?= "-E"
ifeq ($(OSTYPE),darwin)
ifeq ($(HOSTNAME_S),)
HOSTNAME_S = $(shell scutil --get HostName 2>/dev/null ||\
scutil --get ComputerName 2>/dev/null)
ifeq ($(HOSTNAME),)
HOSTNAME = $(shell dscacheutil -q host -a name "$(HOSTNAME_S)" |\
$(SED) $(SEDOPT) -n -e 's/name:[[:blank:]]+([^[:blank:]]+).*/\1/p' )
endif
endif
IPADDR ?= $(shell dscacheutil -q host -a name "$(HOSTNAME)" |\
$(SED) $(SEDOPT) -n -e 's/ip_address:[[:blank:]]+([^[:blank:]]+).*/\1/p' )
else
# IPADDR ?= $(shell getent hosts "$(HOSTNAME)" | cut -d\ -f1) ### 2020/3/4 IPV6アドレスが使われてしまうことがある
IPADDR ?= $(shell getent ahostsv4 "$(HOSTNAME)" |\
cut -d\ -f1 | sort | uniq | tr -d '[:space:]' |\
tr '\n' ',' | $(SED) $(SEDOPT) 's/,$$//g')
### 2020/3/4 複数IPアドレスエントリが表示される場合に対応
endif
INFO_CN ?= $(HOSTNAME)
CA_CRT_EXTSEC ?= san_attr_ca
CA_HOSTNAME ?= $(HOSTNAME)
CA_HOSTNAME_S ?= $(HOSTNAME_S)
CA_EXPIREDAYS ?= $(CRT_EXPIREDAYS)
CA_IPADDR ?= $(IPADDR)
CA_INFO_C ?= $(INFO_C)
CA_INFO_ST ?= $(INFO_ST)
CA_INFO_L ?= $(INFO_L)
CA_INFO_O ?= $(INFO_O)
CA_INFO_OU ?= $(INFO_OU)
CA_INFO_CN ?= $(CA_HOSTNAME)
CA_INFO_EM ?=
ifndef INFO_SUBJ
INFO_SUBJ ?= "/C=$(INFO_C)/ST=$(INFO_ST)/L=$(INFO_L)/O=$(INFO_O)/OU=$(INFO_OU)/CN=$(INFO_CN)"
ifdef INFO_EM
ifneq ($(INFO_EM),)
INFO_SUBJ := "$(INFO_SUBJ)/emailAddress=$(INFO_EM)"
endif
endif
endif
ifndef CA_INFO_SUBJ
CA_INFO_SUBJ ?= "/C=$(CA_INFO_C)/ST=$(CA_INFO_ST)/L=$(CA_INFO_L)/O=$(CA_INFO_O)/OU=$(CA_INFO_OU)/CN=$(CA_INFO_CN)"
ifdef CA_INFO_EM
ifneq ($(CA_INFO_EM),)
CA_INFO_SUBJ := "$(CA_INFO_SUBJ)/emailAddress=$(CA_INFO_EM)"
endif
endif
endif
OPENSSL_DIR ?= $(shell $(OPENSSL) version -d | sed -e 's/^OPENSSLDIR: //g')
OPENSSL_CONF ?= $(shell echo $(OPENSSL_DIR))/openssl.cnf
.PHONY: clean distclean usage show_keys show_fingerprints
.SUFFIXES: .key .csr .crt .pem .passphrase
.PRECIOUS: %.key %.enc.key %.csr %.crt %.pem %.passphrase %.cnf
CLEAN_SUFIXES := .passphrase .csr .key .enc.key .crt .cnf .cer .pem
all: $(TARGET)
usage:
@ echo "[usage] make [TARGET=xxx.pem] [options]"
@ echo ""
@ echo "[options] PASSPHRASE_READ=[0|1]"
@ echo ""
@ echo "[examples]"
@ echo "% make TARGET=pop3s.pem INFO_C=JP INFO_ST='My Prefecture' INFO_L='My City' INFO_O='My organization name' INFO_OU='My branch name'"
@ echo ""
show_keys: $(TARGET)
@for i in $^; do { if [ x"$${i##*.}" != x'pem' ] ; then continue ; else { echo $${i} ; $(OPENSSL) x509 -noout -text -in $${i} ; } ; fi ;} ; done
show_fingerprints: $(TARGET)
@for i in $^; do { if [ x"$${i##*.}" != x'pem' ] ; then continue ; else { echo $${i} ; $(OPENSSL) x509 $(CRT_ENCOPT) -fingerprint -noout -in $${i} ; } ; fi ; } ; done
ifeq ($(PASSPHRASE_READ),0)
# Automatic passpharase generation
%.passphrase:
umask 077 ; \
head /dev/urandom \
| env LC_CTYPE=C tr -dc "$(PASSPHRASE_CHARS)" \
| head -c $(PASSPHRASE_LENGTH) > $@
else
# Manual passpharase generation
%.passphrase:
@umask 077 ; read -sp "$(@:.passphrase=) passphrase: " passphrase ; echo $${passphrase} > $@
endif
%.enc.key: %.passphrase
umask 077; $(OPENSSL) genrsa -passout file:$(patsubst %.enc.key,%.passphrase,$@) \
-out $@ $(KEY_ENCOPT) $(KEY_NBITS)
%.key: %.passphrase %.enc.key
umask 077; $(OPENSSL) rsa -passin file:$(addsuffix .passphrase,$(basename $(@))) \
-in $(addsuffix .enc.key,$(basename $(@))) \
-out $@
%.csr: %.key
umask 077 ; $(OPENSSL) req -new \
-key $(addsuffix .key,$(basename $(@))) \
-subj $(INFO_SUBJ) $(CSR_ENCOPT) -out $@
%.cnf: %.key
umask 077; printf "[$(CRT_EXTSEC)]\nbasicConstraints=CA:FALSE\nkeyUsage=nonRepudiation,digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth,clientAuth,emailProtection\nsubjectAltName=IP:$(IPADDR),DNS:$(HOSTNAME)\n" > $@
%.crt: %.csr %.cnf $(CA_HOSTNAME_S)_ca_certs.pem $(CA_HOSTNAME_S)_ca.key
umask 077 ; $(OPENSSL) x509 -req \
-CA $(CA_HOSTNAME_S)_ca_certs.pem \
-CAkey $(CA_HOSTNAME_S)_ca.key -CAcreateserial \
-CAserial $(CA_HOSTNAME_S)_ca.srl \
-days $(CRT_EXPIREDAYS) $(CRT_ENCOPT) \
-extfile $(addsuffix .cnf,$(basename $(@))) \
-extensions "$(CRT_EXTSEC)" \
-in $(addsuffix .csr,$(basename $(@))) -out $@
umask 077; openssl x509 -text -in $@ # -inform der
%.pem: %.key %.crt
umask 077 ; cat $^ > $@
$(CA_HOSTNAME_S)_ca.enc.key: $(CA_HOSTNAME_S)_ca.passphrase
$(CA_HOSTNAME_S)_ca.csr: $(CA_HOSTNAME_S)_ca.enc.key $(CA_HOSTNAME_S)_ca.passphrase
umask 077 ; $(OPENSSL) req -passin file:$(addsuffix .passphrase,$(basename $(@))) \
-key $(addsuffix .enc.key,$(basename $(@))) \
-config $(OPENSSL_CONF) -utf8 -new \
-subj $(CA_INFO_SUBJ) $(CSR_ENCOPT) -out $@
$(CA_HOSTNAME_S)_ca.cnf: $(OPENSSL_CONF)
umask 077 ; cat $(OPENSSL_CONF) > $(addsuffix .cnf,$(basename $(@)))
printf "[$(CA_CRT_EXTSEC)]\nbasicConstraints=critical,CA:true,pathlen:1\nkeyUsage=digitalSignature,keyCertSign,cRLSign\nextendedKeyUsage=serverAuth,clientAuth,codeSigning,emailProtection\nsubjectAltName=IP:$(CA_IPADDR),DNS:$(CA_HOSTNAME)" >> $(addsuffix .cnf,$(basename $(@)))
$(CA_HOSTNAME_S)_ca.crt: $(CA_HOSTNAME_S)_ca.enc.key $(CA_HOSTNAME_S)_ca.csr $(CA_HOSTNAME_S)_ca.passphrase $(CA_HOSTNAME_S)_ca.cnf
umask 077 ; $(OPENSSL) req -passin file:$(addsuffix .passphrase,$(basename $(@))) \
-key $(addsuffix .enc.key,$(basename $(@))) \
-in $(addsuffix .csr,$(basename $(@))) \
-config $(addsuffix .cnf,$(basename $(@))) \
-utf8 -x509 -new -set_serial $(CA_CRT_SERIALNO) -subj $(CA_INFO_SUBJ) \
-days $(CA_EXPIREDAYS) -sha256 -extensions $(CA_CRT_EXTSEC) -nodes -out $@
openssl x509 -text -in $@
$(CA_HOSTNAME_S)_ca_certs.pem: $(CA_HOSTNAME_S)_ca.enc.key $(CA_HOSTNAME_S)_ca.crt
umask 077 ; cat $^ > $@
$(CA_HOSTNAME_S)_ca_certs.cer: $(CA_HOSTNAME_S)_ca_certs.pem $(CA_HOSTNAME_S)_ca.passphrase
umask 077 ; $(OPENSSL) x509 -outform der -inform pem \
-passin file:$(patsubst %_ca_certs.cer,%_ca.passphrase,$@) \
-in $(addsuffix .pem,$(basename $(@))) -out $@
umask 077 ; openssl x509 -text -inform der -in $@
clean:
rm -f *~
distclean: clean
rm -f $(TARGET)
rm -f $(foreach tgt,$(TARGET),$(foreach suf,$(CLEAN_SUFIXES),$(addsuffix $(suf),$(basename $(tgt)))))
rm -f $(CA_HOSTNAME_S)_ca.passphrase $(CA_HOSTNAME_S)_ca.enc.key $(CA_HOSTNAME_S)_ca.key \
$(CA_HOSTNAME_S)_ca.csr $(CA_HOSTNAME_S)_ca.cnf $(CA_HOSTNAME_S)_ca.srl \
$(CA_HOSTNAME_S)_ca.crt $(CA_HOSTNAME_S)_ca_certs.pem $(CA_HOSTNAME_S)_ca_certs.cer