mcpField
V
vetkeys

vetkeys

github.com/dfinityセキュリティ3975 回インストール今日更新
リポジトリ

vetkeys について

IC 上で vetKeys と vetKD 閾値鍵導出プロトコルを利用するための開発者向けリファレンスです。Rust、Motoko、フロントエンドでの暗号化鍵配布、対称鍵導出、IBE、BLS 署名、検証可能な乱数を扱い、ライブラリの範囲と注意点も説明します。

主な機能

  1. 01暗号化された閾値鍵の配布
  2. 02対称鍵導出
  3. 03IBE とオフライン公開鍵導出
  4. 04閾値 BLS 署名
  5. 05VRF による検証可能な乱数
  6. 06Rust・Motoko・フロントエンドの解説

活用シーン

  1. 01ユーザーやリソース別の暗号鍵導出
  2. 02Principal 宛てメッセージの暗号化
  3. 03期限後にデータを公開する仕組み
  4. 04オンチェーンでの検証可能な乱数生成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dfinity/icskills --skill vetkeys

インストール後、Claude Code で/vetkeysを入力して読み込まれたか確認します。

同梱ファイルreferences/bls-signing.mdreferences/ibe.md

詳細

vetKeys(検証可能な暗号化しきい値鍵)

vetKeys は vetKD プロトコルを通じて IC にオンチェーンプライバシーをもたらします。キャニスターはサブネットのしきい値鍵導出インフラストラクチャから導出された鍵を要求し、クライアントが提供したトランスポート鍵で暗号化された鍵を受け取ります。復号できるのはクライアントだけで、しかもローカルで行います。サブネットノードが生の鍵を見ることはなく、この標準的なクライアント配信パターンではキャニスターも鍵を見ることはありません。キャニスターは、暗号化されたままの鍵をクライアントに中継します。(キャニスターが鍵素材を自ら取得するフローもあります。しきい値 BLS 署名とキャニスター内のタイムロック復号については、それぞれのセクションを参照してください。)導出は決定論的です。同じ (canister, context, input) からは、常に同じ鍵が生成されます。

保守されているライブラリを使用してください。暗号処理や Candid インターフェースを独自に実装しないでください。

レイヤーRustMotokoフロントエンド
パッケージic-vetkeys 0.9 (crates.io)ic-vetkeys 0.6 (mops)@icp-sdk/vetkeys 0.7 (npm)
管理 APIic-cdk-management-canister, ic_vetkeys::management_canistermo:ic-vetkeys/ManagementCanister—
低レベルプリミティブクレートのルート(ic_vetkeys::…)—(利用できません。下記を参照)パッケージのルート(@icp-sdk/vetkeys)

@dfinity/vetkeys はレガシーです(0.4.0 で凍結されています)。パッケージ名は 0.5.0 で @icp-sdk/vetkeys に変更されました。フロントエンドのエージェント/ID 型は @icp-sdk/core(@icp-sdk/core/agent、@icp-sdk/core/principal)からインポートしてください。@dfinity/agent/@dfinity/principal ではありません。

0.7 以降を使用してください。0.5 と 0.6 では @icp-sdk/core が通常の依存関係として宣言されていたため、失敗する代わりに、アプリがインストールするものとは別の core がネストされる可能性がありました。0.7 ではピア依存関係として ^5 || ^6 が指定されています。API は 3 つのバージョンすべてで同一です。

また、以下も必要です。Rust では ic-cdk = "0.20" と ic-cdk-management-canister = "0.1"(IBE では ic-dummy-getrandom-for-wasm も必要)、Motoko では ic-vetkeys 0.6 に moc ≥ 1.13.0/core ≥ 2.6.1 が必要です。フロントエンドでは @icp-sdk/core@^6 が必要です。vetKeys 自体は ^5 || ^6 に対応していますが、SDK のほかの部分は ^6 をピア依存関係として要求します。

必要なスキル/機能

