Oracle DatabaseでのTLS通信・認証の検証

# 目的
Oracle DBでTLSでの接続をするためにはどのような手順が必要かをまとめます。

大まかな流れとしては以下のようになります。

1. 使用する証明書および証明書を格納するためのOracle Walletの準備（サーバ/クライアント双方）
2. listenerをTCPSでlistenするように構成変更（local_listener/scanの双方）
3. 【オプション】ユーザの認証を外部認証に切り替え、証明書によるユーザ認証を行う

# 構成
この検証は以下のような構成で実施します。
| 項目 | 構成 |
| --- | --- |
| Database | Oracle DB EE 19c |
| オプション | RAC, Advanced Security |
| DB名 | TLSTEST |
| 冗長化 | 2node RAC, SCANリスナーを使用  |

手順内で使用する証明書は以下のCNのようなものを用意します。これは使用する証明書名に応じて適当に読み替えてください
| 環境 | 証明書 | CN |
| --- | --- | --- | 
| DBサーバ | ユーザ証明書 | CN=sample.xxx.co.jp |
| | root CA証明書 | CN=root CAs name |
| | 中間CA証明書 | CN=intermediate CAs name |
| クライアント | ユーザ証明書 | CN=client.name |
| | root CA証明書 | CN=clients root CAs name |

# 検証手順
sqlplus等で最低限の接続が可能なことを確認します。

## TLS通信のための設定

### 証明書作成とimport
DBサーバ上で証明書発行のためにCSRを作成します。証明書のCNは導入するDBのSCANリスナーのDNS名を設定しています。
```sh
## Walletの作成
$ORACLE_HOME/bin/orapki wallet create \
  -wallet $ORACLE_HOME/network/admin/tlstest \
  -auto_login \
  -pwd [walletのpassword]
 
## walletのリストを表示
$ORACLE_HOME/bin/orapki wallet display \
  -wallet $ORACLE_HOME/network/admin/tlstest \
  -pwd [walletのpassword]
 
----- 以下のようになっているはず -----
Requested Certificates:
Subject:        CN=oracle
User Certificates:
Oracle Secret Store entries:
ORACLE.SECURITY.DB.ENCRYPTION.AdUzqfRsSE8/vy/+63H+C4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
（以下略）
 
## CSRの作成
$ORACLE_HOME/bin/orapki wallet add \
  -wallet $ORACLE_HOME/network/admin/tlstest \
  -dn "CN=sample.xxx.co.jp, O=XXXXX Corporation, L=Chiyoda-ku, ST=Tokyo, C=JP" \
  -keysize 2048 \
  -sign_alg sha256 \
  -pwd [walletのpassword]
 
## CSRのexport
$ORACLE_HOME/bin/orapki wallet export \
  -wallet $ORACLE_HOME/network/admin/tlstest \
  -dn "CN=sample.xxx.co.jp, O=XXXXX Corporation, L=Chiyoda-ku, ST=Tokyo, C=JP" \
  -request /home/oracle/work/tns_temp/tlstest.csr \
  -pwd [walletのpassword]

## walletのリストを表示
$ORACLE_HOME/bin/orapki wallet display \
  -wallet $ORACLE_HOME/network/admin/tlstest \
  -pwd [walletのpassword]
 
----- 以下のように追加したCSRの情報が「Requested Certificates」にあるはず -----
Requested Certificates:
Subject:        C=JP,ST=Tokyo,L=Chiyoda-ku,O=XXXXX Corporation,CN=sample.xxx.co.jp
Subject:        CN=oracle
User Certificates:
Oracle Secret Store entries:
ORACLE.SECURITY.DB.ENCRYPTION.AdUzqfRsSE8/vy/+63H+C4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
（以下略）
```

### 証明書の発行とroot証明書、中間証明書の準備
作成したCSRから証明書を発行します。ここでは適当な認証局から証明書を発行させましたが、手順等は環境によると思いますので割愛します。

また、サーバ側で使用するroot証明書や中間証明書を準備しておきます。

### DBの自動ログインWalletの準備
証明書を入れるための自動ログインを準備します。
```sh
## キーストアフォルダ作成
mkdir $ORACLE_BASE/admin/tlstest/wallet
 
## WALLET_ROOTの設定
ORACLE_SID=tlstest1 sqlplus / as sysdba
alter system set WALLET_ROOT='/u01/base/admin/tlstest/wallet' scope=spfile sid='*';
 
## DB再起動
srvctl stop database -db tlstest
srvctl start database -db tlstest
 
## TDE_CONFIGURATION(キーストアの構成)の設定
ORACLE_SID=tlstest1 sqlplus / as sysdba
alter system set TDE_CONFIGURATION="KEYSTORE_CONFIGURATION=FILE" scope=both sid='*';
```


