Validating Alexa Skill Web Requests in c#

Amazon went a little OCD on the security verification for Alexa skill requests to custom https endpoints. During testing it will work fine if you don’t validate, but they check for this as part of the skill submission process.

I have been developing a custom connector for Alexa to work with the Microsoft Bot Framework, and just recently discovered the security requirements. Now I may be reinventing the wheel here a little bit, but I decided to build my own model classes. Maybe I’m a little OCD.

Anyway, to validate the alexa skill requests you must do the following:
1: Validate that the url supplied in a header for a certificate chain is valid
2: Validate the certificate and its chain
3: Use the certificate to verify the digital signature (which is supplied in another header) against the request body
4: Make sure the timestamp in the request body is within 150 seconds of now

Amazon’s instructions with regard to step 3 are a little misleading. They suggest you “decrypt” the digital signature using the public key. From what I can gather, RSA public keys do not decrypt; they only encrypt. Thankfully the digital signature validation process is something that is already implemented in the .NET Framework, so it’s not terribly difficult.

The two “tricky” parts are getting .NET to read certificates from a PEM container, and checking the signing certificate’s SAN list.

So hopefully this will be helpful to somebody.

First we have a helper class for parsing the PEM and a couple other little things

using System.Net.Http;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;

namespace AlexaCustomChannel
{
    public static class PemHelper
    {
        static string CertHeader = "-----BEGIN CERTIFICATE-----";
        static string CertFooter = "-----END CERTIFICATE-----";

        static HttpClient _client = new HttpClient();


        public static IEnumerable<string> ParseSujectAlternativeNames(X509Certificate2 cert)
        {
            Regex sanRex = new Regex(@"^DNS Name=(.*)", RegexOptions.Compiled | RegexOptions.CultureInvariant);

            var sanList = from X509Extension ext in cert.Extensions
                          where ext.Oid.FriendlyName.Equals("Subject Alternative Name", StringComparison.Ordinal)
                          let data = new AsnEncodedData(ext.Oid, ext.RawData)
                          let text = data.Format(true)
                          from line in text.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
                          let match = sanRex.Match(line)
                          where match.Success && match.Groups.Count > 0 && !string.IsNullOrEmpty(match.Groups[1].Value)
                          select match.Groups[1].Value;

            return sanList;
        }


        public static bool ValidateCertificateChain(X509Certificate2 certificate, IEnumerable<X509Certificate2> chain)
        {
            using (var verifier = new X509Chain())
            {
                verifier.ChainPolicy.ExtraStore.AddRange(chain.ToArray());
                var result = verifier.Build(certificate);
                return result;
            }
        }

        public static X509Certificate2 ParseCertificate(string base64CertificateText)
        {
            var bytes = Convert.FromBase64String(base64CertificateText);
            X509Certificate2 cert = new X509Certificate2(bytes);
            return cert;
        }

        public static async Task<X509Certificate2[]> DownloadPemCertificatesAsync(string pemUri)
        {
            var pemText = await _client.GetStringAsync(pemUri);
            if (string.IsNullOrEmpty(pemText)) return null;
            return ReadPemCertificates(pemText);
        }


        public static X509Certificate2[] ReadPemCertificates(string pemString)
        {
            var lines = pemString.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
            List<string> certList = new List<string>();
            StringBuilder grouper = null;
            for (int i = 0; i < lines.Length; i++)
            {
                var curLine = lines[i];
                if (curLine.Equals(CertHeader, StringComparison.Ordinal))
                {
                    grouper = new StringBuilder();
                }
                else if (curLine.Equals(CertFooter, StringComparison.Ordinal))
                {
                    certList.Add(grouper.ToString());
                    grouper = null;
                }
                else
                {
                    if (grouper != null)
                    {
                        grouper.Append(curLine);
                    }
                }
            }

            List<X509Certificate2> collection = new List<X509Certificate2>();

            foreach (var certText in certList)
            {
                var cert = ParseCertificate(certText);
                collection.Add(cert);
            }

            return collection.ToArray();
        }
    }
}

