必需的请求头
除 /v1/health 外,每个请求都必须携带这四个请求头。若任一缺失或无效,服务器将以 HTTP 401 拒绝。
| Header | Format | 含义 |
|---|---|---|
KH-Key | kh_live_[A-Z0-9]{32} | 公开密钥标识符。非机密,可出现在日志中。 |
KH-Timestamp | Unix 秒(10 位数字) | 当前时间戳。容忍窗口 +-300 秒。 |
KH-Nonce | 22 至 44 个 base64url 字符 | 每个请求的一次性随机数。缓存 600 秒后方可重复使用。 |
KH-Signature | 64 hex | HMAC-SHA256(secret, signing_string),十六进制编码。 |
构建签名字符串
待签名字符串由五个部分组成,使用换行符(\n)连接。
signing_string = METHOD + "\n"
+ PATH + "\n"
+ TIMESTAMP + "\n"
+ NONCE + "\n"
+ SHA256_HEX(BODY)
signature = HEX( HMAC_SHA256(secret, signing_string) )
PATH 是从 /v1 开始的路径,例如 /v1/orders,而不是 /cp/kernelhost_api/v1/orders。如果请求带有查询字符串,查询字符串也属于 PATH(例如 /v1/services?limit=10&offset=0)。BODY 为原始请求体;对于没有请求体的请求,BODY 为空字符串,其 SHA256 值为众所周知的常量 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855。
参考实现
PHP:
$method = 'POST';
$path = '/v1/orders';
$body = json_encode(['product_id' => 42, 'billing_cycle' => 'monthly', 'hostname' => 'web01.example.com']);
$idem = 'order-web01-' . date('Ymd');
$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, getenv('KH_SECRET'));
$ch = curl_init('https://www.kernelhost.com/cp/kernelhost_api/v1/orders');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'KH-Key: ' . getenv('KH_KEY'),
"KH-Timestamp: {$ts}",
"KH-Nonce: {$nonce}",
"KH-Signature: {$sig}",
"Idempotency-Key: {$idem}",
],
]);
$res = curl_exec($ch);
Node.js:
import crypto from 'node:crypto';
const method = 'POST';
const path = '/v1/orders';
const body = JSON.stringify({ product_id: 42, billing_cycle: 'monthly', hostname: 'web01.example.com' });
const idem = 'order-web01-' + new Date().toISOString().slice(0, 10).replaceAll('-', '');
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', process.env.KH_SECRET).update(signing).digest('hex');
const res = await fetch('https://www.kernelhost.com/cp/kernelhost_api/v1/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'KH-Key': process.env.KH_KEY,
'KH-Timestamp': ts,
'KH-Nonce': nonce,
'KH-Signature': sig,
'Idempotency-Key': idem,
},
body,
});
重放保护
已成功签名的请求无法再次发送。服务器将每个随机数在数据库中保存 600 秒,使用相同随机数的第二次请求会以 replay_detected 被拒绝。同样,时间戳与服务器时间相差超过 300 秒的请求也会被拒绝。
权限范围模型
每个密钥拥有显式的权限范围列表。各路由会校验所需的权限范围;若缺失,将返回 HTTP 403 forbidden_scope。写入类与敏感类权限范围必须在密钥创建时显式启用。
read:products,read:orders,read:services,read:billing,read:webhooksread:credentials(读取服务凭据(主机名、IP 地址、用户名、密码)。每次调用会额外生成一条 credentials.read 审计条目。)write:orders(提交并支付订单。)write:services(执行服务操作(启动、停止、重启、申请在计费周期结束时取消服务,以及撤回该申请)。)write:webhooks(设置 Webhook URL。)

