Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Application integration with TPM2 PKCS#11 #605

Closed
dnoliver opened this issue Sep 29, 2020 · 5 comments
Closed

Custom Application integration with TPM2 PKCS#11 #605

dnoliver opened this issue Sep 29, 2020 · 5 comments

Comments

@dnoliver
Copy link
Contributor

So, we made the testing and bug fixing work to get tpm2-pkcs11 working for WIFI Authentication and OpenVPN connections.
But it is yet not clear how any application could use this engine to retrieve private keys (or certs like in the case of OpenVPN).

So, I took a shot at trying to use the tpm2-pkcs11 engine from a custom application. And it worked!
Sharing with you how I made it work.
There is a gist as well available here https://gist.github.com/dnoliver/388303c94639b763e9f120e2cbcccf8f

All the test was done with openssl s_server, openssl s_client, EasyRSA and a NodeJS application
It is very similar to the https://github.com/tpm2-software/tpm2-pkcs11/blob/master/test/integration/tls-tests.sh.
I will skip the explanation on how to setup the CA, and create the server and client certs. That can be seen in the gist.

Once you have everything create and setup (and after a successful connection with openssl s_client), we are ready to create the custom application:

Some notes about this very short script:

  1. This is a modified version of https://nodejs.org/en/knowledge/cryptography/how-to-use-the-tls-module/#tls-connect
  2. First, I tried just using the PKCS#11 URI as the key and hope for that to work, but the tls.connect call complained that the key is not a PEM. My understanding was that private key paths, strings, and pkcs11 uri should be magically interchangeable, but that is not the case for NodeJS at least
  3. The engine support was added in NodeJS recently, details can be seen in this PR RFC: support for HSM private keys in TLS handshake nodejs/node#28973
  4. The docs about how to setup a TLS session with the engine were not 100% trivial for me, but I was able to get it working after some attempts. So I think that end to end examples like this below could help a little bit. The engine and private key is configured with a Secure Context (see https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options).
  5. Again, I tried setting the PKCS#11 URI in the privateKeyIdentifier, but got weird error messages. The reason why I want to use the uri is because I can set the pin in the URI and not get a prompt. But after setting the Private Key ID, it all worked fine

So, here is the NodeJS version of it:

var tls = require('tls'),
    fs = require('fs');

var args = process.argv.slice(2);
if (args.length === 0) {
  console.log('Usage: tls.js PRIVATE_KEY_IDENTIFIER');
  process.exit(1);
}

var tlsContext = tls.createSecureContext({
  'privateKeyEngine': 'pkcs11',
  'privateKeyIdentifier': args[0],
  'ca': fs.readFileSync('./tls-client/ca.crt'),
  'cert': fs.readFileSync('./tls-client/client.crt')
});

var options = {
  secureContext: tlsContext,
  // Necessary only if the server's cert isn't for "localhost".
  checkServerIdentity: () => { return null; }
};

var conn = tls.connect(4433, options, function() {
  if (conn.authorized) {
    console.log("Connection authorized by a Certificate Authority.");
  } else {
    console.log("Connection not authorized: " + conn.authorizationError)
  }

  console.log();
});

conn.on("data", function (data) {
  console.log(data.toString());
  conn.end();
});

And the output (server output shown after the tls.js script is executed):

[test@fedora-server tls-server]$ sudo openssl s_server -CAfile ca.crt -cert server.crt -key server.key -Verify 1 <<< '1'                                                                                       
verify depth is 1, must return a certificate                                                                                                                                                                   
Using default temp DH parameters                                                                                                                                                                               
ACCEPT                                                                                                                                                                                                         
depth=1 CN = Easy-RSA CA                                                                                                                                                                                       
verify return:1                                                                                                                                                                                                
depth=0 C = US, ST = Oregon, L = Hillsboro, O = Intel Corp, OU = Internet of Things Group, CN = fedora-server.mshome.net
verify return:1
DONE
shutdown accept socket
shutting down SSL
CONNECTION CLOSED
   0 items in the session cache
   0 client connects (SSL_connect())
   0 client renegotiates (SSL_connect())
   0 client connects that finished
   1 server accepts (SSL_accept())
   0 server renegotiates (SSL_accept())
   1 server accepts that finished
   0 session cache hits
   0 session cache misses
   0 session cache timeouts
   0 callback cache hits
   0 cache full overflows (128 allowed)

[test@fedora-server ~]$ sudo node tls.js 32336239613134376539373761633463
Enter PKCS#11 token PIN for tls:
Connection authorized by a Certificate Authority.

1

Based on the output, I assume that the connection was done correctly :)

Some questions (@williamcroberts):

  1. If the language api does not have a way of setting the PIN, how do you provide it for a non-interactive scenario? Is there a way of creating a private key that does not requires a pin?
  2. Is there a way of replacing the PIN with an integrity policy, so the private key will get automatically unsealed based on the PCR states? This will make much more sense in the IoT/Edge scenario where there is no user.
@williamcroberts
Copy link
Member

  1. No. Most clients have their own methods of getting the PIN. A pin may be empty.
  2. I want to be able to add a policy to objects so it's something like PCR state AND pin. You could create an object with a PCR state and link it into the token with ptool if you wanted to. But I'd like first class citizen support for that.

