Browse Source

Verify incoming Travis-CI webhook requests

kegan/travis
Kegan Dougal 8 years ago
parent
commit
5bf126473e
  1. 23
      src/github.com/matrix-org/go-neb/services/travisci/travisci.go
  2. 95
      src/github.com/matrix-org/go-neb/services/travisci/verify.go

23
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(&notif); err != nil {
if err := json.Unmarshal(payload, &notif); err != nil {
w.WriteHeader(400)
return
}

95
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 requests 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 endpoints 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
}
Loading…
Cancel
Save