```sh
## 自動ログインwalletを作成
ORACLE_SID=tlstest1 sqlplus / as sysdba
ADMINISTER KEY MANAGEMENT CREATE AUTO_LOGIN KEYSTORE
FROM KEYSTORE '/u01/base/admin/tlstest/wallet/tde'
IDENTIFIED BY <PASSWORD>;
 
## cwalletのファイルがOS上にあるか確認
!ls -R /u01/base/admin/tlstest/wallet/
 
## 再起動でOPENされているか確認する
srvctl stop database -d tlstest
srvctl start database -d tlstest
 
## キーストアの確認
## OPENしてる&wallet_typeがAUTOLOGINになってる
ORACLE_SID=tlstest1 sqlplus / as syskm
set linesize 150
col WRL_TYPE for a10
col WRL_PARAMETER for a30
SELECT * FROM V$ENCRYPTION_WALLET;
 
# 暗号化キー作成
ADMINISTER KEY MANAGEMENT SET KEY FORCE KEYSTORE
IDENTIFIED BY <PASSWORD> WITH BACKUP CONTAINER = ALL;
```

### 証明書をWalletへimport
#### DBサーバ側
発行した証明書やroot証明書をimportします
```sh
## import
### user certをimport
$ORACLE_HOME/bin/orapki wallet add \
  -wallet $ORACLE_HOME/network/admin/tlstest \
  -user_cert \
  -cert /home/oracle/work/user_cert.cer \
  -pwd [walletのpassword]
 
### DB側のtrusted certとしてroot証明書と中間証明書をimport
$ORACLE_HOME/bin/orapki wallet add \
  -wallet $ORACLE_HOME/network/admin/tlstest \
  -trusted_cert  \
  -cert /home/oracle/work/trusted_root_cert.crt \
  -pwd [walletのpassword]
 
$ORACLE_HOME/bin/orapki wallet add \
  -wallet $ORACLE_HOME/network/admin/tlstest \
  -trusted_cert  \
  -cert /home/oracle/work/trusted_intermediate_cert.crt \
  -pwd [walletのpassword]
 
### クライアント側の証明書に対する認証局の証明書をtrustedとしてimport
$ORACLE_HOME/bin/orapki wallet add \
  -wallet $ORACLE_HOME/network/admin/tlstest \
  -trusted_cert  \
  -cert /home/oracle/trusted_for_client_cert.crt \
  -pwd [walletのpassword]

## walletのリストを表示して意図したように証明書が入っているか確認する
$ORACLE_HOME/bin/orapki wallet display \
  -wallet $ORACLE_HOME/network/admin/tlstest \
  -complete \
  -pwd [walletのpassword]
-------------------------------------------------------------------
Requested Certificates:
Subject:        CN=sample.xxx.co.jp
Key Length:     2048
 
Subject:        CN=oracle
Key Length:     2048
 
Subject:        CN=sample.xxx.co.jp
Key Length:     2048

## DB側のユーザ証明書
User Certificates:
Subject:        CN=sample.xxx.co.jp,O=XXXXX Corporation,L=Chiyoda-ku,ST=Tokyo,C=JP
Issuer:         CN=CAs Name,O=XXXXX Corporation,L=Chiyoda-ku,ST=Tokyo,C=JP
Serial Number:  XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Key Length      2048
MD5 digest:     XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA digest:     XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
 
Oracle Secret Store entries:
## （省略）

Trusted Certificates:
## DB側のroot証明書
Subject:        CN=root CAs name,O=XXXXX Corporation,ST=Tokyo,C=JP
Issuer:         CN=root CAs name,O=XXXXX Corporation,ST=Tokyo,C=JP
Serial Number:  XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Key Length      2048
MD5 digest:     XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA digest:     XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
## DB側の中間証明書
Subject:        CN=intermediate CAs name,O=XXXXX Corporation,L=Chiyoda-ku,ST=Tokyo,C=JP
Issuer:         CN=root CAs name,O=XXXXX Corporation,ST=Tokyo,C=JP
Serial Number:  XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Key Length      2048
MD5 digest:     XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA digest:     XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
## クライアント側のCAの証明書
Subject:        CN=clients root CAs name,O=XXXXX Corporation,ST=Tokyo,C=JP
Issuer:         CN=clients root CAs name,O=XXXXX Corporation,ST=Tokyo,C=JP
Serial Number:  XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Key Length      2048
MD5 digest:     XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA digest:     XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
-------------------------------------------------------------------

## 証明書をimportしたwalletファイルをRACの各ノードに配布する
$ ssh [他のノード] "mkdir -p $ORACLE_HOME/network/admin/tlstest"
$ scp $ORACLE_HOME/network/admin/tlstest/{cwallet.sso,ewallet.p12} [他のノード]:$ORACLE_HOME/network/admin/tlstest/

## 証明書の読み込みのためにDB全体を再起動する
$ srvctl stop database -db tlstest
$ srvctl start database -db tlstest
```

