Skip to content

check callback signature header #37

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: release-0.3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions callback.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,21 @@ import (
)

type CallbackConfiguration struct {
MaxWaitSeconds uint `json:"max_wait_seconds"`
Port uint `json:"port"`
Route string `json:"route"`
SSL bool `json:"ssl" envconfig:"IMMUNE_SSL"`
SSLKeyFile string `json:"ssl_key_file" envconfig:"IMMUNE_SSL_KEY_FILE"`
SSLCertFile string `json:"ssl_cert_file" envconfig:"IMMUNE_SSL_CERT_FILE"`
IDLocation string `json:"id_location"`
MaxWaitSeconds uint `json:"max_wait_seconds"`
Port uint `json:"port"`
Route string `json:"route"`
SSL bool `json:"ssl" envconfig:"IMMUNE_SSL"`
SSLKeyFile string `json:"ssl_key_file" envconfig:"IMMUNE_SSL_KEY_FILE"`
SSLCertFile string `json:"ssl_cert_file" envconfig:"IMMUNE_SSL_CERT_FILE"`
IDLocation string `json:"id_location"`
Signature SignatureConfiguration `json:"signature"`
}

type SignatureConfiguration struct {
ReplayAttacks bool `json:"replay_attacks" envconfig:"IMMUNE_REPLAY_ATTACKS"`
Secret string `json:"secret" envconfig:"IMMUNE_SIGNATURE_SECRET"`
Header string `json:"header" envconfig:"IMMUNE_SIGNATURE_HEADER"`
Hash string `json:"hash" envconfig:"IMMUNE_SIGNATURE_HASH"`
}

const CallbackIDFieldName = "immune_callback_id"
Expand Down Expand Up @@ -52,3 +60,7 @@ type CallbackServer interface {
Start(ctx context.Context) error
Stop()
}

type CallbackSignatureVerifier interface {
VerifyCallbackSignature(s *Signal) error
}
16 changes: 14 additions & 2 deletions callback/server.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package callback

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"time"
Expand Down Expand Up @@ -95,10 +97,20 @@ func (s *server) Start(ctx context.Context) error {
func handleCallback(outbound chan<- *immune.Signal) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
sig := &immune.Signal{}
err := json.NewDecoder(r.Body).Decode(sig)
clone := r.Clone(context.Background())

buf, err := io.ReadAll(r.Body)
if err != nil {
sig.Err = fmt.Errorf("failed to decode callback body: %v", err)
sig.Err = fmt.Errorf("failed to read callback body: %v", err)
} else {
err = json.Unmarshal(buf, sig)
if err != nil {
sig.Err = fmt.Errorf("failed to decode callback body: %v", err)
}
}

clone.Body = io.NopCloser(bytes.NewBuffer(buf))
sig.Request = clone
w.WriteHeader(http.StatusOK)
outbound <- sig
}
Expand Down
4 changes: 3 additions & 1 deletion callback/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ func Test_handleCallback(t *testing.T) {
require.Equal(t, tt.wantErrMsg, s.Error())
return
}
require.Equal(t, tt.wantSignal, <-tt.args.outbound)
sig := <-tt.args.outbound
sig.Request = nil
require.Equal(t, tt.wantSignal, sig)
})
}
}
Expand Down
113 changes: 113 additions & 0 deletions callback/signature_verifier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package callback

import (
"crypto/hmac"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"hash"
"io/ioutil"
"strconv"
"time"

"github.com/frain-dev/immune"
"github.com/pkg/errors"
"golang.org/x/crypto/sha3"
)

const ConvoyTimestampHeader = "Convoy-Timestamp"

type SignatureVerifier struct {
ReplayAttacks bool `json:"replay_attacks"`
Secret string `json:"secret"`
Header string `json:"header"`
Hash string `json:"hash"`
hashFn func() hash.Hash
}

func NewSignatureVerifier(replayAttacks bool, secret, header, hash string) (immune.CallbackSignatureVerifier, error) {
fn, err := getHashFunction(hash)
if err != nil {
return nil, err
}

return &SignatureVerifier{
ReplayAttacks: replayAttacks,
Secret: secret,
Header: header,
Hash: hash,
hashFn: fn,
}, nil
}

func (sv *SignatureVerifier) VerifyCallbackSignature(s *immune.Signal) error {
r := s.Request
buf, err := ioutil.ReadAll(r.Body)
if err != nil {
return errors.Wrap(err, "unable to read request body")
}

signatureHex := []byte(r.Header.Get(sv.Header))
var signature = make([]byte, hex.DecodedLen(len(signatureHex)))
_, err = hex.Decode(signature, signatureHex)
if err != nil {
return errors.Wrap(err, "unable to hex decode signature body")
}

hasher := hmac.New(sv.hashFn, []byte(sv.Secret))

if sv.ReplayAttacks {
timestampStr := r.Header.Get(ConvoyTimestampHeader)
timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
if err != nil {
return errors.Wrap(err, "unable to parse signature timestamp")
}

t := time.Unix(timestamp, 0)
d := time.Since(t)
if d > time.Minute {
return errors.Errorf("replay attack timestamp is more than a minute ago")
}

hasher.Write([]byte(timestampStr))
hasher.Write([]byte(","))
}

hasher.Write(buf)
if !hmac.Equal(signature, hasher.Sum(nil)) {
return errors.New("signature invalid")
}
return nil
}

func getHashFunction(algorithm string) (func() hash.Hash, error) {
switch algorithm {
case "MD5":
return md5.New, nil
case "SHA1":
return sha1.New, nil
case "SHA224":
return sha256.New224, nil
case "SHA256":
return sha256.New, nil
case "SHA384":
return sha512.New384, nil
case "SHA512":
return sha512.New, nil
case "SHA3_224":
return sha3.New224, nil
case "SHA3_256":
return sha3.New256, nil
case "SHA3_384":
return sha3.New384, nil
case "SHA3_512":
return sha3.New512, nil
case "SHA512_224":
return sha512.New512_224, nil
case "SHA512_256":
return sha512.New512_256, nil
}
return nil, errors.New("unknown hash algorithm")
}
Loading