1. 記事一覧 >
  2. ブログ記事
category logo

EJBCAからPKCS#11でSoftHSM2にキーペアを登録してみた

(更新) (公開)

はじめに

前回記事では、EJBCA Community で TLS 証明書発行をやってみました。そのとき、秘密鍵は、HSM(Hardware Security Module)を使わずに EJBCA 自身で保存するようにしました。
EJBCA 自身で保存するトークンのことを ソフト トークン(soft token) と呼んでいるようですが、今回は、ハード トークン(hard token) すなわち、キーペア(公開鍵/秘密鍵)を HSM に保存するということをやってみます。


ということで、有名そうな HSM の一つ Thales Luna を Ama●on でポチっと...


...というふうに気軽に入手できる代物ではありませんでした!たぶん、マジモンの場合、クルマ買えるくらいの見積書が送られてきます。


無料の opendnssec/SoftHSMv2 を使います。
今回、softhsm2 インストールから EJBCA からのキーペア登録、キーペアの移行について試みていきます。


EJBCA - SoftHSMv2 図


SoftHSMv2 は、オープンソースのソフトウェアで、ソフトウェア的に HSM を模倣しているだけで、HSM の代替になるものではありません。

つまり、「本物の HSM 高いし、予算無いから、SoftHSM でいいや。」と運用を始めるとセキュリティ的に大変危険です。

SoftHSMv2 は、HSM への投資をためらっている開発者のために、HSM のようなことができるテスト用のツールという位置付けです。

SoftHSMv2 自身の自己紹介文でもそう説明しています。

【 PKCS #11 】

PKCS #11(Public-Key Cryptography Standard #11)は、RSA Security によって策定された暗号化情報を保持し、暗号化機能を実行する暗号化デバイスへのアプリケーションプログラミングインターフェース(API)を定義した規格です。

別記事「EJBCA(PKI および証明書管理アプリ)をビルドしてインストールしてみた」で作成した以下の環境です。少しでも環境が異なると、途中で詰まるかもしれません。

Ubuntu Desktop 22.04.3 LTS

  EJBCA 8.2.0.1 Community

  openjdk version "11.0.21" 2023-10-17

  wildfly 26.0.0.Final

  mysql Ver 15.1 Distrib 10.6.16-MariaDB

  Apache Ant(TM) version 1.10.12

本記事情報の設定不足、誤りにより何らかの問題が生じても、一切責任を負いません。


softhsm2 インストール

まずは、softhsm2 をインストールします。

# apt update -y
# apt install softhsm2 -y
取得:1 http://jp.archive.ubuntu.com/ubuntu jammy/universe amd64 softhsm2-common amd64 2.6.1-2ubuntu1 [7,168 B]
取得:2 http://jp.archive.ubuntu.com/ubuntu jammy/universe amd64 libsofthsm2 amd64 2.6.1-2ubuntu1 [268 kB]
取得:3 http://jp.archive.ubuntu.com/ubuntu jammy/universe amd64 softhsm2 amd64 2.6.1-2ubuntu1 [177 kB]
453 kB を 3秒 で取得しました (144 kB/s)
以前に未選択のパッケージ softhsm2-common を選択しています。
(データベースを読み込んでいます ... 現在 210742 個のファイルとディレクトリがインストールされています。)
.../softhsm2-common_2.6.1-2ubuntu1_amd64.deb を展開する準備をしています ...
softhsm2-common (2.6.1-2ubuntu1) を展開しています...
以前に未選択のパッケージ libsofthsm2 を選択しています。
.../libsofthsm2_2.6.1-2ubuntu1_amd64.deb を展開する準備をしています ...
libsofthsm2 (2.6.1-2ubuntu1) を展開しています...
以前に未選択のパッケージ softhsm2 を選択しています。
.../softhsm2_2.6.1-2ubuntu1_amd64.deb を展開する準備をしています ...
softhsm2 (2.6.1-2ubuntu1) を展開しています...
softhsm2-common (2.6.1-2ubuntu1) を設定しています ...

パーミッションを確認します。

# ls -ld /var/lib/softhsm
drwxrws--- 3 root softhsm 4096  2月 11 21:24 /var/lib/softhsm

drwxrws---s は、セット GID(Set Group ID)です。

drwxrws--- のパーミッションが設定されたディレクトリでは、そのディレクトリ内で作成される全ての新しいファイルやディレクトリは、元のディレクトリと同じグループ ID を持ちます。


これにより、softhsm 関連のグループ名が softhsm グループということが分かりますので、EJBCA を実行しているユーザー権限 wildflysofthsm グループに追加します。

ods グループのときもあるようです。

# usermod -aG softhsm wildfly

softhsm2 初期化

トークンを ラベル=slot1 で初期化します。
ここで、SO と User の PIN(パスワード)を入力します。

# softhsm2-util --init-token --free --label slot1
Slot 0 has a free/uninitialized token.
=== SO PIN (4-255 characters) ===
Please enter SO PIN: ********
Please reenter SO PIN: ********
=== User PIN (4-255 characters) ===
Please enter user PIN: ********
Please reenter user PIN: ********
The token has been initialized and is reassigned to slot 661497608

