Skip to main content

Computing Ocelot Digital Signature

Summary#

The Ocelot digital signature is used in verifying requests and webhook events and signing responses.

The signature is the hexidecimal format of the SHA-256 hash for the salted normalized JSON string representation of the message body. We will go thru each piece of this definition.

Normalized JSON String#

The normalized JSON string fulfills these rules:

  • Compact string representation.
  • Sort object keys.
  • Exclude any object property whose value is not JSON compatible.

Explanations#

The compact JSON string representation has no whitespace and no line breaks between elements. The non-compact representations include these for readability purposes. It is important that the readability whitespaces and line break have been removed because their presence will adversely affect the signature verification.

At each level of nesting, the JSON object keys must be sorted. Many JSON libraries by default do not sort the object keys because inconsistent ordering in the string representation does not affect JSON functionality. However an inconsistent ordering will adversely impact the hash value. Explicitly sorting the keys creates a consistent ordering and eliminates one source of adverse effect on signature verification.

A JavaScript object's property value can have a type incompatible with JSON. For example, a JavaScript value that is a function or undefined is JSON incompatible. These values should be excluded from the string representation.

Code Snippets#

The following JavaScript will remove properties whose value is not JSON compatible. The result will be a sanitized JavaScript object.

const sanitizedBody = JSON.parse(JSON.stringify(messageBody));

The following is a recursive JavaScript function that sorts object keys and builds a compact string representation.

const buildNormalizedString = (node: JSONDataType): string => {    if (Array.isArray(node)) {        return node.reduce<string>((acc, elem) => {        const val = buildNormalizedString(elem);        acc += val;        return acc;        }, '');    }    if (typeof node === 'object' && node !== null) {        return Object.keys(node)        .sort()        .reduce((acc, key) => {            const val = buildNormalizedString(node[key]);            return acc.concat(key, val);        }, '');    }    return JSON.stringify(node);};

Calling the recursive function with the sanitized JavaScript object will result in the normalized JSON string:

const normalizedBody = buildNormalizedString(sanitizedBody);

Salted Normalized String#

Secret Key#

The secret key is generated in the service provider page in the Client Admin. For details on generation, see the guide Adding A Service Provider.

Salting#

The salting occurs by prepending and appending the service provider secret key to the normalized string representation.

The following is a JavaScript snippet for salting the normalized string:

var saltedNormalizedBody = secretKey + normalizedBody + secretKey

Hash Computation#

The hashing algorithm used is SHA-256.

The hashing algorithm input should be the UTF-8 encoded byte-array. Hashing algorithms take byte-array's as their input. There are many different ways to encode a string into a byte-array. We use UTF-8. Using another encoding could adversely affect the verification.

The hashing algorithm output should be formatted as a hexidecimal string.

The following is a JavaScript snippet for computing the hexidecimal-formated SHA-256 hash of a UTF-8 encoded string.

import crypto from 'crypto';
...
var signature = crypto    .createHash('sha256')    .update(saltedNormalizedBody, 'utf-8')    .digest('hex');

The function argument to createHash specifies SHA-256. The second function argument to update specifies UTF-8 encoding. The function argument to digest specifies hexidecimal output format.

Example#

This is a working example with assertions so you can easily create unit tests with the example input.

