Message Signing in K6 to match Postman Pre-Request script

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 :frowning:

Any help would be hugely appreciated

Thanks

hi @chrisellison,

I see that you have a bunch of console.log messages. It would be very helpful in trying to debug this if we had the output for those, preferably from both invocations.

My expecation is that you need to use javascript - How to convert UTF8 string to byte array? - Stack Overflow to make the string to utf8bytes before base64 encoding. All js strings are utf16 by specification, so I would expect this is the problem you are currently hitting.

thanks so much for the response @mstoykov

Please see below the message signing code in isolation and each resulting output.

K6 code

for (const interfaceType of interfaceTypes) {
    const url = `${baseUrl}${subPath}${mpidRole.toLowerCase()}/${interfaceType}`
    console.log(`URL: ${url}`)
    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])

      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(`ContentHash (Base64): ${base64ContentHash}`)
      const base64EncodedCert = encoding.b64encode(pemCert)
      console.log(`Certificate (Base64): ${base64EncodedCert}`)
      console.log(`Signature String: ${signatureString}`)
      const signatureBytes = toUTF8Array(signatureString)
      console.log(`SignatureBytes: ${signatureBytes}`)

      // Import the private key
      const privateKey = await importPrivateKey(pemPrivateKey)
      const signatureBuffer = await crypto.subtle.sign(
        'RSASSA-PKCS1-v1_5',
        privateKey,
        signatureHash
      )
      const base64Signature = encoding.b64encode(signatureBuffer)
      console.log(`Signature (Base64): ${base64Signature}`)

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 toUTF8Array(str) {
    var utf8 = [];
    for (var i=0; i < str.length; i++) {
        var charcode = str.charCodeAt(i);
        if (charcode < 0x80) utf8.push(charcode);
        else if (charcode < 0x800) {
            utf8.push(0xc0 | (charcode >> 6), 
                      0x80 | (charcode & 0x3f));
        }
        else if (charcode < 0xd800 || charcode >= 0xe000) {
            utf8.push(0xe0 | (charcode >> 12), 
                      0x80 | ((charcode>>6) & 0x3f), 
                      0x80 | (charcode & 0x3f));
        }
        // surrogate pair
        else {
            i++;
            // UTF-16 encodes 0x10000-0x10FFFF by
            // subtracting 0x10000 and splitting the
            // 20 bits of 0x0-0xFFFFF into two halves
            charcode = 0x10000 + (((charcode & 0x3ff)<<10)
                      | (str.charCodeAt(i) & 0x3ff));
            utf8.push(0xf0 | (charcode >>18), 
                      0x80 | ((charcode>>12) & 0x3f), 
                      0x80 | ((charcode>>6) & 0x3f), 
                      0x80 | (charcode & 0x3f));
        }
    }
    return utf8;
}

K6 console log output