【 softhsm2-util 】

--init-token:トークン初期化プロセスを開始します。このオプションが指定されていない場合、トークンは初期化されません。

--free:最初の空きスロット(未初期化トークン)を使用してトークンを初期化します。このオプションを指定しない場合、スロット番号を明示的に指定する必要があります。

--label:トークンラベルを設定します。ラベルは、トークンを識別するために使用されます。

【 スロット 】

HSM(ハードウェアセキュリティモジュール)の用語である「スロット」は、一般的には「引き出し」や「ロッカー」に例えられます。

この「引き出し」や「ロッカー」は、それぞれ独立した空間で、各々が異なる「トークン」(鍵の保管庫)を保持します。

具体的には、HSM のスロットは以下のような機能を持っています:

・トークン(鍵の保管庫)を保持する

・トークンを初期化する

・トークンを再初期化する

これらの機能により、各スロットは独立したトークンを管理し、それぞれのトークンに対する操作(初期化、再初期化など)を行うことができます。

1 つの HSM デバイスに複数のスロットが存在することがあります。

【 トークン 】

HSM(ハードウェアセキュリティモジュール)の用語である「トークン」は、一般的には「鍵の保管庫」や「金庫」に例えられます。

この「金庫」は、暗号化や電子署名に利用する鍵を安全に保管し、管理する役割を果たします。

【 SO と User 】

Security Officer (SO)は、セキュリティトークンの管理者(セキュリティ担当者)で、トークンの初期化や再初期化などの管理操作を行う役割を持っています。

一方、ユーザーは、SO が初期化したトークンを使用して、秘密鍵や公開鍵などの暗号キーを生成、保存、使用することができます。

具体的には、SO は以下のような権限を持っています:

・トークンの初期化

・ユーザー PIN のリセット

・トークンの再初期化

一方、ユーザーは以下のような権限を持っています:

・暗号キーの生成と使用

・PIN の変更

したがって、SO とユーザーは異なる役割と権限を持っており、SO はトークンの管理者であり、ユーザーはトークンを使用する者という位置付けになります。

このように、SO とユーザーの役割を分けることで、セキュリティの確保と操作の柔軟性が向上します。


利用可能なスロット情報を表示します。

# softhsm2-util --show-slots
Available slots:
Slot 661497608
    Slot info:
        Description:      SoftHSM slot ID 0x276da708
        Manufacturer ID:  SoftHSM project
        Hardware version: 2.6
        Firmware version: 2.6
        Token present:    yes
    Token info:
        Manufacturer ID:  SoftHSM project
        Model:            SoftHSM v2
        Hardware version: 2.6
        Firmware version: 2.6
        Serial number:    c630b01f276da708
        Initialized:      yes
        User PIN init.:   yes
        Label:            slot1
Slot 1
    Slot info:
        Description:      SoftHSM slot ID 0x1
        Manufacturer ID:  SoftHSM project
        Hardware version: 2.6
        Firmware version: 2.6
        Token present:    yes
    Token info:
        Manufacturer ID:  SoftHSM project
        Model:            SoftHSM v2
        Hardware version: 2.6
        Firmware version: 2.6
        Serial number:
        Initialized:      no
        User PIN init.:   no
        Label:

Label: slot1 が登録されました!

スロットの ID は自動的に振られます。


/var/lib/softhsm/tokens/ 配下にトークンが作成されています。

# find /var/lib/softhsm/tokens/ -ls
 23462679      4 drwxrws---   3 root     softhsm      4096  2月 11 21:25 /var/lib/softhsm/tokens/
 23462645      4 drwx--S---   2 root     softhsm      4096  2月 11 21:25 /var/lib/softhsm/tokens/56d61a9e-6da5-b279-c630-b01f276da708
 23462683      4 -rw-------   1 root     softhsm         8  2月 11 21:32 /var/lib/softhsm/tokens/56d61a9e-6da5-b279-c630-b01f276da708/generation
 23462682      0 -rw-------   1 root     softhsm         0  2月 11 21:30 /var/lib/softhsm/tokens/56d61a9e-6da5-b279-c630-b01f276da708/token.lock
 23462647      4 -rw-------   1 root     softhsm       320  2月 11 21:30 /var/lib/softhsm/tokens/56d61a9e-6da5-b279-c630-b01f276da708/token.object

drwx--S---S は、SUID (Set User ID) ビットが設定されていることを意味します。

SUID ビット は、ファイル所有者の権限でプログラムを実行できる特別な権限です。つまり、通常は実行権限がないユーザーでも、SUID ビットが設定された(ディレクトリの中の)プログラムを実行すれば、ファイル所有者と同じ権限で実行することができます。


root にしか参照書き込み権がないため、EJBCA 実行ユーザーである wildfly を owner にします。

# chown -R wildfly /var/lib/softhsm/tokens

キーをトークンに追加

EJBCA を使用して、キーをトークンに追加します。


まずは、wildfly を再起動します。

# systemctl restart wildfly

どハマり注意:これをしないと、スロットが認識されません!