@dnoliver
Copy link
Contributor Author

Thanks Bill,

I changed a little bit the CSR generation, to just use tpm2_ptool instead of p11-tool

CKA_ID=$(tpm2_ptool listobjects --label tls | grep CKA_ID | uniq | awk '{ gsub("\047","", $2); print $2}' | sed 's/.\{2\}/%&/g')
openssl req -new -engine pkcs11 -keyform engine \
        -key "pkcs11:id=${CKA_ID};type=private;pin-value=userpin" \
        -config client.cnf -out client.csr

One weird thing about this:

  1. This URI does not work pkcs11:id=38366236363162643431353537643837;type=private;pin-value=userpin, openssl complains that it is not a valid PKCS#11 URI
  2. But this works: pkcs11:id=%38%36%62%36%36%31%62%64%34%31%35%35%37%64%38%37;type=private;pin-value=userpin. That is why I need to do the last sed to inject the & character every 2 digits of the ID.

I was able to provide the PIN to the NodeJS application using the OPENSSL_CONF environment variable, and providing the PIN in the configuration file. (https://gist.github.com/dnoliver/388303c94639b763e9f120e2cbcccf8f#file-ossl-cnf)

So running the sample NodeJS application like this automatically stablish the connection with openssl s_server:

sudo OPENSSL_CONF=./ossl.cnf node tls.js pkcs11 38366236363162643431353537643837 ./tls-client/ca.crt ./tls-client/client.crt`

I tried removing the PIN requirement by providing an empty PIN, but openssl refused to create the CSR with that key. It request a pin, and the length cannot be empty (minimum 4 characters). I will keep investigating this, let me know if you have any hints.

Additionally, the intention of this sample was to be executed from a container. So I created a simple container using Fedora as the base image and installing the pkcs11 dependencies inside (https://gist.github.com/dnoliver/388303c94639b763e9f120e2cbcccf8f#file-dockerfile). Then, running the container like this get the TLS connection done:

docker run \
  --rm \
  --name test \
  -ti \
  --device /dev/tpm0 \
  --device /dev/tpmrm0 \
  --volume /etc/tpm2_pkcs11/:/etc/tpm2_pkcs11 \
  --volume "$PWD":/root/test \
  --net host \
  test \
  bash -c "OPENSSL_CONF=./ossl.cnf node tls.js pkcs11 38366236363162643431353537643837 ./tls-client/ca.crt ./tls-client/client.crt"
  1. TPM and Resources Manager are provided to the container
  2. The host tpm2_pkcs11 database is mounted in the container
  3. --net host is used because the openssl s_server is running in the host, but should also be possible to run it in another container.

The main idea here is that the system administrator will create private keys in the TPM and get certificates for service using mutual TLS, and then the containers will use those private keys and certs through the PKCS11 interface. The database could be one per host and shared with the containers with a volume mount, or the container itself could generate the db internally.

But I think there should be a better way of allowing a container to interact with the TPM other than mounting it as a device. Will keep looking at options here as well.

@williamcroberts
Copy link
Member

@dnoliver I am confused at what I am supposed to do with this? I see you mentioned, "But it is yet not clear how any application could use this engine to retrieve private keys (or certs like in the case of OpenVPN)".

Could you help clarify what you'd like?

@dnoliver
Copy link
Contributor Author

Hi Bill, sorry for the delay,

I just wanted some review from you on the approach, and you already did that.
The answer to "how an application can use the TPM through PKCS11" apparently depends on the language.

If the language uses OpenSSL to do the crypto operations, then support should be there, but how to use it depends on language implementation and documentation: you cannot change your cert path with a PKCS11 URI and expect it to work unfortunately.

For the PIN: you can create an object without the PIN, but some tools will require it anyways, so you may need to add a PIN. And passing the PIN to the custom application also depends on the language implementation.

And finally, if your software runs in a container, the easy way of making it interact with the host TPM is to bind mount /dev/tpm0 or /dev/tpmrm0. There are existing efforts that apparently are trying to improve this like https://github.com/parallaxsecond/parsec, but in my opinion it is an overkill for this simple use case.

Closing this, thank @williamcroberts!

@williamcroberts
Copy link
Member

Hi Bill, sorry for the delay,

I just wanted some review from you on the approach, and you already did that.
The answer to "how an application can use the TPM through PKCS11" apparently depends on the language.

If the language uses OpenSSL to do the crypto operations, then support should be there, but how to use it depends on language implementation and documentation: you cannot change your cert path with a PKCS11 URI and expect it to work unfortunately.

Yep, and to clarify, PKCS11 URI's are handled at a higher level, usually through something like p11-kit libraries. They are not understood by PKCS11 compliant libraries directly.

For the PIN: you can create an object without the PIN, but some tools will require it anyways, so you may need to add a PIN. And passing the PIN to the custom application also depends on the language implementation.

And finally, if your software runs in a container, the easy way of making it interact with the host TPM is to bind mount /dev/tpm0 or /dev/tpmrm0. There are existing efforts that apparently are trying to improve this like https://github.com/parallaxsecond/parsec, but in my opinion it is an overkill for this simple use case.

You can also fire up qemu with a virtual TPM. See:
tpm2-software/tpm2-software.github.io#51

Closing this, thank @williamcroberts!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants