tpm

nginx with TPM based SSL

2021-12-14

How to configure nginx to use a certificate generated on a Trusted Platform Module (TPM) … and in my case, on a raspberry pi with one of these:

optiga TPM

Essentially, we will

  1. Install the tpm2-tss Engine for Openssl which allow for nginx to talk to the tpm using openssl’s Engine system.

  2. Generate an RSA key on the TPM

  3. Use openssl to generate a Certificate Signing Request (CSR)

  4. Use our own CA to sign that CSR and generate an x509 certificate suitable for a webserver

  5. Configure nginx to read the private key on the TPM and the certificate

  6. access site!


Some notes…

  • the TPM is a device (/dev/tpm0) and has limited throughput.
  • the RSA key is created on the device itself (which is really the way to go). If you wanted to import your own RSA private key, here is the feature request Import existing keys into TPM for use with openssl.
    Just note that you can certainly just use golang for TLS too…but since this is about nginx…

Install TPM2TSS Stack

Yep, you need to install a bunch of stuff…i did this using the sources here but you can also just package install

make sure you install in sequence

  • tpm2-tss
  • tpm2-tools (you don’t need this but its good to have around)
  • tpm2-tss-engine

Verify that the engine is available by generating some random bytes

$ openssl engine -t
(dynamic) Dynamic engine loading support
     [ unavailable ]
(tpm2tss) TPM2-TSS engine for OpenSSL
     [ available ]

$ openssl rand -hex -engine tpm2tss 32
    engine "tpm2tss" set.
    2798d69532b579b9db8bb2bb5d0f6c76fcea162ca243b01bfacb679bfcdb3431

Create RSA Key

Use the tpm2tss-genkey utility to create the key

  • generate key
tpm2tss-genkey -a rsa -s 2048 tpm.key
  • export the public key
openssl rsa -engine tpm2tss -inform engine -in tpm.key -pubout -outform pem -out tpm.pem

In my case, the public key was the following…but the private key is of course on the TPM

$ more tpm.pem 
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmNEgTnW2qXSVjza5M84y
cvdrXh3VGIqWFWSLi5qcdWriWo3S3aVjDuN4c42uF5xhqx71PRCI5bGYqkG4U6Y7
WB/6jrbKIrTuOTWOfwnTqXelbahRHWWVq+z8P/Z1ABNVS8kxJQEz6zOVgF9rh+oK
XHteMwFjt/368cTShpoSq3CUQcOybP6B/pHyMibBz1jkLfCfWc7u+VvZQAsINAtB
oucV/aRbLtkMjRXdiOe/fQNsAqtKacpZCtqClSoRyg0rBjwMuKZpXiZZ3rvIzpjt
ToFimCaZJUlf+wV1pdHB29Xcblv6JmPcUw61BM4d9iQXPtZn4p7R+nypUn2vJQ7P
LwIDAQAB
-----END PUBLIC KEY-----
  • Test rsa/sign and verification (why not?)

Just for fun, create a file, sign and verify with the public key:

echo -n "foo" > mydata
openssl pkeyutl -engine tpm2tss -keyform engine -inkey tpm.key -sign -in mydata -out mysig
openssl pkeyutl -pubin -inkey tpm.pem -verify -in mydata -sigfile mysig
  • Create an openssl config file (shown below)

Create an openssl config file (shown in the appendix). We will use this to set the specifications for the CSR

  • req.conf

At this point, you could just take the easy route and create a self-signed key

# to use self signed
# openssl req -config req.conf -new -x509 -engine tpm2tss -key tpm.key  -keyform engine -out tpm.crt -days 1000
# openssl x509 -in  tpm.crt -text -noout

but what what i did is created a CSR instead

# to generate csr to sign
openssl req -config req.conf -new -engine tpm2tss -key tpm.key -keyform engine -out tpm.csr

My CSR looked like this

  • tpm.csr