やりたいこと使用するもの
暗号化されたキーと値のデータを保存・共有する(パスワードマネージャー、メモ、保管庫)encrypted-maps スキル(より高レベルなので、まずはこちらから)
あるプリンシパル宛てに暗号化し、その本人だけが復号できるようにする(メッセージング)IBE → references/ibe.md
期限を過ぎたときだけデータを公開する(封印入札オークション、タイムロック)タイムロック IBE → references/ibe.md
誰でも検証できる署名をキャニスターに生成させるしきい値 BLS → references/bls-signing.md
ユーザー/リソースごとの対称(AES)鍵を導出する対称鍵導出 → このファイル
キャニスターを呼び出さずにプリンシパル宛てに暗号化するオフライン公開鍵導出 → このファイル
オンチェーンで検証可能な乱数を生成する検証可能な乱数(VRF) → このファイル
ユーザーを認証する/ログインを実装するvetKeys ではありません。internet-identity スキルを使用してください

コアコンセプト

  • context — カナスター内で派生キーを名前空間化するドメイン分離用のデータです(例:"my_app"、または"symmetric_key"のような用途別の値)。公開鍵の呼び出し、派生呼び出し、クライアント側の検証/復号の間で同一でなければなりません。そうでないとキーが一致せず、decryptAndVerifyは例外をスローします(Rust APIではエラーを返すため、成功を前提とせず失敗を処理してください)。
  • input — 派生するキーを特定するアプリケーションデータです(例:呼び出し元のプリンシパル、ドキュメント ID)。管理カナスターに平文で送信されるため、シークレットデータには使わず、識別子としてのみ使用してください。
  • トランスポートキー — クライアントがリクエストごとに生成する一時的なキーペアです。派生キーを配信するためにサブネットが暗号化できるよう、公開部分を送信します。シークレット部分を持つ者だけが復号できます。リクエストごとに新しいものを生成してください(TransportSecretKey.random())。
  • 暗号化 vetKey と非暗号化 vetKey — IBE と対称キーの派生では、暗号化された配信フロー(トランスポートキー → decryptAndVerify → VetKey)を使用します。しきい値 BLS では、非暗号化の vetKey を直接使用します。ライブラリのsign_with_bls/signWithBlsが処理するため、暗号化された vetKeyをBLSに渡さないでください。
  • Motokoとの非対称性 — Motokoのic-vetkeysライブラリが公開するのは、管理 API とKeyManager/EncryptedMapsのみです。IBE、トランスポートキー、MasterPublicKey/DerivedPublicKey、vetKeyの復号には対応していません。Motokoアプリでは、カナスターが暗号化された vetKeyを返し、トランスポートキーの生成、decryptAndVerify、IBE、対称キーの派生はフロントエンド(@icp-sdk/vetkeys)が行います。これらのプリミティブはRustとTypeScriptでのみ利用できます。
キー名とサイクル
キー名場所vetkd_derive_keyのコスト
test_key_1ローカル+メインネット(テスト)10_000_000_000
key_1ローカル+メインネット(本番)26_153_846_153

vetkd_public_keyは無料です。vetkd_derive_keyにはサイクルがかかります。test_key_1とkey_1は、ローカルとメインネットで同じように動作します。金額の処理はヘルパーに任せてください。Rustバインディングが正確なコストを計算し、MotokoのManagementCanisterは26_153_846_153を添付し、超過分を返金します。必要なのは、カナスターに十分な残高を維持することだけです。管理カナスターはaaaaa-aaです。呼び出しはマスターキーを保持するサブネットにルーティングされます。

  • Rustは、icp.yamlのinit_args経由で渡される#[init]引数からキー名を読み取ります。
  • Motokoは、カナスター環境変数VETKD_KEY_NAMEから読み取り、デフォルトはtest_key_1です。名前は初回インストール時に安定状態へ保存され、カナスターのデータが存在する限り変更できません。後から変更しても無視されます(状態を破棄するreinstallのみがキーを切り替えます)。test_key_1は有効なメインネットキーでもあるため、VETKD_KEY_NAMEの設定を忘れたまま本番環境にデプロイすると、気付かないままそのキーで動作します。問題となる場合は、デプロイ時に想定したキーであることをアサートしてください。

vetKD管理 API(基盤+対称暗号化)

管理 APIには2つのエンドポイントがあります。vetkd_public_key(検証/オフライン暗号化用の公開鍵)とvetkd_derive_key(呼び出し元の暗号化されたキー)です。これが対称暗号化、IBE、BLSの基盤となります。Candidの型とサイクルが正しくなるよう、ライブラリのヘルパー経由で呼び出してください。

バックエンド — Rust
use ic_cdk::update;
use ic_cdk_management_canister::{VetKDCurve, VetKDDeriveKeyArgs, VetKDKeyId, VetKDPublicKeyArgs};

const CONTEXT: &[u8] = b"symmetric_key"; // domain separator; must match on the client

fn key_id() -> VetKDKeyId {
    // name comes from an #[init] arg in real code; "test_key_1" for local + mainnet testing
    VetKDKeyId { curve: VetKDCurve::Bls12_381_G2, name: "test_key_1".to_string() }
}

#[update]
async fn symmetric_verification_key() -> Vec<u8> {
    let res = ic_cdk_management_canister::vetkd_public_key(&VetKDPublicKeyArgs {
        canister_id: None, // defaults to this canister
        context: CONTEXT.to_vec(),
        key_id: key_id(),
    })
    .await
    .expect("vetkd_public_key failed");
    res.public_key // no cycles required
}

#[update]
async fn encrypted_symmetric_key_for_caller(transport_public_key: Vec<u8>) -> Vec<u8> {
    let caller = ic_cdk::api::msg_caller(); // capture BEFORE the await
    let res = ic_cdk_management_canister::vetkd_derive_key(&VetKDDeriveKeyArgs {
        input: caller.as_slice().to_vec(), // key identifier (plaintext) — never secret data
        context: CONTEXT.to_vec(),
        transport_public_key,
        key_id: key_id(),
    })
    .await // the binding attaches the required cycles automatically
    .expect("vetkd_derive_key failed");
    res.encrypted_key
}
バックエンド — Motoko
import ManagementCanister "mo:ic-vetkeys/ManagementCanister";
import Principal "mo:core/Principal";
import Text "mo:core/Text";
import Runtime "mo:core/Runtime";

