#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
| Kopf | Inhalt |
|---|---|
Content-Type | application/json, UTF-8. |
User-Agent | Kennung des Absenders mit Link auf diese Dokumentation. |
X-LogiCar-Event | Das Ereignis, z. B. vehicle.delivered — dasselbe wie type im Umschlag. |
X-LogiCar-Delivery | Kennung der Zustellung: je Endpunkt eine, über alle Versuche gleich. Die Kennung des Ereignisses steht als id im Umschlag. |
X-LogiCar-Timestamp | Unix-Sekunden dieses Versuchs. |
X-LogiCar-Signature | sha256= und der HMAC-SHA256 (hex) über <Timestamp>.<Rumpf> mit dem Geheimnis des Endpunkts. |
X-LogiCar-Environment | production oder sandbox. |
#Das Verfahren
- Rumpf roh lesen — die Bytes, wie sie ankamen. Nicht erst als JSON einlesen und wieder ausgeben: schon ein anderes Leerzeichen ergibt eine andere Signatur.
- Zeitstempel prüfen:
X-LogiCar-Timestampdarf höchstens fünf Minuten von Ihrer Uhr abweichen. Sonst verwerfen — so nützt eine mitgeschnittene Nachricht später niemandem. - Erwartete Signatur bilden: HMAC-SHA256 mit dem Geheimnis des Endpunkts über
<Timestamp>.<Rumpf>(Zeitstempel, ein Punkt, der rohe Rumpf), als Hex, davorsha256=. - Zeitkonstant vergleichen mit
X-LogiCar-Signature. Ein gewöhnlicher Zeichenkettenvergleich verrät über die Laufzeit, wie viele Zeichen stimmen. - 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)
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)
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
$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)
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:
sha256=2933626e01df77f51bfa3d14a613746fd4940c10b5db523e523c72d8ea9fdb0aStimmt 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:
| Kopf | Inhalt |
|---|---|
X-Webhook-Signature | Bisheriges 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-ID | Bisheriger Name von X-LogiCar-Delivery. |
X-Webhook-Test | true 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.