// Copyright 2016 The go-github AUTHORS. All rights reserved. // // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This file provides functions for validating payloads from GitHub Webhooks. // GitHub API docs: https://developer.github.com/webhooks/securing/#validating-payloads-from-github package github import ( "crypto/hmac" "crypto/sha1" "crypto/sha256" "crypto/sha512" "encoding/hex" "encoding/json" "errors" "fmt" "hash" "io" "mime" "net/http" "net/url" "strings" ) const ( // sha1Prefix is the prefix used by GitHub before the HMAC hexdigest. sha1Prefix = "sha1" // sha256Prefix and sha512Prefix are provided for future compatibility. sha256Prefix = "sha256" sha512Prefix = "sha512" // SHA1SignatureHeader is the GitHub header key used to pass the HMAC-SHA1 hexdigest. SHA1SignatureHeader = "X-Hub-Signature" // SHA256SignatureHeader is the GitHub header key used to pass the HMAC-SHA256 hexdigest. SHA256SignatureHeader = "X-Hub-Signature-256" // EventTypeHeader is the GitHub header key used to pass the event type. EventTypeHeader = "X-Github-Event" // DeliveryIDHeader is the GitHub header key used to pass the unique ID for the webhook event. DeliveryIDHeader = "X-Github-Delivery" ) var ( // eventTypeMapping maps webhooks types to their corresponding go-github struct types. eventTypeMapping = map[string]string{ "branch_protection_rule": "BranchProtectionRuleEvent", "check_run": "CheckRunEvent", "check_suite": "CheckSuiteEvent", "code_scanning_alert": "CodeScanningAlertEvent", "commit_comment": "CommitCommentEvent", "content_reference": "ContentReferenceEvent", "create": "CreateEvent", "delete": "DeleteEvent", "deploy_key": "DeployKeyEvent", "deployment": "DeploymentEvent", "deployment_status": "DeploymentStatusEvent", "discussion": "DiscussionEvent", "fork": "ForkEvent", "github_app_authorization": "GitHubAppAuthorizationEvent", "gollum": "GollumEvent", "installation": "InstallationEvent", "installation_repositories": "InstallationRepositoriesEvent", "issue_comment": "IssueCommentEvent", "issues": "IssuesEvent", "label": "LabelEvent", "marketplace_purchase": "MarketplacePurchaseEvent", "member": "MemberEvent", "membership": "MembershipEvent", "merge_group": "MergeGroupEvent", "meta": "MetaEvent", "milestone": "MilestoneEvent", "organization": "OrganizationEvent", "org_block": "OrgBlockEvent", "package": "PackageEvent", "page_build": "PageBuildEvent", "ping": "PingEvent", "project": "ProjectEvent", "project_card": "ProjectCardEvent", "project_column": "ProjectColumnEvent", "public": "PublicEvent", "pull_request": "PullRequestEvent", "pull_request_review": "PullRequestReviewEvent", "pull_request_review_comment": "PullRequestReviewCommentEvent", "pull_request_review_thread": "PullRequestReviewThreadEvent", "pull_request_target": "PullRequestTargetEvent", "push": "PushEvent", "repository": "RepositoryEvent", "repository_dispatch": "RepositoryDispatchEvent", "repository_import": "RepositoryImportEvent", "repository_vulnerability_alert": "RepositoryVulnerabilityAlertEvent", "release": "ReleaseEvent", "secret_scanning_alert": "SecretScanningAlertEvent", "star": "StarEvent", "status": "StatusEvent", "team": "TeamEvent", "team_add": "TeamAddEvent", "user": "UserEvent", "watch": "WatchEvent", "workflow_dispatch": "WorkflowDispatchEvent", "workflow_job": "WorkflowJobEvent", "workflow_run": "WorkflowRunEvent", } ) // genMAC generates the HMAC signature for a message provided the secret key // and hashFunc. func genMAC(message, key []byte, hashFunc func() hash.Hash) []byte { mac := hmac.New(hashFunc, key) mac.Write(message) return mac.Sum(nil) } // checkMAC reports whether messageMAC is a valid HMAC tag for message. func checkMAC(message, messageMAC, key []byte, hashFunc func() hash.Hash) bool { expectedMAC := genMAC(message, key, hashFunc) return hmac.Equal(messageMAC, expectedMAC) } // messageMAC returns the hex-decoded HMAC tag from the signature and its // corresponding hash function. func messageMAC(signature string) ([]byte, func() hash.Hash, error) { if signature == "" { return nil, nil, errors.New("missing signature") } sigParts := strings.SplitN(signature, "=", 2) if len(sigParts) != 2 { return nil, nil, fmt.Errorf("error parsing signature %q", signature) } var hashFunc func() hash.Hash switch sigParts[0] { case sha1Prefix: hashFunc = sha1.New case sha256Prefix: hashFunc = sha256.New case sha512Prefix: hashFunc = sha512.New default: return nil, nil, fmt.Errorf("unknown hash type prefix: %q", sigParts[0]) } buf, err := hex.DecodeString(sigParts[1]) if err != nil { return nil, nil, fmt.Errorf("error decoding signature %q: %v", signature, err) } return buf, hashFunc, nil } // ValidatePayload validates an incoming GitHub Webhook event request body // and returns the (JSON) payload. // The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded". // If the Content-Type is neither then an error is returned. // secretToken is the GitHub Webhook secret token. // If your webhook does not contain a secret token, you can pass nil or an empty slice. // This is intended for local development purposes only and all webhooks should ideally set up a secret token. // // Example usage: // // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { // // read signature from request // signature := "" // payload, err := github.ValidatePayloadFromBody(r.Header.Get("Content-Type"), r.Body, signature, s.webhookSecretKey) // if err != nil { ... } // // Process payload... // } func ValidatePayloadFromBody(contentType string, readable io.Reader, signature string, secretToken []byte) (payload []byte, err error) { var body []byte // Raw body that GitHub uses to calculate the signature. switch contentType { case "application/json": var err error if body, err = io.ReadAll(readable); err != nil { return nil, err } // If the content type is application/json, // the JSON payload is just the original body. payload = body case "application/x-www-form-urlencoded": // payloadFormParam is the name of the form parameter that the JSON payload // will be in if a webhook has its content type set to application/x-www-form-urlencoded. const payloadFormParam = "payload" var err error if body, err = io.ReadAll(readable); err != nil { return nil, err } // If the content type is application/x-www-form-urlencoded, // the JSON payload will be under the "payload" form param. form, err := url.ParseQuery(string(body)) if err != nil { return nil, err } payload = []byte(form.Get(payloadFormParam)) default: return nil, fmt.Errorf("webhook request has unsupported Content-Type %q", contentType) } // Only validate the signature if a secret token exists. This is intended for // local development only and all webhooks should ideally set up a secret token. if len(secretToken) > 0 { if err := ValidateSignature(signature, body, secretToken); err != nil { return nil, err } } return payload, nil } // ValidatePayload validates an incoming GitHub Webhook event request // and returns the (JSON) payload. // The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded". // If the Content-Type is neither then an error is returned. // secretToken is the GitHub Webhook secret token. // If your webhook does not contain a secret token, you can pass nil or an empty slice. // This is intended for local development purposes only and all webhooks should ideally set up a secret token. // // Example usage: // // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { // payload, err := github.ValidatePayload(r, s.webhookSecretKey) // if err != nil { ... } // // Process payload... // } func ValidatePayload(r *http.Request, secretToken []byte) (payload []byte, err error) { signature := r.Header.Get(SHA256SignatureHeader) if signature == "" { signature = r.Header.Get(SHA1SignatureHeader) } contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) if err != nil { return nil, err } return ValidatePayloadFromBody(contentType, r.Body, signature, secretToken) } // ValidateSignature validates the signature for the given payload. // signature is the GitHub hash signature delivered in the X-Hub-Signature header. // payload is the JSON payload sent by GitHub Webhooks. // secretToken is the GitHub Webhook secret token. // // GitHub API docs: https://developer.github.com/webhooks/securing/#validating-payloads-from-github func ValidateSignature(signature string, payload, secretToken []byte) error { messageMAC, hashFunc, err := messageMAC(signature) if err != nil { return err } if !checkMAC(payload, messageMAC, secretToken, hashFunc) { return errors.New("payload signature check failed") } return nil } // WebHookType returns the event type of webhook request r. // // GitHub API docs: https://docs.github.com/en/developers/webhooks-and-events/events/github-event-types func WebHookType(r *http.Request) string { return r.Header.Get(EventTypeHeader) } // DeliveryID returns the unique delivery ID of webhook request r. // // GitHub API docs: https://docs.github.com/en/developers/webhooks-and-events/events/github-event-types func DeliveryID(r *http.Request) string { return r.Header.Get(DeliveryIDHeader) } // ParseWebHook parses the event payload. For recognized event types, a // value of the corresponding struct type will be returned (as returned // by Event.ParsePayload()). An error will be returned for unrecognized event // types. // // Example usage: // // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { // payload, err := github.ValidatePayload(r, s.webhookSecretKey) // if err != nil { ... } // event, err := github.ParseWebHook(github.WebHookType(r), payload) // if err != nil { ... } // switch event := event.(type) { // case *github.CommitCommentEvent: // processCommitCommentEvent(event) // case *github.CreateEvent: // processCreateEvent(event) // ... // } // } func ParseWebHook(messageType string, payload []byte) (interface{}, error) { eventType, ok := eventTypeMapping[messageType] if !ok { return nil, fmt.Errorf("unknown X-Github-Event in message: %v", messageType) } event := Event{ Type: &eventType, RawPayload: (*json.RawMessage)(&payload), } return event.ParsePayload() }