From 5bf126473e38764b355627f4efa555420977a83d Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 15 Nov 2016 14:37:59 +0000 Subject: [PATCH] Verify incoming Travis-CI webhook requests --- .../go-neb/services/travisci/travisci.go | 23 ++++- .../go-neb/services/travisci/verify.go | 95 +++++++++++++++++++ 2 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 src/github.com/matrix-org/go-neb/services/travisci/verify.go diff --git a/src/github.com/matrix-org/go-neb/services/travisci/travisci.go b/src/github.com/matrix-org/go-neb/services/travisci/travisci.go index 3660940..652a458 100644 --- a/src/github.com/matrix-org/go-neb/services/travisci/travisci.go +++ b/src/github.com/matrix-org/go-neb/services/travisci/travisci.go @@ -3,6 +3,7 @@ package travisci import ( "encoding/json" "fmt" + "io/ioutil" "net/http" "regexp" "strconv" @@ -24,8 +25,11 @@ const DefaultTemplate = (`%{repository}#%{build_number} (%{branch} - %{commit} : Change view : %{compare_url} Build details : %{build_url}`) +// Matches 'owner/repo' var ownerRepoRegex = regexp.MustCompile(`^([A-z0-9-_.]+)/([A-z0-9-_.]+)$`) +var httpClient = &http.Client{} + // Service contains the Config fields for the Travis-CI service. // // This service will send notifications into a Matrix room when Travis-CI sends @@ -81,7 +85,7 @@ type Service struct { type webhookNotification struct { ID int `json:"id"` Number string `json:"number"` - Status *string `json:"status"` + Status *string `json:"status"` // 0 (success) or 1 (incomplete/fail). StartedAt *string `json:"started_at"` FinishedAt *string `json:"finished_at"` StatusMessage string `json:"status_message"` @@ -178,8 +182,23 @@ func travisToGoTemplate(travisTmpl string) (goTmpl string) { // // See https://docs.travis-ci.com/user/notifications#Webhook-notifications for more information. func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *matrix.Client) { + payload, err := ioutil.ReadAll(req.Body) + if err != nil { + log.WithError(err).Error("Failed to read incoming Travis-CI webhook") + w.WriteHeader(500) + return + } + if err := verifyOrigin(req.Host, payload, req.Header.Get("Signature")); err != nil { + log.WithFields(log.Fields{ + "Signature": req.Header.Get("Signature"), + log.ErrorKey: err, + }).Warn("Received unauthorised Travis-CI webhook request.") + w.WriteHeader(403) + return + } + var notif webhookNotification - if err := json.NewDecoder(req.Body).Decode(¬if); err != nil { + if err := json.Unmarshal(payload, ¬if); err != nil { w.WriteHeader(400) return } diff --git a/src/github.com/matrix-org/go-neb/services/travisci/verify.go b/src/github.com/matrix-org/go-neb/services/travisci/verify.go new file mode 100644 index 0000000..9454c2b --- /dev/null +++ b/src/github.com/matrix-org/go-neb/services/travisci/verify.go @@ -0,0 +1,95 @@ +package travisci + +import ( + "crypto" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "net/http" + "strings" +) + +// Host => Public Key. +// Travis has a .com and .org with different public keys. +var travisPublicKeyMap = map[string]*rsa.PublicKey{ + "api.travis-ci.org": nil, + "api.travis-ci.com": nil, +} + +func verifyOrigin(host string, payload []byte, sigHeader string) error { + /* + From: https://docs.travis-ci.com/user/notifications#Verifying-Webhook-requests + 1. Pick up the payload data from the HTTP request’s body. + 2. Obtain the Signature header value, and base64-decode it. + 3. Obtain the public key corresponding to the private key that signed the payload. + This is available at the /config endpoint’s config.notifications.webhook.public_key on + the relevant API server. (e.g., https://api.travis-ci.org/config) + 4. Verify the signature using the public key and SHA1 digest. + */ + // 2. Get signature bytes + sig, err := base64.StdEncoding.DecodeString(sigHeader) + if err != nil { + return err + } + + // 3. Get public key + pubKey, validURL := travisPublicKeyMap[host] + if !validURL { + return fmt.Errorf("Invalid host: %s", host) + } + if pubKey == nil { + pemPubKey, err := fetchPEMPublicKey("https://" + host + "/config") + if err != nil { + return err + } + block, _ := pem.Decode([]byte(pemPubKey)) + if block == nil { + return fmt.Errorf("public_key at %s doesn't have a valid PEM block", host) + } + + k, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return err + } + pubKey = k.(*rsa.PublicKey) + travisPublicKeyMap[host] = pubKey + } + + // 4. Verify with SHA1 + h := sha1.New() + h.Write(payload) + digest := h.Sum(nil) + return rsa.VerifyPKCS1v15(pubKey, crypto.SHA1, digest, sig) +} + +func fetchPEMPublicKey(travisURL string) (key string, err error) { + var res *http.Response + res, err = httpClient.Get(travisURL) + if res != nil { + defer res.Body.Close() + } + if err != nil { + return + } + configStruct := struct { + Config struct { + Notifications struct { + Webhook struct { + PublicKey string `json:"public_key"` + } `json:"webhook"` + } `json:"notifications"` + } `json:"config"` + }{} + if err = json.NewDecoder(res.Body).Decode(&configStruct); err != nil { + return + } + key = configStruct.Config.Notifications.Webhook.PublicKey + if key == "" || strings.HasPrefix(key, "-----BEGIN PUBLIC KEY-----") { + err = fmt.Errorf("Couldn't fetch Travis-CI public key. Missing or malformed key.") + } + return +}