必須のリクエストヘッダー

すべてのリクエスト(/v1/health を除く)はこれら 4 つのヘッダーを必ず付与する必要があります。いずれかが欠落または不正な場合、サーバーは HTTP 401 で拒否します。

HeaderFormatDescription
KH-Keykh_live_[A-Z0-9]{32}公開キー識別子です。シークレットではなく、ログに記録されても問題ありません。
KH-TimestampUnix 秒(10 桁)現在のタイムスタンプです。許容範囲は +-300 秒です。
KH-Noncebase64url 22~44 文字リクエストごとに一回限り使用される値です。600 秒間キャッシュされ、その後は再利用が可能です。
KH-Signature64 hexHMAC-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 にはクエリ文字列を含む完全なリクエストパスが入りますが、ホストやフラグメントは含めません。BODY は生のボディバイト列で、ボディのない GET/DELETE では空文字列となり、その SHA256 は周知の定数 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 となります。

リファレンス実装

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

リプレイ防止

正しく署名されたリクエストは再送できません。サーバーはすべてのノンスを 600 秒間データベースに保存し、同じノンスを持つ 2 回目のリクエストは replay_detected として拒否されます。同様に、タイムスタンプがサーバー時刻から 300 秒を超えてずれているリクエストも拒否されます。

スコープモデル

各キーには明示的なスコープリストが設定されます。ルートは要求されたスコープを確認し、不足している場合は HTTP 403 forbidden_scope を返します。書き込みおよび機微なスコープはキー作成時に明示的に有効化する必要があります。

  • read:products, read:orders, read:services, read:billing, read:webhooks
  • read:credentials (サービス認証情報(root パスワード、FTP、VNC)の読み取り。呼び出しごとに credentials.read の監査エントリーを追加で生成します。)
  • write:orders (発注および支払い。)
  • write:services (サービスアクションの実行(start/stop/reboot/reinstall/terminate)。)
  • write:webhooks (Webhook URL の設定。)