EJBCA Admin UI から、CA Functions - Crypto Tokens にアクセスして、Create new... をクリックします。

CA Functions - Crypto Tokens - Create new


Name に 任意の 暗号トークン名を入力します。ここでは、 SoftHSM2Token とします。
Type のところを PKCS#11 に変更します。
PKCS#11 : Library のところが SoftHSM 2 になっていることを確認して、
PKCS#11 : Reference TypeSlot ID とします。
PKCS#11 : Reference に スロット ID すなわち、
先ほど、softhsm2-util --show-slots で確認した、
661497608 を入力します。
Authentication Code は、この EJBCA の Crypto Token をアクティベートするパスワードを入力します。(任意の文字列です。PIN ではありません。)
入力したら、Save をクリックします。

Crypto Token Save


PKCS#11 : Reference Type について、Slot/Token Label を選択すると、自動的に見つかった slot1 が表示されるため、それでも良いです。
Slot/Token Label


Crypto Token が登録されました

Crypto Token が登録されました!(まだ鍵を登録したわけではありません。)


続いて、キーペアを作成します。


signKey と入力されているところの Generate new key pair ボタンをクリックします。

Generate new key pair

signKey は、Alias で PKCS #11 属性で言うところの ID になります。任意の名前を付けられます。

RSA 4096 と表示されているところは、アルゴリズム選択で、いろいろな種類の ECDSA(楕円曲線暗号)から選択できたり、ポスト量子暗号を選べたり、いろいろあります。

アルゴリズム選択

キーペア作成後

登録されました!


登録に失敗したときは、以下のようにエラー表示されます。
Error: Error when creating Crypto Token with ID -1759357103.
Crypto Token登録エラー


エラーケース別に
/opt/wildfly-26.0.0.Final/standalone/log/server.log
に出力されたログは、以下です。


再起動していない
2024-02-11 11:38:06,957 ERROR [com.keyfactor.util.keys.token.pkcs11.SunP11SlotListWrapper] (default task-4) Wrong arguments were passed to sun.security.pkcs11.wrapper.PKCS11.CK_C_INITIALIZE_ARGS.getInstance threw an exception for log.error(msg, e): java.lang.reflect.InvocationTargetException
(...略...)
Caused by: sun.security.pkcs11.wrapper.PKCS11Exception: CKR_GENERAL_ERROR
at jdk.crypto.cryptoki/sun.security.pkcs11.wrapper.PKCS11.C_Initialize(Native Method)
at jdk.crypto.cryptoki/sun.security.pkcs11.wrapper.PKCS11$SynchronizedPKCS11.C_Initialize(PKCS11.java:1631)
at jdk.crypto.cryptoki/sun.security.pkcs11.wrapper.PKCS11.getInstance(PKCS11.java:166)


/var/lib/softhsm/tokens パーミッションエラー
2024-02-11 11:47:58,014 ERROR [com.keyfactor.util.keys.token.pkcs11.Pkcs11SlotLabel] (default task-1) Error constructing pkcs11 provider: null: java.lang.reflect.InvocationTargetException
(...略...)
Caused by: java.security.ProviderException: Initialization failed
at jdk.crypto.cryptoki/sun.security.pkcs11.SunPKCS11.(SunPKCS11.java:396)
at jdk.crypto.cryptoki/sun.security.pkcs11.SunPKCS11$1.run(SunPKCS11.java:116)
at jdk.crypto.cryptoki/sun.security.pkcs11.SunPKCS11$1.run(SunPKCS11.java:113)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at jdk.crypto.cryptoki/sun.security.pkcs11.SunPKCS11.configure(SunPKCS11.java:113)
... 167 more
Caused by: sun.security.pkcs11.wrapper.PKCS11Exception: CKR_GENERAL_ERROR
at jdk.crypto.cryptoki/sun.security.pkcs11.wrapper.PKCS11.C_GetTokenInfo(Native Method)
at jdk.crypto.cryptoki/sun.security.pkcs11.Token.(Token.java:135)
at jdk.crypto.cryptoki/sun.security.pkcs11.SunPKCS11.initToken(SunPKCS11.java:1006)
at jdk.crypto.cryptoki/sun.security.pkcs11.SunPKCS11.(SunPKCS11.java:387)


スロット ID 間違い
Caused by: sun.security.pkcs11.wrapper.PKCS11Exception: CKR_SLOT_ID_INVALID
at jdk.crypto.cryptoki/sun.security.pkcs11.wrapper.PKCS11.C_GetSlotInfo(Native Method)
at jdk.crypto.cryptoki/sun.security.pkcs11.SunPKCS11.(SunPKCS11.java:385)


キー確認

EJBCA の Client Toolbox を使用して、SoftHSM の slot1 というラベルのトークンに格納されているキーをテストします。
ここで、ユーザー PIN の入力が必要です。

# cd /opt/ejbca/dist/clientToolBox/
# ./ejbcaClientToolBox.sh PKCS11HSMKeyTool test /usr/lib/softhsm/libsofthsm2.so TOKEN_LABEL:slot1

