Skip to content

Instantly share code, notes, and snippets.

@chriskillpack
Created August 12, 2022 05:13
Show Gist options
  • Save chriskillpack/dc97c367c648b86268df0530a472f6a4 to your computer and use it in GitHub Desktop.
Save chriskillpack/dc97c367c648b86268df0530a472f6a4 to your computer and use it in GitHub Desktop.
// Middleware provides an http.Handler that verifies an incoming request as coming from the Alexa
// web service using the steps described at https://developer.amazon.com/en-US/docs/alexa/custom-skills/host-a-custom-skill-as-a-web-service.html#manually-verify-request-sent-by-alexa
// Note: validation requires an HTTP GET of an Amazon certificate. Configure the middleware with an
// http.Client to have some control over the fetch.
package alexa
import (
"bytes"
"context"
"crypto"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
type Middleware struct {
// HTTP client to use to fetch the Amazon signing certificate
// The URL of the signing certificate is provided in the "SignatureCertChainUrl"
// request header.
Client *http.Client
}
func (m *Middleware) HTTPHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if !m.verify(req) {
w.WriteHeader(http.StatusBadRequest)
return
}
next.ServeHTTP(w, req)
})
}
// Validates the incoming request as being from the Alexa service
// Heads-up: validation requires consuming the request body, so a successfully validated
// request will have replaced req.Body with an io.ReadCloser that contains the original
// body contents.
func (m *Middleware) verify(req *http.Request) bool {
certChainURL := verifyCertURL(req.Header.Get("SignatureCertChainUrl"))
if certChainURL == "" {
return false
}
cert, err := m.retrieveAndVerifyCert(req.Context(), certChainURL)
if err != nil {
return false
}
// We assume that Alexa certificate uses an RSA public key
pubKey, ok := cert.PublicKey.(*rsa.PublicKey)
if !ok {
return false
}
sig, err := base64.StdEncoding.DecodeString(req.Header.Get("Signature-256"))
if err != nil {
return false
}
var bodyBuf bytes.Buffer
s := sha256.New()
io.Copy(s, io.TeeReader(req.Body, &bodyBuf))
bodyHash := s.Sum(make([]byte, 0, s.Size()))
err = rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, bodyHash, sig)
if err != nil {
return false
}
// Replace the request body with a new ReadCloser
req.Body = io.NopCloser(&bodyBuf)
return true
}
func verifyCertURL(certURL string) string {
u, err := url.Parse(certURL)
if err != nil {
return ""
}
if u.Scheme != "https" {
return ""
}
if u.Host != "s3.amazonaws.com" && u.Host != "s3.amazonaws.com:443" {
return ""
}
// Normalize the path to eliminate /./, /../ and //
sections := strings.Split(u.Path, "/")
newS := []string{}
for _, s := range sections {
if s == ".." {
// If this ".." is an attempt to go back beyond the root of the
// path then this is an invalid path.
if len(newS)-1 < 0 {
return ""
}
newS = newS[:len(newS)-1]
continue
}
if s == "." || s == "" {
continue
}
newS = append(newS, s)
}
u.Path = strings.Join(newS, "/")
if !strings.HasPrefix(u.Path, "echo.api") {
return ""
}
return u.String()
}
func (m *Middleware) retrieveAndVerifyCert(ctx context.Context, path string) (*x509.Certificate, error) {
client := m.Client
if client == nil {
client = http.DefaultClient
}
reqCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, path, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
cancel()
certBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
block, rest := pem.Decode(certBytes)
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
var intermediatePool *x509.CertPool
if len(rest) > 0 {
intermediatePool = x509.NewCertPool()
if !intermediatePool.AppendCertsFromPEM(rest) {
return nil, fmt.Errorf("could not append additional certs in PEM")
}
}
// Verify the cert using any intermediate certs included in the PEM
// The leaf cert has to be for the Alexa domain
// cert.Verify also validates the certificate NotBefore and NotAfter fields
vopts := x509.VerifyOptions{
DNSName: "echo-api.amazon.com",
Intermediates: intermediatePool,
}
if _, err := cert.Verify(vopts); err != nil {
return nil, err
}
return cert, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment