github.com/e154/smart-home@v0.17.2-0.20240311175135-e530a6e5cd45/plugins/alexa/secure.go (about)

     1  // This file is part of the Smart Home
     2  // Program complex distribution https://github.com/e154/smart-home
     3  // Copyright (C) 2016-2023, Filippov Alex
     4  //
     5  // This library is free software: you can redistribute it and/or
     6  // modify it under the terms of the GNU Lesser General Public
     7  // License as published by the Free Software Foundation; either
     8  // version 3 of the License, or (at your option) any later version.
     9  //
    10  // This library is distributed in the hope that it will be useful,
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    13  // Library General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Lesser General Public
    16  // License along with this library.  If not, see
    17  // <https://www.gnu.org/licenses/>.
    18  
    19  package alexa
    20  
    21  import (
    22  	"crypto/tls"
    23  	"crypto/x509"
    24  	"encoding/pem"
    25  	"errors"
    26  	"io"
    27  	"net/http"
    28  	"net/url"
    29  	"strings"
    30  	"time"
    31  )
    32  
    33  // HTTPError is a convenience method for logging a message and writing the provided error message
    34  // and error code to the HTTP response.
    35  func HTTPError(w http.ResponseWriter, logMsg string, err string, errCode int) {
    36  	if logMsg != "" {
    37  		log.Error(logMsg)
    38  	}
    39  
    40  	http.Error(w, err, errCode)
    41  }
    42  
    43  // IsValidAlexaRequest handles all the necessary steps to validate that an incoming http.Request has actually come from
    44  // the Server service. If an error occurs during the validation process, an http.Error will be written to the provided http.ResponseWriter.
    45  // The required steps for request validation can be found on this page:
    46  // --insecure-skip-verify flag will disable all validations
    47  // https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/developing-an-alexa-skill-as-a-web-service#hosting-a-custom-skill-as-a-web-service
    48  func IsValidAlexaRequest(w http.ResponseWriter, r *http.Request) bool {
    49  
    50  	certURL := r.Header.Get("SignatureCertChainUrl")
    51  
    52  	// Verify certificate URL
    53  	if !verifyCertURL(certURL) {
    54  		HTTPError(w, "Invalid cert URL: "+certURL, "Not Authorized", 401)
    55  		return false
    56  	}
    57  
    58  	// Fetch certificate data
    59  	certContents, err := readCert(certURL)
    60  	if err != nil {
    61  		HTTPError(w, err.Error(), "Not Authorized", 401)
    62  		return false
    63  	}
    64  
    65  	// Decode certificate data
    66  	block, _ := pem.Decode(certContents)
    67  	if block == nil {
    68  		HTTPError(w, "Failed to parse certificate PEM.", "Not Authorized", 401)
    69  		return false
    70  	}
    71  
    72  	cert, err := x509.ParseCertificate(block.Bytes)
    73  	if err != nil {
    74  		HTTPError(w, err.Error(), "Not Authorized", 401)
    75  		return false
    76  	}
    77  
    78  	// Check the certificate date
    79  	if time.Now().Unix() < cert.NotBefore.Unix() || time.Now().Unix() > cert.NotAfter.Unix() {
    80  		HTTPError(w, "Amazon certificate expired.", "Not Authorized", 401)
    81  		return false
    82  	}
    83  
    84  	// Check the certificate alternate names
    85  	foundName := false
    86  	for _, altName := range cert.Subject.Names {
    87  		if altName.Value == "echo-api.amazon.com" {
    88  			foundName = true
    89  		}
    90  	}
    91  
    92  	if !foundName {
    93  		HTTPError(w, "Amazon certificate invalid.", "Not Authorized", 401)
    94  		return false
    95  	}
    96  
    97  	return true
    98  }
    99  
   100  func readCert(certURL string) ([]byte, error) {
   101  	certPool, err := x509.SystemCertPool()
   102  	if err != nil || certPool == nil {
   103  		log.Error("Can't open system cert pools")
   104  	}
   105  
   106  	tr := &http.Transport{
   107  		TLSClientConfig: &tls.Config{RootCAs: certPool, InsecureSkipVerify: insecureSkipVerify},
   108  	}
   109  	hc := &http.Client{Timeout: 2 * time.Second, Transport: tr}
   110  
   111  	cert, err := hc.Get(certURL)
   112  	if err != nil {
   113  		return nil, errors.New("could not download Amazon cert file: " + err.Error())
   114  	}
   115  	defer cert.Body.Close()
   116  	certContents, err := io.ReadAll(cert.Body)
   117  	if err != nil {
   118  		return nil, errors.New("could not read Amazon cert file: " + err.Error())
   119  	}
   120  
   121  	return certContents, nil
   122  }
   123  
   124  func verifyCertURL(path string) bool {
   125  	link, _ := url.Parse(path)
   126  
   127  	if link.Scheme != "https" {
   128  		return false
   129  	}
   130  
   131  	if link.Host != "s3.amazonaws.com" && link.Host != "s3.amazonaws.com:443" {
   132  		return false
   133  	}
   134  
   135  	if !strings.HasPrefix(link.Path, "/echo.api/") {
   136  		return false
   137  	}
   138  
   139  	return true
   140  }