./ejbcaClientToolBox.sh PKCS11HSMKeyTool test:EJBCA の Client Toolbox の一部である PKCS11HSMKeyTool を使用して、HSM 上のキーをテストします。

/usr/lib/softhsm/libsofthsm2.so:SoftHSM の PKCS#11 ライブラリへのパスを指定します。このライブラリは、HSM とのインターフェースを提供します。

TOKEN_LABEL:slot1:テストするトークンのラベルを指定します。この例では、slot1 というラベルのトークンをテストします。


Testing of key: signKey
Private part:
SunPKCS11-libsofthsm2.so-slot661497608 RSA private key, 4096 bitstoken object, sensitive, unextractable)
RSA key:
  modulus: adb65f0e17d306edc3e2e2bbc92dcc200559100b7dbe4ad380c51d99290399a411804d0742009b4a2baf5e2f97483e66fb4e658b61ac55bc61ce97ef3c085b732e0a6048ceedb2ea4abdc4824d28e3b947c32308690590ff8f3eb1b3806b7431c45954506198431a9fcbb38755fad1f1e358010a7131e2ceddcaecf08ec6376b720d10c5726c3f8c0fc953c732f1c20fd8f231a143b615908c8b7adaf294fc36e41a5b201560a519457093fe9e9d941b168a0f62859fcad7b85bbe97e17f443587ed10efdb989d00fb929ce9bc1995cad7754b61e4134bf794efe7adbe4c2848688a61404bf5360d74f66d4aea9ee8d959731bdbbce0bac4378966d0567cea50a76351129ccfd817dce7003f3e08a550c057b7517fdff641995356482528174b7999b34cb4d4275ef0839517c9a6a1cbe31115d5825e0b103ddcfb051a6e4b4e5b90fc7dabad257f80bc4b48ca768fac580409ce6ef8ea1475659c20ff7d4eff1bcc426dee71215c5009aba8c5a06a8f26d2379f77eec9d5cf34fe9028335a6233c71156f725ce14a58a306105a54da763fbe18da1c01e42833a7a7f50803addd74f9ff3f3559a669e952027ddf5121ce7290691e374908fea625becf71dcdf945a0bd1a319153f44875add88e8badffedcb6d16994d72f3ef9dd2c6f11ffbd9d4a6e42d851a2dbcdae6e271f55a2b1891b828ee76777e22f0e2344b6468eac7
  public exponent: 10001
encryption provider: SunJCE version 11; decryption provider: SunPKCS11-libsofthsm2.so-slot661497608 version 11; modulus length: 4096; byte length 501. The decoded byte string is equal to the original!
2024-02-13 17:20:20,017 INFO  [com.keyfactor.util.keys.SignWithWorkingAlgorithm] Signature algorithm 'SHA1WithRSA' working for provider 'SunPKCS11-libsofthsm2.so-slot661497608 version 11'.
Signature test of key signKey: signature length 512; first byte a7; verifying true
Signings per second: 93
Decryptions per second: 96

テストに成功し、以下の結果が示されています。


SunPKCS11-libsofthsm2.so-slot661497608 RSA private key, 4096 bitstoken object, sensitive, unextractable)
秘密鍵のテスト:signKey というラベルの RSA 秘密鍵がテストされ、その鍵は 4096 ビットの長さで、トークンオブジェクトであり、センシティブ(sensitive)で、抽出不可能(unextractable)です。


RSA key:
modulus: (略)
public exponent: 10001
encryption provider: SunJCE(略)
RSA 鍵の詳細:モジュラス(modulus)と公開指数(public exponent)が表示されています。また、暗号化プロバイダ(encryption provider)は SunJCE version 11 で、復号化プロバイダ(decryption provider)は SunPKCS11-libsofthsm2.so-slot661497608 version 11 です。モジュラスの長さは 4096 ビットで、バイト長は 501 です。元のバイト文字列とデコードされたバイト文字列が等しいことが確認されています。


Signature algorithm 'SHA1WithRSA' working for provider 'SunPKCS11-libsofthsm2.so-slot661497608 version 11'.
署名アルゴリズムのテスト:署名アルゴリズム SHA1WithRSA がプロバイダ SunPKCS11-libsofthsm2.so-slot661497608 version 11 で動作していることが確認されています。


Signature test of key signKey: signature length 512; first byte a7; verifying true
署名テスト:signKey の署名テストが行われ、署名の長さは 512 で、最初のバイトは a7、そして検証は真(true)です。


Signings per second: 93
Decryptions per second: 96
性能テスト:1 秒あたりの署名数(Signings per second)は 93 で、1 秒あたりの復号化数(Decryptions per second)は 96 です。

【 モジュラス(modulus) 】

RSA 暗号では、大きな 2 つの素数 p と q を生成し、それらの積 n (= pq)を求めます。この n をモジュラスと呼びます。モジュラスは、公開鍵と秘密鍵の両方で使用され、その長さ(ビット数)が RSA 鍵の強度を決定します。

【 公開指数(public exponent)】

