I'm facing an issue with implementing webhook signature validation in Go compared to a successful implementation I have in Node.js. The problem lies in the incorrect generation of HMAC SHA256 signatures by my Go code, and I'm struggling to pinpoint the cause of this discrepancy. Check out the Node.js and Go code snippets below for reference.
Despite adjusting the hex conversion method for the secret in the Go code, it still fails to validate the webhook signature accurately, unlike the Node.js version that performs flawlessly. Here are some discrepancies I've identified:
An error in handling the secret conversion to hex before feeding it into the HMAC function within the Go code has been rectified.
What lingering issue could be hindering the correct generation of the HMAC SHA256 signature in the Go version? Any insights or recommendations would be highly valued!
import crypto from "crypto";
import express from "express";
export const computeWebhookSignature = ({
requestBody,
secret,
timestamp,
}: {
requestBody: any;
secret: string;
timestamp: number;
}): string => {
return crypto
.createHmac("sha256", Buffer.from(secret, "hex"))
.update(Buffer.from(`${timestamp}.${JSON.stringify(requestBody)}`))
.digest("hex");
};
const FIVE_MINUTES = 300000;
export const isValidWebhookRequest = ({
requestBody,
secret,
timestamp,
signature,
}: {
requestBody: any;
secret: string;
timestamp: string;
signature: string;
}): boolean => {
const timestampD = new Date(Number.parseInt(timestamp));
const timestampMs = timestampD.getTime();
const now = Date.now();
console.log(timestampMs, "timestamp");
if (timestampMs > now || now - timestampMs > FIVE_MINUTES) {
return false;
}
const computedSignature = computeWebhookSignature({ requestBody, secret, timestamp: timestampMs });
if (computedSignature !== signature) {
return false;
}
return true;
};
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = 'Zkoc9w7zM0twuBKGWtcDue3F1Ptj/VaOGwWmb/+gtT6IcGan2OXWsEzfEDjo+pTOj91mADx+8kfTbN4F2mrU8w==';
app.post("/", (req, res) => {
const { body } = req;
const signature = req.headers['planetplay-signature'] as string;
const timestamp = req.headers['planetplay-timestamp'] as string;
console.log(signature);
console.log(timestamp);
console.log(body);
const isValid = isValidWebhookRequest({
requestBody: body,
secret: WEBHOOK_SECRET,
timestamp,
signature
});
console.log(isValid);
});
app.listen(3000, () => {
console.log("Server running on port 3000");
});
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"log"
"net/http"
"strconv"
)
const WEBHOOK_SECRET = "Zkoc9w7zM0twuBKGWtcDue3F1Ptj/VaOGwWmb/+gtT6IcGan2OXWsEzfEDjo+pTOj91mADx+8kfTbN4F2mrU8w=="
func computeWebhookSignature(requestBody string, secret string, timestamp int64) string {
message := fmt.Sprintf("%d.%s", timestamp, requestBody)
secertHex := hex.EncodeToString([]byte(secret))
h := hmac.New(sha256.New, []byte(secertHex))
h.Write([]byte(message))
return hex.EncodeToString(h.Sum(nil))
}
func isValidWebhookRequest(requestBody string, secret, timestamp, signature string) bool {
timestampInt, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
log.Println("Invalid timestamp")
return false
}
computedSignature := computeWebhookSignature(requestBody, secret, timestampInt)
log.Printf("Computed signature: %s", computedSignature)
log.Printf("Received signature: %s", signature)
return computedSignature == signature
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusBadRequest)
return
}
body := string(bodyBytes)
signature := r.Header.Get("planetplay-signature")
timestamp := r.Header.Get("planetplay-timestamp")
if isValidWebhookRequest(body, WEBHOOK_SECRET, timestamp, signature) {
fmt.Fprintln(w, "Valid webhook request")
} else {
http.Error(w, "Invalid webhook request", http.StatusForbidden)
}
}
func main() {
http.HandleFunc("/", webhookHandler)
log.Fatal(http.ListenAndServe(":3000", nil))
}
Your input is greatly appreciated.