Zum Inhalt springen

HMAC-SHA256-Signatur in Jitterbit Studio generieren

Einführung

Viele APIs authentifizieren eine Anfrage, indem sie einen Schlüssel-Hash der Anfrage-Nutzlast erfordern. Der Aufrufer kombiniert die Nutzlast mit einem gemeinsamen Geheimnis unter Verwendung der HMAC-Konstruktion (Hash-based Message Authentication Code) und sendet die resultierende Signatur in einem Anfrage-Header. Das empfangende System berechnet die Signatur aus der empfangenen Nutzlast neu und akzeptiert die Anfrage nur, wenn die beiden Werte übereinstimmen. Dies bestätigt sowohl, dass die Nutzlast während der Übertragung nicht verändert wurde, als auch dass der Aufrufer das Geheimnis kennt. Eingehende Webhooks nutzen denselben Mechanismus in umgekehrter Richtung: Der Anbieter signiert die Nutzlast, die er an Sie sendet, und der Endpunkt berechnet die Signatur neu, um zu bestätigen, dass die Anfrage authentisch ist.

Dieser Leitfaden behandelt die Berechnung einer HMAC-SHA256-Signatur in einem Studio-Projekt mithilfe eines JavaScript-Skripts sowie die Verwendung der resultierenden Signatur in einer ausgehenden Anfrage oder zur Validierung einer eingehenden Anfrage.

Die Signatur wird in JavaScript statt in einem Jitterbit Script berechnet. Ein HMAC ist nicht der Hash des Geheimnisses und der zusammengefügten Nutzlast: Die Konstruktion leitet zunächst zwei Schlüssel aus dem Geheimnis ab, indem Bits in jedem Byte umgekehrt werden, dann wird SHA-256 zweimal ausgeführt, einmal mit jedem abgeleiteten Schlüssel. Diese Berechnung erfordert die bitweisen Operatoren ^, &, << und >>>, die JavaScript bereitstellt. Die Funktion SHA256 führt einen einfachen Hash durch, was eine separate Berechnung ist: Das Hashen des Geheimnisses und der Nutzlast als einzelne verkettete Zeichenkette ergibt einen anderen Wert als die HMAC-Signatur, die das empfangende System erwartet.

Das Skript erzeugt eine 64-stellige Hexadezimalzeichenkette in Kleinbuchstaben, die dem Wert entspricht, den jede Standard-HMAC-SHA256-Implementierung für dieselbe Nutzlast und dasselbe Geheimnis erzeugt. Eine auf diese Weise generierte Signatur ist daher austauschbar mit einer von einem externen Tool oder einer externen Bibliothek generierten Signatur.

Verwenden Sie dieses Muster, wenn ein Projekt ansonsten die veralteten Plugins HMAC-SHA256 Generator oder HMAC-SHA1 Generator über RunPlugin aufrufen würde. Verwenden Sie für Hashing-Aufgaben, die kein Geheimnis beinhalten, wie Änderungserkennung oder Fingerprinting von Datensätzen, die Funktion SHA256 direkt, wie unter Datensätze mithilfe von Hash-Funktionen erkennen und deduplizieren beschrieben.

Dieser Leitfaden setzt folgendes voraus:

  • Sie sind mit dem Erstellen von Skripten als Projektkomponenten vertraut.
  • Eine Operation existiert, die den ausgehenden API-Aufruf durchführt oder die als API-Endpunkt für eingehende Webhooks bereitgestellt wird.

Hinweis

Um ein JSON Web Token mit HS256 zu signieren, verwenden Sie stattdessen den JWT-Connector oder die JWT-Funktionen. Diese erzeugen ein vollständiges Token statt einer eigenständigen Signatur. Ein vollständiges Beispiel finden Sie unter API-Endpunkte mit JWT authentifizieren.

Designmuster

Die Signierungslogik befindet sich in ihrer eigenen Skriptkomponente, damit jede Operation im Projekt sie aufrufen kann:

flowchart LR A["Script
(Nutzlast und Geheimnis festlegen,
Signierungsskript aufrufen)"] --> B["HTTP v2-Aktivität
(signierte Anfrage senden)"]

Ein Jitterbit Script legt die Nutzlast und das Geheimnis als globale Variablen fest und ruft dann das JavaScript-Signierungsskript mit RunScript auf. Das Signierungsskript schreibt die Signatur in eine dritte globale Variable, die der Rest der Operation liest.

Globale Variablen sind die einzige Möglichkeit, Werte in ein in JavaScript geschriebenes Skript zu übertragen, da Argumente, die in einem RunScript-Aufruf enthalten sind, dafür nicht verfügbar sind. Vermeiden Sie Punkte in den Namen globaler Variablen, die ein JavaScript-Skript liest oder schreibt, wie unter Globale Variablen beschrieben.

Teil 1: Den gemeinsamen Secret speichern

Speichern Sie das Signatur-Secret als Projektvariable, anstatt es in einem Script hartcodiert zu hinterlegen, damit der Wert pro Umgebung unterschiedlich sein kann und nicht in Operationsprotokollen angezeigt wird.

  1. Öffnen Sie in Studio das Aktionsmenü des Projekts und wählen Sie Project Variables aus.

  2. Fügen Sie eine Variable mit dem Namen api_signing_secret hinzu und geben Sie das von dem Zielsystem bereitgestellte Secret ein.

  3. Aktivieren Sie Hide value.

  4. Klicken Sie auf Save.

Für die vollständigen Schritte und zusätzliche Best Practices für den Umgang mit Anmeldedaten finden Sie unter Manage endpoint credentials.

Teil 2: Das Signatur-Script erstellen

  1. Erstellen Sie ein neues Script im Projekt. Das neue Script wird im Script-Editor geöffnet.

  2. Klicken Sie auf den Script-Namen in der oberen linken Ecke und geben Sie einen Namen ein (beispielsweise Generate HMAC Signature).

  3. Öffnen Sie das Menü Script Type und wählen Sie JavaScript aus.

  4. Geben Sie das folgende Script ein. Es implementiert SHA-256 und die HMAC-Konstruktion unter Verwendung nur von ECMAScript 5.1-Funktionen, dem Standard, den die Harmony JavaScript Engine unterstützt.

    Die zwei Zuweisungen unter Jitterbit usage am Ende des Scripts legen eine bekannte Payload und einen bekannten Secret fest, damit Sie das Script eigenständig ausführen und die Ausgabe bestätigen können, bevor Sie es in eine Operation integrieren. Teil 3 ersetzt diese.

    HMAC-SHA256 Signatur-Script
    <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. Verwenden Sie im Script-Editor die Umschaltfläche Edit/Test, um den Modus Test auszuwählen, und klicken Sie auf Run test. Bestätigen Sie, dass die globale Variable hmacSignature auf f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8 gesetzt ist. Dies ist der veröffentlichte Testwert für HMAC-SHA256 mit der Payload The quick brown fox jumps over the lazy dog und dem Secret key. Weitere Informationen finden Sie unter Test a script.

Teil 3: Eine ausgehende Anfrage signieren

Schritt 1: Das Script wiederverwendbar machen

Ersetzen Sie im Signatur-Script die zwei Zeilen, die die Beispiel-Payload und den Secret zuweisen, damit das Script alles signiert, das die aufrufende Operation bereitstellt:

Jitterbit usage (letzte Zeilen des Signatur-Scripts)
/*
 * Jitterbit usage
 */
var payload = $payload;
var secret = $secret;

$hmacSignature = hmacSha256(secret, payload);

Der Rest des Scripts bleibt unverändert.

Schritt 2: Das Script aus der Operation aufrufen

Fügen Sie der Operation einen Script-Schritt hinzu, vor der Aktivität, die die Anfrage sendet. Anders als das Signatur-Script verwendet dieses Jitterbit Script, die Standardsprache für ein neues Script. Es erstellt die Payload, liest den Secret aus und ruft das Signatur-Script auf:

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

Die Signatur befindet sich nun in der globalen Variable hmacSignature und bleibt für den Rest der Operation und alle nachgelagerten Operationen in der Kette verfügbar.

Vorsicht

Protokollieren Sie die Länge der Payload oder das Ergebnis des Aufrufs, aber nicht den Secret oder die Signatur selbst. Eine Signatur ist ein Anmeldedaten für die Anfrage, die sie signiert.

Schritt 3: Die Signatur mit der Anfrage senden

Fügen Sie in der Aktivität, die die Anfrage sendet, die Signatur als Request-Header hinzu. Verwenden Sie für eine HTTP v2-Aktivität eine Zeile in der Tabelle Request Headers mit dem Header-Namen, den das Zielsystem erwartet, und geben Sie [hmacSignature] als Wert ein.

Header-Namen unterscheiden sich je nach API. Überprüfen Sie die Dokumentation des Zielsystems für den Header-Namen, ob die Signatur im Hex- oder Base64-Format vorliegen muss, und ob sie ein Präfix tragen muss (beispielsweise sha256=) oder mit einem Zeitstempel kombiniert werden muss.

Schritt 4: Die Signatur in Base64 konvertieren (optional)

Das Signatur-Script gibt eine hexadezimal codierte Signatur zurück. Wenn das Zielsystem stattdessen Base64 erwartet, konvertieren Sie es nach dem Aufrufen des Signatur-Scripts mit HexToBinary und Base64Encode:

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

Für die Beispiel-Payload und das Geheimnis aus Teil 2 ergibt dies 97yD9DBThCSxMpjmqm+xQ+9NWaFJRhdZl0edvC0aPNg=.

Teil 4: Eine eingehende Webhook-Signatur validieren

Ein als API-Manager-Endpunkt bereitgestellter Vorgang kann das gleiche Signaturskript verwenden, um zu bestätigen, dass eine eingehende Anfrage vom erwarteten Absender stammt. Berechnen Sie die Signatur über den Request-Body und vergleichen Sie sie mit dem Wert im Signature-Header:

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

Die Funktion Get ist erforderlich, um einen Header zu lesen, dessen Name einen Bindestrich enthält. Ein Header-Name ohne Bindestrich kann direkt als $jitterbit.api.request.headers.<name> referenziert werden. Siehe jitterbit.api.request.headers.*.

Anbieter unterscheiden sich darin, was sie signieren und wie sie das Ergebnis codieren. Signieren Sie genau die Zeichenkette, die der Anbieter angibt, die normalerweise der rohe Request-Body ist, aber möglicherweise einen Zeitstempel oder andere Felder enthält, und vergleichen Sie sie mit der gleichen Codierung, die der Anbieter sendet. Ein Anbieter, der eine Base64-Signatur oder eine hexadezimale Signatur in Großbuchstaben sendet, erfordert, dass der Vergleichswert zuerst konvertiert wird.

Überlegungen

  • Payload-Größe: Ein in JavaScript geschriebenes Skript unterliegt einer Grenze von 50.000 Schleifeniterationen, die über das gesamte Skript gezählt werden, nicht pro Schleife. Die Signatursfunktionen verwenden ungefähr drei Iterationen für jedes Byte der Payload, sodass eine Payload in der Größenordnung von 16 KB die Grenze bereits erreichen kann. Kleinere Payloads können die Grenze erreichen, wenn das Skript andere Arbeiten ausführt, die Schleifen enthalten, oder wenn die Payload Zeichen außerhalb des ASCII-Bereichs enthält, die jeweils mehr als ein Byte benötigen. Testen Sie mit Payloads, die für Ihre Integration repräsentativ sind, statt eine bestimmte Größe als unterstütztes Maximum zu behandeln. Bei privaten Agenten kann die Grenze wie unter Schleifeniterationen beschrieben erhöht werden.

  • Payload-Konsistenz: Die Signatur umfasst die genauen signierten Bytes. Leerzeichen, Eigenschaftsreihenfolge und Zeichenkodierung müssen zwischen der hier signierten Payload und der vom Zielsystem verifizierten Payload übereinstimmen. Erstellen Sie die Payload einmal, signieren Sie diese Zeichenkette und senden Sie die gleiche Zeichenkette als Request-Body.

  • Geheimnis-Codierung: Das Skript behandelt das Geheimnis als UTF-8-Zeichenkette. Falls das Zielsystem das Geheimnis als Base64 oder Hex bereitstellt, dekodieren Sie es zunächst in seine Zeichenkettenform, bevor Sie es der secret-Variable zuweisen.

  • Skript-Platzierung: JavaScript ist in Skripten verfügbar, die als Projektkomponenten erstellt wurden, nicht in Skripten, die in einer Transformation verwendet werden. Um einen Wert während einer Transformation zu signieren, rufen Sie das Signaturskript aus einem Vorgangsschritt vor der Transformation auf und ordnen Sie die resultierende globale Variable zu.

Die Integration überprüfen

  1. Führen Sie das Signaturskript eigenständig mit der Beispiel-Payload und dem Geheimnis aus Teil 2 aus und bestätigen Sie, dass die Signatur f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8 ist.

  2. Ersetzen Sie Ihre eigene Payload und Ihr Geheimnis und vergleichen Sie das Ergebnis mit einer unabhängigen HMAC-SHA256-Implementierung. Jede Standardimplementierung gibt für die gleichen Eingaben den gleichen Wert zurück.

  3. Stellen Sie den Vorgang bereit und führen ihn aus und bestätigen Sie, dass das Zielsystem die Anfrage akzeptiert.

  4. Falls das Zielsystem die Signatur ablehnt, prüfen Sie Folgendes der Reihe nach:

    • Die signierte Payload ist byte-für-byte mit der im Request-Body gesendeten Payload identisch.
    • Die Codierung entspricht dem, was das Ziel erwartet: Hexadezimal oder Base64, Groß- oder Kleinbuchstaben.
    • Alle erforderlichen Präfix- oder Zeitstempelkomponenten sind in der signierten Zeichenkette enthalten, in der Reihenfolge, die das Ziel angibt.
    • Das Geheimnis stimmt mit dem Wert überein, der im Zielsystem konfiguriert ist, ohne führende oder nachfolgende Leerzeichen.
  5. Falls der Vorgang mit einem Schleifeniterationsfehler fehlschlägt, prüfen Sie die Payload-Größe gegen die unter Überlegungen beschriebene Grenze.