import crypto from 'crypto';
const secretKey = 'notAGoodSecretKey';const messageBody = {    providerId: 'Capture IDK Form Submissions',    capabilityId: 'form_contact',    requestTime: '2023-03-15T20:32:02.690Z',    chatbotId: '4bc15974-001d-4837-b298-2df9f0f00000',    conversationId: '3e146665-1765-45be-a8e3-9a699d200000',    clientData: {    userAgent:        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36',    hostname: 'ai.ocelotbot.com',    origin: 'https://fakeschool.edu',    },    eventType: 'Form',    payload: {    formId: 'contact',    values: {        body: "User:\nHow do I test the webhook?\n\nTechie:\nI'm not sure I understand your question. Do any of these suggestions help? If they don't, you can explore additional suggestions by clicking on one of the icons below or ask me another question.\n\nUser:\nContact the office\n\nTechie:\nI can help you reach someone. Which office would you like to contact?\n\nUser:\nOneStop Center\n\nTechie:\nWhat is your first and last name?\n\nUser:\nJohn Smith\n\nTechie:\nWhat is your e-mail address?\n\nUser:\njohn.smith@fakeschool.com\n\nTechie:\nWhat is the best mobile number to reach you?\n\nUser:\n1231231234\n\nTechie:\nEnter your school Tech ID # below, otherwise enter 'I don't have one' or 'I don't know my Tech ID.'\n\nUser:\n12345\n\nTechie:\nYou're almost done! Answer this last question and the transcript of our conversation will be sent. Is there anything else you'd like to add?\n\nUser:\nThis is a test\n\nTechie:\nGreat, your message has been sent. Someone from One Stop will get back to you as soon as possible.\n\n",        name: 'John Smith',        email: 'john.smith@fakeschool.edu',        phone: '1231231234',        triggerMessage: 'Contact the office',        campus: 'Main',        office: 'OneStop Center',        studentId: '12345',    },    },};
const sanitizedBody = JSON.parse(JSON.stringify(messageBody));const buildNormalizedString = (node: JSONDataType): string => {    if (Array.isArray(node)) {    return node.reduce<string>((acc, elem) => {        const val = buildNormalizedString(elem);        acc += val;        return acc;    }, '');    }    if (typeof node === 'object' && node !== null) {    return Object.keys(node)        .sort()        .reduce((acc, key) => {        const val = buildNormalizedString(node[key]);        return acc.concat(key, val);        }, '');    }    return JSON.stringify(node);};const normalizedBody = buildNormalizedString(sanitizedBody);
const expectedNormalizedBody =    'capabilityId"form_contact"chatbotId"4bc15974-001d-4837-b298-2df9f0f00000"clientDatahostname"ai.ocelotbot.com"origin"https://fakeschool.edu"userAgent"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"conversationId"3e146665-1765-45be-a8e3-9a699d200000"eventType"Form"payloadformId"contact"valuesbody"User:\\nHow do I test the webhook?\\n\\nTechie:\\nI\'m not sure I understand your question. Do any of these suggestions help? If they don\'t, you can explore additional suggestions by clicking on one of the icons below or ask me another question.\\n\\nUser:\\nContact the office\\n\\nTechie:\\nI can help you reach someone. Which office would you like to contact?\\n\\nUser:\\nOneStop Center\\n\\nTechie:\\nWhat is your first and last name?\\n\\nUser:\\nJohn Smith\\n\\nTechie:\\nWhat is your e-mail address?\\n\\nUser:\\njohn.smith@fakeschool.com\\n\\nTechie:\\nWhat is the best mobile number to reach you?\\n\\nUser:\\n1231231234\\n\\nTechie:\\nEnter your school Tech ID # below, otherwise enter \'I don\'t have one\' or \'I don\'t know my Tech ID.\'\\n\\nUser:\\n12345\\n\\nTechie:\\nYou\'re almost done! Answer this last question and the transcript of our conversation will be sent. Is there anything else you\'d like to add?\\n\\nUser:\\nThis is a test\\n\\nTechie:\\nGreat, your message has been sent. Someone from One Stop will get back to you as soon as possible.\\n\\n"campus"Main"email"john.smith@fakeschool.edu"name"John Smith"office"OneStop Center"phone"1231231234"studentId"12345"triggerMessage"Contact the office"providerId"Capture IDK Form Submissions"requestTime"2023-03-15T20:32:02.690Z"';console.assert(normalizedBody === expectedNormalizedBody, 'sanitizedBody is not correct');
const saltedNormalizedBody = secretKey + normalizedBody + secretKey;const expectedSaltedNormalizedBody = secretKey + expectedNormalizedBody + secretKey;console.assert(    saltedNormalizedBody === expectedSaltedNormalizedBody,    'saltedNormalizedBody is not correct',);
const signature = crypto    .createHash('sha256')    .update(saltedNormalizedBody, 'utf-8')    .digest('hex');const expectedSignature = '0c958b6fef24a995fc751eb5b2793be5b0c588606ab7f333f697bb4b76aecbab';console.assert(expectedSignature === signature, 'signature is not correct');