Created
August 12, 2022 05:13
-
-
Save chriskillpack/dc97c367c648b86268df0530a472f6a4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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