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 }