Solving the Issue with Inconsistent HMAC SHA256 Webhook Signature Validation

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.

Answer №1

Within your Typescript code snippet, you currently have the following:

.createHmac("sha256", Buffer.from(secret, "hex"))

In this snippet, an attempt is made to hex decode the variable secret. However, due to the fact that secret begins with the character Z, NodeJS abruptly halts the processing and instead generates an empty instance of a Buffer.

This behavior came as quite a surprise to me initially, causing me to question if I had erred in my attempts to create a minimal recreation scenario. It later came to light that this phenomenon is briefly mentioned within the documentation:

'hex': Every individual byte will be encoded into two hexadecimal characters. When decoding strings that consist of an odd number of hexadecimal characters, data truncation may occur.

Conversely, in your Go code segment, the following lines are present:

    secertHex := hex.EncodeToString([]byte(secret))
    h := hmac.New(sha256.New, []byte(secertHex))

This section effectively converts secret into a hex-encoded format which is then utilized as the HMAC key. Presumably, this aligns more closely with your intended objective.

To rectify this issue, consider modifying the Typescript section to resemble the following:

.createHmac("sha256", Buffer.from(secret, 'utf8').toString('hex'))

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

When working with Angular 5, the question arises: how and where to handle type conversion between form field values (typically strings) and model properties (such

As a newcomer to Angular, I am struggling with converting types between form field values (which are always strings) and typed model properties. In the following component, my goal is to double a number inputted by the user. The result will be displayed i ...

Grab the content from a contenteditable HTML Element using React

Currently, I am developing an EditableLabel React component using Typescript in conjunction with the contenteditable attribute. My goal is to enable the selection of the entire text content when the user focuses on it, similar to the behavior showcased in ...

The compilation of @types/socket.io-redis fails because it cannot locate the Adapter exported by @types/socket.io, which is necessary for its functionality

It seems like there may be an issue with my tsconfig file or something similar. npm run compile > <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="0c69626b6562694c3d223c">[email protected]</a> compile /User ...

Is there a program available that can efficiently convert or translate JSON objects into TypeScript types or interfaces?

Can anyone recommend a tool that can easily convert a JSON object into a TypeScript type or interface? An example input would be something like this: I'm hoping to paste the JSON object into the tool and receive an output structure similar to: expor ...

Having issues with Craco not recognizing alias configuration for TypeScript in Azure Pipeline webpack

I am encountering an issue with my ReactJs app that uses Craco, Webpack, and Typescript. While the application can run and build successfully locally, I am facing problems when trying to build it on Azure DevOps, specifically in creating aliases. azure ...

Tips for creating a TypeScript-compatible redux state tree with static typing and immutability:

One remarkable feature of TypeScript + Redux is the ability to define a statically typed immutable state tree in the following manner: interface StateTree { readonly subState1: SubState1; readonly subState2: SubState2; ...

Generating dynamic components using React and TypeScript

Creating a multi-step form using a set of components can be tricky. One approach is to compile all the components into an array and then use the map() method to render them in sequence. const stepComponents = [ <SelectCoach/>, <SelectDate/> ...

Encountering an issue when using npm to add a forked repository as a required package

While attempting to install my fork of a repository, I encountered the error message "Can't install github:<repo>: Missing package name." The original repository can be accessed here, but the specific section I am modifying in my fork is located ...

Tips for correctly specifying the theme as a prop in the styled() function of Material UI using TypeScript

Currently, I am utilizing Material UI along with its styled function to customize components like so: const MyThemeComponent = styled("div")(({ theme }) => ` color: ${theme.palette.primary.contrastText}; background-color: ${theme.palette.primary.mai ...

I encountered an issue with the date input stating, "The parameters dictionary includes a missing value for 'Fromdate' parameter, which is of non-nullable type 'System.DateTime'."

An error message is popping up that says: '{"Message":"The request is invalid.","MessageDetail":"The parameters dictionary contains a null entry for parameter 'Fromdate' of non-nullable type 'System.DateTime' for method 'Syste ...

Disable rendering on the second component upon state change in the first

In my React POC, I have a Main component with two child components where the state is managed within the Main component. Whenever there is a change in the state, the child components are re-rendered by passing the new state as props. import * as React fro ...

The NextRouter failed to mount in Next.JS

When you use import { useRouter } from "next/router"; instead of import { useRouter } from "next/navigation";, it results in the error message "Argument of type '{ pathname: string; query: { search: string; }; }' is not assign ...

Empowering your Angular2 application with data binding

I am currently working with the following template: <table width="700"> <caption>All Users</caption> <thead> <tr> <th>name</th> <th>surname</th> < ...

Maintaining consistency between two fields? Foo<X> (x:X leads to y:Y)

Is there a way to establish a relationship between two types in Typescript? For instance, I need to ensure that the TypeContent in the example below is appropriate for the type T. export type DataReport<T extends Type, TypeContent> = { id: number ...

Understanding the appropriate roles and attributes in HTML for a modal backdrop element in a TypeScript React project

Trying to create a Modal component using React and TypeScript with a feature that allows closing by clicking outside. This is being achieved by adding a backdrop element, a greyed out HTML div with an onClick event calling the onClose method. Encountering ...

How can CSS variables be applied to a hover property within an Angular directive?

Check out the directive below: import { Directive, ElementRef, HostListener } from '@angular/core'; @Directive({ selector: 'd-btn', host: {} }) export class ButtonDirective { constructor(private el: ElementRef){} @HostLis ...

The transition from an unknown type to a known type occurs through type inference when a method is

In my current project, I have a class with a single generic parameter T. The challenge arises when this class is sometimes constructed with a known value for T, and other times without a value, leaving T in an unknown state. It becomes cumbersome to always ...

The parameter 'host: string | undefined; user: string | undefined' does not match the expected type 'string | ConnectionConfig' and cannot be assigned

My attempt to establish a connection to an AWS MySQL database looks like this: const config = { host: process.env.RDS_HOSTNAME, user: process.env.RDS_USERNAME, password: process.env.RDS_PASSWORD, port: 3306, database: process.env.RDS_DB_NAME, } ...

What is the best way to ensure that all function parameters using a shared generic tuple type have a consistent length?

Understanding that [number, number] | [number] is an extension of [number, ...number[]] is logical, but I'm curious if there's a method to enforce the length of tuples based on the initial parameter so that the second tuple must match that same l ...

What is the best way to send props (or a comparable value) to the {children} component within RootLayout when using the app router in Next.js

As I work on updating an e-commerce website's shopping cart to Next.js 13+, I refer back to an old tutorial for guidance. In the previous version of the tutorial, the _app.ts file utilized page routing with the following code snippet: return ( < ...