Saltar al contenido

Generar una firma HMAC-SHA256 en Jitterbit Studio

Introducción

Muchas APIs autentican una solicitud requiriendo un hash con clave del payload de la solicitud. El llamador combina el payload con un secreto compartido utilizando la construcción HMAC (código de autenticación de mensaje basado en hash) y envía la firma resultante en un encabezado de solicitud. El sistema receptor recomputa la firma a partir del payload que recibió y acepta la solicitud solo si los dos valores coinciden. Esto confirma tanto que el payload no fue alterado en tránsito como que el llamador posee el secreto. Los webhooks entrantes utilizan el mismo mecanismo en la dirección opuesta: el proveedor firma el payload que te envía, y tu endpoint recomputa la firma para confirmar que la solicitud es genuina.

Esta guía cubre cómo calcular una firma HMAC-SHA256 en un proyecto de Studio utilizando un script JavaScript, y cómo usar la firma resultante en una solicitud saliente o para validar una entrante.

La firma se calcula en JavaScript en lugar de en un Script de Jitterbit. Un HMAC no es un hash del secreto y el payload unidos: la construcción primero deriva dos claves del secreto volteando bits dentro de cada uno de sus bytes, luego ejecuta SHA-256 dos veces, una con cada clave derivada. Realizar ese cálculo requiere los operadores bit a bit ^, &, << y >>>, que JavaScript proporciona. La función SHA256 realiza un hash simple, que es un cálculo separado: hacer hash del secreto y el payload como una única cadena concatenada produce un valor diferente al de la firma HMAC que el sistema receptor espera.

El script produce una cadena hexadecimal en minúsculas de 64 caracteres que coincide con el valor producido por cualquier implementación estándar de HMAC-SHA256 para el mismo payload y secreto, por lo que una firma generada de esta manera es intercambiable con una generada por una herramienta o biblioteca externa.

Utiliza este patrón donde un proyecto de otro modo llamaría a los plugins deprecados HMAC-SHA256 Generator o HMAC-SHA1 Generator a través de RunPlugin. Para tareas de hash que no involucran un secreto, como detección de cambios o creación de huellas dactilares de registros, utiliza la función SHA256 directamente, como se describe en Detectar y deduplicar registros usando funciones hash.

Esta guía asume lo siguiente:

  • Estás familiarizado con crear scripts como componentes de proyecto.
  • Existe una operación que realiza la llamada API saliente, o que está expuesta como un endpoint API para recibir webhooks entrantes.

Nota

Para firmar un JSON Web Token con HS256, utiliza el conector JWT o las funciones JWT en su lugar. Estas producen un token completo en lugar de una firma independiente. Para un ejemplo completo, consulta Autenticar endpoints API usando JWT.

Patrón de diseño

La lógica de firma reside en su propio componente de script para que cualquier operación en el proyecto pueda llamarlo:

flowchart LR A["Script
(establecer payload y secreto,
llamar al script de firma)"] --> B["Actividad HTTP v2
(enviar la solicitud firmada)"]

Un Script de Jitterbit establece el payload y el secreto como variables globales, luego llama al script de firma JavaScript con RunScript. El script de firma escribe la firma en una tercera variable global, que el resto de la operación lee.

Las variables globales son la única manera de pasar valores a un script escrito en JavaScript, ya que los argumentos incluidos en una llamada a RunScript no están disponibles para este. Evita períodos en los nombres de las variables globales que un script JavaScript lee o escribe, como se describe en Variables globales.

Parte 1: Almacena el secreto compartido

Almacena el secreto de firma como una variable de proyecto en lugar de codificarlo en un script, para que su valor pueda diferir por entorno y no aparezca en los registros de operación.

  1. En Studio, abre el menú de acciones del proyecto y selecciona Variables de proyecto.

  2. Agrega una variable llamada api_signing_secret e ingresa el secreto proporcionado por el sistema destino.

  3. Habilita Ocultar valor.

  4. Haz clic en Guardar.

Para ver los pasos completos y prácticas adicionales sobre el manejo de credenciales, consulta Administrar credenciales de endpoint.

Parte 2: Crea el script de firma

  1. Crea un nuevo script en el proyecto. El nuevo script se abre en el editor de scripts.

  2. Haz clic en el nombre del script en la esquina superior izquierda e ingresa un nombre (por ejemplo, Generate HMAC Signature).

  3. Abre el menú Tipo de script y selecciona JavaScript.

  4. Ingresa el siguiente script. Implementa SHA-256 y la construcción HMAC utilizando únicamente características de ECMAScript 5.1, el estándar compatible con el motor JavaScript Harmony.

    Las dos asignaciones bajo Uso de Jitterbit al final del script establecen una carga conocida y un secreto para que puedas ejecutar el script por sí solo y confirmar su salida antes de conectarlo a una operación. La Parte 3 las reemplaza.

    Script de firma HMAC-SHA256
    <javascript>
    /*
     * HMAC-SHA256 for Jitterbit JavaScript
     * ES5 compatible - no Node.js crypto dependency
     */
    
    function utf8Bytes(str) {
        str = unescape(encodeURIComponent(str));
        var bytes = [];
        for (var i = 0; i < str.length; i++) {
            bytes.push(str.charCodeAt(i));
        }
        return bytes;
    }
    
    function rightRotate(value, amount) {
        return (value >>> amount) | (value << (32 - amount));
    }
    
    function sha256(bytes) {
        var k = [
            0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
            0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
            0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
            0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
            0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
            0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
            0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
            0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
            0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
            0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
            0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
            0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
            0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
            0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
            0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
            0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
        ];
    
        var h = [
            0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
            0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
        ];
    
        var data = bytes.slice();
        var bitLength = data.length * 8;
    
        data.push(0x80);
    
        while ((data.length % 64) !== 56) {
            data.push(0);
        }
    
        // High 32 bits
        data.push(0, 0, 0, 0);
    
        // Low 32 bits
        data.push(
            (bitLength >>> 24) & 0xff,
            (bitLength >>> 16) & 0xff,
            (bitLength >>> 8) & 0xff,
            bitLength & 0xff
        );
    
        for (var offset = 0; offset < data.length; offset += 64) {
            var w = new Array(64);
    
            var i;
            for (i = 0; i < 16; i++) {
                var j = offset + i * 4;
    
                w[i] =
                    ((data[j] << 24) |
                     (data[j + 1] << 16) |
                     (data[j + 2] << 8) |
                      data[j + 3]);
            }
    
            for (i = 16; i < 64; i++) {
                var s0 =
                    rightRotate(w[i - 15], 7) ^
                    rightRotate(w[i - 15], 18) ^
                    (w[i - 15] >>> 3);
    
                var s1 =
                    rightRotate(w[i - 2], 17) ^
                    rightRotate(w[i - 2], 19) ^
                    (w[i - 2] >>> 10);
    
                w[i] = (w[i - 16] + s0 + w[i - 7] + s1) | 0;
            }
    
            var a = h[0];
            var b = h[1];
            var c = h[2];
            var d = h[3];
            var e = h[4];
            var f = h[5];
            var g = h[6];
            var hh = h[7];
    
            for (i = 0; i < 64; i++) {
                var S1 =
                    rightRotate(e, 6) ^
                    rightRotate(e, 11) ^
                    rightRotate(e, 25);
    
                var ch = (e & f) ^ ((~e) & g);
    
                var temp1 =
                    (hh + S1 + ch + k[i] + w[i]) | 0;
    
                var S0 =
                    rightRotate(a, 2) ^
                    rightRotate(a, 13) ^
                    rightRotate(a, 22);
    
                var maj =
                    (a & b) ^
                    (a & c) ^
                    (b & c);
    
                var temp2 = (S0 + maj) | 0;
    
                hh = g;
                g = f;
                f = e;
                e = (d + temp1) | 0;
                d = c;
                c = b;
                b = a;
                a = (temp1 + temp2) | 0;
            }
    
            h[0] = (h[0] + a) | 0;
            h[1] = (h[1] + b) | 0;
            h[2] = (h[2] + c) | 0;
            h[3] = (h[3] + d) | 0;
            h[4] = (h[4] + e) | 0;
            h[5] = (h[5] + f) | 0;
            h[6] = (h[6] + g) | 0;
            h[7] = (h[7] + hh) | 0;
        }
    
        var result = [];
    
        for (var x = 0; x < h.length; x++) {
            result.push((h[x] >>> 24) & 0xff);
            result.push((h[x] >>> 16) & 0xff);
            result.push((h[x] >>> 8) & 0xff);
            result.push(h[x] & 0xff);
        }
    
        return result;
    }
    
    function bytesToHex(bytes) {
        var hex = "";
    
        for (var i = 0; i < bytes.length; i++) {
            var value = bytes[i].toString(16);
    
            if (value.length < 2) {
                value = "0" + value;
            }
    
            hex += value;
        }
    
        return hex;
    }
    
    function hmacSha256(secret, message) {
        var blockSize = 64;
    
        var key = utf8Bytes(secret);
    
        if (key.length > blockSize) {
            key = sha256(key);
        }
    
        while (key.length < blockSize) {
            key.push(0);
        }
    
        var outerKey = [];
        var innerKey = [];
    
        for (var i = 0; i < blockSize; i++) {
            outerKey[i] = key[i] ^ 0x5c;
            innerKey[i] = key[i] ^ 0x36;
        }
    
        var messageBytes = utf8Bytes(message);
    
        var innerHash = sha256(
            innerKey.concat(messageBytes)
        );
    
        var finalHash = sha256(
            outerKey.concat(innerHash)
        );
    
        return bytesToHex(finalHash);
    }
    /*
     * Jitterbit usage
     */
    $payload = "The quick brown fox jumps over the lazy dog";
    $secret = "key";
    var payload = $payload;
    var secret = $secret;
    
    $hmacSignature = hmacSha256(secret, payload);
    </javascript>
    
  5. En el editor de scripts, utiliza el toggle Editar/Prueba para seleccionar modo Prueba y luego haz clic en Ejecutar prueba. Confirma que la variable global hmacSignature esté establecida en f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8. Este es el valor de prueba publicado para HMAC-SHA256 con la carga The quick brown fox jumps over the lazy dog y el secreto key. Para más información, consulta Prueba un script.

Parte 3: Firma una solicitud saliente

Paso 1: Haz el script reutilizable

En el script de firma, reemplaza las dos líneas que asignan la carga y el secreto de muestra para que el script firme lo que proporcione la operación que lo llama:

Uso de Jitterbit (líneas finales del script de firma)
/*
 * Jitterbit usage
 */
var payload = $payload;
var secret = $secret;

$hmacSignature = hmacSha256(secret, payload);

El resto del script no cambia.

Paso 2: Llama el script desde la operación

Agrega un paso de script a la operación, antes de la actividad que envía la solicitud. A diferencia del script de firma, este utiliza Jitterbit Script, el idioma predeterminado para un nuevo script. Construye la carga, lee el secreto y llama al script de firma:

<trans>
// The payload must match, byte for byte, what the target system signs
$payload = JSONStringify($requestBody);
$secret = [api_signing_secret];

RunScript("<TAG>script:Generate HMAC Signature</TAG>");

WriteToOperationLog("Signature generated for a payload of " + Length($payload) + " characters");
</trans>

La firma ahora está en la variable global hmacSignature y permanece disponible para el resto de la operación y para cualquier operación descendente en la cadena.

Precaución

Registra la longitud de la carga o el resultado de la llamada, pero no el secreto ni la firma en sí. Una firma es una credencial para la solicitud que firma.

Paso 3: Envía la firma con la solicitud

En la actividad que envía la solicitud, agrega la firma como encabezado de solicitud. Para una actividad HTTP v2, agrega una fila a la tabla Encabezados de solicitud utilizando el nombre de encabezado que espera el sistema destino e ingresa [hmacSignature] como el valor.

Los nombres de encabezado varían según la API. Consulta la documentación del sistema destino para conocer el nombre del encabezado, si la firma debe estar en hexadecimal o Base64, y si debe llevar un prefijo (por ejemplo, sha256=) o combinarse con una marca de tiempo.

Paso 4: Convierte la firma a Base64 (opcional)

El script de firma devuelve una firma codificada en hexadecimal. Si el sistema destino espera Base64 en su lugar, conviértela con HexToBinary y Base64Encode después de llamar al script de firma:

<trans>
$hmacSignatureBase64 = Base64Encode(HexToBinary($hmacSignature));
</trans>

Para la carga útil de ejemplo y el secreto de la Parte 2, esto devuelve 97yD9DBThCSxMpjmqm+xQ+9NWaFJRhdZl0edvC0aPNg=.

Parte 4: Validar la firma de un webhook entrante

Una operación expuesta como un endpoint de API Manager puede utilizar el mismo script de firma para confirmar que una solicitud entrante proviene del remitente esperado. Se calcula la firma sobre el cuerpo de la solicitud y se compara contra el valor en el encabezado de firma:

<trans>
$payload = $jitterbit.api.request.body;
$secret = [api_signing_secret];

RunScript("<TAG>script:Generate HMAC Signature</TAG>");

$receivedSignature = Get("jitterbit.api.request.headers.X-Signature");

If($hmacSignature != $receivedSignature,
  $jitterbit.api.response.status_code = 401;
  $jitterbit.api.response = "Invalid signature";
  Return();
);
</trans>

La función Get es necesaria para leer un encabezado cuyo nombre contiene un guion. Se puede hacer referencia a un nombre de encabezado sin guion directamente como $jitterbit.api.request.headers.<name>. Consulta jitterbit.api.request.headers.*.

Los proveedores difieren en lo que firman y cómo codifican el resultado. Se firma exactamente la cadena que el proveedor especifica, que suele ser el cuerpo de la solicitud sin procesar pero puede incluir una marca de tiempo u otros campos, y se compara contra la misma codificación que envía el proveedor. Un proveedor que envía una firma Base64 o una firma hex en mayúsculas requiere que el valor de comparación se convierta primero.

Consideraciones

  • Tamaño de carga útil: Un script escrito en JavaScript está sujeto a un límite de 50 000 iteraciones de bucle, contadas en todo el script en lugar de por bucle. Las funciones de firma utilizan aproximadamente tres iteraciones por cada byte de la carga útil, por lo que una carga útil del orden de 16 KB puede acercarse al límite por sí sola. Las cargas útiles más pequeñas pueden alcanzarlo cuando el script realiza otro trabajo que incluye bucles, o cuando la carga útil contiene caracteres fuera del rango ASCII, que ocupan más de un byte cada uno. Se recomienda probar con cargas útiles representativas de la integración en lugar de tratar cualquier tamaño particular como un máximo compatible. En agentes privados, el límite se puede aumentar como se describe en Iteraciones de bucle.

  • Coherencia de carga útil: La firma cubre los bytes exactos firmados. Los espacios en blanco, el orden de las propiedades y la codificación de caracteres deben coincidir entre la carga útil firmada aquí y la carga útil que verifica el sistema de destino. Se construye la carga útil una sola vez, se firma esa cadena y se envía la misma cadena como el cuerpo de la solicitud.

  • Codificación secreta: El script trata el secreto como una cadena UTF-8. Si el sistema de destino proporciona el secreto como Base64 o hex, se decodifica a su forma de cadena antes de asignarlo a la variable secret.

  • Ubicación del script: JavaScript está disponible en scripts creados como componentes de proyecto, no en scripts utilizados dentro de una transformación. Para firmar un valor durante una transformación, se llama al script de firma desde un paso de operación anterior a la transformación y se asigna la variable global resultante.

Verificar la integración

  1. Se ejecuta el script de firma por sí solo utilizando la carga útil de ejemplo y el secreto de Parte 2 y se confirma que la firma es f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8.

  2. Se sustituye la carga útil y el secreto propios y se compara el resultado contra una implementación HMAC-SHA256 independiente. Cualquier implementación estándar devuelve el mismo valor para las mismas entradas.

  3. Se implementa y ejecuta la operación y se confirma que el sistema de destino acepta la solicitud.

  4. Si el sistema de destino rechaza la firma, se verifica lo siguiente en orden:

    • La carga útil firmada es idéntica byte por byte a la carga útil enviada en el cuerpo de la solicitud.
    • La codificación coincide con lo que espera el destino: hex o Base64, mayúsculas o minúsculas.
    • Cualquier componente de prefijo o marca de tiempo requerido se incluye en la cadena que se firma, en el orden que especifica el destino.
    • El secreto coincide con el valor configurado en el sistema de destino, sin espacios en blanco al inicio o al final.
  5. Si la operación falla con un error de iteración de bucle, se verifica el tamaño de la carga útil contra el límite descrito en Consideraciones.