JS7® JobScheduler  https接続用KeyStore、TrustStore準備

# 概要

JS7 各コンポーネント間の通信を https で行う際に必要となるKeyStore、TrustStore の作成方法についてのメモ。
2パターン書きますが、どちらでも通信できることは確認済みです。

基本は手段１のRoot CA + サーバー毎の証明書で対応。
ただし各ホストの宛先をドメイン名で指定可能ならば手段２がお手軽。

# 動作確認環境

JS7 JobScheduler 2.5.3
OpenSSL 1.0.2k-fips
openjdk 11.0.18 2023-01-17 LTS

# 手段１  Root CA のTrustStore、サーバー毎のKeyStoreを生成

各サーバーの証明書は、Root CA が発行する。
またSANに、宛先として指定する値（ホスト名、ドメイン名、IPアドレス）を登録。
Root CA の証明書をTrustStoreに格納することで、各サーバー証明書のチェックが可能となる。

Root CA 証明書を管理しておけば、後から追加でサーバー証明書を作成することも可能。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2529537/afe1f3ab-4406-17b5-2713-81dcc166d9bf.png" width=70%>

## Root CA 証明書（自己署名）作成

各証明書発行用のRoot CA 証明書を自己署名で作成
あわせて TrustStore用に、PKCS#12形式証明書を作成
※要keytoolコマンド (openjdkインストールで利用可)

```shell
cn="JS7 Root CA"
company="Saitamano KUSA Inc."
country="JP"
crtfile="js7_ca.crt"
keyfile="js7_ca.key"
truststore="https-truststore.p12"
p12pass="jobscheduler"
p12alias="$cn"

subject_text="/C=${country}/O=${company}/CN=${cn}"
# ※最小はCNのみ
subject_text="/CN=${cn}"

# 自己署名証明書、秘密鍵を作成
openssl req -new -x509 -days 3650 -newkey rsa:2048 -nodes -out $crtfile -keyout $keyfile -subj "$subject_text"

# 上記証明書より、TrustStore用にPKCS#12形式証明書を作成  ※要openjdk
[ -f $truststore ] && rm $truststore
keytool -importcert -noprompt -file $crtfile -alias "$p12alias" -keystore $truststore -storepass $p12pass -storetype PKCS12 -trustcacerts
```

## 個々のサーバー証明書を作成

各サーバー用の証明書を発行。認証用にSANを追加する。
あわせてKeyStore用に、PKCS#12形式証明書を作成

```shell
#!/usr/bin/env bash

gen_keystore() {
    # SAN 対応のPEM形式証明書を作成し、CA証明書で署名。
    # あわせてKeyStore用のPKCS#12形式証明書を作成
    # $1 でCommon Name を指定（ホスト名、ドメイン名）
    # --domain , --ip でSANに登録する値を指定可能

    declare -a ip_ranges=()
    declare ip_range
    declare -a domain_names=()
    declare domain_name
    declare -a san_values=()
    declare san_value
    declare common_name
    declare subject_text

    declare keyfile
    declare csrfile
    declare crtfile
    declare keystore
    declare p12alias
    declare p12pass="jobscheduler"
    # あらかじめ用意しておいた Root CA 証明書、秘密鍵
    declare ca_keyfile="js7_ca.key"
    declare ca_crtfile="js7_ca.crt"

    options=$(getopt --options i:d:h \
        --longoptions ip:,domain:,help \
        -- "$@") || return 1
    eval set -- "$options"

    while true; do
        case "$1" in
        -i | --ip)
            [ "${2:0:1}" == "-" ] || [ ! "$2" ] && { echo "ip が指定されていません" >&2 && return 1; }
            ip_ranges+=("$2")
            shift 2
            ;;
        -d | --domain)
            [ "${2:0:1}" == "-" ] || [ ! "$2" ] && { echo "domain が指定されていません" >&2 && return 1; }
            domain_names+=("$2")
            shift 2
            ;;
        -h | --help)
            # usage (省略)
            exit
            ;;
        --)
            shift
            break
            ;;
        *)
            echo Error
            return 1
            ;;
        esac
    done

    shift $((OPTIND - 1))

    common_name=$1
    if echo "$common_name" | grep -E '^([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.?)+([a-zA-Z]{2,})?$' >/dev/null; then
        subject_text="/CN=${common_name}"
        san_values+=("DNS:${common_name}")
    else
        echo "CNの指定に問題あり : ${common_name}"
        return 1
    fi

    # 各種出力ファイルはCN名ベースとする
    keyfile="${common_name}.key"
    csrfile="${common_name}.csr"
    crtfile="${common_name}.crt"
    keystore="${common_name}-keystore.p12"
    p12alias="${common_name}"

    # SNI対応用にSAN値を生成
    for domain_name in "${domain_names[@]}"; do
        if echo "$domain_name" | grep -E '^(\*\.)?([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.?)+([a-zA-Z]{2,})?$' >/dev/null; then
            [ "$domain_name" != "$common_name" ] && san_values+=("DNS:${domain_name}")
        else
            echo "ドメイン名またはホスト名ではありません : ${domain_name}"
            return 1
        fi
    done

    for ip_range in "${ip_ranges[@]}"; do
        if echo "$ip_range" | grep -E '^([0-9]{1,3}\.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$' >/dev/null; then
            san_values+=("IP:${ip_range}")
        else
            echo "CIDR表記ではありません : ${ip_range}"
            return 1
        fi
    done

    san_value=$(
        IFS=","
        echo "${san_values[*]}"
    )
    san_value="subjectAltName = $san_value"

    # 秘密鍵、CSR生成
    openssl req -new -newkey rsa:2048 -nodes -out "$csrfile" -keyout "$keyfile" -subj "$subject_text"

    # CSRより証明書をPEM形式で作成、CA証明書で署名  あわせてSANも追加する
    openssl x509 -req -in "$csrfile" -out "$crtfile" -days 3650 -CA $ca_crtfile -CAkey $ca_keyfile -CAcreateserial -extfile <(echo "$san_value")

    # 上記証明書と秘密鍵を使ってPKCS#12 形式で出力（KeyStore用）
    openssl pkcs12 -export -inkey "$keyfile" -in "$crtfile" -out "$keystore" -name "$p12alias" -password "pass:$p12pass"

}

# 証明書作成  --domain, --ip でSANに登録する値を指定
gen_keystore js7joc        --domain "*.local" --domain "*.mshome.net" --domain "localhost" --ip "192.168.11.101" --ip "127.0.0.1"
gen_keystore js7controller --domain "*.local" --domain "*.mshome.net" --domain "localhost" --ip "192.168.11.102" --ip "127.0.0.1"
gen_keystore js7agent      --domain "*.local" --domain "*.mshome.net" --domain "localhost" --ip "192.168.11.103" --ip "127.0.0.1"

```

これにより作られた `https-truststore.p12`をTrustStoreとして、
`js7joc-keystore.p12` `js7controller-keystore.p12` `js7agent-keystore.p12` を各サーバーのKeyStoreとして使用する。

:::note warn
この手段を用いた場合は、Root CA の証明書、秘密鍵を適切に管理してください。
:::

# 手段２  各サーバー共通のKeyStore、TrustStoreを生成

TrustStore、KeyStoreを、共通の証明書ファイルとするやり方

SANの登録値にワイルドカードドメイン名を指定。(alt_names に記載)
※controller, agent 登録時のURLは、このドメイン名で指定する必要あり
（IP指定は非推奨。IPが変更となった場合に、証明書の作り直し、全差し替えが発生する）

内部DNSやmDNS利用で、同一ドメインで名前解決ができるならば、１つの証明書のみで、お手軽に済ませられる。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2529537/a640823b-e11c-e316-5c11-5554e2c631b2.png" width=70%>

:::note
下記のコマンド例は、以下の環境で動作確認済みです
openssl 1.1.1 および 3.0
openjdk 11 および 17
:::

```shell
cn="JobScheduler"
crtfile="https-keystore.crt"
keyfile="https-keystore.key"
keystore="https-keystore.p12"
truststore="https-truststore.p12"
p12pass="jobscheduler"
p12alias="$cn"


# 自己署名でSANに vault-cluster を含めた証明書を生成
openssl req -x509 -days 3650 -nodes -newkey rsa:2048 -out "$crtfile" -keyout "$keyfile" -config - <<EOF
[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no

[req_distinguished_name]
CN = $cn

[v3_req]
keyUsage = digitalSignature, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = localhost
DNS.2 = *.local
DNS.3 = *.mshome.net
IP.1 = 127.0.0.1
EOF

# 上記証明書と秘密鍵を使ってPKCS#12 形式で出力（KeyStore用）
openssl pkcs12 -export -inkey "$keyfile" -in "$crtfile" -out "$keystore" -name "$p12alias" -password "pass:$p12pass"
# 上記証明書を使ってPKCS#12 形式で出力（TrustStore用）
keytool -importcert -noprompt -file $crtfile -alias "$p12alias" -keystore $truststore -storepass $p12pass -storetype PKCS12 -trustcacerts

```

これにより作られた `https-keystore.p12`をKeyStore `https-truststore.p12`をTrustStoreとして使用する。
（上記の例では、宛先に localhost、xxx.local、 xxx.mshome.net、127.0.0.1 のいずれかの指定ならば対応可能）

# 用語

## KeyStore

証明書+秘密鍵を PKCS#12形式で保存したもの
自サーバーの情報を表すために使用

## TrustStore

発行者の証明書を PKCS#12形式で保存したもの
証明書認証時に、接続相手サーバーの証明書が信頼できることを確認するために使用

## SAN（Subject Alternative Name）

CommonNameの別名として、自サーバーの情報を表す。
ドメイン、ホスト名、IPアドレスなどの情報を格納できる。
JOC、Controller、Agentとの接続時に、接続先URLとして指定した値（IP、ホスト名など）と、接続先サーバー証明書上のSANに記載された情報が一致する必要がある。

本例では、SANにホスト名、ドメイン名（ワイルドカード使用可）、IPアドレスを指定する

※接続先がSANに含まれていない場合、認証エラーとなる。
※接続時に常にドメイン名を使う場合は、CommonNameにワイルドカード付ドメイン名などを指定することで、SANは指定しなくても接続可能。ただしこの場合、IP指定での接続はできない

## keytool

証明書の生成、管理を行うコマンド
TrustStore 生成時に必須  ( openssl コマンドでは不可 )
OpenJDK インストールで利用可能
※JS7 インストール環境であれば、OpenJDK が入っている

## KeyStore Explorer

https://keystore-explorer.org/
GUIでの証明書管理が可能。
KeyStore 、TrustStore の内容を確認する際に、インストールしてあると便利。

# 参考

- keytool コマンドの使い方
  - https://docs.oracle.com/javase/jp/6/technotes/tools/windows/keytool.html
  - https://codezine.jp/article/detail/105
- 公式ドキュメント
  - https://kb.sos-berlin.com/pages/viewpage.action?pageId=71009909