main About Projects Back to Blog Résumé Contact
I've started experimenting with exposing some of my home lab services to the world without needing a VPN. Instead, I use Caddy (which is an excellent web server, and much easier to work with than Nginx) as a reverse proxy with TLS client certificates for authentication. Caddy's built-in Let's Encrypt functionality provides the server certificate, and my internal PKI provides the client certificates. I still want to have 2-factor authentication, though — a certificate is merely “something you have”, and I want to require “something you know,” too. Since all my PCs are recent enough to feature a TPM, I decided to store the keys on said TPM and configure it to require a password before allowing any authentication to take place. I was inspired by Microsoft Hello for Business — which is really cool and I can't wait to work with it some day — but using the TPM directly is much lighter weight as no domain controller or Windows AD Domain is needed. The previous iteration of Microsoft's “Passwordless Future,” TPM-backed virtual smart cards would be exactly what I want, except the docs come with a big ol’ deprecation warning. Instead, I found how to interact with the base TPM crypto provider to generate certificates.
Normally when you generate a certificate, the full certificate and private key are stored in your user certificate store. This is encrypted with your login password, and you can mark certificates as non-exportable. However, anyone that is able to execute code as your user (e.g., malware), or gains administrative access to the computer can bypass that restriction and steal the certificate and key. Disk encryption (which you should be using) will protect against some ways of doing this, but not all of them. Also, the default certificate store's encryption can be attacked offline, which means anyone with a large AWS budget can probably get into it.
By generating the key in the TPM, we solve all of those issues. The TPM stores the key material and does all the cryptographic operations on itself, so the key cannot be stolen. When you supply a password at generation (which is optional but recommended), the TPM also requires that password before performing any operations, providing a second layer of protection. This forms the second factor of 2FA if done properly. The TPM has physical protections in-place to prevent attackers from gaining access to its data through side channels or chip decapping, and it has anti-hammering protections to prevent password brute-forcing. Taken together, this is probably the strongest form of key protection (and therefore, authentication) the average user has access to short of a physical crypto dongle like a Yubikey. Since every relatively recent Intel/AMD processor has a firmware TPM and Microsoft requires some kind of TPM in new Windows machines, you probably have one already! There really isn't a reason not to use this method of storing keys unless you need the key to be long-lived (such as disk or mail encryption certificates).
If you've been paying attention, you'll notice that this is not 2 factor authentication as typically understood: the server that validates the credential (Caddy, in my case) is only validating the certificate, which is under normal circumstances a single factor (“something you have”). The certificate is password-protected (“something you know”), but the web server doesn't have any way of verifying that — if you had a valid certificate signed by my CA that wasn't password protected, the server would still accept it. In my small case, the second factor is basically enforced by my personal policy, as I won't issue certificates for this use that I haven't personally verified are protected by hardware security and a PIN/Password in some way (such as via a TPM, smart card, or Yubikey).
In an enterprise deployment such as Hello For Business, this is accomplished cryptographically by the provisioning server that signs new certificates. When provisioning a new device, the platform security module provides a certificate signing request as normal. It also provides a statement signed with a burned-in manufacturer key that attests to the certificate's parameters (non-exportability, password protection, etc.). The provisioning server validates this statement before signing the CSR, which allows it to enforce the second factor policy across an entire enterprise. Highly secure applications will addiitonally require this attestation on every authentication request; this isn't really possible with traditional client TLS certificates, so it's traditionally done using Kerberos — the Kerberos server requires both the credential certificate and the attestation before issuing tickets. The same attestation key can combine with the TPM's Trusted Boot functionality to remotely attest that the system is in a given state, allowing the authentication server to ensure that clients are in a secure state (e.g., using an up-to-date firmware and operating system and configured according to company policy) before granting them access to secure resources.
Make an INF file with the following (I called mine
Then issue the command
certreq -new -f .\tpm_csr.inf csr_name.csr. You will get a prompt offering to protect the generated key with a password — this is the second factor in your 2FA, so I recommend it. After that, sign the certificate request with your enterprise CA (this is out of scope of this blog post, but you probably know how to do this if you've gotten this far) and import the resulting certificate into your user's certificate store (double click on the
crt file and click next a bunch of times) and you'll have a TPM-backed certificate ready for usage.
Warning: You cannot export this key from the TPM. Ever. If you lose your PC, or if the TPM gets reset accidentally (this can happen if you use multiple operating systems and accidentally try to use it with both, or if the BIOS on your machine is reset), you will lose the key. Ensure that you have an alternate way of accessing anything you protect this way.
You can also use this for SSH, by the way, using putty-cac. If you want to integrate it with the built-in Windows SSH tools (either the native Windows port or via WSL), see my next blog post for more information.