Using certificate chain-of-trust to verify JWT offline
This post is a follow up on the previous post A Journey to offline JWT authentication, if you haven’t read the previous one, I highly recommend reading it first to establish the basis for this one.
To summarize, in the previous post we addressed one of the main challenges that could arise when validating JWT on an offline device, which is establishing root of trust. Towards the end we realized that the approach so far is not very scalable and once the sender decides to rotate it’s RSA Key Pair, things fall apart.
In this post we’ll explore the ways in which we can make the overall flow key rotation agnostic. So that even if sender rotate it’s key pair, the receiver would still be able to validate the actual tokens and invalidate the tampered ones.
NOTE: The commands and scripts used in this post are only for demonstration purposes and are not intended to be used in production systems.
Before jumping into the rotation agnostic part, let’s take a step back. RSA Public Key has no additional information or metadata associated with it, hence we don’t have anyway to validate it’s authenticity by itself. Here, we can leverage X509 certificates that associate the owner’s identity (and corresponding issuer) with itself and allow us to validate the authenticity of the certificate and the embedded public key, inherently.
NOTE: Before moving forward, please clone
this repo
to obtain helper methods which will come handy during illustrations in this post.
Now let’s generate a self signed X509 Certificate with RSA Public Key, which in real world scenario could be embedded into the firmware to establish the same root of trust that we were doing using RSA Public Key.
mkdir -p ../root_cert
cd ../root_cert
# Copy OpenSSL config file to cwd
cp /path/to/repo/scripts/openssl_ca.cnf .
source /path/to/repo/scripts/token_helper.sh
# Generate new RSA Private Key and corresponding X509 certificate with RSA public key
openssl req -x509 -nodes -newkey rsa:4096 -days 1 -config openssl_ca.cnf -extensions ca_cert -sha256 -keyout private.pem -out certificate.crt -subj "/CN=root"
Now let’s modify the original token and validate the updated token against generated X509 Certificate. And since this certificate would be embedded into the receiver’s firmware (or communicated initially over a secure channel), we can be sure of it’s authenticity.
# Modify original token (update exp claim) and sign with own private key
modify_token `cat ../original_token/token` private.pem > updated_token
# Validate updated token against certificate.crt
validate_token `cat updated_token` certificate.crt
X509 certificates provide us with a feature to validate their authenticity via chain of trust, so instead of trusting the leaf certificate that is used to sign the token, we can trust the issuer certificate that signs the leaf certificate which will eventually sign the token.
We can actually leverage this feature of X509 certificate to attain some level of scalability and making the whole flow agnostic to key/ certificate rotation to some extent.
Let’s generate a leaf certificate which will be signed by previously generated root certificate.
mkdir -p ../leaf_cert
cd ../leaf_cert
# Copy OpenSSL config file to cwd
cp /path/to/repo/scripts/openssl_ca.cnf .
# Generate RSA Private Key and CSR for leaf certificate
openssl req -nodes -newkey rsa:4096 -sha256 -keyout private.pem -out certificate.csr -subj "/CN=leaf"
# Sign leaf CSR with RSA keypair of root cert
openssl x509 -req -days 1 -CA ../root_cert/certificate.crt -CAkey ../root_cert/private.pem -CAcreateserial -extfile openssl_ca.cnf -extensions server_cert -in certificate.csr -out certificate.crt
# Validate certificate.crt against root_cert/certificate.crt
openssl verify -CAfile ../root_cert/certificate.crt certificate.crt
Since certificate.crt
is signed locally and not by a globally trusted CA, we need to provide our own trusted CA certificate to OpenSSL for verification.
# Modify original token (update exp claim) and sign with own private key
modify_token `cat ../original_token/token` private.pem > updated_token
# Validate updated token against certificate.crt
validate_token `cat updated_token` certificate.crt
In order to verify that the flow is actually rotation agnostic, let’s rotate the leaf certificate (which signs JWT), which is essentially just creating a fresh pair of RSA key pair & certificates and see how things behave.
mkdir -p ../leaf_cert_rotated
cd ../leaf_cert_rotated
# Copy OpenSSL config file to cwd
cp /path/to/repo/scripts/openssl_ca.cnf .
# Generate RSA Private Key and CSR for leaf certificate
openssl req -nodes -newkey rsa:4096 -sha256 -keyout private.pem -out certificate.csr -subj "/CN=leaf_rotated"
# Sign leaf CSR with RSA keypair of root cert
openssl x509 -req -days 1 -CA ../root_cert/certificate.crt -CAkey ../root_cert/private.pem -CAcreateserial -extfile openssl_ca.cnf -extensions server_cert -in certificate.csr -out certificate.crt
# Validate certificate.crt against root_cert/certificate.crt
openssl verify -CAfile ../root_cert/certificate.crt certificate.crt
# Modify original token (update exp claim) and sign with own private key
modify_token `cat ../original_token/token` private.pem > updated_token
# Validate updated token against certificate.crt
validate_token `cat updated_token` certificate.crt
Now we are not directly dependent on the leaf certificate for the verification so the flow is rotation agnostic as long as the key pair associated with the issuer certificate is not rotated. Once that is rotated, we are back at the same problem.
I assume the solution is pretty evident by now, in order to establish root of trust we embed the parent certificate of issuer certificate into the firmware and not the issuer certificate. And during validation we would need this chain of certificates where the first certificate is issued by the next certificate in the list and eventually we reach a self signed root certificate. On the receiver’s side, we only need trust the self signed root certificate.
pic credit: https://www.keyfactor.com/blog/certificate-chain-of-trust
In order to illustrate multiple intermediate CAs and verification via CA Chain, let’s use the certificate_helper.sh
from this repo.
# Move into parent directory
cd ..
# Prepare directory structure for certificate_helper.sh
mkdir scenarios
# Copy OpenSSL config file to cwd
cp /path/to/repo/scripts/openssl_ca.cnf .
source /path/to/repo/scripts/certificate_helper.sh
# Create certificate chain with 4 intermediate CAs, you can use any number
create_certificate_chain 4
cd scenarios/leaf
# Validate certificate.crt against root_cert/certificate.crt
openssl verify -CAfile root_certificate.crt -untrusted ca-chain.crt certificate.crt
In this case, since the leaf certificate is not directly signed by root_certificate, we also need to provide the intermediate chain of CAs, which by default is untrusted. This will allow OpenSSL to build an whole certificate chain and verify.
# Modify and sign original token (update exp claim)
modify_token `cat ../../original_token/token` private.pem > updated_token
# Validate updated token against certificate.crt
validate_token `cat updated_token` certificate.crt
And theoretically, this can be extended for any number of intermediate CA certificates.
Now you might be wondering what if the key pair associated with this self signed root certificate needs rotation. Won’t we be back to square one?
Well the answer for that is yes and no.
Of course, if the key pair of root certificate is rotated the receiver will no longer be usable because of the same limitation but in general the lifetime of these root certificates is pretty long ~10 years (or more in some cases), compared ~6 months for leaf certificate Certificate Lifetimes. This ideally gives us enough time to plan firmware update(s) and push the new Root Certificate on the device.
Also the organisations are supposed to be responsible for keeping the private part of their root Key pair secure in some limited access or offline zone. That is because if the private part of the certificate is leaked or lost, the organisation would anyway need to reissue all it’s certificates from the beginning. So I think we can safely assume that the probability of rotating root certificates out of cycle would be pretty low compared to intermediate certificates.
Conclusion
JWT is very versatile and easy to use technology for transmitting information securely between two parties. As we saw recently, this technology can very easily extended to be used for performing validation on offline devices as well using X509 Certificate trust chains.
Remarks
I actually came across this problem as a part of an open source project, Aerobridge I’m working with. Aerobridge is a management server for drone manufacturers, assemblers and operators. Once deployed, it aims at providing a security layer for the organization’s drone operations. This security mechanism is called Trusted Flight
and uses JWT tokens, issued by commonly available OAUTH providers (in this case the opensource Flight Passport). Aerobridge is linked to an OAUTH provider and issues JWT tokens against an operation stored in the server, this token is verified on the drone and only after the token is verified against valid certificate chain, the drone is armed. For more details about the implementation of JWT verification for drones, please refer to this pull request.
Please checkout this repo for further details about JWT manipulation and verification using X509 Certificate Chain. Currently this repository contains implementation for various scenarios in C++
and bash
using OpenSSL
and json parsing library
.
Acknowledgements
Special thanks for Karel Rymeš, Hrishikesh Ballal and Siddharth Purohit for their contribution to the ideas and discussions.