#### クライアント側
クライアント側で使用するためのWalletを作成し、証明書をimportする。
```sh
## Walletの作成
$ORACLE_HOME/bin/orapki wallet create \
  -wallet /home/oracle/work/tns_temp \
  -auto_login \
  -pwd [walletのpassword]
 
## クライアント側のCAの証明書をimport
$ORACLE_HOME/bin/orapki wallet add \
  -wallet /home/oracle/work/tns_temp \
  -trusted_cert  \
  -cert /home/oracle/work/tns_temp/trusted_for_client_cert.crt \
  -pwd [walletのpassword]
 
## DB側のtrusted certとしてroot証明書と中間証明書をimport
$ORACLE_HOME/bin/orapki wallet add \
  -wallet /home/oracle/work/tns_temp \
  -trusted_cert  \
  -cert /home/oracle/work/tns_temp/trusted_root_cert.crt \
  -pwd [walletのpassword]
 
$ORACLE_HOME/bin/orapki wallet add \
  -wallet /home/oracle/work/tns_temp \
  -trusted_cert  \
  -cert /home/oracle/work/tns_temp/trusted_intermediate_cert.crt \
  -pwd [walletのpassword]
 
## Walletへのユーザ証明書のimportにあたり、事前に秘密鍵をPKCS#12にしてimportしてやる必要があるためPKCS#12のがファイルを作成する
openssl pkcs12 -export \
  -in /home/oracle/work/tns_temp/service.crt \
  -inkey /home/oracle/work/tns_temp/private.key \
  -out ./service.p12
 
## 作成したPKCS#12のファイルをimport
$ORACLE_HOME/bin/orapki wallet import_pkcs12 \
  -wallet /home/oracle/work/tns_temp \
  -pkcs12file /home/oracle/work/tns_temp/service.p12 \
  -pwd [walletのpassword]
 
## ユーザ証明書をimport
$ORACLE_HOME/bin/orapki wallet add \
  -wallet /home/oracle/work/tns_temp \
  -user_cert  \
  -cert /home/oracle/work/tns_temp/service.crt \
  -pwd [walletのpassword]

## walletのリストを表示して意図したように証明書が入っているか確認する
$ORACLE_HOME/bin/orapki wallet display \
  -wallet /home/oracle/work/tns_temp \
  -complete \
  -pwd [walletのpassword]
-------------------------------------------------------------------
Requested Certificates:
## ユーザ証明書
User Certificates:
Subject:        CN=client.name,O=XXXXX Corporation,ST=Tokyo,C=JP
Issuer:         CN=clients CA name,O=XXXXX Corporation,ST=Tokyo,C=JP
Serial Number:  XX:XX:XX:XX:XX:XX:XX:XX:XX
Key Length      2048
MD5 digest:     XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA digest:     XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX

Trusted Certificates:
## クライアント側のCAの証明書
Subject:        CN=clients root CAs name,O=XXXXX Corporation,ST=Tokyo,C=JP
Issuer:         CN=clients root CAs name,O=XXXXX Corporation,ST=Tokyo,C=JP
Serial Number:  XX:XX:XX:XX:XX:XX:XX:XX:XX
Key Length      2048
MD5 digest:     XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA digest:     XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
## DB側のroot証明書
Subject:        CN=root CAs name,O=XXXXX Corporation,ST=Tokyo,C=JP
Issuer:         CN=root CAs name,O=XXXXX Corporation,ST=Tokyo,C=JP
Serial Number:  XX:XX:XX:XX:XX:XX:XX:XX:XX
Key Length      2048
MD5 digest:     XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA digest:     XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
## DB側の中間証明書
Subject:        CN=intermediate CAs name,O=XXXXX Corporation,L=Chiyoda-ku,ST=Tokyo,C=JP
Issuer:         CN=root CAs name,O=XXXXX Corporation,ST=Tokyo,C=JP
Serial Number:  XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Key Length      2048
MD5 digest:     XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA digest:     XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
-------------------------------------------------------------------
```

### Listener設定/ファイル設定
| 種別 | 設定項目 | 値 |
| --- | --- | --- |
| ローカルリスナー | リスナー名 | LISTENER_TLSTEST |
|  | プロトコル | TCPS |
|  | ポート | 50001 |
| SCANリスナー | プロトコル | TCP, TCPS |
|  | ポート | TCP:50000, TCPS:60000 |
|  | netnum | 1 |

