While we were certifying the skill we ran into an unexpected snag: Amazon will automatically verify that your endpoint checks the authenticity of the messages it receives. It’s not enough to give you an API key, because anyone could, at least in theory, get between their servers and yours and cause all sorts of trouble.
So Amazon signs their own messages using an X509 certificate and a private key. And there’s this whole crazy dance that has to happen between your server and Amazon in order to verify this message.
This is you verifying Alexa’s message
Elixir is still pretty new as far as programming languages go. So we quickly discovered that we would have to solve this problem ourselves. Now we could have tried going down the quick and dirty route of shelling out to the command line and using the
openssl utility. But we always try to stay away from shelling out to command line utilities given user input — that can always leave you open to gaping security holes.
Could we solve this problem using strictly what Elixir, Erlang and OTP provide us with? We knew Erlang had a
public_key module so we figured we’d start there.
Before you get to the signature part, which is the slightly tricky part of the process, there are a few basics checks that need to be made. They are:
All of this is pretty straightforward stuff. Refer to the package’s code if you want to see the actual implementations for these parts.
The first thing we need to do in order to verify the signature is decode the data in the PEM we downloaded, using
:public_key.pem_decode/1 This returns a bundle of certificates that we then need to decode into Erlang records.
certs = pem |> :public_key.pem_decode() |> Enum.map(&get_cert_value/1) decoded_certs = Enum.map(certs, &decode_cert/1)
get_cert_value/1 is a simple pattern match, but
decode_cert/1 is where we start to deal with records. The way Erlang’s :public_key module works is that an Erlang header gets generated from an ASN.1 schema for what the contents of a public key look like. So first we need to grab hold of this schema:
@pubkey_schema Record.extract_all(from_lib: "public_key/include/OTP-PUB-KEY.hrl”)`
And then we need to write a little helper to help us traverse the nested structure of this record. Each of the entries in this record is actually a tuple, hence the use of
defp get_field(record, field) do record_type = elem(record, 0) idx = @pubkey_schema[record_type] |> Keyword.keys |> Enum.find_index(&(&1 == field)) elem(record, idx + 1) end
So with that out of the way, let’s take a look at our
defp decode_cert(cert) do cert |> :public_key.pkix_decode_cert(:otp) |> get_field(:tbsCertificate) end
In case you’re wondering,
tbs stands for to be signed. This is where all of the certificate data we need is located.
So there are a few checks that LessVerifiesAlexa runs against the certificate.
check_cert_dates/1ensures that certificates in the chain are still valid based on their dates.
check_san/1checks that the cert chain does indeed hold a certificate that is signed by Amazon by looking at the domain the certificate is registered against.
check_cert_path/1Ensures that the certificate chain we have ends with a trusted root-level certificate. The
certifipackage was a godsend for this, and the fact that it’s bundled with quite a few Erlang/Elixir packages means there’s a good chance it’s already a part of your dependencies.
confirm_payload_signature/3does the actual work of verifying the body of the request against the signature. It looks like this:
defp confirm_payload_signature(cert, req_signature, raw_body) do :public_key.verify( raw_body, :sha, Base.decode64!(req_signature, ignore: :whitespace), public_key(cert) ) end
public_key/1 helper function looks like this:
defp public_key(cert) do cert |> get_field(:subjectPublicKeyInfo) |> get_field(:subjectPublicKey) end
Checking the signature is one of the simplest parts of the process. I won’t bore you to death with all the details of how the other parts work, feel free to look at the source and ask any questions you want in the comments section. The other parts are more complicated than what you’re seeing here. But after those checks go through, we know that our certificate is valid.
Once all these issues were resolved, it was time to package the solution up and publish it. Testing wasn’t too complicated: - you inject the HTTP client rather than hardcoding it so you can stub out the HTTP requests. - badssl.com had all kinds of invalid certs we needed for testing this stuff out. Big props to the folks behind that project, they saved us a ton of tinkering with SSL certificates.
ExDoc makes writing documentation so easy it’s not even worth mentioning.
And one last thing. We used dialyxir in this project and it is so worth the price of admission. With few specs written, it caught a couple of bugs after a refactoring session which the tests didn’t catch. Yes, that means the coverage wasn’t good enough, but I would have missed that had it not been for dialyzer. Seriously, use it, you’ll feel much more confident in your code if you put in just a little bit of effort.
Tucked away behind the neat description you’re reading here is a whole lot of reading documentation, consulting the header I mentioned before, trying to make sense of RFC 5280, and actually reading through public_key.erl.
Dealing with this stuff often seems so daunting that many developers just give up on it altogether. But if there’s one thing that will magically change your life as a developer, that’s the realization that nothing you deal with is magic. Sometimes you have to get your hands dirty and poke at complex nested data that you can’t make heads or tail of — that was the case here. Sometimes you have to, gasp, look an algorithm or data structure up and see what the implications of using it. Sometimes it’s jumping head first into a new framework or stack, or even a different domain altogether.
But there is no magic. Code is code. At the end of the day, especially if you’re dealing with open source and have the luxury of popping the trunk and inspecting things for yourself, you will figure out how things work. Just don’t give up and say “I don’t know how to do this”. Take a break, thing of something else and come back at the problem with a fresh pair of eyes. Constantly ask yourself “what is it that I don’t understand here?” and find the answer to that question. Soon enough, you’ll find you know everything you need to get the job done.
If you wanted it to build a product you’d find a way to get time to work on it. If you really wanted to start that new hobby you’d sacrifice something to find the time and money to do it.
I'll define a "Wannabe Entrepreneur" as someone who has never made money from their businesses. Here are the different types of wannabes.
In the past few years I've built go-carts, built a 200+ sq ft workshop, written several eBooks. How do I create a life where I have time to work on side projects?
Receive 5 Software projects mistakes we have made over the years and how to avoid them.