You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

109 lines
3.2 KiB

  1. package travisci
  2. import (
  3. "crypto"
  4. "crypto/rsa"
  5. "crypto/sha1"
  6. "crypto/x509"
  7. "encoding/base64"
  8. "encoding/json"
  9. "encoding/pem"
  10. "fmt"
  11. "net/http"
  12. "strings"
  13. )
  14. // Host => Public Key.
  15. // Travis has a .com and .org with different public keys.
  16. // .org is the public one and is one we will try first, then .com
  17. var travisPublicKeyMap = map[string]*rsa.PublicKey{
  18. "api.travis-ci.org": nil,
  19. "api.travis-ci.com": nil,
  20. }
  21. func verifyOrigin(payload []byte, sigHeader string) error {
  22. /*
  23. From: https://docs.travis-ci.com/user/notifications#Verifying-Webhook-requests
  24. 1. Pick up the payload data from the HTTP requests body.
  25. 2. Obtain the Signature header value, and base64-decode it.
  26. 3. Obtain the public key corresponding to the private key that signed the payload.
  27. This is available at the /config endpoints config.notifications.webhook.public_key on
  28. the relevant API server. (e.g., https://api.travis-ci.org/config)
  29. 4. Verify the signature using the public key and SHA1 digest.
  30. */
  31. sig, err := base64.StdEncoding.DecodeString(sigHeader)
  32. if err != nil {
  33. return fmt.Errorf("verifyOrigin: Failed to decode signature as base64: %s", err)
  34. }
  35. if err := loadPublicKeys(); err != nil {
  36. return fmt.Errorf("verifyOrigin: Failed to cache Travis public keys: %s", err)
  37. }
  38. // 4. Verify with SHA1
  39. // NB: We don't know who sent this request (no Referer header or anything) so we need to try
  40. // both public keys at both endpoints. We use the .org one first since it's more popular.
  41. var verifyErr error
  42. for _, host := range []string{"api.travis-ci.org", "api.travis-ci.com"} {
  43. h := sha1.New()
  44. h.Write(payload)
  45. digest := h.Sum(nil)
  46. verifyErr = rsa.VerifyPKCS1v15(travisPublicKeyMap[host], crypto.SHA1, digest, sig)
  47. if verifyErr == nil {
  48. return nil // Valid for this key
  49. }
  50. }
  51. return fmt.Errorf("verifyOrigin: Signature verification failed: %s", verifyErr)
  52. }
  53. func loadPublicKeys() error {
  54. for _, host := range []string{"api.travis-ci.com", "api.travis-ci.org"} {
  55. pubKey := travisPublicKeyMap[host]
  56. if pubKey == nil {
  57. pemPubKey, err := fetchPEMPublicKey("https://" + host + "/config")
  58. if err != nil {
  59. return err
  60. }
  61. block, _ := pem.Decode([]byte(pemPubKey))
  62. if block == nil {
  63. return fmt.Errorf("public_key at %s doesn't have a valid PEM block", host)
  64. }
  65. k, err := x509.ParsePKIXPublicKey(block.Bytes)
  66. if err != nil {
  67. return err
  68. }
  69. pubKey = k.(*rsa.PublicKey)
  70. travisPublicKeyMap[host] = pubKey
  71. }
  72. }
  73. return nil
  74. }
  75. func fetchPEMPublicKey(travisURL string) (key string, err error) {
  76. var res *http.Response
  77. res, err = httpClient.Get(travisURL)
  78. if res != nil {
  79. defer res.Body.Close()
  80. }
  81. if err != nil {
  82. return
  83. }
  84. configStruct := struct {
  85. Config struct {
  86. Notifications struct {
  87. Webhook struct {
  88. PublicKey string `json:"public_key"`
  89. } `json:"webhook"`
  90. } `json:"notifications"`
  91. } `json:"config"`
  92. }{}
  93. if err = json.NewDecoder(res.Body).Decode(&configStruct); err != nil {
  94. return
  95. }
  96. key = configStruct.Config.Notifications.Webhook.PublicKey
  97. if key == "" || !strings.HasPrefix(key, "-----BEGIN PUBLIC KEY-----") {
  98. err = fmt.Errorf("Couldn't fetch Travis-CI public key. Missing or malformed key: %s", key)
  99. }
  100. return
  101. }