Signature Validation and Authentication

Overview

This document outlines the purpose, functionality, and implementation of a signature validation service written in Node.js using TypeScript. The service ensures secure communication by verifying the authenticity and integrity of incoming requests using a cryptographic signature scheme.

Features

The service performs the following key tasks:

  1. Canonicalization - Orders and structures the HTTP request’s method, URI, query parameters, headers, and body payload in a consistent manner to create a canonical string.

  2. Hashing - Hashes the ordered request body (payload) using SHA-256.

  3. Signature Generation - Generates a signature by combining the canonical string, a secret key, and a timestamp using the HMAC-SHA256 algorithm.

  4. Signature Validation - Compares the provided signature (from the incoming request) with the generated signature using a timing-safe comparison to mitigate timing attacks.

  5. Error Handling - Throws an error if the signatures do not match or if other issues (e.g., timestamp expiration) are detected.


Key Functions and Methods

1. orderJsonByKeysAscending(jsonObj: Record<string, any>): Record<string, any>

  • Purpose: Recursively orders all keys of a JSON object and sorts elements in arrays in ascending order.

  • Objects: Both the top-level keys and the keys of any nested objects are sorted alphabetically.

  • Arrays:

    • If the array contains primitive values (e.g., strings, numbers), the elements are sorted in ascending order.

    • If the array contains objects, the elements are sorted based on the lexicographical order of their JSON string representations, after their keys have been sorted.

    • Arrays containing mixed types (both primitives and objects) or complex structures are returned as is without sorting their elements.

  • Input: Any JavaScript value, including:

  • Primitive types: string, number, boolean, null, undefined .

  • Objects: Objects with key-value pairs, including nested objects.

  • Arrays: Arrays containing any type of elements (primitives, objects, or mixed).

  • Output: Returns a new JSON object or value with the following modifications:

  • Objects: All keys are sorted alphabetically at every level of nesting.

  • Arrays of primitives: Elements are sorted in ascending order.

  • Arrays of objects: Elements are sorted based on the lexicographical order of their JSON string representations.

  • Mixed-type arrays: Arrays containing mixed types are returned as is without sorting their elements.

If the input is invalid (e.g., null, undefined, or not an object/array), the function returns the input as is without modification.

Example Usage:

 1 const input = {
 2  b: 2,
 3  a: 1,
 4  array: ["banana", "apple", "cherry"],
 5  nestedArray: [{ z: 3, y: 2 }, { x: 1 }],
 6  mixedArray: [1, { b: 2 }, "string"],
 7  object: {
 8    d: 4,
 9    c: {
10      f: 6,
11      e: 5
12    }
13  }
14 };
15 const orderedPayload = orderJsonByKeysAscending(input);
16 console.log(JSON.stringify(orderedPayload, null, 2));

Example Output:

  1 {
  2  "a": 1,
  3  "array": [
  4    "apple",
  5    "banana",
  6    "cherry"
  7  ],
  8  "b": 2,
  9  "mixedArray": [
 10    1,
 11    {
 12      "b": 2
 13    },
 14    "string"
 15  ],
 16  "nestedArray": [
 17    {
 18      "x": 1
 19    },
 20    {
 21      "y": 2,
 22      "z": 3
 23    }
 24  ],
 25  "object": {
 26    "c": {
 27     "e": 5,
 28     "f": 6
 29    },
 30    "d": 4
 31  }
 32 }
  1. createCanonicalQueryString(queryParams: { [key: string]: string }): string

  • Purpose: Sorts query parameters by key and concatenates them into a canonical string.

  • Input: An object containing query parameters.

  • Output: A query string with sorted keys.

Example Usage:

1 const queryString = createCanonicalQueryString({ c: '3', a: '1', b: '2' });
2 console.log(queryString); // a=1&b=2&c=3
  1. createCanonicalHeadersString(headers: { [key: string]: string }): string

  • Purpose: Sorts specific headers (like x-authorization-api-key and x-authorization-timestamp) alphabetically and formats them.

  • Input: An object containing headers.

  • Output: A string with sorted headers.

Example Usage:

 1 const headers = createCanonicalHeadersString({ 'x-authorization-api-key': 'key', 'x-authorization-timestamp': '123456' });
 2 console.log(headers); // x-authorization-api-key:key\nx-authorization-timestamp:123456
  1. hashPayload(payload: Record<string, any>): string

  • Purpose: Serializes and hashes a sorted payload using SHA-256.

  • Input: A JSON object (payload).

  • Output: A SHA-256 hash string

Example Usage:

1 const hashedPayload = hashPayload({ b: 2, a: 1 }); //inside hashPayload we call orderJsonByKeysAscending to obtain the ordered json payload before hashing it.
2 console.log(hashedPayload); // A SHA-256 hash of the sorted payload.
  1. createCanonicalString(method: string, uri: string, queryParams: { [key: string]: string }, headers: { [key: string]: string }, body: Record<string, any>): string

  • Purpose: Creates a canonical string that combines the http method, URI, query parameters, headers, and hashed payload.

  • Input: HTTP method, URI, query parameters, headers, and body payload.

  • Output: A canonical string for signing with format ${method}\n${uri}\n${canonicalQueryString}\n${canonicalHeaders}\n${hashedPayload}

Example Usage:

1 const canonicalString = createCanonicalString('POST', '/api/resource', queryParams, headers, payload);
2 console.log(canonicalString); // A canonical string used for signature generation.
  1. generateCanonicalSignature(secretKey: string, stringToSign: string, timestamp: number): string

  • Purpose: Generates a HMAC-SHA256 signature using a secret key, the string to sign, and a timestamp.

  • Input: Secret key, canonical string, and timestamp.

  • Output: A cryptographic signature string

Example Usage:

1 const signature = generateCanonicalSignature('mySecretKey', 'stringToSign', 1733747167010);
2 ${stringToSign}\n${timestamp} //Final string to be signed with secret key
3 console.log(signature); // A generated HMAC-SHA256 signature.

Conclusion

This service provides a robust solution for validating incoming HTTP requests based on cryptographic signatures. It ensures the integrity and authenticity of the request by comparing a provided signature with a generated one, preventing tampering and unauthorized access.

Was this helpful?