Header di richiesta obbligatori
Ogni richiesta (eccetto /v1/health) deve contenere questi quattro header. Se uno manca o è invalido, il server rifiuta con HTTP 401.
| Header | Format | Description |
|---|---|---|
KH-Key | kh_live_[A-Z0-9]{32} | Identificatore pubblico della chiave. Non è un segreto, può apparire nei log. |
KH-Timestamp | Secondi Unix (10 cifre) | Timestamp corrente. Finestra di tolleranza +-300s. |
KH-Nonce | 22-44 caratteri base64url | Valore monouso per richiesta. Memorizzato per 600s, poi nuovamente riutilizzabile. |
KH-Signature | 64 hex | HMAC-SHA256(secret, signing_string), codificato in hex. |
Costruzione della signing string
La stringa da firmare è composta da cinque componenti uniti da newline (\n).
signing_string = METHOD + "\n"
+ PATH + "\n"
+ TIMESTAMP + "\n"
+ NONCE + "\n"
+ SHA256_HEX(BODY)
signature = HEX( HMAC_SHA256(secret, signing_string) )
PATH include il percorso completo della richiesta, query string compresa, ma senza host né fragment. BODY è il body grezzo in byte; per GET/DELETE senza body, BODY è la stringa vuota il cui SHA256 è la costante nota e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.
Implementazioni di riferimento
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,
});
Protezione replay
Una richiesta firmata correttamente NON può essere replicata. Il server memorizza ogni nonce per 600s nel database; una seconda richiesta con lo stesso nonce viene rifiutata con replay_detected. Allo stesso modo viene rifiutata una richiesta il cui timestamp si discosta di oltre 300s dall'ora del server.
Modello dei permessi
Ogni chiave dispone di una lista esplicita di permessi. Le route verificano il permesso richiesto; se manca, viene restituito HTTP 403 forbidden_scope. I permessi di scrittura e quelli sensibili devono essere abilitati esplicitamente alla creazione della chiave.
read:products,read:orders,read:services,read:billing,read:webhooksread:credentials(Lettura delle credenziali dei servizi (password root, FTP, VNC). Genera una voce di audit aggiuntiva credentials.read per ogni chiamata.)write:orders(Effettuare e pagare ordini.)write:services(Eseguire azioni sui servizi (start/stop/reboot/reinstall/terminate).)write:webhooks(Impostare l'URL del webhook.)