INFO[0000] URL: https://somesite/process/tst3p/IF-003  source=console
INFO[0000] ContentHash (Base64): GMRCM+HFXS/ltaxpk6uqaKDsgXqlpkXQbvG1m363uWw=  source=console
INFO[0000] Certificate (Base64): LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZZVENDQTBtZ0F3SUJBZ0lDRUJzd0RRWUpLb1pJaHZjTkFRRUxCUUF3Z1kweEN6QUpCZ05WQkFZVEFrZEMKTVJBd0RnWURWUVFJREFkRmJtZHNZVzVrTVJVd0V3WURWUVFLREF4VVpYTjBJRXhwYldsMFpXUXhLekFwQmdOVgpCQXNNSWxSbGMzUWdUR2x0YVhSbFpDQkRaWEowYVdacFkyRjBaU0JCZFhSb2IzSnBkSGt4S0RBbUJnTlZCQU1NCkgxUmxjM1FnVEdsdGFYUmxaQ0JKYm5SbGNtMWxaR2xoZEdVZ1NVa2dRMEV3SGhjTk1qTXdOekEyTVRVME9EQTIKV2hjTk1qWXdOekExTVRVME9EQTJXakI3TVFzd0NRWURWUVFHRXdKSFFqRWRNQnNHQTFVRUNnd1VUVWhJVXlCQgpaR0Z3ZEdWeUlGTmxjblpwWTJVeEh6QWRCZ05WQkFzTUZrTW1ReUJIY205MWNDQkliMnhrYVc1bmN5Qk1kR1F4CkxEQXFCZ05WQkFNTUkzQnBkQzV6ZDJsdExYTnZZV3N1WTJGdVpHTjBaV05vYm05c2IyZDVMbU52TG5Wck1JSUIKSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQTF6VXcrckxRWXBFQ2FkRzM2WFpybmRwMApNbzdKREZvS1gyZUlER0lyR29sSjhvdkJVSXZaQ29za2JUbGdtMDJFNVJKcEpXUm12a0FDQUtURzA5OENncjlrCm9QUGlOdzVGU0VrdVIvZDd1YkpLdDVlaC9YallTU2JJU2FHbmg0UTdUTlFxY0xwSUJiRFAvWmZrUjRyQXhYczAKbGowdXBkUEdlQm5tbGRHL3FuZ1EwTlc1eDRGWC9rbXZPYWhYSnBLZ1FISUZxZGtWdXI0MWNOcTZWMjRKam1mSQorN2ZGOW4vTjB5ZlRsWWdsMlA3WEhINlZxRUpNT2pPK2lHRi8yK3AvYXBHY3VSSm8zTDJIV2w4MFQ5U0N4NFhrCk9Cc2FObmJWY3NTSzI3NWtDMzJoWkZGRUZ0c0dQd0FpNVFDSFFOWGJ5MVRBUzdreUpObXQ2MFJTbEdHWURRSUQKQVFBQm80SGJNSUhZTUI4R0ExVWRJd1FZTUJhQUZBMUl5MkpTTTI4d1VGRDA0Yk5tc0NBclJCS1dNQXNHQTFVZApEd1FFQXdJRHFEQWRCZ05WSFNVRUZqQVVCZ2dyQmdFRkJRY0RBUVlJS3dZQkJRVUhBd0l3V1FZRFZSMGZCRkl3ClVEQk9vRXlnU29aSWFIUjBjRG92TDJGNmNIQXRjSEp2YW1WamRDMTBaWE4wTFdOeWJDNWhlblZ5WlhkbFluTnAKZEdWekxtNWxkQzl3YTJrdmNHeGhkR2x1ZFcwdlkzSnNMMlZsZGpJdVkzSnNNQzRHQTFVZEVRUW5NQ1dDSTNCcApkQzV6ZDJsdExYTnZZV3N1WTJGdVpHTjBaV05vYm05c2IyZDVMbU52TG5Wck1BMEdDU3FHU0liM0RRRUJDd1VBCkE0SUNBUUI3VkdJNlFGdGlpekducEt3cVVGT3JER09ydnFJK0hTeXQvOVExNXY3aWtNeHg2cFh6NnZ3VU1sTlcKZWp6ZXBSRDVPU0plMVdhcnE5RFM3TWJ5L2crY1ErSDBsbWdEenk5L1VrRDAzZjVpUzBuMnVSVTZZWHl6MXp5bQoyOHVMK1R4bm1MUlhuSnZ6cGtBQTBsQWZEaUc2NGFOaVRaRVkzUU9WNjJMcHdFYW5wSHJiOFJ2SjA5UmE1ajJpCmRsZkF2aE1xUUNTb1RpZWxGUzl2Tk81TTV4S2NsUGliY1NJRnlvVUpzdnV6N202eWJ6WDVKNEt5ZjhkY0VubmEKTmRIZ0FneDdNT1FwR3ZhN1JYeVBYemF6OXRqN25GblJFWTNRZG8wb3hJYXhkOTNLZkVlUThoV0dwWnZ3cEZ5MApoT01XZVlia1RoWkt5ZGlQNFJYT1dqL2xZWjdLZFdBMTZxV2lSM291eGtQV1ZoV2o1Nnc1ZWpnTHM5UlJpQk4xCmlvN2dndk1jNk5VcytLcUNpMUViWnpxdEt1RmwyRHB4TFVyY2V4Z2Ftc2N6cUpiN3JQeEM2MW5ob083WnBTOC8KbHhLMTlxK3h2S3ltSUV5RzFwMGprWWRyMDJKNTdnNi9odk1iaDFmVE5OZzZDbjZGbnpLMXkrSDVIM2kvcWdWcwptSkEwcnhaQ1AybzdmMnFKVm1wLzJLUFI3aklmeE1mU1E4VDBGZVJIL3RHWDlSTzBXSnMvb1FWNXpQWkV0bjRCClcrcUtZRWRnVHNSQjlNQ0t1Zi9JbUFDZ2RLYWRLZWttQTZQVG5BRUYxTXp6bzNLdlVYUDkrQ2VlRHdpMjlrM1cKNDNCYjJ5Lzd2NEF2ZThkMGREYUJDMUJmb0x3K24xSU54SjN2L01MQlF5NkxSQU5uckE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t  source=console
INFO[0000] Signature String: POST;https://somesite/process/tst3p/if-003;2025-01-21T15:58:05.614Z;GMRCM+HFXS/ltaxpk6uqaKDsgXqlpkXQbvG1m363uWw=  source=console
INFO[0000] SignatureBytes: 80,79,83,84,59,104,116,116,112,115,58,47,47,112,105,116,46,115,119,105,109,45,115,111,97,107,46,99,97,110,100,99,116,101,99,104,110,111,108,111,103,121,46,99,111,46,117,107,47,112,114,111,99,101,115,115,47,116,115,116,51,112,47,105,102,45,48,48,51,59,50,48,50,53,45,48,49,45,50,49,84,49,53,58,53,56,58,48,53,46,54,49,52,90,59,71,77,82,67,77,43,72,70,88,83,47,108,116,97,120,112,107,54,117,113,97,75,68,115,103,88,113,108,112,107,88,81,98,118,71,49,109,51,54,51,117,87,119,61  source=console
INFO[0000] Signature (Base64): FzGtItUI7NbRNRFRQK7eHMCQfN9NUSpqcQJPiHYy0Afe4MvigkSe0xl1uY+6nWUB9b8UDfaprvcRkBp1TKL+zEfFVTZqE0+mvMqSwcR+uUz3lZNzGNze8MmkR/LQMqSoSmm2wiUTlkkZ5iAjPUhR+LjZu3pCC0RtIp+f0yfVss4eP1AkLmdAJtA1N+VPhrOxZ6IfL6Q5k3nY5rePPlDz34OoaZr+OoVFrtpwxj0xIJGkoAf1pZJDKfgEsooLhuUCBzY+aXu9ovwPk3U5RQ/zfCbqAxBebvJ8LGafLYSDJNFGnJBmZdfrFn84PvMUPpDCOl72DMWG/cFN9J84TgKc9w==  source=console

