Skip to content

Commit

Permalink
add X-Hub-Signature-256 webhook signature validation
Browse files Browse the repository at this point in the history
Resolves: #11
  • Loading branch information
greenpau committed Mar 12, 2022
1 parent 8b9a6e7 commit adab583
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 5 deletions.
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,10 @@ Inspired by [this comment](https://github.com/vrongmeal/caddygit/pull/5#issuecom
Please ask questions either here or via LinkedIn. I am happy to help you! @greenpau

Please see other plugins:
* [caddy-auth-portal](https://github.com/greenpau/caddy-auth-portal)
* [caddy-authorize](https://github.com/greenpau/caddy-authorize)
* [caddy-security](https://github.com/greenpau/caddy-security)
* [caddy-trace](https://github.com/greenpau/caddy-trace)
* [caddy-systemd](https://github.com/greenpau/caddy-systemd)


<!-- begin-markdown-toc -->
## Table of Contents

Expand Down
64 changes: 62 additions & 2 deletions pkg/service/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@ package service

import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"go.uber.org/zap"
"io/ioutil"
"net/http"
"strings"
"sync"
"time"
)
Expand Down Expand Up @@ -84,17 +90,50 @@ func (m *Endpoint) ServeHTTP(ctx context.Context, w http.ResponseWriter, r *http
if hdr == "" {
continue
}
if hdr != webhook.Secret {

var authFailed bool
var authFailMessage string

switch webhook.Header {
case "X-Hub-Signature-256", strings.ToUpper("X-Hub-Signature-256"):
if r.Method != "POST" {
authFailed = true
authFailMessage = "non-POST request"
break
}
hdrParts := strings.SplitN(hdr, "=", 2)
if len(hdrParts) != 2 {
authFailed = true
authFailMessage = fmt.Sprintf("malformed %s header", webhook.Header)
break
}
if hdrParts[0] != "sha256" {
authFailMessage = fmt.Sprintf("malformed %s header, sha256 not found", webhook.Header)
}
if err := validateSignature(r, strings.TrimSpace(hdrParts[1]), webhook.Secret); err != nil {
authFailed = true
authFailMessage = fmt.Sprintf("signature validation failed: %v", err)
}
default:
if hdr != webhook.Secret {
authFailed = true
authFailMessage = "auth header value mismatch"
}
}

if authFailed {
resp["status_code"] = http.StatusUnauthorized
m.logger.Warn(
"webhook authentication failed",
zap.String("repo_name", repo.Config.Name),
zap.String("webhook_header", webhook.Header),
zap.String("error", "auth header value mismatch"),
zap.String("error", authFailMessage),
)
return m.respondHTTP(ctx, w, r, resp)
}

authorized = true
break
}

if !authorized {
Expand Down Expand Up @@ -128,3 +167,24 @@ func (m *Endpoint) respondHTTP(ctx context.Context, w http.ResponseWriter, r *ht
w.Write(b)
return nil
}

func validateSignature(r *http.Request, wantSig, secret string) error {
if wantSig == "" {
return fmt.Errorf("empty signature")
}
if len(wantSig) != 64 {
return fmt.Errorf("malformed sha256 hash, length %d", len(wantSig))
}

respBody, err := ioutil.ReadAll(r.Body)
if err != nil {
return fmt.Errorf("failed reading request body")
}
h := hmac.New(sha256.New, []byte(secret))
h.Write(respBody)
gotSig := hex.EncodeToString(h.Sum(nil))
if wantSig != gotSig {
return fmt.Errorf("signature mismatch")
}
return nil
}

0 comments on commit adab583

Please sign in to comment.