2017-03-05

Alexa Skills with Azure Function in PowerShell

Today it is an elementary task to create Microservices, I find the most challenging aspect the actual service idea. Last year I purchased an Amazon Alexa, and soon after I decided to build an Alexa skill to automatically recommend a movie based on genre and play it.
While ostentatiously simple, I soon encountered a number of issues.
The 1st issue was unifying my movie services for available content resources, this ultimately made it impossible to publicly publish the skill.
The 2nd issue and the topic of the post was the certificate validation in PowerShell. I first started writing the skill in Node.JS, but the library at the time did not work correctly, and my weekend project soon entered the nasty world of debugging node packages. So I reverted to my scripting language of choice PowerShell :). Actually writing the skill text is easy, I used the TheMovieDB which required some service location testing to reduce latency, but it provided the bulk speech output.

An Alexa Skill is essential a web service which return JSON strings representing what you want Alexa to say... super easy, but they also use hash-based messaging code (HMAC) to sign the request. HMAC is a specific type of message authentication code (MAC) involving a cryptographic hash function and a secret cryptographic key. HMAC is a method extensively used by Amazon’s S3, Alexa, and other APIs for AWS and in parts of the OAuth specification. With HMAC the body request along with a private key is hashed, and the resulting hash is sent along with the request. The server then uses its own copy of the private key and the request body to re-create the hash. If it matches, it allows the request. This prevents man in the middle interference with the request, as the hash will not match and the server knows the request has been tampered with. And the private key is never sent in the request, so it cannot be compromised in transit.
Like most I google'd HMAC MAC PowerShell, but to no avail, so I asked on Stack, and ultimately had to write my own function...

The requirements to validate the REST request are detailed on Amazon's Developer Portal but in brief:
  1. Get the Certificate and validate it
  2. Validate the source of the Certificate and the request
  3. Validate the signature of the request, and compare it to the body
The skill is published here https://www.amazon.com/IIOS-Movie-Wizard/dp/B01MZ91Z2B and is about as popular as soggy toast.
function verifySign($Json) { #Bypass validation if($bDebug) { return $true } try { #Assign variable [DateTime]$timestamp = $Json.requestBody.request.timestamp [System.Uri]$awsUrl = $Json.signaturecertchainurl $now = Get-Date } catch { return "Bad JSON request" } #start request validation #check request is less than 150sec old if($(New-Timespan -Start $timestamp -End $now).TotalSeconds -gt 150) { $validErr += "`nRequest Timestamp out of bounds." } #check certificate comes from amazon if($awsUrl.Host -ne "s3.amazonaws.com") { $validErr += "`nCertificate invalid host name." } #check certificate is on 443 if($awsUrl.Port -ne 443) { $validErr += "`nCertificate invalid protocol." } #check path is correct @ AWS if($awsUrl.LocalPath -cnotmatch "/echo.api/") { $validErr += "`nCertificate invalid path" } #get signature from HTTP headers try { $encryptedSignatureBytes = [System.Convert]::FromBase64String($REQ_HEADERS_SIGNATURE) } catch { $encryptedSignatureBytes = $false } if(!($encryptedSignatureBytes)) { $validErr += "`nSignature not provided or invalid." } else { #define path for downloading Cert $dlPath = "$cd\echo-api-cert-4.pem" if(!(Test-Path $dlPath)) { Invoke-WebRequest -Uri $awsUrl.AbsoluteUri -OutFile $dlPath } #Load Cert $cert = Get-PfxCertificate -FilePath $dlPath #Generate a SHA-1 hash value from the full HTTPS request body to produce the derived hash value $requestBodyBytes = [System.IO.File]::ReadAllBytes($req) $sha1Oid = [System.Security.Cryptography.CryptoConfig]::MapNameToOID('SHA1') #Compare the asserted hash value and derived hash values to ensure that they match if(!($cert.PublicKey.Key.VerifyData($requestBodyBytes, $sha1Oid, $encryptedSignatureBytes))) { $validErr += "`nFailed request hash comparison to signature." } #Validate Cert date if(($now -lt $cert.NotBefore) -or ($now -gt $cert.NotAfter)) { $validErr += "`nCertificate out of date." } #Verify Cert if(!($cert.Verify())) { $validErr += "`nCertificate validation failed." } #Check Cert domain match Amazon if($cert.DnsNameList -cnotmatch "echo-api.amazon.com") { $validErr += "`nCertificate invalid domain SAN." } } #return an error list or true; :) I don't even have to overload this method LOVE PS! if($validErr) { return $validErr } else { return $true } } #pass the request in to the function $validRtn = $(verifySign $($requestCertBody | ConvertFrom-Json)) if($validRtn -eq $true) { switch($requestBody.request.intent.name) { "AMAZON.HelpIntent" { $outJson = $(Get-HelpIntent) } ("AMAZON.StopIntent") { $outJson = $(Get-StopIntent) } ("AMAZON.CancelIntent") { $outJson = $(Get-StopIntent) } default { $outJson = $(Get-Recommendation $requestBody) } } } else { $certFail = @" {{"Status":"400","Headers":{{"content-type":"application/json"}},"Body":{{"Status":"{0}"}}}} "@ $outJson = $certFail -f $validRtn }