You will need to change the signature of your controller to accept something that gives you access to the raw request body – such as an HttpRequestMessage.

Then you can call the following method with your request to validate per Amazon’s requirements
(Note the AlexaRequestBody is my custom request model. You just need to get the timestamp from the request)

        static Dictionary<string, X509Certificate2> _validatedCertificateChains = new Dictionary<string, X509Certificate2>();
//...
        async Task ValidateRequestSecurity(HttpRequestMessage httpRequest, byte[] requestBytes, AlexaRequestBody requestBody)
        {
            if (requestBody == null || requestBody.Request == null || requestBody.Request.Timestamp == null)
            {
                throw new InvalidOperationException("Alexa Request Invalid: Request Timestamp Missing");
            }

            var ts = requestBody.Request.Timestamp.Value;
            var tsDiff = (DateTimeOffset.UtcNow - ts).TotalSeconds;

            if (System.Math.Abs(tsDiff) >= 150)
            {
                throw new InvalidOperationException("Alexa Request Invalid: Request Timestamp outside valid range");
            }

            httpRequest.Headers.TryGetValues("SignatureCertChainUrl", out var certUrls);
            httpRequest.Headers.TryGetValues("Signature", out var signatures);

            var certChainUrl = certUrls.FirstOrDefault();
            var signature = signatures.FirstOrDefault();

            if (string.IsNullOrEmpty(certChainUrl))
            {
                throw new InvalidOperationException("Alexa Request Invalid: missing SignatureCertChainUrl header");
            }

            if (string.IsNullOrEmpty(signature))
            {
                throw new InvalidOperationException("Alexa Request Invalid: missing Signature header");
            }

            var uri = new Uri(certChainUrl);

            if (uri.Scheme.ToLower() != "https")
            {
                throw new InvalidOperationException("Alexa Request Invalid: SignatureCertChainUrl bad scheme");
            }

            if (uri.Port != 443)
            {
                throw new InvalidOperationException("Alexa Request Invalid: SignatureCertChainUrl bad port");
            }

            if (uri.Host.ToLower() != "s3.amazonaws.com")
            {
                throw new InvalidOperationException("Alexa Request Invalid: SignatureCertChainUrl bad host");
            }

            if (!uri.AbsolutePath.StartsWith("/echo.api/"))
            {
                throw new InvalidOperationException("Alexa Request Invalid: SignatureCertChainUrl bad path");
            }

            X509Certificate2 signingCertificate = null;

            if (!_validatedCertificateChains.ContainsKey(uri.ToString()))
            {
                System.Diagnostics.Trace.WriteLine("Validating cert URL: " + certChainUrl);

                var certList = await PemHelper.DownloadPemCertificatesAsync(uri.ToString());

                if (certList == null || certList.Length < 2)
                {
                    throw new InvalidOperationException("Alexa Request Invalid: SignatureCertChainUrl download failed or too few certificates");
                }

                var primaryCert = certList[0];

                var subjectAlternativeNameList = PemHelper.ParseSujectAlternativeNames(primaryCert);

                if (!subjectAlternativeNameList.Contains("echo-api.amazon.com"))
                {
                    throw new InvalidOperationException("Alexa Request Invalid: SignatureCertChainUrl certificate missing echo-api.amazon.com from Subject Alternative Names");
                }

                List<X509Certificate2> chainCerts = new List<X509Certificate2>();

                for (int i = 1; i < certList.Length; i++)
                {
                    chainCerts.Add(certList[i]);
                }

                if (!PemHelper.ValidateCertificateChain(primaryCert, chainCerts))
                {
                    throw new InvalidOperationException("Alexa Request Invalid: SignatureCertChainUrl certificate chain validation failed");
                }


                signingCertificate = primaryCert;

                lock (_validatedCertificateChains)
                {
                    if (!_validatedCertificateChains.ContainsKey(uri.ToString()))
                    {
                        System.Diagnostics.Trace.WriteLine("Adding validated cert url: " + uri.ToString());
                        _validatedCertificateChains[uri.ToString()] = primaryCert;
                    }
                    else
                    {
                        System.Diagnostics.Trace.WriteLine("Race condition hit while adding validated cert url: " + uri.ToString());
                    }
                }
            }
            else
            {
                signingCertificate = _validatedCertificateChains[uri.ToString()];
            }

            if (signingCertificate == null)
            {
                throw new InvalidOperationException("Alexa Request Invalid: SignatureCertChainUrl certificate generic failure");
            }


            var signatureBytes = Convert.FromBase64String(signature);

            var thing = signingCertificate.GetRSAPublicKey();
            if (!thing.VerifyData(requestBytes, signatureBytes, System.Security.Cryptography.HashAlgorithmName.SHA1, System.Security.Cryptography.RSASignaturePadding.Pkcs1))
            {
                throw new InvalidOperationException("Alexa Request Invalid: Signature verification failed");
            }
        }

