Zum Inhalt springen
LOGICAR Entwickler Suchen

#Signatur prüfen

Auf dieser Seite

Ihre Webhook-Adresse ist öffentlich. Jeder kann ihr etwas schicken — deshalb prüft Ihr System bei jeder Nachricht, ob sie von LOGICAR kommt, unverändert ist und nicht aus einer alten Aufzeichnung stammt.

#Die Köpfe

KopfInhalt
Content-Typeapplication/json, UTF-8.
User-AgentKennung des Absenders mit Link auf diese Dokumentation.
X-LogiCar-EventDas Ereignis, z. B. vehicle.delivered — dasselbe wie type im Umschlag.
X-LogiCar-DeliveryKennung der Zustellung: je Endpunkt eine, über alle Versuche gleich. Die Kennung des Ereignisses steht als id im Umschlag.
X-LogiCar-TimestampUnix-Sekunden dieses Versuchs.
X-LogiCar-Signaturesha256= und der HMAC-SHA256 (hex) über <Timestamp>.<Rumpf> mit dem Geheimnis des Endpunkts.
X-LogiCar-Environmentproduction oder sandbox.

#Das Verfahren

  1. Rumpf roh lesen — die Bytes, wie sie ankamen. Nicht erst als JSON einlesen und wieder ausgeben: schon ein anderes Leerzeichen ergibt eine andere Signatur.
  2. Zeitstempel prüfen: X-LogiCar-Timestamp darf höchstens fünf Minuten von Ihrer Uhr abweichen. Sonst verwerfen — so nützt eine mitgeschnittene Nachricht später niemandem.
  3. Erwartete Signatur bilden: HMAC-SHA256 mit dem Geheimnis des Endpunkts über <Timestamp>.<Rumpf> (Zeitstempel, ein Punkt, der rohe Rumpf), als Hex, davor sha256=.
  4. Zeitkonstant vergleichen mit X-LogiCar-Signature. Ein gewöhnlicher Zeichenkettenvergleich verrät über die Laufzeit, wie viele Zeichen stimmen.
  5. Erst danach den Inhalt verarbeiten.

Achtung

Prüfen Sie nie nur, ob der Kopf vorhanden ist. Und legen Sie das Geheimnis nicht in den Quelltext, sondern in eine Umgebungsvariable oder einen Secret-Store.

#Node.js (Express)

js
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const GEHEIMNIS = process.env.LOGICAR_WEBHOOK_SECRET;

// express.raw: der Rumpf bleibt ein Buffer — genau die Bytes, die signiert sind.
app.post('/logicar/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  const zeit = req.get('X-LogiCar-Timestamp') ?? '';
  const signatur = req.get('X-LogiCar-Signature') ?? '';
  if (!/^\d+$/.test(zeit) || Math.abs(Date.now() / 1000 - Number(zeit)) > 300) {
    return res.sendStatus(400);
  }

  const erwartet = 'sha256=' + crypto
    .createHmac('sha256', GEHEIMNIS)
    .update(`${zeit}.`)
    .update(req.body)
    .digest('hex');
  const a = Buffer.from(signatur);
  const b = Buffer.from(erwartet);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.sendStatus(401);
  }

  const nachricht = JSON.parse(req.body.toString('utf8'));
  res.sendStatus(204);          // erst bestätigen …
  verarbeite(nachricht);        // … dann verarbeiten; entdoppeln über nachricht.id
});

#Python (Flask)

python
import hashlib
import hmac
import os
import time

from flask import Flask, abort, request

app = Flask(__name__)
GEHEIMNIS = os.environ["LOGICAR_WEBHOOK_SECRET"].encode()


@app.post("/logicar/webhooks")
def logicar_webhook():
    zeit = request.headers.get("X-LogiCar-Timestamp", "")
    signatur = request.headers.get("X-LogiCar-Signature", "")
    if not zeit.isdigit() or abs(time.time() - int(zeit)) > 300:
        abort(400)

    roh = request.get_data()  # die unveränderten Bytes
    erwartet = "sha256=" + hmac.new(GEHEIMNIS, zeit.encode() + b"." + roh, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(signatur, erwartet):
        abort(401)

    nachricht = request.get_json()
    # in eine Warteschlange legen; entdoppeln über nachricht["id"]
    return "", 204

#PHP

php
<?php
$geheimnis = getenv('LOGICAR_WEBHOOK_SECRET');
$roh = file_get_contents('php://input');
$zeit = $_SERVER['HTTP_X_LOGICAR_TIMESTAMP'] ?? '';
$signatur = $_SERVER['HTTP_X_LOGICAR_SIGNATURE'] ?? '';

if (!ctype_digit($zeit) || abs(time() - (int) $zeit) > 300) {
    http_response_code(400);
    exit;
}

$erwartet = 'sha256=' . hash_hmac('sha256', $zeit . '.' . $roh, $geheimnis);
if (!hash_equals($erwartet, $signatur)) {
    http_response_code(401);
    exit;
}

$nachricht = json_decode($roh, true);
// verarbeiten; entdoppeln über $nachricht['id']
http_response_code(204);

#C# (ASP.NET Core)

csharp
using System.Security.Cryptography;
using System.Text;

app.MapPost("/logicar/webhooks", async (HttpRequest request) =>
{
    using var reader = new StreamReader(request.Body, Encoding.UTF8);
    var roh = await reader.ReadToEndAsync();
    var zeit = request.Headers["X-LogiCar-Timestamp"].ToString();
    var signatur = request.Headers["X-LogiCar-Signature"].ToString();

    if (!long.TryParse(zeit, out var sekunden) ||
        Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - sekunden) > 300)
        return Results.BadRequest();

    var geheimnis = Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("LOGICAR_WEBHOOK_SECRET")!);
    var hash = HMACSHA256.HashData(geheimnis, Encoding.UTF8.GetBytes($"{zeit}.{roh}"));
    var erwartet = "sha256=" + Convert.ToHexString(hash).ToLowerInvariant();

    if (!CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(erwartet), Encoding.UTF8.GetBytes(signatur)))
        return Results.Unauthorized();

    // verarbeiten; entdoppeln über die id im Umschlag
    return Results.NoContent();
});

#Zum Nachrechnen

Mit dem Geheimnis 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef (Geheimnisse sind 64 Hexzeichen), dem Zeitstempel 1790951532 und dem Rumpf {"type":"webhook.test"} ergibt sich:

text
sha256=2933626e01df77f51bfa3d14a613746fd4940c10b5db523e523c72d8ea9fdb0a

Stimmt Ihr Ergebnis nicht, liegt es fast immer am Rumpf: ein Framework hat ihn bereits als JSON gelesen und neu geschrieben. Lesen Sie ihn roh (siehe oben).

#Das bisherige Verfahren

Endpunkte im bisherigen Format bekommen zusätzlich diese Köpfe:

KopfInhalt
X-Webhook-SignatureBisheriges Verfahren: HMAC-SHA256 (hex) über den Rumpf allein, ohne Zeitstempel. Nur noch für bestehende Empfänger — neue prüfen X-LogiCar-Signature.
X-Webhook-Delivery-IDBisheriger Name von X-LogiCar-Delivery.
X-Webhook-Testtrue bei einer Testnachricht aus dem Portal (Ereignis webhook.test).

X-Webhook-Signature ist ein HMAC-SHA256 über den Rumpf allein, ohne sha256= und ohne Zeitstempel. Er kommt weiter, damit ältere Anbindungen nicht brechen — schützt aber nicht vor wieder eingespielten Nachrichten. Prüfen Sie auch dort X-LogiCar-Signature; Endpunkte der API v1 bekommen nur noch diese.