From dc3c2d5533cea2725add8c4687aae92e2a3f2e01 Mon Sep 17 00:00:00 2001 From: Jon Chen Date: Tue, 22 Dec 2020 00:54:59 +0000 Subject: [PATCH] implement interaction request signing verification --- interactions.go | 54 +++++++++++++++++++++++++++++++++++ interactions_test.go | 68 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 interactions.go create mode 100644 interactions_test.go diff --git a/interactions.go b/interactions.go new file mode 100644 index 0000000..6fc2f55 --- /dev/null +++ b/interactions.go @@ -0,0 +1,54 @@ +package discordgo + +import ( + "bytes" + "crypto/ed25519" + "encoding/hex" + "io" + "io/ioutil" + "net/http" +) + +// VerifyInteraction implements message verification of the discord interactions api +// signing algorithm, as documented here: +// https://discord.com/developers/docs/interactions/slash-commands#security-and-authorization +func VerifyInteraction(r *http.Request, key ed25519.PublicKey) bool { + var msg bytes.Buffer + + signature := r.Header.Get("X-Signature-Ed25519") + if signature == "" { + return false + } + + sig, err := hex.DecodeString(signature) + if err != nil { + return false + } + + if len(sig) != ed25519.SignatureSize { + return false + } + + timestamp := r.Header.Get("X-Signature-Timestamp") + if timestamp == "" { + return false + } + + msg.WriteString(timestamp) + + defer r.Body.Close() + var body bytes.Buffer + + // at the end of the function, copy the original body back into the request + defer func() { + r.Body = ioutil.NopCloser(&body) + }() + + // copy body into buffers + _, err = io.Copy(&msg, io.TeeReader(r.Body, &body)) + if err != nil { + return false + } + + return ed25519.Verify(key, msg.Bytes(), sig) +} diff --git a/interactions_test.go b/interactions_test.go new file mode 100644 index 0000000..caaf28a --- /dev/null +++ b/interactions_test.go @@ -0,0 +1,68 @@ +package discordgo + +import ( + "bytes" + "crypto/ed25519" + "encoding/hex" + "net/http/httptest" + "strconv" + "strings" + "testing" + "time" +) + +func TestVerifyInteraction(t *testing.T) { + pubkey, privkey, err := ed25519.GenerateKey(nil) + if err != nil { + t.Errorf("error generating signing keypair: %s", err) + } + timestamp := "1608597133" + + t.Run("success", func(t *testing.T) { + body := "body" + request := httptest.NewRequest("POST", "http://localhost/interaction", strings.NewReader(body)) + request.Header.Set("X-Signature-Timestamp", timestamp) + + var msg bytes.Buffer + msg.WriteString(timestamp) + msg.WriteString(body) + signature := ed25519.Sign(privkey, msg.Bytes()) + request.Header.Set("X-Signature-Ed25519", hex.EncodeToString(signature[:ed25519.SignatureSize])) + + if !VerifyInteraction(request, pubkey) { + t.Error("expected true, got false") + } + }) + + t.Run("failure/modified body", func(t *testing.T) { + body := "body" + request := httptest.NewRequest("POST", "http://localhost/interaction", strings.NewReader("WRONG")) + request.Header.Set("X-Signature-Timestamp", timestamp) + + var msg bytes.Buffer + msg.WriteString(timestamp) + msg.WriteString(body) + signature := ed25519.Sign(privkey, msg.Bytes()) + request.Header.Set("X-Signature-Ed25519", hex.EncodeToString(signature[:ed25519.SignatureSize])) + + if VerifyInteraction(request, pubkey) { + t.Error("expected false, got true") + } + }) + + t.Run("failure/modified timestamp", func(t *testing.T) { + body := "body" + request := httptest.NewRequest("POST", "http://localhost/interaction", strings.NewReader("WRONG")) + request.Header.Set("X-Signature-Timestamp", strconv.FormatInt(time.Now().Add(time.Minute).Unix(), 10)) + + var msg bytes.Buffer + msg.WriteString(timestamp) + msg.WriteString(body) + signature := ed25519.Sign(privkey, msg.Bytes()) + request.Header.Set("X-Signature-Ed25519", hex.EncodeToString(signature[:ed25519.SignatureSize])) + + if VerifyInteraction(request, pubkey) { + t.Error("expected false, got true") + } + }) +}