Autenticación

API de Revendedores

Cabeceras de petición obligatorias

Toda petición (salvo /v1/health) debe llevar estas cuatro cabeceras. Si falta alguna o no es válida, el servidor responde con HTTP 401.

HeaderFormatDescription
KH-Keykh_live_[A-Z0-9]{32}Identificador de clave pública. No es un secreto, puede aparecer en los registros.
KH-TimestampSegundos Unix (10 dígitos)Marca de tiempo actual. Ventana de tolerancia +-300s.
KH-Nonce22-44 caracteres base64urlValor de un solo uso por petición. Se almacena en caché durante 600s, después puede reutilizarse.
KH-Signature64 hexHMAC-SHA256(secret, signing_string), codificado en hexadecimal.

Construcción de la cadena a firmar

La cadena que se firma se compone de cinco elementos unidos por salto de línea (\n).

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

signature = HEX( HMAC_SHA256(secret, signing_string) )

PATH incluye la ruta completa de la petición con la cadena de consulta, sin host ni fragmento. BODY son los bytes del cuerpo en bruto; en GET/DELETE sin cuerpo, BODY es la cadena vacía cuyo SHA256 es la constante conocida e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.

Implementaciones de referencia

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,
});

Protección contra repetición

Una petición firmada correctamente NO puede reenviarse. El servidor almacena cada nonce durante 600s en la base de datos; una segunda petición con el mismo nonce se rechaza con replay_detected. Del mismo modo, una petición cuya marca de tiempo se desvíe más de 300s respecto al servidor se rechaza.

Modelo de permisos

Cada clave tiene una lista explícita de permisos. Las rutas comprueban el permiso requerido; si falta, se devuelve HTTP 403 forbidden_scope. Los permisos de escritura y sensibles deben activarse explícitamente al crear la clave.

  • read:products, read:orders, read:services, read:billing, read:webhooks
  • read:credentials (Lectura de credenciales de servicio (contraseñas de root, FTP, VNC). Genera una entrada de auditoría adicional credentials.read por cada llamada.)
  • write:orders (Realizar y pagar pedidos.)
  • write:services (Ejecutar acciones de servicio (start/stop/reboot/reinstall/terminate).)
  • write:webhooks (Configurar la URL del webhook.)