公開指数 e は、平文の暗号化に使用される指数で、RSA 鍵を作成するために用いられます。公開指数は公開鍵の一部であり、通常は小さい値(一般的には 65537)が選ばれます。これは、小さい値を選ぶことで、暗号化処理を高速化できるからです。

【 SunJCE 】

SunJCE(Sun Java Cryptography Extension)は、Java のセキュリティーフレームワークの一部で、暗号化、公開鍵インフラストラクチャー、認証、安全な通信、アクセス制御などの主要なセキュリティー分野にわたる一連の API を提供します。

SunJCE は、Java Development Kit (JDK)の一部として提供されており、さまざまな暗号化アルゴリズムとサービスを実装しています。

これにより、開発者はアプリケーションコードにセキュリティー機構を簡単に統合できます。

また、SunJCE は PKCS#11(暗号トークンインタフェース標準)をサポートしており、ハードウェア暗号化アクセラレータやスマートカードなどの暗号化機構に対するネイティブプログラミングインタフェースを提供しています。

適切に構成すると、アプリケーションは標準の JCA/JCE API を使用してネイティブ PKCS#11 ライブラリにアクセスできるようになります。

【 JCA/JCE API 】

JCA/JCE API は、Java プログラムで暗号化機能を利用するための標準的な API です。

Java Cryptography Architecture (JCA):デジタル署名、メッセージダイジェスト、証明書、暗号化、鍵管理など暗号化サービスを提供します。

Java Cryptography Extensions (JCE):より強力な暗号化アルゴリズムや新しい暗号化スキームを Java プラットフォームに追加するための拡張パッケージです。


ここで、以下のエラーになった場合、ユーザー PIN が間違っている可能性があります。
2024-02-04 17:10:35,580 ERROR [org.ejbca.ui.cli.KeyStoreContainerTest] Not possible to load keys.
java.security.KeyStoreException: KeyStore instantiation failed
(...略...)
at java.security.KeyStore$Builder$2.getKeyStore(KeyStore.java:2237) ~[?:?]
... 11 more
Caused by: javax.security.auth.login.FailedLoginException
(...略...)
at java.security.KeyStore$Builder$2.getKeyStore(KeyStore.java:2237) ~[?:?]
... 11 more
Caused by: sun.security.pkcs11.wrapper.PKCS11Exception: CKR_PIN_INCORRECT
(...略...)
at java.security.KeyStore$Builder$2.getKeyStore(KeyStore.java:2237) ~[?:?]
... 11 more
Not possible to load keys. Maybe a smart card should be inserted or maybe you just typed the wrong PIN. Press enter when the problem is fixed or 'x' enter to quit.


pkcs11-tool をインストールして、トークン上のすべてのオブジェクトの情報を表示します。

# apt install opensc -y
# pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --token-label slot1 --pin foo123 -O
Certificate Object; type = X.509 cert
label: signKey
subject: DN: CN=Dummy certificate created by a CESeCore application
ID: 7369676e4b6579
Private Key Object; RSA
label:
ID: 7369676e4b6579
Usage: decrypt, sign, unwrap
Access: sensitive, always sensitive, never extractable, local

PKCS#11 準拠の HSM(Hardware Security Module)上のオブジェクトをリストアップしています。

--module /usr/lib/softhsm/libsofthsm2.so:SoftHSM の PKCS#11 ライブラリへのパスを指定します。このライブラリは、HSM とのインターフェースを提供します。

--token-label slot1:操作対象のトークンのラベルを指定します。この例では、slot1 というラベルのトークンが操作対象となります。

--pin foo123:トークンにアクセスするための PIN を指定します。この例では、foo123 が PIN として使用されています。

-O:トークン内のすべてのオブジェクトをリストアップします。


ここで、以下のエラーになった場合、ユーザー PIN(foo123 部分)が間違っている可能性があります。
error: PKCS11 function C_Login failed: rv = CKR_PIN_INCORRECT (0xa0)
Aborting.


該当するスロットが無い場合、以下のエラーです。
No slot with token named "slotx" found


キー移行

SoftHSM2 に登録したキーを別のサーバーの EJBCA に読み込ませようと思います。...簡単にできたら、すぐに盗まれてしまいますので、SoftHSM2 で登録された秘密鍵は、例えば DER や PEM などにしてエクスポートできないようになっているようです。


しかし、エクスポートはできなくても移行は簡単にできました。

これは、SoftHSM だからなせるわざです。通常の HSM でこんなことはできません。


