Enter any website URL to analyze its complete technology stack

Executive Summary for shulerent.com

1072 Response Time (ms)
200 HTTP Status
12 Scripts
4 Images
17 Links
HTTP/1.1 Protocol

SEO & Content Analysis

Basic Information
Page Title
Jason Shuler's Tech Blog | Making the internet less fail one post at a time…
Meta Description
Not detected
HTML Language
en-US
Robots.txt Present
Sitemap Present
total_urls: 2
SEO Meta Tags
content-type: text/html; charset=UTF-8
Page Content

Jason Shuler's Tech Blog | Making the internet less fail one post at a time…

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 valid2: Validate the certificate and its chain3: Use the certificate to verify the digital signature (which is supplied in another header) against the request body4: Make sure the timestamp in the request body is within 150 seconds of nowAmazon’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"); } };

Network & Infrastructure

DNS & Hosting
IP Address
168.61.42.251
Reverse DNS
deviatone.com
SSL/TLS Certificate
Issuer
CN=R12, O=Let's Encrypt, C=US
Protocol Tls13
Expires In 84 days

Technology Stack

Content Management Systems
WordPress WordPress (robots.txt)
Server Technologies
Generator: WordPress 6.9 PHP (inferred from WordPress)

Services & Integrations

Analytics & Tracking
Google Analytics GA4
E-commerce Platforms
Magento PrestaShop

CDN & Media Providers

Media Providers
AWS S3 YouTube

Dynamic Analysis & Security

Dynamic JavaScript Analysis
Bootstrap (CSS Classes) ES6+ JavaScript Features Foundation (CSS Classes) Web Server: (Ubuntu) Web Server: Apache/2.4.29
Security Headers
Referrer-Policy
Server Headers
(Ubuntu)
Apache/2.4.29

Resource Analysis

External Resource Hosts
c0.wp.com
gmpg.org
i0.wp.com
secure.gravatar.com
shulerent.com
stats.wp.com
v0.wordpress.com
wp.me
UI Frameworks & Libraries
Angular Material (Class Names) Bootstrap (Class Names) Ionic (Class Names) Slate Swiper Vuetify (Class Names)

Social Media Integrations

Analysis Complete

Analyzed shulerent.com with 3 technologies detected across 6 categories

Analysis completed in 1072 ms • 2026-03-23 11:04:21 UTC