Step 2 - Signing the requests
Algorithm and encodingCopied!
In order to send signed requests, you need to use a specific algorithm and encoding format to calculate the necessary signature.
Supported algorithms and required encoding are:
-
Algorithms: sha256
-
Encoding: base64
Prepare the Request componentsCopied!
The first step in the process is to gather all the necessary components for the request. These components are:
-
HTTP Method: The HTTP method used in the request (i.e. post)
-
Path: The specific endpoint or path of the request (i.e.
/auth/token). -
Headers: The headers that will be involved in the signature are:
-
Accept -
Content-Type -
Date -
Digest -
Authorization
-
-
Body: The request body which need to be hashed and base64 encoded to generate the Digest.
Description of Header parametersCopied!
|
Parameter |
Required |
Remark |
|---|---|---|
|
Accept |
Yes |
application/json |
|
Content-Type |
Yes |
application/json |
|
Date |
Yes |
The datetime when the request is made. Cannot be more than 5 minutes before request is made. RFC 1123 format (GMT/UTC date time) Example: "Wed, 02 Feb 2025 12:00:00 GMT" |
|
Digest |
Yes |
Calculation explained below |
|
Authorization |
Yes |
Calculation explained below |
Create the digestCopied!
The digest is a cryptographic hash of the request body, calculated using the algorithm SHA-256. The digest ensures the integrity of the request body: if the body is modified in transit, the digest will not match.
The digest is a base64 encoded hash of the body:
-
Pass the body of request through a hashing algorithm (SHA 256).
-
Make sure the hashed output is Binary format.
-
Base64encode the output.
-
Add the result to digest header declaring the used hashing algorithm, i.e. RSA-SHA256.
Following body example is a request to the /auth/token endpoint.
{"tenantUserId": "user674638475"}
Next we use the Algorithm and Encoding to calculate a Digest of the request body. The NodeJS code samples below indicate how this Digest is created and the corresponding output:
const hash = crypto
.createHash("sha256")
.update(JSON.stringify({ tenantUserId: "user674638475" }))
.digest("base64");
zc1CKvxXQT0ONwLoIi1LlFzBuJKnNCVRcTIgg0G2F2Y=
Finally, the Digest is to be included in the Digest Header in the following manner:
Digest: 'SHA-256=zc1CKvxXQT0ONwLoIi1LlFzBuJKnNCVRcTIgg0G2F2Y='
Creating the string to signCopied!
Once we have all the necessary components, we need to construct the string to sign. This string is composed of:
-
The HTTP method (e.g., post, get).
-
The request path (/auth/token).
-
The relevant headers that are part of the signing process.
Example of string to signCopied!
Suppose we are making a post request to /auth/token. The headers involved in the signature include Accept, Content-Type, Date, and Digest. The string to sign might look like this:
request-target: post /auth/token
date: Mon, 11 Mar 2024 10:34:17 GMT
content-type: application/json
accept: application/json
digest: SHA-256=zc1CKvxXQT0ONwLoIi1LlFzBuJKnNCVRcTIgg0G2F2Y=
This string includes:
-
request-target: The method and path, i.e., post /auth/token.
-
date: The date when the request was made.
-
content-type: The type of content being sent (e.g., application/json).
-
accept: The type of response expected (e.g., application/json).
-
digest: The base64-encoded hash of the body.
The headers must be listed in the exact order specified for signing. The string to sign is case-sensitive and must exactly match the headers in the request i.e. all header attributes belonging to the signing string must be lower case.
Sign the string with the Private keyCopied!
Now that we have the string to sign, we need to sign it with the private key associated with the public key that the server knows.
-
RSA Key Pair: The server uses the public key to verify the signature. The client uses the corresponding private key to sign the string.
-
Signing Algorithm: We use an algorithm (SHA-256) to sign the string.
-
RSA Signing: The RSA private key signs the string, creating a digital signature.
In C#, signing the string would look like this:
Clientside.cs
public static HttpSignature GenerateSignature(string method, string path, Dictionary<string, string> headers, SignatureAlgorithm algorithm, string privateKey)
{
var orderedHeaders = new SortedDictionary<string, string>();
foreach (var header in headers)
{
orderedHeaders.Add(header.Key, header.Value);
}
orderedHeaders.Add("request-target", $"{method.ToLower()} {path}");
var sb = new StringBuilder();
foreach (var header in orderedHeaders)
{
sb.Append($"{header.Key.ToString()?.ToLowerInvariant()}:{header.Value}\n");
}
using RSA rsa = RSA.Create();
rsa.ImportFromPem(privateKey.ToCharArray());
using var algo = HttpSignatureHelpers.GetHashAlgorithm(algorithm);
byte[] hash = algo.ComputeHash(Encoding.UTF8.GetBytes(sb.ToString()));
byte[] signature = rsa.SignHash(hash, HttpSignatureHelpers.GetHashAlgorithmName(algorithm), RSASignaturePadding.Pkcs1);
return new HttpSignature(algorithm, orderedHeaders.Keys.Cast<string>().Aggregate((x, y) => $"{x.ToLowerInvariant()} {y.ToLowerInvariant()}"), Convert.ToBase64String(signature));
}
In NodeJS, the string would look like this:
const REQUEST_TARGET = "post /auth/token";
const CONTENT_TYPE = "application/json";
const ACCEPT = "*/*";
const DATE = "Tue, 25 Feb 2025 15:25:00 GMT";
const DIGEST = "SHA-256=zc1CKvxXQT0ONwLoIi1LlFzBuJKnNCVRcTIgg0G2F2Y=";
const dataToBeSigned = [
`request-target: ${REQUEST_TARGET}`,
`date: ${DATE}`,
`content-type: ${CONTENT_TYPE}`,
`accept: ${ACCEPT}`,
`digest: ${DIGEST}`,
].join("\n");
const signature = crypto
.createSign("sha256")
.update(dataToBeSigned)
.sign(privateKey, "base64");
The output will be a base64-encoded signature; this is the digital signature for /auth/token:
YrpRebSLs5ynT8HNYCkB6LfsYZCOfKcjcKZUmpddR8kFlABO8HpAHS6vWG4l4YT9UF3yXfDMnykTF8XZ82OPV+vXQuu7ZAB2iOm14r/83uJRHxxuCBSPfY0s+uXcT9Lfl4lrux6Uqh16j6M9GWTp1NaMG62eHfcIJo3Vdd43GYjx9b383O+qZikJjFB5acL+GHqd453jmKKpQwyDjNLqOoKIdZRG44NK72iBjJMQPMiQSMBPtVkaBeL/0maMR+/rn+R/wCeSE/Xj1HHP8AmGoj6K4v086hdTxNBst0eOwUvar7vMZc0uUglTcMU+q16t2YYrZk3FDegsoDuLODR1MogAEB4/k9NpLfCAcQICc20KD1Q3e0WqnUbInNFo2g9OC8/H3DPSaTOcPJe9xEOHuChQE1F4wn2D5tg0ttfuqgdv3ngK0b3k8gRkGZ1bspVUA5KfrvmTVGX5tpT9LYu3f01flCvit4qaDtZCrAyXakWMRhltrcWKa5ehwQbzKJCHKje+15OWmSWFKy53pxl4+3IQRmJiG8qYQ/4D1ohQ1yVrvasknmtjL1M231YS1bQlVXnnFvdNuLDPM0ntigHDishwHEGKXJ8jf/BHUC0VnejJ9BJjhtrwlkwAgAjggkHfIa6C4KxvI2xU/p/cHAEbk1fi5wkxVNGk3IBmqjSUC0g=
The signing string should always be constructed as follows:
request-target: <lowercase method> <path>\ndate: <date>\ncontent-type: application/json\naccept: application/json\ndigest: <digest>
Construct the Authorization headerCopied!
Once we have the signature, the final Authorization header is constructed using the following format:
algorithm="rsa-sha256",headers="request-target date content-type accept digest",signature=<base64-encoded-signature>
|
Component |
Example |
Description |
|---|---|---|
|
algorithm |
rsa-sha256 |
Specify which algorithm was used when generating the signature |
|
headers |
request-target date content-type accept digest |
The list of headers contained in the signature: · Lowercase · Separated by a space · In the same order as they have in the signing string |
|
signature |
|
Signature calculated for Authorization in previous above step |