#### Localリスナー
local_listenerおよびファイルの修正を行う
```sh
## リスナー作成用のレスポンスファイルを作成
$ vi /tmp/netca_createListener_dev.rsp
----------以下を記載------------------------------------------------
[GENERAL]
RESPONSEFILE_VERSION="19.0.0"
CREATE_TYPE="CUSTOM"
LOG_FILE="/tmp/netca_listener_dev.log"
[oracle.net.ca]
INSTALLED_COMPONENTS={"server"}
INSTALL_TYPE=""custom""
LISTENER_NUMBER=1
LISTENER_NAMES={"LISTENER_TLSTEST"}
LISTENER_PROTOCOLS={"TCPS;50001"}
LISTENER_START=""LISTENER_TLSTEST""
NAMING_METHODS={"TNSNAMES","EZCONNECT"}
-------------------------------------------------------------------
  
## listener_standby作成と確認
$ netca -silent -responseFile /tmp/netca_createListener_dev.rsp
$ /u01/grid/bin/crsctl stat res ora.LISTENER_TLSTEST.lsnr -t

## 環境変数を設定
$ srvctl setenv listener -l listener_tlstest -env "TNS_ADMIN=/u01/base/app/database/network/admin/tlstest/"
 
## DB側の設定修正、PROTOCOLをTCPSにする部分の修正を見落とさないように注意
$ sqlplus / as sysdba
SQL> alter system set local_listener='(ADDRESS=(PROTOCOL=TCPS)(HOST=192.168.0.1)(PORT=50001))' scope=both sid='tlstest1';
SQL> alter system set local_listener='(ADDRESS=(PROTOCOL=TCPS)(HOST=192.168.0.2)(PORT=50001))' scope=both sid='tlstest2';
 
## オプション
## もしTCPSと並行してTCPもlistenさせたい場合、以下のように複数指定することで設定できる
$ srvctl modify listener -endpoints "TCPS:50002/TCP:50003"
SQL> alter system set local_listener='(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=192.168.0.1)(PORT=50003))(ADDRESS=(PROTOCOL=TCPS)(HOST=192.168.0.1)(PORT=50002))))' scope=both sid='tlstest1';
SQL> alter system set local_listener='(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=192.168.0.2)(PORT=50003))(ADDRESS=(PROTOCOL=TCPS)(HOST=192.168.0.2)(PORT=50002))))' scope=both sid='tlstest2';
 
## DB側のTNS_ADMINのディレクトリにあるファイルを修正する
$ export TNS_ADMIN=/u01/base/app/database/network/admin/tlstest/
$ vi $TNS_ADMIN/{sqlnet,listener}.ora
 
## 編集後にファイルをRACの各ノードに配布する
$ ssh [他のノード] "mkdir /u01/base/app/database/network/admin/tlstest/"
$ scp $TNS_ADMIN/{sqlnet,listener}.ora [他のノード]:/u01/base/app/database/network/admin/tlstest/
```
sqlnet.oraとlistener.oraのファイルに追記する。
* sqlnet.oraのサンプル

TCPも併用する場合はAUTHENTICATION_SERVICESにTCPも入れること。その場合はおそらくSSL_CLIENT_AUTHENTICATIONはFALSEにする必要あり
```sh
#Set the Authentication method to include SSL Authentication. TCPS means SSL authentication.
SQLNET.AUTHENTICATION_SERVICES = (BEQ,TCPS)
 
#Specify the wallet location with wallet_location parameter
wallet_location =
 (SOURCE=
  (METHOD=File)
  (METHOD_DATA=
   (DIRECTORY=/u01/base/admin/tlstest/wallet/tde)
  )
 )
 
## このパラメータは19cのマニュアルに存在しない？
## セキュリティ・ガイドによるとTRUEだとmTLS必須になるとのこと
#Set the below parameter to TRUE, for users to be authenticated by Database with SSL certificates,
SSL_CLIENT_AUTHENTICATION=TRUE
 
#This parameter is recommended to force the use of TLS latest version 1.2
SSL_VERSION=1.2
```
* listener.oraのサンプル

Doc ID 2732618.1によると仕様としてTCPSのみのエンドポイントを持つリスナーは起動できない。そのためIPCのエンドポイントは必須
```sh
LISTENER_tlstest=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=IPC)(KEY=LISTENER_tlstest))))          # line added by Agent
ENABLE_GLOBAL_DYNAMIC_ENDPOINT_LISTENER_tlstest=ON             # line added by Agent
VALID_NODE_CHECKING_REGISTRATION_LISTENER_tlstest=SUBNET               # line added by Agent
 
#listener uses the same wallet that database uses
wallet_location =
 (SOURCE=
  (METHOD=File)
  (METHOD_DATA=
   (DIRECTORY=/u01/base/admin/tlstest/wallet/tde)
  )
 )
 
#This parameter should be false as listener is not going to authenticate the clients. It is the server process that authenticates the clients.
SSL_CLIENT_AUTHENTICATION=TRUE
 
#This parameter is recommended to force the use of TLS latest version 1.2
SSL_VERSION=1.2
```

listener設定変更内容の反映のために再起動する。
```sh
$ srvctl stop listener -l listener_tlstest
$ srvctl start listener -l listener_tlstest
```

DBにTNS_ADMINの環境変数を設定して再起動する
```sh
$ srvctl setenv database -db tlstest -env TNS_ADMIN=$ORACLE_HOME/network/admin/tlstest/
$ srvctl stop database -db tlstest
$ srvctl start database -db tlstest
```

