I have a postman collection and a pre-request script to POST requests to an API endpoint whilst dynamically adding signature headers to the outbound request.
We also have a console app which generates a json or a csv file containing pre-signed requests for our current performance test tool: NeoLoad to consume and process.
I do not like our current performance test tool as I do not find it user friendly and is too flakey when it comes to reconciling requests made with what appears in our Api logs.
I’ve been looking at K6 and have a working solution which reads from the json file I mentioned earlier however quite quickly runs out of memory when using sharedArray to store the data. We cannot process the same request twice as we have duplication checks.
I have been attempting to get K6 to read several small json array template files into memory, then I’d sign the contents and subsequently POST the request with all required headers and request body to an endpoint.
Please see below the Postman pre-request script code I am trying to recreate in K6
const addSignatureHeaders = (forgeLib) => {
const timestamp = new Date().toISOString();
const url = xDestination;
console.log(url)
pm.request.headers.add({ key: 'X-DIP-Signature-Date', value: timestamp });
let pemCert, pemPrivateKey;
if (url.includes("dipsim")) {
console.log("Going to use the DS signing cert and key.");
// Your certificate excluding private key in PEM format
pemCert = pm.collectionVariables.get('SigningClientCertPEMdipsim83');
// Private key for certificate
pemPrivateKey = pm.collectionVariables.get('SigningClientPrivateKeyPEMdipsim83');
} else if (url.includes("sita")){
console.log("Going to use the internal SA signing cert and key.");
// Your certificate excluding private key in PEM format
pemCert = pm.collectionVariables.get('SigningClientCertPEMsita');
// Private key for certificate
pemPrivateKey = pm.collectionVariables.get('SigningClientPrivateKeyPEMsita');
} else {
console.log("Going to use the internal P signing cert and key.");
// Your certificate excluding private key in PEM format
pemCert = pm.collectionVariables.get('SigningClientCertPEM');
// Private key for certificate
pemPrivateKey = pm.collectionVariables.get('SigningClientPrivateKeyPEM');
}
// const messageBody = (pm.request && pm.request.body && pm.request.body.raw) ? pm.request.body.raw : '{}';
// Compute SHA-256 hash of body
const md = forgeLib.md.sha256.create();
const messageBody = (pm.request && pm.request.body && pm.request.body.raw) ? pm.request.body.raw : '{}';
md.update(messageBody);
// Get a 256 hash of the body. This will output in lowercase hex.
const hashBytes = md.digest().getBytes();
// base64Encode hash
const base64Hash = forgeLib.util.encode64(hashBytes);
pm.request.headers.add({ key: 'X-DIP-Content-Hash', value: base64Hash });
const signature = pm.request.method.toUpperCase() + ";" + url.toLowerCase() + ';' + timestamp + ';' + base64Hash;
console.log("Signature String: " + signature)
const signatureBytes = forgeLib.util.encodeUtf8(signature);
// base64Encode certificate
const base64Certificate = forgeLib.util.encode64(pemCert);
// Add header with certificate minus private key
pm.request.headers.add({ key: 'X-DIP-Signature-Certificate', value: base64Certificate });
const privateKey = forgeLib.pki.privateKeyFromPem(pemPrivateKey);
const mdx = forgeLib.md.sha256.create();
// Set the message for the digest
mdx.update(signatureBytes);
// Get the digest value from the mdx object
const mdxDigestValue = mdx.digest().toHex();
console.log(`SigBytes: ${mdxDigestValue}`)
// Sign the message digest
const sign = privateKey.sign(mdx);
const signBase64 = forgeLib.util.encode64(sign);
pm.request.headers.add({ key: "X-DIP-Signature", value: signBase64 });
// console.log("Request headers are: " + pm.request.headers)
}
window = {};
const hasSigningVariables = pm.collectionVariables.has("SigningClientCertPEMdipsim") && pm.collectionVariables.has("SigningClientPrivateKeyPEMdipsim") && pm.collectionVariables.has("SigningClientCertPEM") && pm.collectionVariables.has("SigningClientPrivateKeyPEM");
console.log(hasSigningVariables)
if (hasSigningVariables) {
if (!pm.collectionVariables.get("forge_library")) {
pm.sendRequest("https://cdnjs.cloudflare.com/ajax/libs/forge/1.3.1/forge.min.js", (err, res) => {
// Convert the response to text and save it as a collection variable
pm.collectionVariables.set("forge_library", res.text());
// eval will evaluate the JavaScript code and initialize forge library.
eval(pm.collectionVariables.get("forge_library"));
addSignatureHeaders(window.forge);
});
} else {
// eval will evaluate the JavaScript code and initialize the forge library.
eval(pm.collectionVariables.get("forge_library"));
addSignatureHeaders(window.forge);
}
} else {
console.log("variables 'SigningClientCertPEMdipsim83', 'SigningClientCertPEM', 'SigningClientPrivateKeyPEMdipsim83' and 'SigningClientPrivateKeyPEM' must be set on the environment to enable signing");
}
Please see below the K6 script I have so far. Everything BUT the signature matches with Postman, so the response I get from our API is that the message signature is invalid.
import http from 'k6/http'
import { check, sleep } from 'k6'
import { Counter } from 'k6/metrics'
import encoding from 'k6/encoding'
// import { SharedArray } from 'k6/data';
// import { scenario } from 'k6/execution';
import { crypto } from 'k6/experimental/webcrypto'
// Load configuration from config.json
const config = JSON.parse(open('/config/config.json', 'r'))
if (!config) {
throw new Error(`Failed to open file: config file`)
}
const messages = JSON.parse(open('/data/IF-003.json'))
const errorCount = new Counter('errors')
// This is the test configuration
export let options = {
vus: 1, // 100 Virtual Users
iterations: 1, // 1000 iterations in total
tlsAuth: [
{
cert: `-----BEGIN CERTIFICATE-----
some string
-----END CERTIFICATE-----`,
key: `-----BEGIN PRIVATE KEY-----
some string
-----END PRIVATE KEY-----`
}
]
}
// Extract MPIDs with `include` set to true and their corresponding names
const includedMPIDs = config.mpidDetails
.filter(item => item.include === true)
.map(item => item.name)
// Main test function
export default async function () {
const baseUrl = config.endpoint
const subPath = config.subPathProcess
const mpidRole = includedMPIDs[0]
const matchingObject = config.mpidDetails.find(item => item.name === mpidRole)
const interfaceTypes = matchingObject.includedIfTypes
if (!interfaceTypes.length) {
console.error(`No interface types configured for MPID: ${mpidRole}`)
return
}
// Determine certificate and key
const pemCert = `-----BEGIN CERTIFICATE-----
some string
-----END CERTIFICATE-----`
const pemPrivateKey = `-----BEGIN PRIVATE KEY-----
some string
-----END PRIVATE KEY-----`
// const privateKey = await importPrivateKey(pemPrivateKey)
// console.log(`Private key: ${privateKey}`)
for (const interfaceType of interfaceTypes) {
const url = `${baseUrl}${subPath}${interfaceType}`
for (const originalMessage of messages) {
// Deep copy the original message
const message = JSON.parse(JSON.stringify(originalMessage))
// message.interfaceType = interfaceType
// const interfaceID = message.payload.CommonBlock.S0.interfaceID
const messageBody = JSON.stringify([message])
// console.log(`Message: ${[messageBody]}`)
const contentHash = await getContentHash(messageBody)
const base64ContentHash = encoding.b64encode(contentHash)
const timestamp = new Date().toISOString()
const signatureString = `POST;${url.toLowerCase()};${timestamp};${base64ContentHash}`
const signatureHash = await getSigStringHash(signatureString)
// console.log(`Timestamp: ${timestamp}`)
console.log(`ContentHash (Base64): ${base64ContentHash}`)
// console.log(`SignatureHash: ${signatureHash}`)
// Import the private key
const privateKey = await importPrivateKey(pemPrivateKey)
// console.log(`PrivateKey: ${privateKey}`)
const signatureBuffer = await crypto.subtle.sign(
'RSASSA-PKCS1-v1_5',
privateKey,
signatureHash
)
const base64Signature = encoding.b64encode(signatureBuffer)
console.log(`Signature (Base64): ${base64Signature}`)
// Base64 encode the PEM certificate
const base64EncodedCert = encoding.b64encode(pemCert)
// const base64EncodedCert = encoding.b64encode(pemCert)
// console.log(`Certificate (Base64): ${base64EncodedCert}`)
const headers = {
'Content-Type': 'application/json',
'X-DIP-Signature': base64Signature,
'X-DIP-Content-Hash': base64ContentHash,
'X-DIP-Signature-Date': timestamp,
'X-DIP-Signature-Certificate': base64EncodedCert
}
// console.log(`MessageBodyPreSign: ${messageBody}`)
const res = http.post(url, messageBody, { headers })
// console.log(`MessageBodyPostSign: ${res.request.body}`)
let responseBody
try {
responseBody =
res.body.trim() !== '' ? res.json() : 'Empty response body'
} catch (e) {
responseBody = res.body
}
console.log(
JSON.stringify({
// mpid: mpid,
status: res.status,
response: responseBody
})
)
const successStatusCode = check(res, {
'status is 201': r => r.status === 201
})
const successBodyParse = check(res, {
'response body can be parsed to JSON': r => {
try {
if (r.body.trim() !== '') {
JSON.parse(r.body)
return true
}
} catch (e) {
console.error(`Failed to parse response body: ${e.message}`)
}
return false
}
})
if (!successStatusCode || !successBodyParse) {
errorCount.add(1)
}
}
}
}
// Function to compute the SHA-256 hash of JSON content
async function getContentHash (content) {
// Default to '{}' if content is empty
const body = !content || content.trim() === '' ? '{}' : content
// Parse and re-stringify to ensure valid and consistent JSON
// const jsonContent = JSON.stringify(JSON.parse(body))
// Encode JSON to a Uint8Array
const data = stringToArrayBuffer(body)
// Compute the SHA-256 hash
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
return hashBuffer
}
// Function to compute the SHA-256 hash of a signature string
async function getSigStringHash (content) {
// Default to '{}' if content is empty
const body = !content || content.trim() === '' ? '{}' : content
// console.log(`SigStringToHash: ${body}`)
// Encode JSON to a Uint8Array
const data = stringToArrayBuffer(body)
// Compute the SHA-256 hash
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
console.log(arrayBufferToHex(hashBuffer)) // Logs the hash in hexadecimal format
return hashBuffer
}
// Function to import a PEM-formatted private key
async function importPrivateKey (pemPrivateKey) {
// Strip the PEM header and footer
const pemContents = pemPrivateKey
.replace(/-----BEGIN PRIVATE KEY-----/, '')
.replace(/-----END PRIVATE KEY-----/, '')
.replace(/\n/g, '')
const keyBuffer = encoding.b64decode(pemContents)
// Import the private key into the web crypto API
const privateKey = await crypto.subtle.importKey(
'pkcs8', // Import format
keyBuffer, // Key data
{
// Algorithm specifications
name: 'RSASSA-PKCS1-v1_5',
hash: { name: 'SHA-256' }
},
false, // Key is not extractable
['sign'] // The key will be used for signing
)
return privateKey
}
function stringToArrayBuffer (s) {
return Uint8Array.from(new String(s), x => x.charCodeAt(0))
}
function arrayBufferToHex (buffer) {
return [...new Uint8Array(buffer)]
.map(x => x.toString(16).padStart(2, '0'))
.join('')
}
This is a bit of a deal breaker for us if we are unable to implement this functionality. I’m sure it is highly likely I’m doing it wrong but I cannot work it out
Any help would be hugely appreciated
Thanks