Authentification

API Revendeur

En-têtes de requête requis

Chaque requête (sauf /v1/health) doit porter ces quatre en-têtes. Si l'un est manquant ou invalide, le serveur rejette avec HTTP 401.

HeaderFormatDescription
KH-Keykh_live_[A-Z0-9]{32}Identifiant de clé publique. N'est pas un secret, peut apparaître dans les journaux.
KH-TimestampSecondes Unix (10 chiffres)Horodatage actuel. Fenêtre de tolérance de +-300s.
KH-Nonce22-44 caractères base64urlValeur à usage unique par requête. Mise en cache pendant 600s, puis réutilisable.
KH-Signature64 hexHMAC-SHA256(secret, signing_string), encodé en hexadécimal.

Construire la chaîne à signer

La chaîne à signer est composée de cinq éléments joints par un saut de ligne (\n).

signing_string = METHOD + "\n"
               + PATH    + "\n"
               + TIMESTAMP + "\n"
               + NONCE   + "\n"
               + SHA256_HEX(BODY)

signature = HEX( HMAC_SHA256(secret, signing_string) )

PATH inclut le chemin complet de la requête y compris la chaîne de requête, mais sans hôte ni fragment. BODY correspond aux octets bruts du corps ; pour GET/DELETE sans corps, BODY est la chaîne vide dont le SHA256 est la constante bien connue e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.

Implémentations de référence

PHP:

$method = 'POST';
$path   = '/v1/orders';
$body   = json_encode(['product_id' => 42, 'billing_cycle' => 'monthly']);
$ts     = (string) time();
$nonce  = bin2hex(random_bytes(16));

$signing = "{$method}\n{$path}\n{$ts}\n{$nonce}\n" . hash('sha256', $body);
$sig = hash_hmac('sha256', $signing, $KH_SECRET);

$ch = curl_init('https://www.kernelhost.com/cp/kh_reseller_api/v1/orders');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_POSTFIELDS     => $body,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        "KH-Key: {$KH_KEY}",
        "KH-Timestamp: {$ts}",
        "KH-Nonce: {$nonce}",
        "KH-Signature: {$sig}",
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
]);
$res = curl_exec($ch);

Node.js:

import crypto from 'crypto';

const method = 'POST';
const path   = '/v1/orders';
const body   = JSON.stringify({ product_id: 42, billing_cycle: 'monthly' });
const ts     = Math.floor(Date.now() / 1000).toString();
const nonce  = crypto.randomBytes(16).toString('hex');

const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
const signing  = `${method}\n${path}\n${ts}\n${nonce}\n${bodyHash}`;
const sig      = crypto.createHmac('sha256', KH_SECRET).update(signing).digest('hex');

const res = await fetch('https://www.kernelhost.com/cp/kh_reseller_api/v1/orders', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'KH-Key': KH_KEY,
        'KH-Timestamp': ts,
        'KH-Nonce': nonce,
        'KH-Signature': sig,
        'Idempotency-Key': crypto.randomBytes(16).toString('hex'),
    },
    body,
});

Protection anti-rejeu

Une requête correctement signée NE peut PAS être rejouée. Le serveur stocke chaque nonce pendant 600s en base de données ; une deuxième requête portant le même nonce est rejetée avec replay_detected. De même, une requête dont l'horodatage s'écarte de plus de 300s de l'heure serveur est rejetée.

Modèle de permissions

Chaque clé possède une liste de permissions explicite. Les routes vérifient la permission requise ; si elle manque, HTTP 403 forbidden_scope. Les permissions d'écriture et sensibles doivent être activées explicitement à la création de la clé.

  • read:products, read:orders, read:services, read:billing, read:webhooks
  • read:credentials (Lecture des identifiants de service (mots de passe root, FTP, VNC). Génère une entrée d'audit supplémentaire credentials.read par appel.)
  • write:orders (Passer et payer des commandes.)
  • write:services (Exécuter des actions de service (start/stop/reboot/reinstall/terminate).)
  • write:webhooks (Définir l'URL du webhook.)