listenerのstatusが正常かを確認する
```sh
$ lsnrctl status listener_tlstest
 -------------------------------------------------------------------
LSNRCTL for Linux: Version 19.0.0.0.0 - Production on 22-DEC-2022 18:06:47
 
Copyright (c) 1991, 2021, Oracle.  All rights reserved.
 
Connecting to (DESCRIPTION=(ADDRESS=(PROTOCOL=IPC)(KEY=LISTENER_tlstest)))
STATUS of the LISTENER
------------------------
Alias                     listener_tlstest
Version                   TNSLSNR for Linux: Version 19.0.0.0.0 - Production
Start Date                22-DEC-2022 16:07:37
Uptime                    0 days 1 hr. 59 min. 10 sec
Trace Level               off
Security                  ON: Local OS Authentication
SNMP                      OFF
Listener Parameter File   /u01/base/app/database/network/admin/tlstest/listener.ora
Listener Log File         /u01/base/diag/tnslsnr/oradev5025/listener_tlstest/alert/log.xml
Listening Endpoints Summary...
-------------------------------------------------------------------

## 疎通確認
$ tnsping "(ADDRESS=(PROTOCOL=tcps)(host=192.168.0.1)(port=50001))"
TNS Ping Utility for Linux: Version 10.2.0.4.0 - Production on 10-SEP-2008 15:09:12
 
Copyright (c) 1997,  2007, Oracle.  All rights reserved.
 
Attempting to contact (ADDRESS=(PROTOCOL=tcps)(HOST=xxx.yyy.zz)(PORT=XXXX))
OK (100 msec) ## TCPと比べるとtnspingのレスポンスが恐ろしく遅い
```

#### SCANリスナー
既存のSCNAリスナー（TCP:50000）にTCPSのエンドポイント（TCPS:60000）を追加する想定での手順となります。SCANリスナーは1つのネットワークに1つしか作成できないため、既存のDBをTLS化する場合はこのようにエンドポイントを追加する形となります。
```sh
## 既存のSCANにTCPS:60000のエンドポイントを追加する
## netnumは環境に応じて変更してください
$ sudo su - grid
$ srvctl config scan_listener -netnum 1
$ srvctl modify scan_listener -netnum 1 -endpoints "TCP:50000/TCPS:60000"
 
## DB側の設定
$ sudo su - oracle
$ export ORACLE_SID=tlstest1
$ sqlplus / as sysdba
SQL> alter system set remote_listener='(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCPS)(HOST=sample.xxx.co.jp)(PORT=60000)))' scope=both sid='*';
```

SCAN用のOracle Walletを用意します。
```sh
## Walletディレクトリの作成
$ mkdir -p /u01/base/admin/scan_listener/wallet/tde
 
## Walletの作成
$ORACLE_HOME/bin/orapki wallet create \
  -wallet /u01/base/admin/scan_listener/wallet/tde \
  -auto_login \
  -pwd [walletのpassword]
 
## import
### user certをimport
$ORACLE_HOME/bin/orapki wallet add \
  -wallet $ORACLE_BASE/admin/scan_listener/wallet/tde \
  -user_cert \
  -cert /home/grid/work/user_cert.cer \
  -pwd [walletのpassword]

### DB側のtrusted certとしてroot証明書と中間証明書をimport
$ORACLE_HOME/bin/orapki wallet add \
  -wallet $ORACLE_BASE/admin/scan_listener/wallet/tde \
  -trusted_cert  \
  -cert /home/oracle/work/trusted_root_cert.crt \
  -pwd [walletのpassword]
 
$ORACLE_HOME/bin/orapki wallet add \
  -wallet $ORACLE_BASE/admin/scan_listener/wallet/tde \
  -trusted_cert  \
  -cert /home/grid/work/trusted_intermediate_cert.crt \
  -pwd [walletのpassword]
 
### クライアント側の証明書に対する認証局の証明書をtrustedとしてimport
$ORACLE_HOME/bin/orapki wallet add \
  -wallet $ORACLE_BASE/admin/scan_listener/wallet/tde \
  -trusted_cert  \
  -cert /home/grid/trusted_for_client_cert.crt \
  -pwd [walletのpassword]
 
## walletのリストを表示して意図したように証明書が入っているか確認する（DBと同じになるはず）
$ORACLE_HOME/bin/orapki wallet display \
  -wallet $ORACLE_BASE/admin/scan_listener/wallet/tde \
  -complete \
  -pwd [walletのpassword]
 
## 作成したwalletファイルを各ノードに配布する
$ ssh [他のノード] "mkdir -p $ORACLE_BASE/admin/scan_listener/wallet/tde"
$ scp $ORACLE_BASE/admin/scan_listener/wallet/tde/{cwallet.sso,ewallet.p12} [他のノード]:$ORACLE_BASE/admin/scan_listener/wallet/tde/
```