Postman code

const timestamp = new Date().toISOString();
    const url = xDestination;
    console.log(`URL: ${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 DIP SIM 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 C&C SITA 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 C&C PIT 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');
    }

    // 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);
    console.log(`ContentHash (Base64): ${base64Hash}`)

    // base64Encode certificate
    const base64Certificate = forgeLib.util.encode64(pemCert);
    console.log(`Certificate (Base64): ${base64Certificate}`)

    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);
    const signatureBytes2 = forgeLib.util.binary.raw.decode(forgeLib.util.encodeUtf8(signature));
    console.log("SignatureBytes how it looks in Postman: " + signatureBytes) 
    console.log("SignatureBytes like in K6: " + signatureBytes2)

    // Add header with certificate minus private key
    pm.request.headers.add({ key: 'X-DIP-Signature-Certificate', value: base64Certificate });

    const privateKey = forgeLib.pki.privateKeyFromPem(pemPrivateKey);
    console.log(`PrivateKey: ${privateKey}`)

    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();
    // Sign the message digest
    const sign = privateKey.sign(mdx);
    const signBase64 = forgeLib.util.encode64(sign);
    console.log(`Signature (Base64): ${signBase64}`)

Postman console log output

URL: https://somesite/process/tst3p/IF-003
 
ContentHash (Base64): GMRCM+HFXS/ltaxpk6uqaKDsgXqlpkXQbvG1m363uWw=
 
Certificate (Base64): LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZZVENDQTBtZ0F3SUJBZ0lDRUJzd0RRWUpLb1pJaHZjTkFRRUxCUUF3Z1kweEN6QUpCZ05WQkFZVEFrZEMKTVJBd0RnWURWUVFJREFkRmJtZHNZVzVrTVJVd0V3WURWUVFLREF4VVpYTjBJRXhwYldsMFpXUXhLekFwQmdOVgpCQXNNSWxSbGMzUWdUR2x0YVhSbFpDQkRaWEowYVdacFkyRjBaU0JCZFhSb2IzSnBkSGt4S0RBbUJnTlZCQU1NCkgxUmxjM1FnVEdsdGFYUmxaQ0JKYm5SbGNtMWxaR2xoZEdVZ1NVa2dRMEV3SGhjTk1qTXdOekEyTVRVME9EQTIKV2hjTk1qWXdOekExTVRVME9EQTJXakI3TVFzd0NRWURWUVFHRXdKSFFqRWRNQnNHQTFVRUNnd1VUVWhJVXlCQgpaR0Z3ZEdWeUlGTmxjblpwWTJVeEh6QWRCZ05WQkFzTUZrTW1ReUJIY205MWNDQkliMnhrYVc1bmN5Qk1kR1F4CkxEQXFCZ05WQkFNTUkzQnBkQzV6ZDJsdExYTnZZV3N1WTJGdVpHTjBaV05vYm05c2IyZDVMbU52TG5Wck1JSUIKSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQTF6VXcrckxRWXBFQ2FkRzM2WFpybmRwMApNbzdKREZvS1gyZUlER0lyR29sSjhvdkJVSXZaQ29za2JUbGdtMDJFNVJKcEpXUm12a0FDQUtURzA5OENncjlrCm9QUGlOdzVGU0VrdVIvZDd1YkpLdDVlaC9YallTU2JJU2FHbmg0UTdUTlFxY0xwSUJiRFAvWmZrUjRyQXhYczAKbGowdXBkUEdlQm5tbGRHL3FuZ1EwTlc1eDRGWC9rbXZPYWhYSnBLZ1FISUZxZGtWdXI0MWNOcTZWMjRKam1mSQorN2ZGOW4vTjB5ZlRsWWdsMlA3WEhINlZxRUpNT2pPK2lHRi8yK3AvYXBHY3VSSm8zTDJIV2w4MFQ5U0N4NFhrCk9Cc2FObmJWY3NTSzI3NWtDMzJoWkZGRUZ0c0dQd0FpNVFDSFFOWGJ5MVRBUzdreUpObXQ2MFJTbEdHWURRSUQKQVFBQm80SGJNSUhZTUI4R0ExVWRJd1FZTUJhQUZBMUl5MkpTTTI4d1VGRDA0Yk5tc0NBclJCS1dNQXNHQTFVZApEd1FFQXdJRHFEQWRCZ05WSFNVRUZqQVVCZ2dyQmdFRkJRY0RBUVlJS3dZQkJRVUhBd0l3V1FZRFZSMGZCRkl3ClVEQk9vRXlnU29aSWFIUjBjRG92TDJGNmNIQXRjSEp2YW1WamRDMTBaWE4wTFdOeWJDNWhlblZ5WlhkbFluTnAKZEdWekxtNWxkQzl3YTJrdmNHeGhkR2x1ZFcwdlkzSnNMMlZsZGpJdVkzSnNNQzRHQTFVZEVRUW5NQ1dDSTNCcApkQzV6ZDJsdExYTnZZV3N1WTJGdVpHTjBaV05vYm05c2IyZDVMbU52TG5Wck1BMEdDU3FHU0liM0RRRUJDd1VBCkE0SUNBUUI3VkdJNlFGdGlpekducEt3cVVGT3JER09ydnFJK0hTeXQvOVExNXY3aWtNeHg2cFh6NnZ3VU1sTlcKZWp6ZXBSRDVPU0plMVdhcnE5RFM3TWJ5L2crY1ErSDBsbWdEenk5L1VrRDAzZjVpUzBuMnVSVTZZWHl6MXp5bQoyOHVMK1R4bm1MUlhuSnZ6cGtBQTBsQWZEaUc2NGFOaVRaRVkzUU9WNjJMcHdFYW5wSHJiOFJ2SjA5UmE1ajJpCmRsZkF2aE1xUUNTb1RpZWxGUzl2Tk81TTV4S2NsUGliY1NJRnlvVUpzdnV6N202eWJ6WDVKNEt5ZjhkY0VubmEKTmRIZ0FneDdNT1FwR3ZhN1JYeVBYemF6OXRqN25GblJFWTNRZG8wb3hJYXhkOTNLZkVlUThoV0dwWnZ3cEZ5MApoT01XZVlia1RoWkt5ZGlQNFJYT1dqL2xZWjdLZFdBMTZxV2lSM291eGtQV1ZoV2o1Nnc1ZWpnTHM5UlJpQk4xCmlvN2dndk1jNk5VcytLcUNpMUViWnpxdEt1RmwyRHB4TFVyY2V4Z2Ftc2N6cUpiN3JQeEM2MW5ob083WnBTOC8KbHhLMTlxK3h2S3ltSUV5RzFwMGprWWRyMDJKNTdnNi9odk1iaDFmVE5OZzZDbjZGbnpLMXkrSDVIM2kvcWdWcwptSkEwcnhaQ1AybzdmMnFKVm1wLzJLUFI3aklmeE1mU1E4VDBGZVJIL3RHWDlSTzBXSnMvb1FWNXpQWkV0bjRCClcrcUtZRWRnVHNSQjlNQ0t1Zi9JbUFDZ2RLYWRLZWttQTZQVG5BRUYxTXp6bzNLdlVYUDkrQ2VlRHdpMjlrM1cKNDNCYjJ5Lzd2NEF2ZThkMGREYUJDMUJmb0x3K24xSU54SjN2L01MQlF5NkxSQU5uckE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t
 
Signature String: POST;https://somesite/process/tst3p/if-003;2025-01-21T16:00:29.859Z;GMRCM+HFXS/ltaxpk6uqaKDsgXqlpkXQbvG1m363uWw=
 
SignatureUTF8EncodedString how it looks in Postman: POST;https://somesite/process/tst3p/if-003;2025-01-21T16:00:29.859Z;GMRCM+HFXS/ltaxpk6uqaKDsgXqlpkXQbvG1m363uWw=
 
SignatureBytes like in K6: 80,79,83,84,59,104,116,116,112,115,58,47,47,112,105,116,46,115,119,105,109,45,115,111,97,107,46,99,97,110,100,99,116,101,99,104,110,111,108,111,103,121,46,99,111,46,117,107,47,112,114,111,99,101,115,115,47,116,115,116,51,112,47,105,102,45,48,48,51,59,50,48,50,53,45,48,49,45,50,49,84,49,54,58,48,48,58,50,57,46,56,53,57,90,59,71,77,82,67,77,43,72,70,88,83,47,108,116,97,120,112,107,54,117,113,97,75,68,115,103,88,113,108,112,107,88,81,98,118,71,49,109,51,54,51,117,87,119,61
 
Signature (Base64): D9Q1nscWGkl0HhPohuqaXYXGrKcD952q1ouWyXgrvPHer0DOtbBI2IXfO7/j9++564hhyXSB8idPr9u4Di/aLMBgsD5oyBF+aOyO0KsPg44PxSFENwAmTz4pyYVNSd6q7rRKXZ63E4x8rdae/TaO18RIotO/d6Tur0kR4xaf88+UkMWlZ2j/AI8+HEbiNAd74azCL0VqzYcju/sj3sovUQlAshwMWGVE5Y+IVoNJgTUg7RK7xJVbAVvCuUqJjLvbhodW5iDr438aTO2Gef8GtH6KvgwdvO4fxqk/uJJdq2pio8wRBuu9asi7vhJifo0mMDcxDjVRuF+kXq77qmVUcA==

In the latest code you are not using SignatureBytes in the k6 code but SignatureHash when signing - I also do not know that tha hash is as that method is not available but I expect this is the issue.

@mstoykov

This is the method for getSigStringHash

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
}

When signing using the signatureBytes instead of signatureHash I get the following error in console:

ERRO[0000] Uncaught (in promise) OperationError: data is neither an ArrayBuffer, nor a TypedArray nor DataView executor=shared-iterations scenario=default

You just need to do new Uint8Array(signatureBytes) IMO instead of getting the hash

1 Like

Amazing, thank you so much