Notes on using a TPM2 module on Linux

Here I collected some notes on using the TPM2 on Linux, specifically Arch Linux. The motherboard used is an ASRock E3C222D4U with the ASRock TPM2 module.

This article focuses on enabling the usage of the keys stored in the TPM2 by various tools using the PKCS#11 interface. The module should be configured to allow the access to these keys only after some measurement of the state of the system is done, to guarantee that it was not tampered. This part is outside of the scope of these notes. Here the protections offered to the user are the physical possession and the requirement to enter a PIN.

PKCS#11 exposes N separate slots, each containing a token. We will used a separate slot/token for each application. For the basic support under Arch Linux, the following packages need to be installed: tpm2-abrmd,tpm2-pkcs11, tpm2-tools and tpm2-tss. The latest package also sets up udev rules to allow the abrmd broker daemon to access the device /dev/tpm0. You can test if the TPM2 module is working by printing some random characters by using its random number generator:

$ tpm2_getrandom --hex 16

Note that tpm-pkcs11 needs to save some information on disk, I've set the TPM2_PKCS11_STORE environment variable to a suitable directory in my .bashrc file.

SSH in slot 1

Using the TPM2 is pretty straightforward. From here on [PIN] denotes the user PIN. Avoid saving the command lines containing it in the bash history, for example by prepending the line with a space.

$ tpm2_ptool init
action: Created
id: 1
$ tpm2_ptool addtoken --pid=1 --sopin=[PIN] --userpin=[PIN] --label=ssh_token
$ tpm2_ptool addkey --algorithm=rsa2048 --label=ssh_token --key-label=ssh_token --userpin=[PIN]
action: add
CKA_ID: '33333334333132626661393734663161'
CKA_ID: '33333334333132626661393734663161'

To output the public key (to be added to the authorized_keys file on the remote system):

$ ssh-keygen -D /usr/lib/pkcs11/
ssh-rsa AAAAB3NzaC1yc2EAAAADAQA...

And you can connect to a system by offering the key from the TPM2:

$ ssh -I /usr/lib/pkcs11/ [destination host]

GnuPG in slot 2

Supporting GnuPG is more complicated. You will need openssl for creating a certificate from the public key and the package gnupg-pkcs11-scd to interface GnuPG with the TPM2 via the PKCS#11 API. Unfortunately, there is a bug at the moment in the upstream program which doesn't correctly inform the GnuPG agent daemon about the key padding. I prepared a Pull Request for the upstream, until it is merged, you need to use the version from my github repository.

The initial part is similar to the previous case, but then you need to create a certificate from the public key to allow GnuPG to use it:

$ tpm2_ptool init
action: Created
id: 2
$ tpm2_ptool addtoken --pid=2 --label=gpg_token --sopin=[PIN] --userpin=[PIN]
$ tpm2_ptool addkey --algorithm=rsa2048 --label=gpg_token --key-label=gpg_token --userpin=[PIN]
$ openssl <<EOF
req -engine pkcs11 -new -key pkcs11:model=rls;manufacturer=Nuvoton;serial=0000000000000000;token=gpg_token;type=private;pin-value=[PIN] -keyform engine -out req.pem -text -x509 -subj /CN=[Your Common Name]
x509 -engine pkcs11 -signkey pkcs11:model=rls;manufacturer=Nuvoton;serial=0000000000000000;token=gpg_token;type=private;pin-value=[PIN] -keyform engine -in req.pem -out cert.pem
$ tpm2_ptool addcert --label gpg_token --key-label gpg_token cert.pem

Afterwards, we need to configure the SCD (smart card driver) for the GnuPG Agent which uses the PKCS#11 to work with the private key. Unfortunately, GnuPG Agent supports only a single SCD, so you cannot use both the TPM2 and a normal smart card at the same time. Also, note that the patched version of gnupg-pkcs11-scd needs to be used until upstream is fixed. Two configuration files need changes:

  • ~/.gnupg/gpg-agent.conf:
    scdaemon-program /usr/bin/gnupg-pkcs11-scd
  • scdaemon-program /usr/bin/gnupg-pkcs11-scd:
    providers tpm
    provider-tpm-library /usr/lib/pkcs11/
  • for the latter, the following debug directives might be useful if you encounter any problems:
    log-file /tmp/scd.log

GnuPG Agent has to be restarted and you need to trigger a reload of the smart card:

$ systemctl --user restart gpg-agent.service
$ gpg --card-status

The next step consist in identifying the keygrip for the RSA keypair stored in the TPM2 module. The procedure for creating a keypair should support automatically identifying the smartcard. Unfortunately, this doesn't seem working, so you need to manually list the available keygrips:

$ gpg-agent --server gpg-connect-agent << EOF
gnupg-pkcs11-scd[9221]: S KEY-FRIEDNLY 01B4D88E8F24441E1773472EFAD1CFE020072CF2 /CN=[The CN you used] on gpg_tokenchan_0 ...

