Using AWS IoT for mutual TLS in a web application

Alex Smolen
5 min readSep 12, 2020

We use HTTPS to verify web server identities with X.509 certificates, but TLS also supports mutual authentication, where the server uses certs to verify the client’s identity. Given that passwords are a bottomless source of compromise from credential stuffing, phishing, and so on, the idea of using a cryptographically secure, phishing-resistant authentication mechanism already included in every major browsing and operating system seems like a big win.

People have been wondering why is nobody using SSL client certificates? since 2008, and in 2015 a post titled In defense of client certificates admits the “UX can be pretty horrible”. In 2020, client certificates are still too difficult to distribute and use for an internet-scale app. However, they make sense for corporate web applications, where you can use MDM to deploy client certificates to managed devices. For example, the Okta Device Trust feature uses client certificates. Is there a way to support a client certificate-based “device trust” feature natively in AWS?

I had heard a rumor that you could “hack” support for mutual TLS in an AWS web application using AWS IoT. This post confirms that rumor, although it’s not… elegant. AWS doesn’t offer an obvious way to support mutual TLS for a web application without using your own code. With the recent release of API Gateway support for Lambda and IAM authorization, it seems likely that API Gateway will eventually support mutual TLS. But the current recommendation is to handle client certificates with custom code.

I spent some time playing around with AWS IoT APIs to see how I could support a system where browsers could present a client certificate and a web application could verify it without writing custom TLS handling code. I did get something working and I thought it was important to document since I didn’t see a solution like it anywhere else.

Generating an IoT certificate for the MacOS keychain

The first step is to put a certificate in the MacOS keychain. While I did this manually, it’s an “exercise to the reader” to implement something similar with an MDM system.

I modeled my computer as an AWS IoT Thing and generated a certificate for it using the AWS CLI:

aws iot create-keys-and-certificate --certificate-pem-outfile "iot.cert.pem" --public-key-outfile "iot.public.key" --private-key-outfile "iot.private.key"

I noted the certificate ID that was returned and associated it with a new Thing:

aws iot register-thing \--template-body '{"Parameters":{"ThingName":{"Type":"String"},"AWS::IoT::Certificate::Id":{"Type":"String"}},"Resources": {"certificate":{"Properties":{"CertificateId":{"Ref":"AWS::IoT::Certificate::Id"},"Status":"Active"},"Type":"AWS::IoT::Certificate"},"thing":{"OverrideSettings":{"AttributePayload":"MERGE","ThingGroups":"DO_NOTHING","ThingTypeName":"REPLACE"},"Properties":{"AttributePayload":{},"ThingGroups":[],"ThingName":{"Ref":"ThingName"}},"Type":"AWS::IoT::Thing"}}}' \--parameters '{"ThingName":"{thing-name}","AWS::IoT::Certificate::Id":"{certificate-id}"}'

Next, I created a pfx file that could be imported into the Mac OSX keychain:

openssl pkcs12 -export -out iot.pfx -inkey iot.private.key -in iot.cert.pem

Finally, I needed to mark this certificates as “trusted” for SSL using the keychain manager:

MacOS Keychain Access settings for AWS IoT certificate

Creating an AWS IOT role to assume

Now that I have a certificate associated with an AWS IOT Thing installed on my Mac, how do I use it to authenticate to a web application?

I went down a few paths here. The AWS IoT HTTP Rest API recently added support for client certificates, but you can only publish data — there’s no way to get data back that would allow you to authenticate. I briefly considered implementing an MQTT-based JS client, which seemed like overkill. Eventually, I stumbled upon the AWS IoT credentials provider feature, which supports issuing AWS IAM credentials using X.509 client certificates.

First, I created a plain old AWS IAM Role that could be assumed by the AWS IOT credentials service. This is the active part of the trust relationship policy:

{
"Effect": "Allow",
"Principal": {
"Service": "credentials.iot.amazonaws.com"
},
"Action": "sts:AssumeRole"
}

Next, you have to create a AWS IOT role alias, which “allows you to change the role without having to update the device”.

aws iot create-role-alias --role-alias {role-alias-name} --role-arn arn:aws:iam::{account-id}:role/service-role/{role-name}

Finally, you need to configure the AWS IoT IAM policy to support assuming a role using a certificate:

{
"Effect": "Allow",
"Action": "iot:AssumeRoleWithCertificate",
"Resource": "arn:aws:iot:us-east-1:{account-id}:rolealias/{role-alias-name}"
}

Configuring Chrome to assume a role with the certificate

How do we tie everything together, so that Chrome can use the X.509 certificate in the Mac OS keychain to assume the role and get the credentials?

First, I had to configure Chrome to use the certificate with the subject “AWS IoT Certificate” for the credentials endpoint domain without presenting a dialog box and requiring a password:

defaults write com.google.Chrome AutoSelectCertificateForUrls -array-add -string '{"pattern":"https://{credentials-endpoint}.credentials.iot.us-east-1.amazonaws.com","filter":{"SUBJECT":{"CN":"AWS IoT Certificate"}}}'

You can retrieve your “credentials endpoint” using the following command:

aws iot describe-endpoint --endpoint-type iot:CredentialProvider

Now I could visit the credentials endpoint in Chrome and get AWS IAM credentials back in JSON format without user interaction, but I hit another sticking point. Let’s say I wanted to support authenticating to certain domains (e.g. *.alexsmolen.com). There’s no CORS policy configured for the credentials endpoint that would allow me to retrieve the JSON from arbitrary domains in client-side JS. So, I had to create a Chrome extension. The source code is in this gist, but the general idea is that a content script loads for trusted domains and sends a message to the background page (which is where you can break the Same-Origin Policy), which calls out to the AWS IOT credentials endpoint and pass the credentials back to the content script, which can place them in the DOM of the page in the trusted domain.

So then what?

At this point, you’ve bootstrapped trust in the browser, and you can use the credentials to verify trust in a web application in a variety of ways. You could use the AWS IAM credentials to make direct calls to AWS services using the JS AWS-SDK. You could use a Confidant-style KMS-based authentication scheme to verify trust in your custom web application. This should be the easy part 😀

If you have any feedback about this approach or ideas about how to use AWS IAM credentials in the browser to establish trust, let me know on Twitter.

--

--