Ir para o conteúdo

Gerar uma assinatura HMAC-SHA256 no Jitterbit Studio

Introdução

Muitas APIs autenticam uma solicitação exigindo um hash com chave do payload da solicitação. O chamador combina o payload com um segredo compartilhado usando a construção HMAC (código de autenticação de mensagem baseado em hash) e envia a assinatura resultante em um cabeçalho de solicitação. O sistema receptor recomputa a assinatura a partir do payload recebido e aceita a solicitação apenas se os dois valores corresponderem. Isso confirma tanto que o payload não foi alterado em trânsito quanto que o chamador possui o segredo. Os webhooks de entrada usam o mesmo mecanismo na direção oposta: o provedor assina o payload que envia a você, e seu endpoint recomputa a assinatura para confirmar que a solicitação é genuína.

Este guia aborda como calcular uma assinatura HMAC-SHA256 em um projeto Studio usando um script JavaScript e como usar a assinatura resultante em uma solicitação de saída ou para validar uma de entrada.

A assinatura é calculada em JavaScript em vez de em um Jitterbit Script. Um HMAC não é um hash do segredo e do payload unidos: a construção primeiro deriva duas chaves do segredo invertendo bits em cada um de seus bytes, depois executa SHA-256 duas vezes, uma com cada chave derivada. Realizar esse cálculo exige os operadores bit a bit ^, &, << e >>>, que o JavaScript fornece. A função SHA256 executa um hash simples, que é um cálculo separado: fazer hash do segredo e do payload como uma única string concatenada produz um valor diferente da assinatura HMAC que o sistema receptor espera.

O script produz uma string hexadecimal minúscula de 64 caracteres que corresponde ao valor produzido por qualquer implementação padrão de HMAC-SHA256 para o mesmo payload e segredo, portanto uma assinatura gerada dessa maneira é intercambiável com uma gerada por uma ferramenta ou biblioteca externa.

Use esse padrão quando um projeto chamaria de outra forma os plugins obsoletos HMAC-SHA256 Generator ou HMAC-SHA1 Generator por meio de RunPlugin. Para tarefas de hash que não envolvem um segredo, como detecção de alterações ou fingerprinting de registros, use a função SHA256 diretamente, conforme descrito em Detectar e desduplicar registros usando funções de hash.

Este guia pressupõe o seguinte:

  • Você está familiarizado com criar scripts como componentes do projeto.
  • Existe uma operação que faz a chamada de API de saída, ou que é exposta como um endpoint de API para receber webhooks de entrada.

Nota

Para assinar um JSON Web Token com HS256, use o conector JWT ou as funções JWT em vez disso. Esses produzem um token completo em vez de uma assinatura autônoma. Para um exemplo completo, consulte Autenticar endpoints de API usando JWT.

Padrão de design

A lógica de assinatura fica em seu próprio componente de script para que qualquer operação no projeto possa chamá-lo:

flowchart LR A["Script
(definir payload e segredo,
chamar o script de assinatura)"] --> B["Atividade HTTP v2
(enviar a solicitação assinada)"]

Um Jitterbit Script define o payload e o segredo como variáveis globais, depois chama o script de assinatura JavaScript com RunScript. O script de assinatura escreve a assinatura em uma terceira variável global, que o resto da operação lê.

Variáveis globais são a única maneira de passar valores para um script escrito em JavaScript, pois argumentos incluídos em uma chamada de RunScript não estão disponíveis para ele. Evite períodos nos nomes das variáveis globais que um script JavaScript lê ou escreve, conforme descrito em Variáveis globais.

Parte 1: Armazenar o segredo compartilhado

Armazene o segredo de assinatura como uma variável de projeto em vez de codificá-lo em um script, para que seu valor possa variar por ambiente e não apareça nos logs de operação.

  1. No Studio, abra o menu de ações do projeto e selecione Variáveis de Projeto.

  2. Adicione uma variável denominada api_signing_secret e insira o segredo fornecido pelo sistema de destino.

  3. Ative Ocultar valor.

  4. Clique em Salvar.

Para obter as etapas completas e práticas adicionais de tratamento de credenciais, consulte Gerenciar credenciais de endpoint.

Parte 2: Criar o script de assinatura

  1. Crie um novo script no projeto. O novo script é aberto no editor de scripts.

  2. Clique no nome do script no canto superior esquerdo e insira um nome (por exemplo, Generate HMAC Signature).

  3. Abra o menu Script Type e selecione JavaScript.

  4. Insira o script a seguir. Ele implementa SHA-256 e a construção HMAC usando apenas recursos ECMAScript 5.1, o padrão suportado pelo mecanismo JavaScript Harmony.

    As duas atribuições sob Jitterbit usage no final do script definem uma carga e um segredo conhecidos para que você possa executar o script isoladamente e confirmar sua saída antes de integrá-lo em uma operação. A Parte 3 as substitui.

    Script de assinatura 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. No editor de scripts, use a alternância Editar/Testar para selecionar o modo Teste e clique em Executar teste. Confirme que a variável global hmacSignature está definida como f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8. Este é o valor de teste publicado para HMAC-SHA256 com a carga The quick brown fox jumps over the lazy dog e o segredo key. Para mais informações, consulte Testar um script.

Parte 3: Assinar uma solicitação de saída

Etapa 1: Tornar o script reutilizável

No script de assinatura, substitua as duas linhas que atribuem a carga e o segredo de exemplo para que o script assine o que a operação chamadora fornecer:

Jitterbit usage (linhas finais do script de assinatura)
/*
 * Jitterbit usage
 */
var payload = $payload;
var secret = $secret;

$hmacSignature = hmacSha256(secret, payload);

O restante do script permanece inalterado.

Etapa 2: Chamar o script da operação

Adicione uma etapa de script à operação, antes da atividade que envia a solicitação. Diferentemente do script de assinatura, este usa Jitterbit Script, a linguagem padrão para um novo script. Ele constrói a carga, lê o segredo e chama o script de assinatura:

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

A assinatura agora está na variável global hmacSignature e permanece disponível para o resto da operação e para qualquer operação a jusante na cadeia.

Cuidado

Registre o comprimento da carga ou o resultado da chamada, mas não o segredo ou a assinatura em si. Uma assinatura é uma credencial para a solicitação que ela assina.

Etapa 3: Enviar a assinatura com a solicitação

Na atividade que envia a solicitação, adicione a assinatura como um cabeçalho de solicitação. Para uma atividade HTTP v2, adicione uma linha à tabela Request Headers usando o nome do cabeçalho que o sistema de destino espera e insira [hmacSignature] como o valor.

Os nomes dos cabeçalhos variam por API. Consulte a documentação do sistema de destino para o nome do cabeçalho, se a assinatura deve estar em hex ou Base64 e se deve incluir um prefixo (por exemplo, sha256=) ou ser combinada com um timestamp.

Etapa 4: Converter a assinatura para Base64 (opcional)

O script de assinatura retorna uma assinatura codificada em hex. Se o sistema de destino espera Base64 em vez disso, converta-a com HexToBinary e Base64Encode depois de chamar o script de assinatura:

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

Para a carga útil de exemplo e o segredo da Parte 2, isso retorna 97yD9DBThCSxMpjmqm+xQ+9NWaFJRhdZl0edvC0aPNg=.

Parte 4: Validar uma assinatura de webhook de entrada

Uma operação exposta como endpoint do API Manager pode usar o mesmo script de assinatura para confirmar que uma solicitação de entrada veio do remetente esperado. Calcule a assinatura sobre o corpo da solicitação e compare-a com o valor no cabeçalho de assinatura:

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

A função Get é necessária para ler um cabeçalho cujo nome contém um hífen. Um nome de cabeçalho sem hífen pode ser referenciado diretamente como $jitterbit.api.request.headers.<name>. Consulte jitterbit.api.request.headers.*.

Os provedores diferem no que assinam e em como codificam o resultado. Assine exatamente a string que o provedor especifica, que geralmente é o corpo bruto da solicitação, mas pode incluir um timestamp ou outros campos, e compare contra a mesma codificação que o provedor envia. Um provedor que envia uma assinatura Base64 ou uma assinatura hex em maiúsculas requer que o valor de comparação seja convertido primeiro.

Considerações

  • Tamanho da carga útil: Um script escrito em JavaScript está sujeito a um limite de 50.000 iterações de loop, contadas em todo o script e não por loop. As funções de assinatura usam aproximadamente três iterações para cada byte da carga útil, então uma carga útil na ordem de 16 KB pode se aproximar do limite por si só. Cargas úteis menores podem atingi-lo quando o script executa outro trabalho que itera, ou quando a carga útil contém caracteres fora do intervalo ASCII, que ocupam mais de um byte cada. Teste com cargas úteis representativas da sua integração em vez de tratar qualquer tamanho específico como máximo suportado. Em agentes privados, o limite pode ser aumentado conforme descrito em Iterações de loop.

  • Consistência da carga útil: A assinatura cobre os bytes exatos assinados. Espaçamento em branco, ordem de propriedades e codificação de caracteres devem corresponder entre a carga útil assinada aqui e a carga útil que o sistema de destino verifica. Compile a carga útil uma vez, assine essa string e envie a mesma string como corpo da solicitação.

  • Codificação do segredo: O script trata o segredo como uma string UTF-8. Se o sistema de destino fornecer o segredo como Base64 ou hex, decodifique-o para sua forma de string antes de atribuí-lo à variável secret.

  • Posicionamento do script: JavaScript está disponível em scripts criados como componentes de projeto, não em scripts usados dentro de uma transformação. Para assinar um valor durante uma transformação, chame o script de assinatura de uma etapa de operação anterior à transformação e mapeie a variável global resultante.

Verificar a integração

  1. Execute o script de assinatura por conta própria usando a carga útil de exemplo e o segredo da Parte 2 e confirme que a assinatura é f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8.

  2. Substitua sua própria carga útil e segredo e compare o resultado com uma implementação HMAC-SHA256 independente. Qualquer implementação padrão retorna o mesmo valor para as mesmas entradas.

  3. Implante e execute a operação e confirme que o sistema de destino aceita a solicitação.

  4. Se o sistema de destino rejeitar a assinatura, verifique o seguinte em ordem:

    • A carga útil assinada é idêntica byte por byte à carga útil enviada no corpo da solicitação.
    • A codificação corresponde ao que o destino espera: hex ou Base64, maiúsculas ou minúsculas.
    • Qualquer componente de prefixo ou timestamp obrigatório é incluído na string sendo assinada, na ordem que o destino especifica.
    • O segredo corresponde ao valor configurado no sistema de destino, sem espaço em branco à esquerda ou à direita.
  5. Se a operação falhar com um erro de iteração de loop, verifique o tamanho da carga útil em relação ao limite descrito em Considerações.