OpenSSLの自己署名証明書の生成の省力化の試み

# OpenSSLのコマンド覚えられない。

**2020/3/4 Makefile 修正 (Linuxでのバグfix)**

内部ネットワークで（試験的に）SSL/TLSを用いるサービスを立ち上げるようとすると、まずは`openssl`での自己署名証明書を作成することになる。`openssl`で叩くコマンドは長いので、なんどもやってるはずなのに覚えられない。Web検索すると`openssl`による自己署名証明書の作成方法のメモや解説は山のようにヒットするので、私だけでなく覚えられない人がたくさんいる、ということなのだろうと想像している。

また、作業手順に（そのための引数をつけずに`openssl`を実行すると）、国名や組織名などプロンプトにしたがって入力するところがあり、**入力ミスをしてしまうとBackspaceで修正できず、一からやり直しになってしまう**、というのがストレスの溜まる原因となる。また、最近では証明書を作成するのに「SAN(subjectAltName)を適切に設定すべし」など、さらに設定方法が複雑になってきている。以前書いた記事([iOS13からのメールサーバへの接続にかんするメモ](https://qiita.com/Nanigashi_Uji/items/881e8756631bd4cbfb14))では、「`LibreSSL`をインストールして、その設定ファイルを書き換えて,... 」といったことをしていたが、もっと汎用的に使えるな方法を考えることにした。

# make による自動化

以前書いた記事([iOS13からのメールサーバへの接続にかんするメモ](https://qiita.com/Nanigashi_Uji/items/881e8756631bd4cbfb14))で書いた部分できるだけ再利用するため、同様に`make`により自動化することとした。`macOS`と`linux(CentOS)`での利用を想定している。「一般ユーザー権限でインストールされている標準の`openssl`を利用すること」と、「'Makefile'の冒頭（もしくは`make`のコマンドライン引数で）の変数を適切に設定すれば`openssl`コマンドに対して対話的な入力は必要としないこと」の二点に留意した。

# 使い方

`Makefile`は長くなってしまったのであとまわしにして、さきに実行例を記載する。

```bash:実行例
> 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変数設定の行を書き換えて使う**ことを想定している。

```tcsh:使い方の例を表示させることも可能。
> 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の拡張を使ってしまっていると思います。先頭にある変数を適切に設定して使うことを想定してます。パスフレーズの自動生成の仕方は、[こちらの記事](https://www.skyarch.net/blog/?p=15113)を参考にしました。作成中にわかったTipsとして、「`openssl version`に引数を与えることで、`openssl`のコンパイル時の設定依存のデフォルト設定が採れる」というのがあります。

``` make:Makefile
#
# 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
```