-----BEGIN CERTIFICATE REQUEST-----
MIIDBjCCAe4CAQAwcjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQH
DA1Nb3VudGFpbiBWaWV3MQ8wDQYDVQQKDAZHb29nbGUxEzARBgNVBAsMCkVudGVy
cHJpc2UxGDAWBgNVBAMMD2Jsb2cuNmVxdWo1LmRldjCCASIwDQYJKoZIhvcNAQEB
BQADggEPADCCAQoCggEBAJjRIE51tql0lY82uTPOMnL3a14d1RiKlhVki4uanHVq
4lqN0t2lYw7jeHONrhecYase9T0QiOWxmKpBuFOmO1gf+o62yiK07jk1jn8J06l3
pW2oUR1llavs/D/2dQATVUvJMSUBM+szlYBfa4fqClx7XjMBY7f9+vHE0oaaEqtw
lEHDsmz+gf6R8jImwc9Y5C3wn1nO7vlb2UALCDQLQaLnFf2kWy7ZDI0V3Yjnv30D
bAKrSmnKWQragpUqEcoNKwY8DLimaV4mWd67yM6Y7U6BYpgmmSVJX/sFdaXRwdvV
3G5b+iZj3FMOtQTOHfYkFz7WZ+Ke0fp8qVJ9ryUOzy8CAwEAAaBPME0GCSqGSIb3
DQEJDjFAMD4wCwYDVR0PBAQDAgQwMBMGA1UdJQQMMAoGCCsGAQUFBwMBMBoGA1Ud
EQQTMBGCD2Jsb2cuNmVxdWo1LmRldjANBgkqhkiG9w0BAQsFAAOCAQEAlcjgjL5E
QwhrrkFbq9WqlwKkNeg4zfgawxo93Tw2vupsQifHaqXP3CT/RypzCIFjSr2YGZWR
DCSdg2MtbEo4nT9/N5Iv1rl/lMBDwFO44nfV0qy5LKGybMc6CgPt1fIcp0XSmX7C
WInqfiplGhGH43WPDAV7Mf/0GB42AefUJTDVz98flriIaDteyID5CJI4oARTYcuQ
umhuJLb+BVloDY100vpF0b6Wt7vyCuLxHeO94rQqoByB/Ol8yA+magn8JAqOvBDN
LTHWp87MfTepm13YQOXLgh7Y1T5aRmv8ftYx4bd3hymsafZ/8l1Kuz3JsGSLF2xN
OEJnj6exde18GA==
-----END CERTIFICATE REQUEST-----

I then took the CSR to my own CA and signed it (you can create your own CA CA ScratchPad ) if you want

Sign the CSR.

The cert that i got back was:

  • tpm.crt
-----BEGIN CERTIFICATE-----
MIIEVzCCAz+gAwIBAgIBQTANBgkqhkiG9w0BAQUFADBXMQswCQYDVQQGEwJVUzEP
MA0GA1UECgwGR29vZ2xlMRMwEQYDVQQLDApFbnRlcnByaXNlMSIwIAYDVQQDDBlF
bnRlcnByaXNlIFN1Ym9yZGluYXRlIENBMB4XDTIxMTIxNTE2NDY1NVoXDTI3MDYw
NzE2NDY1NVowcjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1N
b3VudGFpbiBWaWV3MQ8wDQYDVQQKDAZHb29nbGUxEzARBgNVBAsMCkVudGVycHJp
c2UxGDAWBgNVBAMMD2Jsb2cuNmVxdWo1LmRldjCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAJjRIE51tql0lY82uTPOMnL3a14d1RiKlhVki4uanHVq4lqN
0t2lYw7jeHONrhecYase9T0QiOWxmKpBuFOmO1gf+o62yiK07jk1jn8J06l3pW2o
UR1llavs/D/2dQATVUvJMSUBM+szlYBfa4fqClx7XjMBY7f9+vHE0oaaEqtwlEHD
smz+gf6R8jImwc9Y5C3wn1nO7vlb2UALCDQLQaLnFf2kWy7ZDI0V3Yjnv30DbAKr
SmnKWQragpUqEcoNKwY8DLimaV4mWd67yM6Y7U6BYpgmmSVJX/sFdaXRwdvV3G5b
+iZj3FMOtQTOHfYkFz7WZ+Ke0fp8qVJ9ryUOzy8CAwEAAaOCAREwggENMA4GA1Ud
DwEB/wQEAwIHgDAJBgNVHRMEAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1Ud
DgQWBBTaJ8JibkMFng0gmp4Q0ZbzuD7SwzAfBgNVHSMEGDAWgBS/4RzwIkiP/DvP
XdntrohwId/dhjBEBggrBgEFBQcBAQQ4MDYwNAYIKwYBBQUHMAKGKGh0dHA6Ly9w
a2kuZXNvZGVtb2FwcDIuY29tL2NhL3Rscy1jYS5jZXIwOQYDVR0fBDIwMDAuoCyg
KoYoaHR0cDovL3BraS5lc29kZW1vYXBwMi5jb20vY2EvdGxzLWNhLmNybDAaBgNV
HREEEzARgg9ibG9nLjZlcXVqNS5kZXYwDQYJKoZIhvcNAQEFBQADggEBAIJ289RE
WQ/40Oa3b5YG/3RVt2Wmqo0+xNwmT0TG/gDsfNeA3QYjAiT7iNfx79s2ElDjDayG
IQrv3PWkdurIGt+3KNzaaBDkgztYs+WJKA04YeFVThs1dMk0sjM7VHBlehyjXDHy
pScPY7dQcu1yEBZjh0o1Ju/n0I+yAfYRg6P13+r+KexkAtEDaYE6a98ySIc0JBUI
flagANLd7k1g/QzvCRk7BaRbMpHmqY9NmeOMedixE90Yd2z5s5m965u9AfV8MO+E
DKEdWo/4pstiJ0xAMCmIW3ziFbUtog0/BKRUYXPR1r3iw6ALNsX9cua5OveO7pQn
+6MIh8d/BAaaSOg=
-----END CERTIFICATE-----
$ openssl x509 -in tpm.crt -inform PEM -noout -fingerprint
SHA1 Fingerprint=D1:31:4D:67:8A:F5:F6:19:9E:D9:0B:3E:DF:65:D9:F3:F7:ED:91:9A

$ openssl x509 -inform PEM -in tpm.crt -pubkey -noout
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmNEgTnW2qXSVjza5M84y
cvdrXh3VGIqWFWSLi5qcdWriWo3S3aVjDuN4c42uF5xhqx71PRCI5bGYqkG4U6Y7
WB/6jrbKIrTuOTWOfwnTqXelbahRHWWVq+z8P/Z1ABNVS8kxJQEz6zOVgF9rh+oK
XHteMwFjt/368cTShpoSq3CUQcOybP6B/pHyMibBz1jkLfCfWc7u+VvZQAsINAtB
oucV/aRbLtkMjRXdiOe/fQNsAqtKacpZCtqClSoRyg0rBjwMuKZpXiZZ3rvIzpjt
ToFimCaZJUlf+wV1pdHB29Xcblv6JmPcUw61BM4d9iQXPtZn4p7R+nypUn2vJQ7P
LwIDAQAB
-----END PUBLIC KEY-----

of course the public keys match! Copy the tpm.crt file to your webserver (needless to say, don’t copy this file since you will have your own TPM on your own device!)

Configure openssl on webserver

  • Configure openssl.cnf

The location of the openssl file for me was at /usr/lib/ssl/openssl.cnf (for ref, i modified the sample here to account for the .so path).

The libtpm2tss.so file for me was here (yep, arm-linux)

/usr/lib/arm-linux-gnueabihf/engines-1.1/libtpm2tss.so

Edit the openssl config file and point it to the .so above

  • /usr/lib/ssl/openssl.cnf
openssl_conf = openssl_init

[openssl_init]
engines = engine_section

[engine_section]
tpm2tss = tpm2tss_section

[tpm2tss_section]
engine_id = tpm2tss
dynamic_path = /usr/lib/arm-linux-gnueabihf/engines-1.1/libtpm2tss.so
default_algorithms = RSA,ECDSA
init = 1

Configure nginx for ssl_engine

We now need to tell nginx about the engine’s it should use.

Edit /etc/nginx/nginx.conf

and specify the ssl_engine tpm2tss line as shown below

user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
ssl_engine tpm2tss;

Configure nginx server

Now edit your nginx server file at /etc/nginx/sites-enabled/default

and specify the paths to the TPM based private key reference and the certificate you just uploaded

    ssl_certificate /root/tpm_certs/tpm.crt;
    ssl_certificate_key engine:tpm2tss:/root/tpm_certs/tpm.key;

the full structure is:

server {
    listen 443 ssl http2;
    server_name blog.6equj5.dev;
    root /usr/share/nginx/blog;
    index index.html index.htm;
    access_log /var/log/nginx/access_6equj5.log;
    error_log /var/log/nginx/error_6equj5.log;

    ssl on;
    ssl_certificate /root/tpm_certs/tpm.crt;
    ssl_certificate_key engine:tpm2tss:/root/tpm_certs/tpm.key;
    ssl_ciphers         HIGH:!aNULL:!MD5;
  
    location / {
      try_files $uri $uri/ =404;
    }
}

Restart nginx and test

In my case…the server is currently running at: https://blog.6equj5.dev

the x509 is what we setup earlier