/var/lib/softhsm/tokens/* をコピーするだけです。


tar で固めて、移行先のサーバーに転送します。

# tar czf t.tar.gz /var/lib/softhsm/tokens/56d61a9e-6da5-b279-c630-b01f276da708
# scp t.tar.gz admin@xxx.xxx.xxx.xxx:~/.

転送先のサーバーでは、softhsm2 インストール済みとします。 そこへ転送されてきた tar.gz を展開します。

# tar zxf t.tar.gz -C /
# systemctl restart wildfly

EJBCA Admin UI から、CA Functions - Crypto Tokens にアクセスして、 Create new... をクリックします。


スロット ID を指定して、Crypto Token を登録します。 Crypto Token Save(移行先)


キーペア復元確認(移行先)


キーが最初からあります!


ところで、先ほど、「DER や PEM などにしてエクスポートできない」としましたが、エクスポートする方法を見つけました。
(参考:https://github.com/opendnssec/SoftHSMv2/issues/597 の一番下です。)
しかし、EJBCA Admin UI CA Functions - Crypto Tokens で登録したキー(Private key/秘密鍵)のことではありません。
自分で登録する場合、取り出せました。


PKCS#11 属性に EXTRACTABLE という属性があるのですが、これが True の場合、取り出せます。
EJBCA で登録した場合、EXTRACTABLE = False でした。
それを確認する Python プログラムが以下です。
(ついでに他の属性値も確認しています。)

check_pkcs11_attrs.py
#!/usr/bin/python3
import pkcs11  # PKCS#11インターフェースを提供するモジュールをインポートします。
from pkcs11.constants import Attribute, ObjectClass  # PKCS#11の定数をインポートします。

MODULE_PATH = "/usr/lib/softhsm/libsofthsm2.so"  # SoftHSM2のライブラリへのパス
TOKEN_LABEL = "slot1"  # トークンのラベル
PIN = "********"  # PINコード


def get_all_keys(session):
    # セッション内のすべてのオブジェクトを取得
    all_objects = list(session.get_objects())

    # オブジェクトの中からキーだけを抽出
    keys = [
        obj
        for obj in all_objects
        if obj[Attribute.CLASS]
        in (ObjectClass.PRIVATE_KEY, ObjectClass.PUBLIC_KEY, ObjectClass.SECRET_KEY)
    ]

    return keys


def check_key_extractable(session):
    keys = get_all_keys(session)

    # キーペアが存在しない場合、エラーメッセージを表示
    if not keys:
        print(f"No keys found")
        return

    # 最初のキー(通常は秘密鍵)を取得
    key = keys[0]

    # CLASS, KEY_TYPE, ID, SENSITIVE, EXTRACTABLE 属性値取り出し
    try:
        key_val = key[Attribute.CLASS]
        print(f"CLASS: {key_val}")
    except AttributeError:
        print("CLASS: Attribute not available")
    try:
        key_val = key[Attribute.KEY_TYPE]
        print(f"KEY_TYPE: {key_val}")
    except AttributeError:
        print("KEY_TYPE: Attribute not available")
    try:
        key_val = key[Attribute.ID]
        key_id_str = key_val.decode("utf-8")  # Decode the bytes to a string
        print(f"ID: {key_id_str}")
    except AttributeError:
        print("ID: Attribute not available")
    try:
        key_val = key[Attribute.SENSITIVE]
        print(f"SENSITIVE: {key_val}")
    except AttributeError:
        print("SENSITIVE: Attribute not available")
    try:
        key_val = key[Attribute.EXTRACTABLE]
        print(f"EXTRACTABLE: {key_val}")
    except AttributeError:
        print("EXTRACTABLE: Attribute not available")

    # キーがエクスポート可能かどうかを確認
    if key[Attribute.EXTRACTABLE]:
        print("Key is extractable")
    else:
        print("Key is not extractable")


def main():
    lib = pkcs11.lib(MODULE_PATH)  # PKCS#11ライブラリをロード
    token = lib.get_token(token_label=TOKEN_LABEL)  # トークンを取得

    with token.open(user_pin=PIN, rw=True) as session:  # トークンを開く
        check_key_extractable(session)


if __name__ == "__main__":
    main()

pip3 他、依存モジュールもインストールして、実行します。(注意:この記事の最後に実行する Python プログラムに依存しているものもインストールしています。)

# apt update -y
# apt install python3-pip -y
# pip3 install python-pkcs11
# pip3 install pycryptodome
# python3 check_pkcs11_attrs.py
CLASS: 3
KEY_TYPE: 0
ID: signKey
SENSITIVE: True
EXTRACTABLE: False
Key is not extractable

EXTRACTABLE: False であることが確認できました。

【 PKCS#11 属性 】

PKCS#11 のプライベートキーは、通常以下の属性を持つことが期待されます:

CLASS:オブジェクトのクラスを示します。プライベートキーの場合、この値は ObjectClass.PRIVATE_KEY になります。

KEY_TYPE:キーのタイプを示します。例えば、RSA キーの場合、この値は KeyType.RSA になります。

ID:キーの一意の識別子です。同じトークン内の公開キーとプライベートキーは同じ ID を共有することがよくあります。

SENSITIVE:この属性が True に設定されている場合、キーは暗号化されていて、その値はトークン外部に出力できません。

EXTRACTABLE:この属性が True に設定されている場合、キーはラップ(エクスポート)可能です。

【 CLASS: 3 】

CLASS: 3 は、オブジェクトのクラスが 3 であることを示しています。PKCS#11 では、各オブジェクトクラスは特定の数値にマッピングされています。

たとえば、データオブジェクトは 0、証明書は 1、公開キーは 2、プライベートキーは 3、シークレットキーは 4 などです。

したがって、CLASS: 3 は、このオブジェクトがプライベートキーであることを示しています。

ただし、これは一般的なマッピングであり、使用している具体的な PKCS#11 ライブラリやトークンによって異なる場合があります。

【 KEY_TYPE: 0 】

KEY_TYPE: 0 は、キーのタイプが 0 であることを示しています。PKCS#11 では、各キータイプは特定の数値にマッピングされています。たとえば、RSA キーは 0、DSA キーは 1、DH キーは 2 などです。

したがって、KEY_TYPE: 0 は、このキーが RSA キーであることを示しています。ただし、これは一般的なマッピングであり、使用している具体的な PKCS#11 ライブラリやトークンによって異なる場合があります。


以下のようにして、EXTRACTABLE = True の秘密鍵を作成して、.der にエクスポートし、openssl コマンドで その .der を読み込んで復号処理を行います。

暗号化 → 復号化 を行う意味は、取り出した秘密鍵が正しいかどうかの確認の意味です。

1. /tmp/test-softhsm-key-export ディレクトリを使って、トークンを初期化
2. RSA キーペア登録
3. 秘密鍵をラップ(キーを暗号化)して取り出す
4. 秘密鍵をアンラップ(キーを復号化してエクスポート)
5. (秘密鍵とペアの)公開鍵を使用してサンプルテキスト "key extracted!\n" を暗号化
6. エクスポートした秘密鍵を DER 形式で /tmp/test-softhsm-key-export/sample.der に保存
7. openssl コマンドを使用して暗号文を復号化(このとき、6の sample.der を読み込む)
8. 復号化した結果 "key extracted!\n" を出力

参考:https://github.com/opendnssec/SoftHSMv2/issues/597 に掲載されているプログラムを拝借して、少し変えただけです。

pkcs11_key_export.py
#!/usr/bin/python3
# このプログラムは、https://github.com/EverTrust/pkcs11-keyextractor/blob/master/src/main/scala/fr/itassets/p11/PKCS11KeyExtractor.scala を基にしています。

import pathlib  # ファイルパス操作のためのモジュールをインポート
import subprocess  # サブプロセスを実行するためのモジュールをインポート
import os  # OSに依存した機能を使用するためのモジュールをインポート
import struct  # バイナリデータを扱うためのモジュールをインポート

import pkcs11  # PKCS#11インターフェースを提供するモジュールをインポート
from pkcs11.mechanisms import (
    Mechanism,
    KeyType,
)  # PKCS#11のメカニズムとキータイプをインポート
from pkcs11.constants import Attribute, ObjectClass  # PKCS#11の定数をインポート
from Crypto.Cipher import AES  # AES暗号化を提供するモジュールをインポート

MODULE_PATH = "/usr/lib/softhsm/libsofthsm2.so"  # SoftHSM2のライブラリへのパスを定義
TOKEN_LABEL = "test-extract"  # トークンのラベルを定義
KEYPAIR_LABEL = "test-extract-keypair"  # キーペアのラベルを定義
PIN = "1234"  # PINコードを定義
KEK = os.urandom(16)  # 16バイトのランダムなバイト列を生成
SAMPLE_TEXT = "key extracted!\n"  # サンプルテキストを定義


def setup_softhsm():
    # SoftHSM2の設定
    softhsm_dir = pathlib.Path(
        "/tmp/test-softhsm-key-export"
    )  # SoftHSM2のディレクトリパスを定義
    softhsm_config = softhsm_dir / "softhsm2.conf"  # SoftHSM2の設定ファイルパスを定義
    token_dir = softhsm_dir / "tokens"  # トークンのディレクトリパスを定義
    os.environ["SOFTHSM2_CONF"] = str(
        softhsm_config
    )  # 環境変数にSoftHSM2の設定ファイルパスを設定
    if token_dir.exists():  # トークンディレクトリが既に存在する場合、関数を終了
        return
    token_dir.mkdir(parents=True)  # トークンディレクトリを作成
    softhsm_config.write_text(
        f"directories.tokendir = {token_dir}\n"
    )  # SoftHSM2の設定ファイルにトークンディレクトリのパスを書き込み
    subprocess.check_call(
        [
            "softhsm2-util",
            "--init-token",
            "--slot",
            "0",
            "--label",
            TOKEN_LABEL,
            "--so-pin",
            PIN,
            "--pin",
            PIN,
        ]
    )  # softhsm2-utilコマンドを使用してトークンを初期化


def destroy_test_rsa_keypair(session):
    # 既存のテストRSAキーペアを破棄
    for key in session.get_objects(
        {Attribute.LABEL: KEYPAIR_LABEL}
    ):  # ラベルがKEYPAIR_LABELのオブジェクトを取得
        key.destroy()  # キーを破棄


def encrypt_sample_text(session, exported_key):
    # サンプルテキストを暗号化し、その暗号文を復号化
    pubkey = list(session.get_objects({Attribute.LABEL: KEYPAIR_LABEL}))[
        0
    ]  # ラベルがKEYPAIR_LABELの公開鍵を取得
    sample_ciphertext = pubkey.encrypt(
        SAMPLE_TEXT
    )  # 公開鍵を使用してサンプルテキストを暗号化
    inkey = (
        "/tmp/test-softhsm-key-export/sample.der"  # エクスポートされた鍵のパスを定義
    )
    with open(inkey, "wb") as f:  # エクスポートされた鍵を書き込み
        f.write(exported_key)
    openssl = subprocess.Popen(
        [
            "openssl",
            "pkeyutl",
            "-decrypt",
            "-inkey",
            inkey,
            "-keyform",
            "DER",
            "-pkeyopt",
            "rsa_padding_mode:oaep",
        ],
        stdin=subprocess.PIPE,
    )  # opensslコマンドを使用して暗号文を復号化
    openssl.communicate(sample_ciphertext)  # 復号化した結果を出力


def generate_test_rsa_keypair(session):
    # RSAキーペアを生成
    session.generate_keypair(
        KeyType.RSA,
        512,
        mechanism=Mechanism.RSA_PKCS_KEY_PAIR_GEN,
        label=KEYPAIR_LABEL,
        store=True,
        public_template={},
        private_template={
            Attribute.SENSITIVE: False,
            Attribute.EXTRACTABLE: True,
        },
    )  # PKCS#11セッションを使用してRSAキーペアを生成。このキーペアは、後でエクスポートされる。


# Implementation of rfc5649. This doesn't account for the case of ciphertext
# with 2 blocks (since we are using it to export RSA private keys). While
def unwrap_key(kek, ciphertext):
    # RFC5649に基づいてキーをアンラップ。この実装は、2ブロックの暗号文のケースを考慮していない。
    # AES ECBの6ステップを使用
    decrypt = AES.new(kek, AES.MODE_ECB).decrypt
    steps = 6
    # 暗号文を8バイトのブロックに分割
    blocks = []
    block_size = 8
    block_count = len(ciphertext) // block_size - 1
    for i in range(block_count):
        block = (i + 1) * block_size
        blocks.append(ciphertext[block : block + block_size])
    # 64ビット符号なし整数をシリアライズ/デシリアライズするためのstruct.pack形式
    ulong_be = ">Q"
    # 完全性/サイズブロック。復号化後、代替初期値(AIV)と元のプレーンテキストのサイズを含むべき。
    aiv = struct.unpack(ulong_be, ciphertext[:8])[0]
    for j in range(0, steps):
        for i in range(block_count, 0, -1):
            cipherblock = (
                struct.pack(ulong_be, aiv ^ ((5 - j) * block_count + i)) + blocks[i - 1]
            )
            block = decrypt(cipherblock)
            aiv = struct.unpack(ulong_be, block[:8])[0]
            blocks[i - 1] = block[8:]
    assert aiv & 0xFFFFFFFF00000000 == 0xA65959A600000000
    # 完全性チェックが通過。元の長さは、aivの32 LSBで指定。
    length = aiv & 0xFFFFFFFF
    plaintext_with_pad = b"".join(blocks)
    return plaintext_with_pad[:length]  # パディング付きのプレーンテキストを返す


def export_private_key(session, key):
    # 秘密鍵をエクスポート
    kek_handle = session.create_object(
        {
            Attribute.CLASS: ObjectClass.SECRET_KEY,
            Attribute.KEY_TYPE: KeyType.AES,
            Attribute.VALUE: KEK,
            Attribute.WRAP: True,
            Attribute.UNWRAP: True,
            Attribute.TOKEN: False,
        }
    )  # KEKを使用して秘密鍵をラップ
    # KEK = Key Encryption Key(キーを暗号化するキー)の略
    wrapped_key = kek_handle.wrap_key(
        key, mechanism=Mechanism.AES_KEY_WRAP_PAD
    )  # 秘密鍵をラップ
    clear_key = unwrap_key(KEK, wrapped_key)  # ラップされた鍵をアンラップ
    return clear_key  # アンラップされた鍵を返す


def main():
    setup_softhsm()  # SoftHSM2をセットアップ
    lib = pkcs11.lib(MODULE_PATH)  # PKCS#11ライブラリをロード
    token = lib.get_token(token_label=TOKEN_LABEL)  # トークンを取得

    with token.open(user_pin=PIN, rw=True) as session:  # トークンを開く
        destroy_test_rsa_keypair(session)  # テストRSAキーペアを破棄
        generate_test_rsa_keypair(session)  # テストRSAキーペアを生成
        keys = list(
            session.get_objects(
                {
                    Attribute.LABEL: KEYPAIR_LABEL,
                    Attribute.EXTRACTABLE: True,
                }
            )
        )  # エクスポート可能なキーペアを取得
        exported_key = export_private_key(session, keys[0])  # 秘密鍵をエクスポート
        encrypt_sample_text(session, exported_key)  # サンプルテキストを暗号化


if __name__ == "__main__":
    main()
# python3 pkcs11_key_export.py
The token has been initialized and is reassigned to slot 1496171450
key extracted!
# ls -l /tmp/test-softhsm-key-export/sample.der
-rw-r--r-- 1 root root 352  2月 11 18:51 /tmp/test-softhsm-key-export/sample.der

OK!!


loading...