persistent actor {
  // Captured into keyId at first install and fixed for the life of the canister's derived keys;
  // changing VETKD_KEY_NAME on a later upgrade has no effect (see the key-name warning above).
  let keyName = Runtime.envVar<system>("VETKD_KEY_NAME") ?? "test_key_1";
  let keyId : ManagementCanister.VetKdKeyid = { curve = #bls12_381_g2; name = keyName };

  public shared func symmetricVerificationKey() : async Blob {
    // context / domain separator; no cycles required
    await ManagementCanister.vetKdPublicKey(null, Text.encodeUtf8("symmetric_key"), keyId);
  };

  public shared ({ caller }) func encryptedSymmetricKeyForCaller(transportPublicKey : Blob) : async Blob {
    // signature is (input, context, keyId, transportPublicKey); helper attaches cycles automatically
    await ManagementCanister.vetKdDeriveKey(
      Principal.toBlob(caller), Text.encodeUtf8("symmetric_key"), keyId, transportPublicKey);
  };
};
フロントエンド — AES-GCMキーを派生する(TypeScript)

カナスターは暗号化された vetKeyを返します。フロントエンドはトランスポートキーを生成し、復号と検証を行ってVetKeyに変換してから、AES-GCMキーの素材を派生します。

import { TransportSecretKey, DerivedPublicKey, EncryptedVetKey } from "@icp-sdk/vetkeys";
// `backend` is your actor; `myPrincipal` is the authenticated caller's Principal (@icp-sdk/core/principal)

// 1. Fresh transport key per request
const tsk = TransportSecretKey.random();

// 2. Fetch the encrypted derived key + the public verification key
const [encryptedKeyBytes, publicKeyBytes] = await Promise.all([
  backend.encrypted_symmetric_key_for_caller(tsk.publicKeyBytes()),
  backend.symmetric_verification_key(),
]);

// 3. Decrypt & verify -> VetKey. The identity bytes MUST equal the backend `input`
//    (here the caller principal), or verification throws.
const vetKey = EncryptedVetKey.deserialize(new Uint8Array(encryptedKeyBytes)).decryptAndVerify(
  tsk,
  DerivedPublicKey.deserialize(new Uint8Array(publicKeyBytes)),
  myPrincipal.toUint8Array(),
);

// 4. Derive AES-GCM key material and encrypt/decrypt. There is NO `toDerivedKeyMaterial()`.
const keyMaterial = await vetKey.asDerivedKeyMaterial();
const domainSep = "my_app:notes"; // unique per app + usage
const ciphertext = await keyMaterial.encryptMessage("secret message", domainSep, ""); // (msg, domainSep, associatedData)
const plaintext = await keyMaterial.decryptMessage(ciphertext, domainSep, "");
// new TextDecoder().decode(plaintext) === "secret message"

オフラインでの公開鍵の派生

既知のメインネットのマスター公開鍵を起点として、カナスターを呼び出さずに、コンテキストに対するカナスターの公開鍵を派生します。受信者もカナスターもオンラインでないときに、プリンシパル宛てに暗号化(IBE)するために使用します。

TypeScript:

import { MasterPublicKey } from "@icp-sdk/vetkeys";
import { Principal } from "@icp-sdk/core/principal";

const derivedPublicKey = MasterPublicKey.productionKey()            // key_1 (default); MasterPublicKey.pocketicKey() for local
  .deriveCanisterKey(Principal.fromText(canisterId).toUint8Array())
  .deriveSubKey(new TextEncoder().encode("my_app"));               // the context / domain separator
// derivedPublicKey (a DerivedPublicKey) can now be used for IBE encryption offline

Rust:

use ic_vetkeys::{MasterPublicKey, DerivedPublicKey};
use ic_cdk_management_canister::{VetKDCurve, VetKDKeyId};

let master = MasterPublicKey::for_mainnet_key(&VetKDKeyId {
    curve: VetKDCurve::Bls12_381_G2,
    name: "key_1".to_string(),
}).expect("unknown key name");                     // for_pocketic_key(..) for local
let derived: DerivedPublicKey = master
    .derive_canister_key(canister_id.as_slice())
    .derive_sub_key(b"my_app");

検証可能なランダム性(VRF)

vetKey は検証可能なランダム性に変換できます。Rust のカナスターで ic_vetkeys::management_canister::compute_vrf(input, context, key_id) -> VrfOutput を呼び出し(input/context は抽選のラウンドやリーダー選出など、対象の処理に合わせて設定します)、フロントエンドで @icp-sdk/vetkeys の VrfOutput.deserialize(...) を使って証明を検証します。現時点では、エンドツーエンドの標準的なサンプルはありません。(Motoko ライブラリでは利用できません。Rust のカナスターで導出してください。)

注意点

  1. パッケージ/インポートの誤り。 @dfinity/vetkeys(0.4 で凍結)ではなく、@icp-sdk/vetkeys(≥0.7)を使用してください。エージェント/アイデンティティは @icp-sdk/core(@icp-sdk/core/agent、@icp-sdk/core/principal)からインポートし、await HttpAgent.create({ identity, host, rootKey }) でエージェントを作成してください。クライアントクラスが受け取るのは、オプションではなく、作成済みの HttpAgent です。rootKey は safeGetCanisterEnv()(@icp-sdk/core/agent/canister-env)から取得してください。リリースするコードでは fetchRootKey() を呼び出さないでください(icp-cli スキルを参照)。

  2. toDerivedKeyMaterial() は存在しません。 対称暗号化では、const dkm = await vetKey.asDerivedKeyMaterial() を実行し、その後 await dkm.encryptMessage(msg, domainSep, associatedData)/await dkm.decryptMessage(ct, domainSep, associatedData) を呼び出してください(すべて非同期です)。復号した生の vetKey バイト列を AES キーとして直接使用しないでください。

  3. 管理インターフェースを手作業で定義しないでください。 Rust では ic-cdk-management-canister(vetkd_public_key/vetkd_derive_key)または ic_vetkeys::management_canister(sign_with_bls も提供)を使用してください。Motoko では mo:ic-vetkeys/ManagementCanister(vetKdPublicKey、vetKdDeriveKey、signWithBls、blsPublicKey)を使用してください。これらは正しい Candid 型を備え、適切な量のサイクルを自動的に付与します。actor "aaaaa-aa" や vetkd_* のレコードを手作業で宣言する必要はなく、誤りの原因にもなります。

  4. Motoko には低レベルの暗号機能がありません。 Motoko ライブラリでは、IBE、トランスポートキー、MasterPublicKey/DerivedPublicKey、vetKey の復号は利用できません。Motoko のカナスターは暗号化された vetKey を返し、残りの処理はフロントエンドの @icp-sdk/vetkeys(または Rust のオフチェーンクライアント)が行います。

  5. 導出に必要なサイクルをカナスターに用意してください。 vetkd_derive_key はサイクルを消費します(key_1 = 26_153_846_153、test_key_1 = 10_000_000_000。ローカル環境とメインネットで同じです)。vetkd_public_key は無料です。ヘルパーに金額を付与させてください。Rust バインディングは正確なコストを計算し、Motoko ヘルパーは 26_153_846_153 を付与し、超過分は返金されます。また、カナスターの残高を十分に保ってください。

  6. context と input は処理の最初から最後まで一致させてください。 context(または input)が異なると、別のキーが生成されます。その場合、decryptAndVerify は誤った平文を返すのではなく、例外をスローします(Rust API ではエラーを返します)。エラーを処理し、公開鍵の取得、導出、クライアントでの検証/復号の間で context/input のバイト列を完全に一致させてください。

  7. input は平文です。 これは管理カナスターに送信されるキー識別子です。ID(プリンシパル、ドキュメント ID)を使用し、秘密データは使用しないでください。

  8. await の前に呼び出し元を取得してください。 非同期の導出呼び出しの前に、Rust では ic_cdk::api::msg_caller() を、Motoko では ({ caller }) の分解代入で呼び出し元を取得してください。

  9. 導出を手作業で行う場合は、認可を強制してください。 KeyManager/EncryptedMaps を介さずに vetkd_derive_key を直接呼び出す場合、呼び出し元に導出権限のある input(例:自身のプリンシパル)に対してのみ導出するよう、カナスター側で確認する必要があります。そうしないと、任意の呼び出し元が他人のキーを取得できます。

  10. BLS では暗号化されていないキーを使用します。 sign_with_bls/signWithBls を使用してください。暗号化された vetKey を BLS に渡さないでください。また、IBE と BLS で同じ context/input を使い回さないでください。

  11. Wasm での Rust の乱数生成。 IBE のシードには乱数が必要です。ic-dummy-getrandom-for-wasm(または getrandom の Wasm シム)を追加しないと、カナスターがトラップします。(フロントエンドでは WebCrypto を使用するため、問題ありません。)

  12. 最新の ic-cdk 呼び出し API。 ic_cdk::caller() ではなく、ic_cdk::api::msg_caller() を使用してください。バインディングを使わずに管理カナスターを呼び出す場合は、ic_cdk::call::Call::unbounded_wait(..).with_cycles(..) を使用してください。従来の ic_cdk::api::call::call* API は ic-cdk 0.20 以降で削除されています。

機能ガイド

  • ID ベース暗号化(IBE)とタイムロック — IbeCiphertext.encrypt(publicKey, IbeIdentity.fromPrincipal(recipient), plaintext, IbeSeed.random()) で暗号化します。受信者は IbeCiphertext.deserialize(ct).decrypt(vetKey) で復号します。バックエンド、フロントエンド、タイムロックの詳細:references/ibe.md。
  • しきい値 BLS 署名 — ic_vetkeys::management_canister::sign_with_bls(Rust)/ManagementCanister.signWithBls(Motoko)で署名し、verifyBlsSignature(derivedPublicKey, message, signature) で検証します。詳細:references/bls-signing.md。
  • 暗号化キー・バリューストレージ(EncryptedMaps / KeyManager) — encrypted-maps スキル。

デプロイと検証

プロビジョニング、icp.yaml、一般的なデプロイ手順は icp-cli スキルに属します。vetKeys 固有の確認事項:

icp deploy backend                                    # local replica provisions test_key_1
icp canister call backend symmetric_verification_key '()'   # non-empty BLS public-key blob
# derive needs a 48-byte transport public key from the frontend; different callers get different keys

コメント