vetkeys について
IC 上で vetKeys と vetKD 閾値鍵導出プロトコルを利用するための開発者向けリファレンスです。Rust、Motoko、フロントエンドでの暗号化鍵配布、対称鍵導出、IBE、BLS 署名、検証可能な乱数を扱い、ライブラリの範囲と注意点も説明します。
主な機能
- 01暗号化された閾値鍵の配布
- 02対称鍵導出
- 03IBE とオフライン公開鍵導出
- 04閾値 BLS 署名
- 05VRF による検証可能な乱数
- 06Rust・Motoko・フロントエンドの解説
活用シーン
- 01ユーザーやリソース別の暗号鍵導出
- 02Principal 宛てメッセージの暗号化
- 03期限後にデータを公開する仕組み
- 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 インターフェースを独自に実装しないでください。
| レイヤー | Rust | Motoko | フロントエンド |
|---|---|---|---|
| パッケージ | ic-vetkeys 0.9 (crates.io) | ic-vetkeys 0.6 (mops) | @icp-sdk/vetkeys 0.7 (npm) |
| 管理 API | ic-cdk-management-canister, ic_vetkeys::management_canister | mo: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 のカナスターで導出してください。)
注意点
-
パッケージ/インポートの誤り。
@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スキルを参照)。 -
toDerivedKeyMaterial()は存在しません。 対称暗号化では、const dkm = await vetKey.asDerivedKeyMaterial()を実行し、その後await dkm.encryptMessage(msg, domainSep, associatedData)/await dkm.decryptMessage(ct, domainSep, associatedData)を呼び出してください(すべて非同期です)。復号した生の vetKey バイト列を AES キーとして直接使用しないでください。 -
管理インターフェースを手作業で定義しないでください。 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_*のレコードを手作業で宣言する必要はなく、誤りの原因にもなります。 -
Motoko には低レベルの暗号機能がありません。 Motoko ライブラリでは、IBE、トランスポートキー、
MasterPublicKey/DerivedPublicKey、vetKey の復号は利用できません。Motoko のカナスターは暗号化された vetKey を返し、残りの処理はフロントエンドの@icp-sdk/vetkeys(または Rust のオフチェーンクライアント)が行います。 -
導出に必要なサイクルをカナスターに用意してください。
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 を付与し、超過分は返金されます。また、カナスターの残高を十分に保ってください。 -
contextとinputは処理の最初から最後まで一致させてください。context(またはinput)が異なると、別のキーが生成されます。その場合、decryptAndVerifyは誤った平文を返すのではなく、例外をスローします(Rust API ではエラーを返します)。エラーを処理し、公開鍵の取得、導出、クライアントでの検証/復号の間でcontext/inputのバイト列を完全に一致させてください。 -
inputは平文です。 これは管理カナスターに送信されるキー識別子です。ID(プリンシパル、ドキュメント ID)を使用し、秘密データは使用しないでください。 -
awaitの前に呼び出し元を取得してください。 非同期の導出呼び出しの前に、Rust ではic_cdk::api::msg_caller()を、Motoko では({ caller })の分解代入で呼び出し元を取得してください。 -
導出を手作業で行う場合は、認可を強制してください。
KeyManager/EncryptedMapsを介さずにvetkd_derive_keyを直接呼び出す場合、呼び出し元に導出権限のあるinput(例:自身のプリンシパル)に対してのみ導出するよう、カナスター側で確認する必要があります。そうしないと、任意の呼び出し元が他人のキーを取得できます。 -
BLS では暗号化されていないキーを使用します。
sign_with_bls/signWithBlsを使用してください。暗号化された vetKey を BLS に渡さないでください。また、IBE と BLS で同じcontext/inputを使い回さないでください。 -
Wasm での Rust の乱数生成。 IBE のシードには乱数が必要です。
ic-dummy-getrandom-for-wasm(またはgetrandomの Wasm シム)を追加しないと、カナスターがトラップします。(フロントエンドでは WebCrypto を使用するため、問題ありません。) -
最新の 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