TCPS用のSCANリスナー向けにsqlnet.ora等のファイル設定を行う
```sh
$ sudo su - grid
$ export TNS_ADMIN=/u01/grid/network/admin
$ vi $TNS_ADMIN/{sqlnet,listener}.ora
 
// 編集後にファイルを各ノードに配布する
$ scp $TNS_ADMIN/{sqlnet,listener}.ora [他のノード]:/u01/grid/network/admin
```
* sqlnet.ora
```sh
#Set the Authentication method to include SSL Authentication. TCPS means SSL authentication.
#既存のTCP経由のSCAN接続とファイル共有するのでここは設定しない（＝allにする）
#SQLNET.AUTHENTICATION_SERVICES = (BEQ,TCPS)
 
#Specify the wallet location with wallet_location parameter
wallet_location =
 (SOURCE=
  (METHOD=File)
  (METHOD_DATA=
   (DIRECTORY=/u01/base/admin/scan_listener/wallet/tde)
  )
 )
 
## このパラメータはマニュアルに存在しない？
#Set the below parameter to TRUE, for users to be authenticated by Database with SSL certificates,
#既存のTCP経由のSCAN接続とファイル共有するのでここは設定しない
#SSL_CLIENT_AUTHENTICATION=TRUE
 
#This parameter is recommended to force the use of TLS latest version 1.2
SSL_VERSION=1.2
```
* listener.ora
```sh
wallet_location =
 (SOURCE=
  (METHOD=File)
  (METHOD_DATA=
   (DIRECTORY=/u01/base/admin/scan_listener/wallet/tde)
  )
 )
 
#This parameter should be false as listener is not going to authenticate the clients. It is the server process that authenticates the clients.
SSL_CLIENT_AUTHENTICATION=FALSE
 
#This parameter is recommended to force the use of TLS latest version 1.2
SSL_VERSION=1.2
```

SCAN_LISTENERの再起動を行う。
```sh
$ sudo su - grid
 
## SCAN_LISTENERを順番に再起動する。scannumberの数は環境に合わせて修正すること
$ srvctl status scan_listener -netnum 1
$ srvctl stop scan_listener -netnum 1 -scannumber 1
$ srvctl start scan_listener -netnum 1 -scannumber 1
$ srvctl stop scan_listener -netnum 1 -scannumber 2
$ srvctl start scan_listener -netnum 1 -scannumber 2
$ srvctl stop scan_listener -netnum 1 -scannumber 3
$ srvctl start scan_listener -netnum 1 -scannumber 3
```

### クライアント側の設定ファイル（tnsnames.ora/sqlnet.ora）準備
以下のように修正する
* tnsnames.ora
```sh
tlstest=
  (DESCRIPTION =
    (ADDRESS = (PROTOCOL = TCPS)(HOST = 100.70.247.13)(PORT = 50001))
    (CONNECT_DATA =
      (SERVICE_NAME = tlstest)
    )
    (SECURITY=
     (SSL_SERVER_CERT_DN="CN=sample.xxx.co.jp,O=XXXXX Corporation,L=Chiyoda-ku,ST=Tokyo,C=JP")
    )
  )
 
tlstest_scan=
  (DESCRIPTION =
    (ADDRESS = (PROTOCOL = TCPS)(HOST = sample.xxx.co.jp)(PORT = 60000))
    (CONNECT_DATA =
      (SERVICE_NAME = tlstest)
    )
    (SECURITY=
     (SSL_SERVER_CERT_DN="CN=sample.xxx.co.jp,O=XXXXX Corporation,L=Chiyoda-ku,ST=Tokyo,C=JP")
    )
  )
```
* sqlnet.ora
```sh
NAMES.DIRECTORY_PATH= (TNSNAMES, EZCONNECT)
 
#TCPS ensures that SSL authentication will be performed.
SQLNET.AUTHENTICATION_SERVICES= (TCPS)
 
#Specify the client wallet location
WALLET_LOCATION =
 (SOURCE =
   (METHOD = FILE)
   (METHOD_DATA =
     (DIRECTORY = /home/user/tns)
   )
 )
 
#This parameter is recommended to force the use of TLS latest version 1.2
SSL_VERSION=1.2
 
#Set this parameter ON so that SSL_SERVER_CERT_DN in tnsnames.ora is effective
SSL_SERVER_DN_MATCH=ON
```

## DB接続サンプル（with 証明書認証）
### 事前準備
DBにユーザを作成して接続確認を行います。ここではユーザの認証も証明書で行っています（外部認証）
```sql
SQL> create user tlstest_user identified externally as 'CN=client.name,O=XXXXX Corporation,ST=Tokyo,C=JP';
 
User created.
 
SQL> grant create session to tlstest_user;
 
Grant succeeded.
 
-- 設定内容の確認
SQL> select username, external_name from dba_users where username = 'TLSTEST_USER';

USERNAME      EXTERNAL_NAME
------------- -----------------------------------------------------------
TLSTEST_USER  cn=client.name,O=XXXXX Corporation,st=Tokyo,c=JP
```

### sqlplus
sqlplusの場合はTNS_ADMINを適切に指定するだけでOKです。
```sh
$ export TNS_ADMIN=/home/oracle/work/tns_temp
$ sqlplus /@tlstest_scan
 
SQL> show user
USER is "TLSTEST_USER"
```
### Java
いくつか方法はありますが、ここではjksを用います。

まずはここまでで作成したOracle Walletをjksに変換します
```sh
$ORACLE_HOME/bin/orapki wallet pkcs12_to_jks \
  -wallet /home/oracle/work/tns_temp \
  -pwd [Oracle Walletのpassword] \
  -jksKeyStoreLoc /home/oracle/work/tns_temp/keystore.jks \
  -jksKeyStorepwd [Keystoreのpassword] \
  -jksTrustStoreLoc /home/oracle/work/tns_temp/truststore.jks \
  -jksTrustStorepwd [Trust Keystoreのpassword]
```

続いてjarファイルを用意します。19c時点ではTLS接続する場合は通常のjdbcのjar以外に以下のものが必要です。
（厳密にはTLS通信のみの場合は不要かもしれません。TLS認証まで行う場合はこれらのjarが無いとエラーになることを確認しています）
* oraclepki.jar
* osdt_cert.jar
* osdt_core.jar

また、JDKが8u161未満の場合、JCEが必要となるので注意下さい。（※この検証手順では使用していません）
* local_policy.jar
* US_export_policy.jar

以下のような単純に接続するだけのコードで接続を確認します。ここではTLS認証を行うためユーザ名やパスワードを指定しないように注意ください（指定してしまうとパスワード認証を試みてしまいエラーになるケースがありました）

Oracle社のgithubリポジトリの以下のコードも参考になるかと思います。

https://github.com/oracle-samples/oracle-db-examples/blob/main/java/jdbc/ConnectionSamples/DataSourceForJKS.java
```java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.CallableStatement;
import java.io.PrintWriter;
import java.io.IOException;

import java.util.Properties;
import java.util.Scanner;

import java.util.logging.ConsoleHandler;
import java.util.logging.StreamHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
import oracle.jdbc.driver.OracleLog;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;

import oracle.jdbc.OracleConnection;
import oracle.ucp.jdbc.PoolDataSource;
import oracle.ucp.jdbc.PoolDataSourceFactory;
import oracle.ucp.jdbc.ValidConnection;

public class testTLSAuth
{
    private static final String DB_URL = "jdbc:oracle:thin:/@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCPS)(HOST=sample.xxx.co.jp)(PORT=60000))(CONNECT_DATA=(SERVICE_NAME=tlstest))(SECURITY=(SSL_SERVER_CERT_DN=CN=sample.xxx.co.jp,O=XXXXX Corporation,L=Chiyoda-ku,ST=Tokyo,C=JP)))";
    private static final String JKSPASS = "*****";
    private static final String TNSADMIN = "/home/oracle/work/tns_temp/";
    private static final String KEYSTORE = TNSADMIN + "keystore.jks";
    private static final String TRUSTSTORE = TNSADMIN + "truststore.jks";

    private static PoolDataSource pds;
    private static Connection conn;

    private static int loop = 1;

    public static void main(String[] args) throws SQLException, InterruptedException
    {
        pds = PoolDataSourceFactory.getPoolDataSource();

        pds.setConnectionPoolName("FCFSamplePool");
        pds.setFastConnectionFailoverEnabled(true);
        pds.setConnectionFactoryClassName("oracle.jdbc.replay.OracleDataSourceImpl");

        pds.setURL(DB_URL);
        pds.setInitialPoolSize(1);
        pds.setMinPoolSize(1);
        pds.setMaxPoolSize(2);
        pds.setMaxConnectionReuseTime(5);
        pds.setInactiveConnectionTimeout(60);
        pds.setValidateConnectionOnBorrow(false);

        Properties connProps = new Properties();
        connProps.put(OracleConnection.CONNECTION_PROPERTY_THIN_SSL_SERVER_DN_MATCH,"true");
        connProps.put(OracleConnection.CONNECTION_PROPERTY_TNS_ADMIN, TNSADMIN);
        connProps.put(OracleConnection.CONNECTION_PROPERTY_THIN_SSL_VERSION,"1.2");
        connProps.put(OracleConnection.CONNECTION_PROPERTY_THIN_JAVAX_NET_SSL_KEYSTORE, KEYSTORE);
        connProps.put(OracleConnection.CONNECTION_PROPERTY_THIN_JAVAX_NET_SSL_KEYSTOREPASSWORD, JKSPASS);
        connProps.put(OracleConnection.CONNECTION_PROPERTY_THIN_JAVAX_NET_SSL_TRUSTSTORE, TRUSTSTORE);
        connProps.put(OracleConnection.CONNECTION_PROPERTY_THIN_JAVAX_NET_SSL_TRUSTSTOREPASSWORD, JKSPASS);
        pds.setConnectionProperties(connProps);

        System.out.println("Using " + DB_URL);
        System.out.println("Startting to exec SQLs");

        testConnectDB(pds);

    }

    private static void testConnectDB(PoolDataSource pds){
        for (int i=0; i<loop; i++){
            System.out.println("start application");
            try{
                conn = pds.getConnection();

                String sql = "select instance_name as col from v$instance";
                PreparedStatement pStmt1 = conn.prepareStatement(sql);
                ResultSet rs1 = pStmt1.executeQuery();
                while(rs1.next()) {
                    String col = rs1.getString("col");
                    System.out.println("[SQL] result: " + col);
                }
                pStmt1.close();

                conn.close();
                conn = null;
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}
```
### Python
python-oracledbを使用する場合のサンプルとなります。以下のような簡単なコードで接続を確認しました。
```python
import oracledb
import os
 
try:
    oracledb.init_oracle_client(config_dir="/home/oracle/work/tns_temp")
except Exception as err:
    print("Whoops!")
    print(err)
    sys.exit(1)
 
with oracledb.connect(externalauth=True, dsn="tlstest_scan") as connection:
    with connection.cursor() as cursor:
        sql = """select 1 from dual"""
        for r in cursor.execute(sql):
            print(r)
```

