Current section
Files
Jump to
Current section
Files
lib/alexa_request_verifier.ex
defmodule AlexaRequestVerifier do
@moduledoc """
AlexaRequestVerifier verifies an Amazon Alexa Skills request to a Pheonix server.
To add the request, you will need to make 4 changes:
1. mix.exs - the package can be installed by adding `alexa_request_verifier` to your list of dependencies in `mix.exs`:
def deps do
[{:alexa_request_verifier, "~> 0.1.3"}]
end
2. You will need to add AlexaRequestVerifier as an application in your mix.js
applications: [..., :alexa_request_verifier]
3. You will need to modify your endpoint.ex file by adding the JSONRawBody Parser as follows:
parsers: [AlexaRequestVerifier.JSONRawBodyParser, :urlencoded, :multipart, :json],
The parser is needed to collect the raw body of the request as that is needed to verify the signature.
4. You will need to add the verifier plug to your pipeline in your router.ex file
pipeline :alexa_api do
plug :accepts, ["json"]
plug AlexaRequestVerifier
end
"""
# this config list is from: https://aws.amazon.com/blogs/security/how-to-prepare-for-aws-move-to-its-own-certificate-authority/?mkt_tok=eyJpIjoiTkdaa05UVTJabUpsT0RSaSIsInQiOiI1dmhhZHpxVHRlRnZuZktWRDVGNk44czlCT21odWxDeWxrVmZSbHNoVWdcLzZsdTh3clQwNEd4N1ZEYXhrZDFjY085R0w2aVwvUTFtV3pEd0Yxa1JvalhzTlVDV1Z0SXZvc01ZQU5zbnMweW1PdWIwcXBTMU9kUjNcL29KMDhMcm9taiJ9
@root_cas [
"""
-----BEGIN CERTIFICATE-----
MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF
ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6
b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL
MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv
b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj
ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM
9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw
IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6
VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L
93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm
jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA
A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI
U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs
N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv
o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU
5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy
rqXRfboQnoZsG4q5WTP468SQvvG5
-----END CERTIFICATE-----
""",
"""
-----BEGIN CERTIFICATE-----
MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF
ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6
b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL
MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv
b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK
gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ
W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg
1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K
8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r
2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me
z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR
8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj
mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz
7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6
+XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI
0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB
Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm
UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2
LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY
+gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS
k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl
7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm
btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl
urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+
fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63
n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE
76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H
9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT
4PsJYGw=
-----END CERTIFICATE-----
""",
"""
-----BEGIN CERTIFICATE-----
MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5
MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g
Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG
A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg
Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl
ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j
QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr
ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr
BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM
YyRIHN8wfdVoOw==
-----END CERTIFICATE-----
""",
"""
-----BEGIN CERTIFICATE-----
MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5
MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g
Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG
A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg
Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi
9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk
M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB
/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB
MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw
CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW
1KyLa2tJElMzrdfkviT8tQp21KW8EA==
-----END CERTIFICATE-----
""",
"""
-----BEGIN CERTIFICATE-----
MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx
EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT
HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs
ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5
MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD
VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy
ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy
dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p
OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2
8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K
Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe
hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk
6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw
DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q
AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI
bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB
ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z
qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd
iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn
0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN
sSi6
-----END CERTIFICATE-----
"""
]
import Plug.Conn
alias Plug.Conn
require Logger
@amazon_echo_dns "echo-api.amazon.com"
@sig_header "signature"
@sig_chain_header "signaturecertchainurl"
def init(opts), do: opts
def call(conn, _opts) do
case conn.private[:alexa_verify_test_disable] do
true ->
conn
_ ->
conn = conn
|> get_validated_cert
|> verify_time
|> verify_signature
if conn.private[:alexa_verify_error] do
Logger.debug("alexa_request_verifier: #{conn.private[:alexa_verify_error]}")
conn
|> send_resp(401, conn.private[:alexa_verify_error])
|> halt
else
conn
end
end
end
def get_validated_cert(conn) do
case Plug.Conn.get_req_header(conn, @sig_chain_header) do
[] -> Conn.put_private(conn, :alexa_verify_error, "no request parameter named #{@sig_chain_header}" )
[cert_url] -> cert_url = cert_url
cert = ConCache.get(:cert_signature_cache, cert_url)
if is_nil(cert) do
cert = fetch_cert(cert_url)
case validate_cert(cert) do
{:ok, _} ->
Logger.debug("alexa_request_verifier: setting cache for #{cert_url}")
ConCache.put(:cert_signature_cache, cert_url, cert)
Conn.put_private(conn,:signing_cert, cert)
{:error, reason} ->
Logger.debug("alexa_request_verifier: got error - #{reason}")
Conn.put_private(conn, :alexa_verify_error, reason )
end
else
Conn.put_private(conn,:signing_cert, cert)
end
end
end
@doc """
takes a string and confirms URL is in scheme https, has a s3.amazonaws.com host, is port 443 and has a path starting with /echo.api/
"""
def is_correct_alexa_url?(url) when is_binary(url) do
is_correct_alexa_url?(URI.parse(url))
end
def is_correct_alexa_url?(url) when is_nil(url) do
false
end
def is_correct_alexa_url?(%URI{port: 443, host: "s3.amazonaws.com", scheme: "https", path: "/echo.api/" <> _extra}) do
true
end
def is_correct_alexa_url?(_everything_else) do
false
end
def fetch_cert(url) do
if(!is_correct_alexa_url?(url)) do
{:error, "invalid sig chain url"}
else
{:ok, resp} = :httpc.request(:get, {String.to_charlist(url), []}, [], [body_format: :binary])
{_, _headers, certificate_chain_bin} = resp
cert_chain = :public_key.pem_decode(certificate_chain_bin)
Enum.map(cert_chain,
fn {_, bin, _} -> bin
end)
end
end
def verify_fun(_, {:extension, _}, state) do
{:unknown, state}
end
def verify_fun(_, {:bad_cert, reason}, _state) do
{:fail, reason}
end
def verify_fun(_, {:revoked, _}, _state) do
{:fail, :revoked}
end
def verify_fun(_cert, _event, state) do
{:unknown, state}
end
@doc """
Validates the cert checks for hostname, checks that the cert has a valid key, etc...
loop through the configured root CA's
"""
def validate_cert_chain(cert) do
# {:ok, resp} = :httpc.request(:get, {'https://certs.secureserver.net/repository/sf-class2-root.crt', []}, [], [body_format: :binary])
# {_, _headers, root_cert_bin} = resp
# {:ok, root_cert_bin} = :file.read_file("./auth_root.pem")
found_ca = @root_cas
|> Enum.find(fn root_ca ->
case validate_cert_by_binary(root_ca, cert) do
{:ok, _} -> true
{:error, _} -> false
end
end)
case found_ca do
nil -> {:error, :no_root_ca_found}
_ -> {:ok, cert}
end
end
def validate_cert_by_binary(root_cert_bin, cert) do
[{_, root_cer, _}] = :public_key.pem_decode(root_cert_bin)
case :public_key.pkix_path_validation(root_cer, Enum.reverse(cert),
[{:verify_fun, {&__MODULE__.verify_fun/3, {}}}]) do
{:ok, {_public_key_info, _policy_tree}} ->
{:ok, cert}
{:error, {:bad_cert, reason}} ->
{:error, reason}
end
end
def validate_cert(err = {:error, _reason}) do
err
end
def validate_cert(cert) do
validate_cert_chain(cert)
|> validate_cert_domain
end
def validate_cert_domain(error = {:error, _reason}) do
error
end
def validate_cert_domain({:ok, cert}) do
[first|_tail] = cert
if :public_key.pkix_verify_hostname(first, [{:dns_id, @amazon_echo_dns}]) do
{:ok, cert}
else
{:error, "invalid DNS"}
end
end
def is_datetime_valid?(datetime_string) when is_binary(datetime_string) do
case NaiveDateTime.from_iso8601(datetime_string) do
{:ok, datetime} ->
is_datetime_valid?(datetime)
{:error, _reason} ->
false
end
end
def is_datetime_valid?(datetime = %NaiveDateTime{}) do
min = 150 # minimum number of seconds
NaiveDateTime.diff(NaiveDateTime.utc_now(),
datetime, :second) <= min
end
def is_datetime_valid?(datetime) when is_nil(datetime) do
false
end
@doc """
given a Plug.Conn that has a valid Alexa request request/timestamp, it will confirm the timestamp is valid
"""
def verify_time(conn) do
params = conn.body_params
timestamp = params["request"]["timestamp"]
case is_datetime_valid?(timestamp) do
true ->
conn
false ->
Conn.put_private(conn, :alexa_verify_error, "invalid timestamp" )
end
conn
end
@doc """
Assuming :raw_body, :signing_cert, and signature header, it will verify the signature
"""
def verify_signature(conn) do
case conn.private[:signing_cert] do
nil->
Conn.put_private(conn, :alexa_verify_error, "invalid certificate" )
_ ->
verify_signature_with_valid_cert(conn)
end
end
def verify_signature_with_valid_cert(conn) do
message = conn.private[:raw_body]
case Plug.Conn.get_req_header(conn, @sig_header) do
[] -> Conn.put_private(conn, :alexa_verify_error, "no signature" )
[signature] ->
{:ok, signature} = Base.decode64(signature)
[first|_tail] = conn.private[:signing_cert]
decoded = :public_key.pkix_decode_cert(first,:otp)
public_key_der = decoded |> elem(1) |> elem(7) |> elem(2)
if(!is_nil(public_key_der) and :public_key.verify(message, :sha, signature, public_key_der)) do
Logger.debug("alexa_request_verifier: Signature of Alexa request is valid for this message.")
conn
else
Conn.put_private(conn, :alexa_verify_error, "signature did not match" )
end
end
end
end