I’m currently setting up a test script in K6 and I’m trying to dynamically sign a json object and generating an HTTP request with multiple headers and so on.
Currently we have to send our JSON objects to an API endpoint and the response to that request is what we use in our subsequent request to the ACTUAL api endpoint that will consume the data. I’ve been given the c# code behind the signing endpoint and is below:
/// <summary>
/// Helper class to sign messages
/// </summary>
/// <returns></returns>
[HttpPost]
[Route("sign-message")]
public async Task<ActionResult<string>> SignMyMessage([FromBody] object messageToSign)
{
Logger.LogInformation($"message signing request received");
SigningResponse signedMessage = new SigningResponse();
// This variable will read the body of the message as raw text
string plainTextMessage = System.Text.Json.JsonSerializer.Serialize(messageToSign);
//string plainTextMessage = string.Empty;
try
{
//using (var reader = new StreamReader(Request.Body))
//{
// plainTextMessage = await reader.ReadToEndAsync();
//}
Logger.LogInformation($"Checking database for instance {runningInstance}");
//// Use the swim hooks database to get an active instance that will provide the private key
//// to sign the message with (obviously this will be a dummy value)
//using (var context = new swim_hooks_db_context.Models.SwimhooksDbContext(dbConnection))
//{
// var theInstance = context.Instances
// .Where(i => i.InstanceKey == runningInstance)
// .FirstOrDefault();
// if (theInstance != null && !string.IsNullOrEmpty(theInstance.CSSPrivateKey))
// {
// pretendPrivateKey = theInstance.PrivateKey;
// }
//}
Logger.LogInformation($"Sending sign request");
signedMessage = await SignAndValidate.SignMessage(plainTextMessage, pretendPrivateKey, CertCacheMinutes, keyVaultUri);
if (signedMessage.IsSuccessful)
{
Logger.LogInformation($"Message signed successfully");
RootObject obj = new RootObject(signedMessage.Message, signedMessage.Issuer, signedMessage.Version);
return Ok(JsonConvert.SerializeObject(obj));
}
else
{
var ex = signedMessage.LastException;
Logger.LogError(ex, "There was a failure while signing the incoming message");
string msg = $"There was an error - {ex.Message}:\r\n {ex.StackTrace}";
Logger.LogInformation(msg);
Logger.LogInformation(plainTextMessage);
return BadRequest(signedMessage.LastException.Message);
}
}
catch (Exception ex)
{
Logger.LogError(ex, "There was an error while signing the incoming message");
string msg = $"There was an error with key {pretendPrivateKey} - {ex.Message}:\r\n {ex.StackTrace}";
if (signedMessage != null && signedMessage.LastException != null)
{
msg += $"\r\n{signedMessage.LastException.Message}";
}
Logger.LogInformation(msg);
Logger.LogInformation(plainTextMessage);
return BadRequest(msg);
}
}
public static async Task<SigningResponse> SignMessage(string payload, string mpidPrivateKeyPfx, int cacheMinutes, Uri keyVaultUri)
{
SigningResponse encryptionResponse = new SigningResponse
{
IsSuccessful = false
};
try
{
var defaultCreds = new DefaultAzureCredential();
var cert = await GetCertificateAsync(new CertificateClient(keyVaultUri, defaultCreds),
mpidPrivateKeyPfx, cacheMinutes);
string certificateKeysIdentifier = cert.Name; // Azure KV certificate store automatically creates the keys and secrets with the same name as the cert.
var secret = await GetSecretAsync(new SecretClient(keyVaultUri, defaultCreds),
certificateKeysIdentifier, cacheMinutes);
X509Certificate2 x509 = new X509Certificate2(Convert.FromBase64String(secret.Value));
encryptionResponse.Issuer = x509.Issuer;
encryptionResponse.Version = x509.SerialNumber;
try
{
encryptionResponse.Version = BigInteger.Parse(encryptionResponse.Version, NumberStyles.HexNumber).ToString();
}
catch
{//support legacy certs
}
var header = new JwtHeader()
{
{JwtHeaderParameterNames.Cty, "jose+json" },
{JwtHeaderParameterNames.Typ, "jose+json" }
};
header[JwtHeaderParameterNames.Alg] = SignatureAlgorithm.ES256.ToString();
var encoded = Base64UrlEncoder.Encode(payload);
string msg = $"{header.Base64UrlEncode()}.{encoded}";
byte[] byteData = Encoding.UTF8.GetBytes(msg);
// Sign and Verify
var hasher = SHA256.Create();
var digest = hasher.ComputeHash(byteData);
var signature = await new KeyClient(keyVaultUri, defaultCreds)
.GetCryptographyClient(certificateKeysIdentifier)
.SignAsync(SignatureAlgorithm.ES256, digest)
.ConfigureAwait(false);
var sig = Base64UrlEncoder.Encode(signature.Signature);
string tokenString = $"{msg}.{sig}";
encryptionResponse.Message = tokenString;
encryptionResponse.IsSuccessful = true;
}
catch (Exception ex)
{
encryptionResponse.IsSuccessful = false;
encryptionResponse.LastException = ex;
}
return encryptionResponse;
}
Please see below my k6 buildRequest helper method i’ve currently got to try and recreate it
import encoding from 'k6/encoding'
// Function to gather headers
export async function buildRequest (dataRow, privateKey) {
const messageToSign = JSON.stringify(dataRow)
const signedRequestObject = {}
const kid =
'{"iss":"CN=Test Limited Intermediate II CA, OU=Test Limited Certificate Authority, O=Test Limited, S=England, C=GB","ser":"1029"}'
const header = { alg: 'ES256', cty: 'jose+json', typ: 'jose+json' }
// const encodedHeader = encoding.b64encode(JSON.stringify(header))
// Base64Url encode header and payload (no padding, URL safe)
const encodedHeader = encoding.b64encode(JSON.stringify(header), 'rawurl')
const encodedPayload = encoding.b64encode(messageToSign, 'rawurl')
// Concatenate header and payload to form the message
const msg = `${encodedHeader}.${encodedPayload}`
// const contentHash = await getContentHash(messageToSign)
// const base64ContentHash = encoding.b64encode(messageToSign)
// console.log(`Payload: ${base64ContentHash}`)
// console.log(`Protected: ${encodedHeader}`)
// Create the message by concatenating the encoded header and encoded payload
// const msg = `${encodedHeader}.${base64ContentHash}`
// Convert the message to a UTF-8 array for signing
const msgBytes = await toUTF8Array(msg)
const digest = await crypto.subtle.digest('SHA-256', msgBytes)
const sig = new Uint8Array(msgBytes)
// Sign the message using the private key (ES256 algorithm)
const signatureBuffer = await crypto.subtle.sign(
{
name: 'ECDSA',
hash: 'SHA-256'
},
privateKey,
sig
)
// const signatureBuffer = await crypto.subtle.sign(
// {
// name: 'ECDSA',
// hash: 'SHA-256'
// },
// privateKey,
// sig
// )
const base64Signature = encoding.b64encode(signatureBuffer) // Base64Url encode the signature (no padding)
const tokenString = `${msg}.${base64Signature}`
// const signatureString = `POST;${url.toLowerCase()};${timestamp};${base64ContentHash}`
// const signatureBytes = await toUTF8Array(signatureString)
// const sig = new Uint8Array(signatureBytes)
// const signatureBuffer = await crypto.subtle.sign('ECDSA', privateKey, sig)
// const base64Signature = encoding.b64encode(signatureBuffer)
;(signedRequestObject['payload'] = msg),
(signedRequestObject['protected'] = encodedHeader),
(signedRequestObject['header'] = { kid: kid }), // Define header here properly
(signedRequestObject['signature'] = base64Signature)
console.log(JSON.stringify(signedRequestObject))
return signedRequestObject
}
// 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
// Encode JSON to a Uint8Array
const data = await stringToArrayBuffer(body)
// Compute the SHA-256 hash
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
return hashBuffer
}
// Function to import a PEM-formatted private key
export async function importPrivateKey (pemPrivateKey) {
// Strip the PEM header and footer
const pemContents = pemPrivateKey
.replace(/-----BEGIN PRIVATE KEY-----/, '')
.replace(/-----END PRIVATE KEY-----/, '')
.replace(/\s+/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
{ name: 'ECDSA', namedCurve: 'P-256' },
false, // Key is not extractable
['sign'] // The key will be used for signing
)
return privateKey
}
async function stringToArrayBuffer (s) {
return Uint8Array.from(new String(s), x => x.charCodeAt(0))
}
async 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
}
In my main script I’m doing this with the certs
const cert_mtls = open(`${baseLocation}certs/certificate_mtls.pem`) // Read certificate file
const key_mtls = open(`${baseLocation}certs/privatekey_mtls_nopass.pem`) // Read private key file
const cert_signing = open(`${baseLocation}certs/certificate_signing.pem`) // Read certificate file
const key_signing = open(`${baseLocation}certs/privatekey_signing.pem`) // Read private key file
// Precompute static values to avoid redundant processing inside loops
const base64EncodedCert = encoding.b64encode(cert_signing)
const privateKey = await importPrivateKey(key_signing)
and then this to call the buildRequest method from the helpers.js file
let dataRow = sharedData[iterationIndex % sharedData.length] // Ensure it wraps around safely if VUs exceed userData length
let url = `${config.endpoint}`
const requestBody = await buildRequest(dataRow, privateKey)```
I've had success on this forum before when it came to message signing (different method) and hoping I can have more this time around.
Thanks
2 posts - 2 participants