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.
| Header | Format | Description |
|---|---|---|
KH-Key | kh_live_[A-Z0-9]{32} | Identifiant de clé publique. N'est pas un secret, peut apparaître dans les journaux. |
KH-Timestamp | Secondes Unix (10 chiffres) | Horodatage actuel. Fenêtre de tolérance de +-300s. |
KH-Nonce | 22-44 caractères base64url | Valeur à usage unique par requête. Mise en cache pendant 600s, puis réutilisable. |
KH-Signature | 64 hex | HMAC-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:webhooksread: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.)