Visual Studio Publish Events

Suppose you want to be able to run a command line tool to make changes to the transformed web.config during the publish / deploy of a web project in visual studio.

You need to be able to trigger your command after the web.config is transformed but before it is deployed. Additionally, you need to know the folder used for packaging the deploy.

Here is how.

Right click your project and unload it. Now you can right-click and select Edit the .csproj.

Down near the bottom of the csproj, you can add the following

  <Target Name="MyPublishTask" BeforeTargets="MSDeployPublish">
    <Exec Command="$(ProjectDir)..\ToolFolder\Tool.exe $(ProjectDir)$(IntermediateOutputPath)Package\PackageTmp\ web.config" />
  </Target>

Do not simply name your target “MSDeployPublish” – this overrides the publish functionality. If you use AfterTargets it will run your tool after the deploy finishes. Using BeforeTargets is just about right – it will transform the web.config and copy all the output files to the Package Temp folder. The other piece you need is the path where the package is built – $(ProjectDir)$(IntermediateOutputPath)Package\PackageTmp\ will get you there. Your tool can wreak whatever havoc necessary on the files before they are published.

One you are done editing, reload your project. Now when you publish your tool will run.

Error creating Azure IOT Hub

I was trying to create an IOT Hub in the Azure Portal and the UI was displaying errors on the Pricing and scale tier as well as the Subscription selections.

The error details said “Unable to get property ‘value’ of undefined or null reference”

When I attempted to create the IOT Hub using the azure CLI, we got to the real cause of the issue: the subscription was not enabled for “Microsoft.Devices”

To resolve, follow the directions here to register the Microsoft.Devices resource provider on your subscriptions
https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-manager-common-deployment-errors#noregisteredproviderfound

(Open the subscription, look for the “Resource Providers” option, search for “devices” – click register)

Once you have done this, you will be able to create an IOT Hub.

SQLite Studio

If you are doing any work with a SQLite database, you need this tool.

https://sqlitestudio.pl

It’s a fantastic free, open source GUI for working with SQLite databases. Clean, fast, surprisingly well featured – it should really be at the top of the list for SQLite GUI tools.

Make SQL Server Collation act similar to SQLite

The default SQL Server database collation (“SQL_Latin1_General_CP1_CI_AS”) sorts some Unicode values as equal when they are not. If you have an nvarchar field defined as part of a primary key or unique index, you can run into some surprise duplicate keys.

In particular, I was loading data from a SQLite database into an Azure SQL (SQL Server) database. I had removed all the duplicates as far as SQLite was concerned, but there were some records that SQL Server complained as being duplicate. From what I can tell, one record used single byte characters for the word “Final” and the other used double-byte characters.

The solution in this case was to change the collation of the field to one that uses a binary sort.

“Latin1_General_100_BIN” seems to work swimmingly. No more strange collisions.

windows net user add password with special characters

The command line to add a local windows user called “newuser” with the password “p&ssw^rd”

You try

net user newuser p&ssw^rd /ADD