$ openssl s_client --connect blog.6equj5.dev:443
CONNECTED(00000003)
depth=0 C = US, ST = CA, L = Mountain View, O = Google, OU = Enterprise, CN = blog.6equj5.dev
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 C = US, ST = CA, L = Mountain View, O = Google, OU = Enterprise, CN = blog.6equj5.dev
verify error:num=21:unable to verify the first certificate
verify return:1
depth=0 C = US, ST = CA, L = Mountain View, O = Google, OU = Enterprise, CN = blog.6equj5.dev
verify return:1
---
Certificate chain
 0 s:C = US, ST = CA, L = Mountain View, O = Google, OU = Enterprise, CN = blog.6equj5.dev
   i:C = US, O = Google, OU = Enterprise, CN = Enterprise Subordinate CA
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIEVzCCAz+gAwIBAgIBQTANBgkqhkiG9w0BAQUFADBXMQswCQYDVQQGEwJVUzEP
MA0GA1UECgwGR29vZ2xlMRMwEQYDVQQLDApFbnRlcnByaXNlMSIwIAYDVQQDDBlF
bnRlcnByaXNlIFN1Ym9yZGluYXRlIENBMB4XDTIxMTIxNTE2NDY1NVoXDTI3MDYw
NzE2NDY1NVowcjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1N
b3VudGFpbiBWaWV3MQ8wDQYDVQQKDAZHb29nbGUxEzARBgNVBAsMCkVudGVycHJp
c2UxGDAWBgNVBAMMD2Jsb2cuNmVxdWo1LmRldjCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAJjRIE51tql0lY82uTPOMnL3a14d1RiKlhVki4uanHVq4lqN
0t2lYw7jeHONrhecYase9T0QiOWxmKpBuFOmO1gf+o62yiK07jk1jn8J06l3pW2o
UR1llavs/D/2dQATVUvJMSUBM+szlYBfa4fqClx7XjMBY7f9+vHE0oaaEqtwlEHD
smz+gf6R8jImwc9Y5C3wn1nO7vlb2UALCDQLQaLnFf2kWy7ZDI0V3Yjnv30DbAKr
SmnKWQragpUqEcoNKwY8DLimaV4mWd67yM6Y7U6BYpgmmSVJX/sFdaXRwdvV3G5b
+iZj3FMOtQTOHfYkFz7WZ+Ke0fp8qVJ9ryUOzy8CAwEAAaOCAREwggENMA4GA1Ud
DwEB/wQEAwIHgDAJBgNVHRMEAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1Ud
DgQWBBTaJ8JibkMFng0gmp4Q0ZbzuD7SwzAfBgNVHSMEGDAWgBS/4RzwIkiP/DvP
XdntrohwId/dhjBEBggrBgEFBQcBAQQ4MDYwNAYIKwYBBQUHMAKGKGh0dHA6Ly9w
a2kuZXNvZGVtb2FwcDIuY29tL2NhL3Rscy1jYS5jZXIwOQYDVR0fBDIwMDAuoCyg
KoYoaHR0cDovL3BraS5lc29kZW1vYXBwMi5jb20vY2EvdGxzLWNhLmNybDAaBgNV
HREEEzARgg9ibG9nLjZlcXVqNS5kZXYwDQYJKoZIhvcNAQEFBQADggEBAIJ289RE
WQ/40Oa3b5YG/3RVt2Wmqo0+xNwmT0TG/gDsfNeA3QYjAiT7iNfx79s2ElDjDayG
IQrv3PWkdurIGt+3KNzaaBDkgztYs+WJKA04YeFVThs1dMk0sjM7VHBlehyjXDHy
pScPY7dQcu1yEBZjh0o1Ju/n0I+yAfYRg6P13+r+KexkAtEDaYE6a98ySIc0JBUI
flagANLd7k1g/QzvCRk7BaRbMpHmqY9NmeOMedixE90Yd2z5s5m965u9AfV8MO+E
DKEdWo/4pstiJ0xAMCmIW3ziFbUtog0/BKRUYXPR1r3iw6ALNsX9cua5OveO7pQn
+6MIh8d/BAaaSOg=
-----END CERTIFICATE-----
subject=C = US, ST = CA, L = Mountain View, O = Google, OU = Enterprise, CN = blog.6equj5.dev

issuer=C = US, O = Google, OU = Enterprise, CN = Enterprise Subordinate CA

images/cert_details.png


Appendix

Openssl configuration file:

  • req.conf:
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no
[req_distinguished_name]
C = US
ST = CA
L = Mountain View
O = Google
OU = Enterprise
CN = blog.6equj5.dev
[v3_req]
keyUsage = keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = blog.6equj5.dev

Troubleshooting

Note, nginx initially failed to start with errors like this

ERROR:tcti:src/tss2-tcti/tcti-device.c:443:Tss2_Tcti_Device_Init() Failed to open specified TCTI device file /dev/tpm0: Permission denied 
ERROR:tcti:src/tss2-tcti/tctildr-dl.c:154:tcti_from_file() Could not initialize TCTI file: libtss2-tcti-device.so.0 

What that means is of course a permissions issue

# ls -lart /dev/tpm*
crw-rw---- 1 tss tss  240, 65536 Dec 12 18:17 /dev/tpmrm0
crw-rw---- 1 tss root  10,   224 Dec 12 18:17 /dev/tpm0

since nginx runs as www-data for me

# ps -ef|grep nginx
root       899     1  0 10:35 ?        00:00:00 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
www-data   900   899  0 10:35 ?        00:00:00 nginx: worker process
www-data   901   899  0 10:35 ?        00:00:00 nginx: worker process
www-data   902   899  0 10:35 ?        00:00:00 nginx: worker process
www-data   903   899  0 10:35 ?        00:00:00 nginx: worker process
root       907   732  0 10:36 pts/0    00:00:00 grep nginx

All you should do is add the user to that group and restart nginx

usermod -a -G  tss www-data

This site supports webmentions. Send me a mention via this form.