# TLS接続の手順や運用の課題
検証の手順にあたり、私の環境では以下のような点を課題に感じておりますので、共有します。

#### TLS認証ではDB側で認証失敗回数に応じたアカウントロックができない
TLS認証のような外部認証の機構を使用する場合、サポートによると通常のパスワード認証で使用するユーザのprofileが適用されません。

そのため、パスワード認証であればprofileの`FAILED_LOGIN_ATTEMPTS`でできる認証失敗に応じたアカウントロックがTLS認証ではできません。セキュリティ面でアカウントロックの要件があるような場合、TLS認証では別の手段を講じる必要があります。

なお、パスワード認証であれば`user$`のLCOUNTカラムからログイン失敗回数を取得できるが、TLS認証ではこのカラムには計上されないようです。そのため、自前でログイン失敗回数を元にアカウントロックを実装するというのも困難かと思われます。

#### 既存の非TLS認証の環境への導入のハードル
RACの仕様でSCANリスナーは同一ネットワークに1つしか作成できないため、TCPS用のSCANリスナーを別個に作成するといったことが難しいです。

そのため既存の非TLS認証の環境にTLS認証を導入するにはSCANリスナーにエンドポイントを追加する必要があり、なかなかオンラインでの実施は難しいと感じます。

#### 透過的暗号化（TDE）のWalletとの棲み分け
私の環境では表領域の暗号化でTDEを使用しており、TDE用のWalletを元々作成していました。

TLS接続でもWalletは使用しますが、サポートによるとTDE用のWalletとは別に作成することが必須のようです。検証の最初はTDEのWalletにTLS接続用の証明書などを入れていましたが、Walletがopenできなくなってしまったため必ず分けて使用ください。

#### TLS認証時の統合監査の接続元OSユーザ名
TLS認証を行った場合、統合監査の`unified_audit_trail`でOS_USERNAMEにexternal_nameの値が入ってしまいます。

パスワード認証ではOSのユーザ名が入っていましたが、TLS認証では接続元のOS名が識別できなくなってしまいます。

#### orapkiの使用
この検証手順ではクライアント側もorapkiを使用していますが、orapkiはフルクライアントにしか入っていません。

多くのクライアントはインスタントクライアントやjdbc thinを使用しているかと思いますので、クライアント側でフルクライアントを用意していただく手間があります。

# 参考資料
セキュリティ・ガイド（19c）
https://docs.oracle.com/cd/F19136_01/dbseg/index.html
* 12.12 データベース通信のセキュリティを強化するためのパラメータ
* 18.1.4 ネイティブ・ネットワーク暗号化とTransport Layer Securityの間の選択

エンタープライズ・ユーザ・セキュリティガイド（19c）
https://docs.oracle.com/cd/F19136_01/dbimi/index.html

SQL言語リファレンス（19c）（create user）
https://docs.oracle.com/cd/F19136_01/sqlrf/CREATE-USER.html#GUID-F0246961-558F-480B-AC0F-14B50134621C

JDBC開発者ガイド、SSLのサポート
https://docs.oracle.com/cd/F19136_01/jjdbc/client-side-security.html#GUID-2BD2F189-A58C-4A85-8524-CFD9BB9AC575

Oracle Data Safeの管理
https://docs.oracle.com/cd//E83857_01/paas/data-safe/admds/preface.html

TLS接続用のウォレットまたは証明書の作成（自己署名の証明書で実施するケースの手順＋jksの作成の手順）
https://docs.oracle.com/cd//E83857_01/paas/data-safe/admds/create-wallet-or-certificates-tls-connection.html#GUID-D4132279-C55F-43A4-8ADA-B9EBEB02DCE0

Oracle Cloud Infrastructure Documentation（About Connecting to an Autonomous Database Instance）
https://docs.oracle.com/en-us/iaas/autonomous-database-shared/doc/connect-introduction.html
https://docs.oracle.com/cd//E83857_01/paas/autonomous-database/adbsa/connect-preparing.html#GUID-3667EC68-930E-4566-95B3-DFA24203A8FF

SSL Connection to Oracle DB using JDBC, TLSv1.2, JKS or Oracle Wallets (12.2 and lower)
https://blogs.oracle.com/developers/post/ssl-connection-to-oracle-db-using-jdbc-tlsv12-jks-or-oracle-wallets-122-and-lower