Uh-oh – it fails!

C:\> net user newuser p&ssw^rd /ADD
The user name could not be found.

More help is available by typing NET HELPMSG 2221.

'sswrd' is not recognized as an internal or external command,
operable program or batch file.

If the password contains certain special characters – like an ampersand “&” or a caret “^”, the password will be garbled, broken, butchered.

One solution is to have it prompt for the password

net user newuser * /ADD

but if you are scripting, this isn’t really helpful.

No, you cannot use quotes.

The solution: All Ampersands must be escaped with a caret “^”, and all carets in the password must be similarly escaped.

UPDATE: turns out in more recent versions of windows, exclamation marks “!” must also be escape with two carets.
See here for a good list of how to escape things.
http://www.robvanderwoude.com/escapechars.php

So, to use the password p&ssw^rd in a command line, you would need to replace it with p^&ssw^^rd

net user newuser p^&ssw^^rd /ADD

This will do what you expect

Note that if you do not escape the carets, the command may succeed, but the password will be wrong.

Installing Windows Identity Foundation 3.5 in Azure Role Startup Task (Server 2012 VM)

The new VMs used by Azure are Server 2012+, and you cannot use the msu installer for WIF (Like you could here: http://blogs.msdn.com/b/sriharsha/archive/2012/04/07/windows-azure-unable-to-find-assembly-microsoft-identitymodel.aspx).

You must use dism to enable the feature.

I assume you are familiar with creating startup tasks – you need to create a batch file that runs:

Dism /online /Enable-Feature /FeatureName:Windows-Identity-Foundation >> "%TEMP%\WifStartupLog.txt" 2>&1

Here is the error you might be searching for:

Could not load file or assembly ‘Microsoft.IdentityModel Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35’ or one of its dependencie. The system cannot fine the find specified.

SQL Server Configuration Manager RPC Error

On my Dev VM, I’ve been unable to manage SQL Server Express with SQL Server Configuration Manager – I get an error “The Remote Procedure call failed”.

rpc error

The solution: Updates. I found this article:

SQL Server 2012 Configuration Manager WMI Error – Remote Procedure call failed [0x800706be]

Which mentions about things breaking wile installing Sql Server 2012 – I didn’t install that, but I did install Visual Studio 2012 – which includes Server Server 2012 components.

Install sql server 2008 sp3, and sql server 2008 r2 sp2. Problems solved.

 

Windows Live Writer

Windows Live Writer is an oft-overlooked component of the Windows Live Essentials application bundle (also including such gems as Live Photo Gallery, Live Movie Maker, and Live Mesh). If you have a blog (in my case running on WordPress), I highly recommend it for one really important reason: it makes adding images to posts 100% easier.

Think of it like a trimmed down version of Microsoft Word (more like Wordpad really) that can be used to compose blog posts. You can format text according to styles, add hyperlinks effortlessly, perform spell checking – but most importantly – paste images right into your post. Like this one:

image

 

You can the resize, crop, etc.

The contents of the Insert tab alone are worth your time:

image

 

I could be the only person left who primarily posted using the HTML editor, but if there are any other curmudgeons out there who have been skipping the visual aids because of all the extra steps… well give Windows Live Writer a try!

DIRECTV2PC activation key usage limit error

I have been trying to install DIRECTV2PC for a week or so (off and on of course) and I get the error “Activation Failed” with a reason of “activation key usage limit reached”

For some reason, nobody had a solution – on satelliteguys.us or dbstalk.com (or directv forums, or the internet as a whole).
I had already requested two keys, and I had only used them once – it seems ridiculous to me that I cannot use the key again if I, say, reinstall Windows (which I tend to a couple times a year)…

I found a workaround – use a different email address to request a new key. (Many email providers allow you to insert arbitrary periods in your email address…)

DIRECTV2PC is made by cyberlink for Directv. Nobody seems to know why it requires a product key – probably some accounting thing. Rumor has it that it may be going away soon anyway.

Good luck!