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:
- Get the Certificate and validate it
- Validate the source of the Certificate and the request
- Validate the signature of the request, and compare it to the body
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
}
No comments :
Post a Comment