github.com/google/go-github/v53@v53.2.0/github/messages.go (about) 1 // Copyright 2016 The go-github AUTHORS. All rights reserved. 2 // 3 // Use of this source code is governed by a BSD-style 4 // license that can be found in the LICENSE file. 5 6 // This file provides functions for validating payloads from GitHub Webhooks. 7 // GitHub API docs: https://developer.github.com/webhooks/securing/#validating-payloads-from-github 8 9 package github 10 11 import ( 12 "crypto/hmac" 13 "crypto/sha1" 14 "crypto/sha256" 15 "crypto/sha512" 16 "encoding/hex" 17 "encoding/json" 18 "errors" 19 "fmt" 20 "hash" 21 "io" 22 "mime" 23 "net/http" 24 "net/url" 25 "strings" 26 ) 27 28 const ( 29 // sha1Prefix is the prefix used by GitHub before the HMAC hexdigest. 30 sha1Prefix = "sha1" 31 // sha256Prefix and sha512Prefix are provided for future compatibility. 32 sha256Prefix = "sha256" 33 sha512Prefix = "sha512" 34 // SHA1SignatureHeader is the GitHub header key used to pass the HMAC-SHA1 hexdigest. 35 SHA1SignatureHeader = "X-Hub-Signature" 36 // SHA256SignatureHeader is the GitHub header key used to pass the HMAC-SHA256 hexdigest. 37 SHA256SignatureHeader = "X-Hub-Signature-256" 38 // EventTypeHeader is the GitHub header key used to pass the event type. 39 EventTypeHeader = "X-Github-Event" 40 // DeliveryIDHeader is the GitHub header key used to pass the unique ID for the webhook event. 41 DeliveryIDHeader = "X-Github-Delivery" 42 ) 43 44 var ( 45 // eventTypeMapping maps webhooks types to their corresponding go-github struct types. 46 eventTypeMapping = map[string]string{ 47 "branch_protection_rule": "BranchProtectionRuleEvent", 48 "check_run": "CheckRunEvent", 49 "check_suite": "CheckSuiteEvent", 50 "code_scanning_alert": "CodeScanningAlertEvent", 51 "commit_comment": "CommitCommentEvent", 52 "content_reference": "ContentReferenceEvent", 53 "create": "CreateEvent", 54 "delete": "DeleteEvent", 55 "deploy_key": "DeployKeyEvent", 56 "deployment": "DeploymentEvent", 57 "deployment_status": "DeploymentStatusEvent", 58 "deployment_protection_rule": "DeploymentProtectionRuleEvent", 59 "discussion": "DiscussionEvent", 60 "discussion_comment": "DiscussionCommentEvent", 61 "fork": "ForkEvent", 62 "github_app_authorization": "GitHubAppAuthorizationEvent", 63 "gollum": "GollumEvent", 64 "installation": "InstallationEvent", 65 "installation_repositories": "InstallationRepositoriesEvent", 66 "issue_comment": "IssueCommentEvent", 67 "issues": "IssuesEvent", 68 "label": "LabelEvent", 69 "marketplace_purchase": "MarketplacePurchaseEvent", 70 "member": "MemberEvent", 71 "membership": "MembershipEvent", 72 "merge_group": "MergeGroupEvent", 73 "meta": "MetaEvent", 74 "milestone": "MilestoneEvent", 75 "organization": "OrganizationEvent", 76 "org_block": "OrgBlockEvent", 77 "package": "PackageEvent", 78 "page_build": "PageBuildEvent", 79 "ping": "PingEvent", 80 "project": "ProjectEvent", 81 "project_card": "ProjectCardEvent", 82 "project_column": "ProjectColumnEvent", 83 "public": "PublicEvent", 84 "pull_request": "PullRequestEvent", 85 "pull_request_review": "PullRequestReviewEvent", 86 "pull_request_review_comment": "PullRequestReviewCommentEvent", 87 "pull_request_review_thread": "PullRequestReviewThreadEvent", 88 "pull_request_target": "PullRequestTargetEvent", 89 "push": "PushEvent", 90 "repository": "RepositoryEvent", 91 "repository_dispatch": "RepositoryDispatchEvent", 92 "repository_import": "RepositoryImportEvent", 93 "repository_vulnerability_alert": "RepositoryVulnerabilityAlertEvent", 94 "release": "ReleaseEvent", 95 "secret_scanning_alert": "SecretScanningAlertEvent", 96 "security_advisory": "SecurityAdvisoryEvent", 97 "star": "StarEvent", 98 "status": "StatusEvent", 99 "team": "TeamEvent", 100 "team_add": "TeamAddEvent", 101 "user": "UserEvent", 102 "watch": "WatchEvent", 103 "workflow_dispatch": "WorkflowDispatchEvent", 104 "workflow_job": "WorkflowJobEvent", 105 "workflow_run": "WorkflowRunEvent", 106 } 107 ) 108 109 // genMAC generates the HMAC signature for a message provided the secret key 110 // and hashFunc. 111 func genMAC(message, key []byte, hashFunc func() hash.Hash) []byte { 112 mac := hmac.New(hashFunc, key) 113 mac.Write(message) 114 return mac.Sum(nil) 115 } 116 117 // checkMAC reports whether messageMAC is a valid HMAC tag for message. 118 func checkMAC(message, messageMAC, key []byte, hashFunc func() hash.Hash) bool { 119 expectedMAC := genMAC(message, key, hashFunc) 120 return hmac.Equal(messageMAC, expectedMAC) 121 } 122 123 // messageMAC returns the hex-decoded HMAC tag from the signature and its 124 // corresponding hash function. 125 func messageMAC(signature string) ([]byte, func() hash.Hash, error) { 126 if signature == "" { 127 return nil, nil, errors.New("missing signature") 128 } 129 sigParts := strings.SplitN(signature, "=", 2) 130 if len(sigParts) != 2 { 131 return nil, nil, fmt.Errorf("error parsing signature %q", signature) 132 } 133 134 var hashFunc func() hash.Hash 135 switch sigParts[0] { 136 case sha1Prefix: 137 hashFunc = sha1.New 138 case sha256Prefix: 139 hashFunc = sha256.New 140 case sha512Prefix: 141 hashFunc = sha512.New 142 default: 143 return nil, nil, fmt.Errorf("unknown hash type prefix: %q", sigParts[0]) 144 } 145 146 buf, err := hex.DecodeString(sigParts[1]) 147 if err != nil { 148 return nil, nil, fmt.Errorf("error decoding signature %q: %v", signature, err) 149 } 150 return buf, hashFunc, nil 151 } 152 153 // ValidatePayloadFromBody validates an incoming GitHub Webhook event request body 154 // and returns the (JSON) payload. 155 // The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded". 156 // If the Content-Type is neither then an error is returned. 157 // secretToken is the GitHub Webhook secret token. 158 // If your webhook does not contain a secret token, you can pass an empty secretToken. 159 // Webhooks without a secret token are not secure and should be avoided. 160 // 161 // Example usage: 162 // 163 // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { 164 // // read signature from request 165 // signature := "" 166 // payload, err := github.ValidatePayloadFromBody(r.Header.Get("Content-Type"), r.Body, signature, s.webhookSecretKey) 167 // if err != nil { ... } 168 // // Process payload... 169 // } 170 func ValidatePayloadFromBody(contentType string, readable io.Reader, signature string, secretToken []byte) (payload []byte, err error) { 171 var body []byte // Raw body that GitHub uses to calculate the signature. 172 173 switch contentType { 174 case "application/json": 175 var err error 176 if body, err = io.ReadAll(readable); err != nil { 177 return nil, err 178 } 179 180 // If the content type is application/json, 181 // the JSON payload is just the original body. 182 payload = body 183 184 case "application/x-www-form-urlencoded": 185 // payloadFormParam is the name of the form parameter that the JSON payload 186 // will be in if a webhook has its content type set to application/x-www-form-urlencoded. 187 const payloadFormParam = "payload" 188 189 var err error 190 if body, err = io.ReadAll(readable); err != nil { 191 return nil, err 192 } 193 194 // If the content type is application/x-www-form-urlencoded, 195 // the JSON payload will be under the "payload" form param. 196 form, err := url.ParseQuery(string(body)) 197 if err != nil { 198 return nil, err 199 } 200 payload = []byte(form.Get(payloadFormParam)) 201 202 default: 203 return nil, fmt.Errorf("webhook request has unsupported Content-Type %q", contentType) 204 } 205 206 // Validate the signature if present or if one is expected (secretToken is non-empty). 207 if len(secretToken) > 0 || len(signature) > 0 { 208 if err := ValidateSignature(signature, body, secretToken); err != nil { 209 return nil, err 210 } 211 } 212 213 return payload, nil 214 } 215 216 // ValidatePayload validates an incoming GitHub Webhook event request 217 // and returns the (JSON) payload. 218 // The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded". 219 // If the Content-Type is neither then an error is returned. 220 // secretToken is the GitHub Webhook secret token. 221 // If your webhook does not contain a secret token, you can pass nil or an empty slice. 222 // This is intended for local development purposes only and all webhooks should ideally set up a secret token. 223 // 224 // Example usage: 225 // 226 // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { 227 // payload, err := github.ValidatePayload(r, s.webhookSecretKey) 228 // if err != nil { ... } 229 // // Process payload... 230 // } 231 func ValidatePayload(r *http.Request, secretToken []byte) (payload []byte, err error) { 232 signature := r.Header.Get(SHA256SignatureHeader) 233 if signature == "" { 234 signature = r.Header.Get(SHA1SignatureHeader) 235 } 236 237 contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) 238 if err != nil { 239 return nil, err 240 } 241 242 return ValidatePayloadFromBody(contentType, r.Body, signature, secretToken) 243 } 244 245 // ValidateSignature validates the signature for the given payload. 246 // signature is the GitHub hash signature delivered in the X-Hub-Signature header. 247 // payload is the JSON payload sent by GitHub Webhooks. 248 // secretToken is the GitHub Webhook secret token. 249 // 250 // GitHub API docs: https://developer.github.com/webhooks/securing/#validating-payloads-from-github 251 func ValidateSignature(signature string, payload, secretToken []byte) error { 252 messageMAC, hashFunc, err := messageMAC(signature) 253 if err != nil { 254 return err 255 } 256 if !checkMAC(payload, messageMAC, secretToken, hashFunc) { 257 return errors.New("payload signature check failed") 258 } 259 return nil 260 } 261 262 // WebHookType returns the event type of webhook request r. 263 // 264 // GitHub API docs: https://docs.github.com/en/developers/webhooks-and-events/events/github-event-types 265 func WebHookType(r *http.Request) string { 266 return r.Header.Get(EventTypeHeader) 267 } 268 269 // DeliveryID returns the unique delivery ID of webhook request r. 270 // 271 // GitHub API docs: https://docs.github.com/en/developers/webhooks-and-events/events/github-event-types 272 func DeliveryID(r *http.Request) string { 273 return r.Header.Get(DeliveryIDHeader) 274 } 275 276 // ParseWebHook parses the event payload. For recognized event types, a 277 // value of the corresponding struct type will be returned (as returned 278 // by Event.ParsePayload()). An error will be returned for unrecognized event 279 // types. 280 // 281 // Example usage: 282 // 283 // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { 284 // payload, err := github.ValidatePayload(r, s.webhookSecretKey) 285 // if err != nil { ... } 286 // event, err := github.ParseWebHook(github.WebHookType(r), payload) 287 // if err != nil { ... } 288 // switch event := event.(type) { 289 // case *github.CommitCommentEvent: 290 // processCommitCommentEvent(event) 291 // case *github.CreateEvent: 292 // processCreateEvent(event) 293 // ... 294 // } 295 // } 296 func ParseWebHook(messageType string, payload []byte) (interface{}, error) { 297 eventType, ok := eventTypeMapping[messageType] 298 if !ok { 299 return nil, fmt.Errorf("unknown X-Github-Event in message: %v", messageType) 300 } 301 302 event := Event{ 303 Type: &eventType, 304 RawPayload: (*json.RawMessage)(&payload), 305 } 306 return event.ParsePayload() 307 }