必須のリクエストヘッダー
すべてのリクエスト(/v1/health を除く)はこれら 4 つのヘッダーを必ず付与する必要があります。いずれかが欠落または不正な場合、サーバーは HTTP 401 で拒否します。
| Header | Format | 意味 |
|---|---|---|
KH-Key | kh_live_[A-Z0-9]{32} | 公開キー識別子です。シークレットではなく、ログに記録されても問題ありません。 |
KH-Timestamp | Unix 秒(10 桁) | 現在のタイムスタンプです。許容範囲は +-300 秒です。 |
KH-Nonce | base64url 22~44 文字 | リクエストごとに一回限り使用される値です。600 秒間キャッシュされ、その後は再利用が可能です。 |
KH-Signature | 64 hex | HMAC-SHA256(secret, signing_string) を hex エンコードした値です。 |
署名文字列の構築
署名対象の文字列は、改行(\n)で連結された 5 つの構成要素から成り立ちます。
signing_string = METHOD + "\n"
+ PATH + "\n"
+ TIMESTAMP + "\n"
+ NONCE + "\n"
+ SHA256_HEX(BODY)
signature = HEX( HMAC_SHA256(secret, signing_string) )
PATH は /v1 から始まるパスです。たとえば /cp/kernelhost_api/v1/orders ではなく、/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 秒間データベースに保存し、同じノンスを持つ 2 回目のリクエストは 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 の設定。)