Look for a 40 characters long string of letters and numbers after the words KEY-FRIENDLY. With this information you can proceed with the normal generation of a keypair using option 13 Existing key and entering the keygrip when prompted:

$ gpg --expert --full-generate-key
Please select what kind of key you want:
   (1) RSA and RSA (default)
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
   (7) DSA (set your own capabilities)
   (8) RSA (set your own capabilities)
   (9) ECC and ECC
  (10) ECC (sign only)
  (11) ECC (set your own capabilities)
  (13) Existing key
  (14) Existing key from card
Your selection? 13
Enter the keygrip: 01B4D88E8F24441E1773472EFAD1CFE020072CF2

And that's it, you should have a keypair based on the RSA keys in the TPM2. Please test it by encrypting/decrypting/signing some text.

OpenVPN in slot 3

OpenVPN is similar to GPG because it needs a certificate. However, usually an OpenVPN is deployed by defining a custom CA which signs the certificates. The easiest way for doing it is by using EasyRSA. The set up of a CA is outside the scope of this article, there are many good tutorials to get started.

The initial steps are the same as the GPG case:

$ tpm2_ptool init
action: Created
id: 3
$  tpm2_ptool addtoken --pid=3 --label=openvpn_token --sopin=[Your PIN] --userpin=[Your PIN]
$  tpm2_ptool addkey --algorithm=rsa2048 --label=openvpn_token --key-label=openvpn_token --userpin=[Your PIN]
action: add
  CKA_ID: '33313665373362353539386632666162'
  CKA_ID: '33313665373362353539386632666162'

Afterwards, you need to create a Certificate Sign Request with openssl (mind the missing x509 compared to the previous invocation of openssl):

$  openssl req -engine pkcs11 -new -key 'pkcs11:model=rls;manufacturer=Nuvoton;serial=0000000000000000;token=openvpn_token;type=private;pin-value=[Your PIN]' -keyform engine -out req.csr -subj /CN=chri_tpm2_1

Customize the Common Name according to your needs (I used chri_tpm2_1 to identify the client in this case)`. Now you can import and sign the CSR with EasyRSA:

$  easyrsa import-req req.csr chri_tpm_1
$  easyrsa sign-req client chri_tpm_1

and the certificate signed by your OpenVPN deployment's CA can be imported back into the TPM2:

$ tpm2_ptool addcert --label openvpn_token --key-label openvpn_token pki/issued/chri_tpm_1.crt
action: add
  CKA_ID: '33313665373362353539386632666162'

You can check with the openvpn command itself that the certificate is visible:

$ openvpn --show-pkcs11-ids /usr/lib/pkcs11/
       DN:             CN=chri_tpm2_1
       Serial:         2C6524CBF1845D6A662A3E40285FCEF5
       Serialized id:  Nuvoton/rls/0000000000000000/openvpn_token/33313665373362353539386632666162

Take note of the Serialized id because it needs to be used in the configuration file for the client. Here is the configuration that I use, chri_tpm_1.conf, as an example (look for the options starting with pkcs11):

dev tun
remote [Your OpenVPN server] [Your OpenVPN server port] udp
... CA certificate from EasyRSA
pkcs11-providers /usr/lib/pkcs11/
pkcs11-id 'Nuvoton/rls/0000000000000000/openvpn_token/33313665373362353539386632666162'
remote-cert-tls server
key-direction 1
-----BEGIN OpenVPN Static key V1-----
... MAC Static key, recommended but not mandatory
-----END OpenVPN Static key V1-----
verb 3

And here you can see an example of a successful connection:

$ openvpn --config chri_tpm_1.conf
Mon Jun  1 20:42:22 2020 TLS: Initial packet from [AF_INET][your server IP:port], sid=381bca9d 47aaf107
Mon Jun  1 20:42:22 2020 VERIFY OK: depth=1, CN=[the CA for your openvpn]
Mon Jun  1 20:42:22 2020 VERIFY KU OK
Mon Jun  1 20:42:22 2020 Validating certificate extended key usage
Mon Jun  1 20:42:22 2020 ++ Certificate has EKU (str) TLS Web Server Authentication, expects TLS Web Server Authentication
Mon Jun  1 20:42:22 2020 VERIFY EKU OK
Mon Jun  1 20:42:22 2020 VERIFY OK: depth=0, CN=server_1
Enter openvpn_token token Password: ****************
Mon Jun  1 20:42:41 2020 Control Channel: TLSv1.3, cipher TLSv1.3 TLS_AES_256_GCM_SHA384, 2048 bit RSA
Mon Jun  1 20:42:41 2020 [server_1] Peer Connection Initiated with [AF_INET][your server IP:port]

Note that you will be asked for the PIN. If you start openvpn via some other tool (systemd-networkd or NetworManager for example), the right way to provide the PIN is via the management interface (look for the management-query-passwords option in the openvpn's manual page). This function should be provided by the network utility starting openvpn.

This entry was posted in Linux desktop. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *