Help us understand the problem. What is going on with this article?

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

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

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

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

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

make による自動化

以前書いた記事(iOS13からのメールサーバへの接続にかんするメモ)で書いた部分できるだけ再利用するため、同様にmakeにより自動化することとした。macOSlinux(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.keyservice.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のコンパイル時の設定依存のデフォルト設定が採れる」というのがあります。

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
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした