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. // .org is the public one and is one we will try first, then .com var travisPublicKeyMap = map[string]*rsa.PublicKey{ "api.travis-ci.org": nil, "api.travis-ci.com": nil, } func verifyOrigin(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. */ sig, err := base64.StdEncoding.DecodeString(sigHeader) if err != nil { return fmt.Errorf("verifyOrigin: Failed to decode signature as base64: %s", err) } if err := loadPublicKeys(); err != nil { return fmt.Errorf("verifyOrigin: Failed to cache Travis public keys: %s", err) } // 4. Verify with SHA1 // NB: We don't know who sent this request (no Referer header or anything) so we need to try // both public keys at both endpoints. We use the .org one first since it's more popular. var verifyErr error for _, host := range []string{"api.travis-ci.org", "api.travis-ci.com"} { h := sha1.New() h.Write(payload) digest := h.Sum(nil) verifyErr = rsa.VerifyPKCS1v15(travisPublicKeyMap[host], crypto.SHA1, digest, sig) if verifyErr == nil { return nil // Valid for this key } } return fmt.Errorf("verifyOrigin: Signature verification failed: %s", verifyErr) } func loadPublicKeys() error { for _, host := range []string{"api.travis-ci.com", "api.travis-ci.org"} { pubKey := travisPublicKeyMap[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 } } return nil } 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: %s", key) } return }