This article gives a brief explanation of the supported authentication options when using AWS IoT Core and demonstrate how setup Custom Authentication with the token validation feature to prevent unwanted traffic to trigger the Lambda Custom Authorizer.
Introduction
Signature Version 4 is the signing protocol that AWS uses to authenticate requests to AWS services. This is basically a hash-based message authentication code (or HMAC). In December 2015 AWS launched AWS IoT Core. This is one of the first services that in addition to SigV4, it supported Mutual-TLS using x509 client certificates. Later, AWS added support for Custom Authentication giving users maximum flexibility when choosing how to authenticate and authorise incoming requests. This feature can be useful if for example you have devices that use a custom bearer token or MQTT username and password to authenticate.
AWS IoT Core supported protocols and authentication
Protocol | Operations Supported | Authentication |
---|
MQTT over WebSocket | Publish, Subscribe | Signature Version 4 |
MQTT over WebSocket | Publish, Subscribe | Custom authentication |
MQTT | Publish, Subscribe | X.509 client certificate |
MQTT | Publish, Subscribe | X.509 client certificate |
MQTT | Publish, Subscribe | Custom authentication |
HTTPS | Publish only | Signature Version 4 |
HTTPS | Publish only | X.509 client certificate |
HTTPS | Publish only | Custom authentication |
The way Custom Authentication works is that AWS passes the incoming request to a Lambda Function. The Lambda function is responsible of AuthZ/N. You are probably wondering – “That’s a lot of potential Lambdas that I’ll need to pay for if not used properly! (i.e. abused) ” But do not worry, the service comes with a feature called Token Validation to ensure traffic is valid before passing the request to the Authorizer Lambda function. The service validates a token you create using your own private key. AWS does not participate in the signing process. All you have to do is share the respective public key with AWS and the arbitrary string you intend to sign. The Lambda won’t get executed at all if the signature check fails. The best thing is that checking if the token is valid or not does not incur charges.
Setting up Token Validation - A minimal example
First, let’s create a private key. The service supports RSA keys that are at least 2048 bits in size.
openssl genrsa -out private_key.pem 2048
Then, generate the corresponding public key pair using the private key we just generated. We will need to upload the public key file contents to AWS at a later stage.
openssl rsa -in private_key.pem -pubout > public_key.pem
Choose an arbitrary string. I am using MyStringThatNeedsToBeSigned
then I get its digest using the SHA256 algorithm.
echo -n "MyStringThatNeedsToBeSigned" | shasum -a 256
# 8de411c918c5ed2896a7dfdd8f838fdc441c36a22938b024958b2546f8e7bd6a
I should get the following 256 bit string as a result from the above operation 8de411c918c5ed2896a7dfdd8f838fdc441c36a22938b024958b2546f8e7bd6a
. Now take those bits and sign them using your private key. The final step is to base64 encode the signature.
echo -n "8de411c918c5ed2896a7dfdd8f838fdc441c36a22938b024958b2546f8e7bd6a" \
| openssl pkeyutl -sign -inkey private_key.pem -pkeyopt digest:sha256 \
| base64
# Zq3D/…du8F421wVrH2iAkF40SQYNxpXG6KJybD9Am99rA9iWfAtJw9uV37sWxnUIWTD3fpnr60NeCimrp2mx3w==
Here is a simpler command that produces the same result:
echo -n "MyStringThatNeedsToBeSigned" \
| openssl dgst -sha256 -sign private_key.pem \
| base64
The expected result is a base64 encoded RSA signature. This is the string the devices will need to have in order to get past the token validation stage and trigger the Lambda that will do additional checks. For example, checking if the user name and password are valid or if an application-level bearer token is valid.
We need to create a Lambda before creating the Custom Authorizer. Here is a minimum lambda allows anyone with a valid signed token to get permissions to publish to the topic customAuhtTestTopic
.
Warning: this is just an example. Typically the Lambda would check for other things like user name and passwords, etc.
import json
def lambda_handler(event, context):
# Check for stuff (i.e. login / password, bearer token etc)
return {
"isAuthenticated": True,
"principalId": "myPrincipalDevice123",
"disconnectAfterInSeconds": 86400,
"refreshAfterInSeconds": 300,
"policyDocuments": [
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "iot:Publish",
"Effect": "Allow",
"Resource": "arn:aws:iot:*:*:topic/customAuhtTestTopic"
}
]
}
]
}
The next step is to create the actual Custom Authorizer. This setting is configured by going the AWS IoT Console then under Security -> Authorizers find the Create authorizer button. When creating the custom authorizer we need to specify the key-value pair we would like to use for our authentication scheme. I am using X-My-App-custom-key
as the key, MyStringThatNeedsToBeSigned
as the value (also referred as the token key name) and the contents of public_key.pem
as the token signing key. Note that MyStringThatNeedsToBeSigned
is not the value that the IoT device needs to send, instead the value an IoT device needs to send the base64 encoded signature of the sha256 digest of that string. And of course, the signature must be created using the corresponding private key.
We also need to choose a Lambda. You must also ensure that it has the Lambda has enough resource-level permissions to allow custom the AWS IoT Custom authorizer to invoke it. Here is an AWS CLI command to add permissions to Lambda to be invoked by AWS IoT Core:
aws lambda add-permission \
--function-name arn:aws:lambda:eu-west-1:123456789012:function:TestIotCustomAuthorizer \
--principal iot.amazonaws.com \
--source-arn arn:aws:iot:eu-west-1:123456789012:authorizer/MyCustomAuthorizer \
--statement-id test1 \
--action "lambda:InvokeFunction"
We are done with all the setup steps. Now it’s time to test. The AWS CLI comes with a tool to test the authorizer.
aws iot test-invoke-authorizer \
--authorizer-name MyCustomAuthorizer \
--token MyStringThatNeedsToBeSigned \
--token-signature Zq3D/…du8F421wVrH2iAkF40SQYNxpXG6KJybD9Am99rA9iWfAtJw9uV37sWxnUIWTD3fpnr60NeCimrp2mx3w==
The expected result is something like this:
{
"isAuthenticated": true,
"principalId": "myPrincipalDevice123",
"policyDocuments": [
"{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":\"iot:Publish\",\"Effect\":\"Allow\",\"Resource\":\"arn:aws:iot:*:*:topic/customAuhtTestTopic\"}]}"
],
"refreshAfterInSeconds": 300,
"disconnectAfterInSeconds": 86400
}
If this is all good, now we are ready to test using the data plane. I am using the HTTP protocol but the same credentials can be used with any of the other supported protocols like MQTT over TCP or MQTT over WSS.
curl \
--header "x-amz-customauthorizer-name: MyCustomAuthorizer" \
--header "X-My-App-custom-key: MyStringThatNeedsToBeSigned " \
--header "x-amz-customauthorizer-signature: Zq3D/…du8F421wVrH2iAkF40SQYNxpXG6KJybD9Am99rA9iWfAtJw9uV37sWxnUIWTD3fpnr60NeCimrp2mx3w==" \
--request POST \
--data "Hello, world" \
"https://example-ats.iot.eu-west-1.amazonaws.com/topics/customAuhtTestTopic?qos=1"
# {"message":"OK","traceId":"7b3d120b-b057-356b-f092-EXAMPLE"}