mcpField
V
vetkd

vetkd

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

vetkd について

vetkdは、Internet ComputerのCanisterやフロントエンドを開発する人向けのvetKeysとしきい値鍵導出のガイドです。暗号化した鍵の受け渡し、実装方法、セキュリティ上の注意点を説明します。

主な機能

  1. 01しきい値鍵導出の解説
  2. 02Rust、Motoko、TypeScriptに対応
  3. 03トランスポート鍵による暗号化
  4. 04API料金と鍵名の説明
  5. 05実装時のセキュリティ上の注意

活用シーン

  1. 01オンチェーン暗号化ストレージの構築
  2. 02プライベートメッセージ機能の作成
  3. 03IDベース暗号の実装

インストール

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

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

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

詳細

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

注記: vetKeys は IC の新しい機能です。ic-vetkeys Rust クレートと @dfinity/vetkeys npm パッケージは公開されていますが、API は今後変更される可能性があります。 依存関係のバージョンを固定し、アップグレード後は移行ガイドが公開されていないか、DFINITY フォーラムを確認してください。

これは何か

vetKeys(検証可能な暗号化しきい値キー)は、vetKD プロトコルを通じて IC にオンチェーンプライバシーをもたらします。つまり、公開ブロックチェーンが秘密データを保持し、処理できるようにする、安全なオンデマンドの鍵導出です。キーは検証可能(ユーザーが正当性と改ざんの有無を確認できます)、暗号化済み(導出されたキーはユーザーが指定したトランスポートキーで暗号化され、ノードもカナリスターも生のキーを見ることはありません)、しきい値方式(サブネットノードのクォーラムが協力してキーを導出し、単独でマスターキーを持つ主体はいません)です。カナリスターはサブネットのしきい値インフラストラクチャに導出キーを要求し、クライアントのトランスポート公開鍵で暗号化されたキーを受け取り、クライアントのみがローカルで復号します。これにより、分散型鍵管理(DKMS)、暗号化オンチェーンストレージ、プライベートメッセージング、ID ベース暗号(IBE)、タイムロック暗号、しきい値 BLS、検証可能な乱数といったユースケースが可能になります。

前提条件

  • Rust:ic-vetkeys = "0.6"(crates.io)
  • Motoko:以下に示す管理カナリスターの直接呼び出し方式を使用します
  • フロントエンド:@dfinity/vetkeys v0.4.0

カナリスター ID

カナリスターID用途
管理カナリスターaaaaa-aavetkd_public_key および vetkd_derive_key システム API を公開します
Chain-key テスト用カナリスターvrqyr-saaaa-aaaan-qzn4q-caiテスト専用: 本番 API 手数料を支払わずにキー導出をテストするための、偽の vetKD 実装です。安全ではないため、本番環境では使用しないでください。

管理カナリスターは実際のカナリスターではなく、システムレベルの API エンドポイントです。aaaaa-aa への呼び出しは、key_id で指定されたマスターキーを保持する vetKD 対応サブネットへシステムによってルーティングされ、そのサブネットのノードがしきい値鍵導出を実行します。カナリスターは任意のサブネットから呼び出せます。

テスト用カナリスター: chain-key testing canister はメインネットにデプロイされており、偽の vetKD 実装(ハードコードされたキー、しきい値方式なし)を提供します。これを使うと、本番環境のサイクル費用をかけずにキー導出を試せます。キー名 insecure_test_key_1 を使用してください。安全ではなく、テスト専用です: 本番環境や機密データには決して使用しないでください。リポジトリから独自のインスタンスをデプロイすることもできます。

マスターキー名と API 手数料

IC 上の任意のカナリスターは、カナリスターやキーがどのサブネットに存在するかにかかわらず、利用可能な任意のマスターキーを使用できます。管理カナリスターは、マスターキーを保持するサブネットへ呼び出しをルーティングします。

キー名環境用途サイクル(概算)備考
test_key_1ローカル + メインネット開発とテスト10_000_000_000(メインネット)ローカルとメインネットの両方で動作します。開発とテストに使用してください。
key_1メインネット本番環境26_153_846_153サブネット pzp6e(uzr34 にバックアップ)

手数料は、呼び出し元カナリスターのサブネットではなく、マスターキーが存在するサブネット(およびそのサイズ)によって決まります。カナリスターがブラックホール化される可能性がある場合や、ほかのカナリスターから使用される場合は、サブネットのサイズが今後増えても呼び出しが失敗しないよう、現在のコストより多くのサイクルを送信してください。未使用のサイクルは返金されます。現在の米ドル換算額の見積もりについては、vetKD API — API feesを参照してください。

重要な概念

  • vetKey:(canister_id, context, input) から決定論的に導出される鍵素材です。同じ入力からは常に同じ鍵が生成されます。canister もサブネットノードも生の鍵を目にすることはありません。ローカルで復号されるまで、クライアントのトランスポート鍵で暗号化されているためです。
  • トランスポート鍵:クライアントが生成する一時的な鍵ペアです。IC が導出鍵を暗号化して配信できるよう、公開鍵を canister に送信します。対応する秘密鍵を持つクライアントだけが、結果を復号できます。
  • コンテキスト:ドメイン分離用のデータです。用途(機能や鍵の目的など)ごとに導出サブキーを分離し、同じ canister 内での鍵の衝突を防ぎます。名前空間のようなものです。
  • 入力:どの鍵を導出するかを識別する、アプリケーション定義のデータです(ユーザープリンシパル、ファイル ID、チャットルーム ID など)。管理 canister に平文で送信されます。識別子としてのみ使用し、秘密データには使用しないでください。
  • IBE(ID ベース暗号化):導出公開鍵を使い、ID(プリンシパルなど)宛てに暗号化する方式です。vetKeys により IC 上で IBE を利用できます。誰でも canister の導出公開鍵を使ってプリンシパル宛てに暗号化できますが、一致する vetKey を取得して復号できるのはそのプリンシパルだけです。

ビルドを壊す間違い

  1. 依存関係のバージョンを固定しない。 ic-vetkeys クレートと @dfinity/vetkeys npm パッケージは公開されていますが、新しいリリースで API が変更される可能性があります。バージョンを固定し、アップグレード後に再テストしてください。アップグレード後に動作しなくなった場合は、該当する変更履歴を確認して原因を把握してください。

  2. セッション間でトランスポート鍵を再利用する。 セッションごとに新しいトランスポート鍵ペアを生成する必要があります。Rust と TypeScript のライブラリには安全に鍵を生成する機能が含まれています。可能な限り、それらを使用してください。

  3. 生の vetkd_derive_key 出力を暗号化鍵として使う。 出力は暗号化されたデータです。トランスポート秘密鍵で復号して vetKey(生の鍵素材)を取得する必要があります。その後の処理は用途によって異なります。たとえば、toDerivedKeyMaterial() または同等の方法で対称鍵(AES 用など)を導出できます。復号したバイト列をそのまま AES 鍵として使用しないでください。IBE の復号や署名など、その他の用途ではそれぞれ異なる方法で vetKey を使用します。各用途に適した方法はライブラリのドキュメントを参照してください。

  4. vetKD を従来の公開鍵暗号と混同する。 ユーザーごとの静的な鍵ペアはありません。鍵はサブネットのしきい値マスター鍵から、vetKD プロトコルを介してオンデマンドで導出されます。同じ(canister、context、input)からは常に同じ導出鍵が生成されます。

  5. 秘密データを input フィールドに入れる。 input は管理 canister に平文で送信されます。これは鍵の識別子であり、暗号化されたペイロードではありません。ID(プリンシパル、ドキュメント ID など)に使用し、実際の秘密データには決して使用しないでください。

  6. vetkd_derive_key が非同期の canister 間呼び出しであることを忘れる。 呼び出しには cycles が必要で、await も必要です。防御的な慣行として、await の前に caller を取得してください。

  7. context の使い方に一貫性がない。 バックエンドでコンテキストとして b"my_app_v1" を使用し、フロントエンドでの検証では b"my_app" を使用すると、導出鍵が一致せず、復号が失敗しても通知されません。

  8. vetkd_derive_key に十分な cycles を添付しない。 vetkd_derive_key は cycles を消費しますが、vetkd_public_key は消費しません。derive_key の場合、key_1 は約 26B cycles、test_key_1 は約 10B cycles を消費します。

  9. 適切な認可チェックなしで独自の IBE を実装する。 IBE を手動で実装する場合(KeyManager/EncryptedMaps を介さない場合)、canister は vetkd_derive_key が認可された呼び出し元にのみ導出鍵を返すようにしなければなりません。たとえば、input に本人の ID を使用したプリンシパルに限定します。このチェックがないと、どの呼び出し元でも任意の導出鍵を要求し、他のユーザー宛てのメッセージを復号できてしまいます。提供されている ic-vetkeys/@dfinity/vetkeys ライブラリはこの処理を正しく行うため、独自実装よりもこちらを優先してください。

システム API(Candid)

vetKD API を使用すると、カニスターはしきい値プロトコルから導出された vetKey を要求できます。導出は決定的です。同じ入力からは常に同じキーが生成されるため、キーを確実に取得できます。異なる入力からは異なるキーが生成されるため、カニスターは一意のキーを無制限に導出できます。概要は以下のとおりです。完全な仕様については、vetKD API および IC インターフェース仕様をご覧ください。

vetkd_public_key

vetkd_derive_key で導出したキーの検証に使用する公開鍵を返します。空のコンテキストを指定するとカニスター単位のマスター公開鍵が取得でき、空でないコンテキストを指定すると、そのコンテキストに対応する導出済みサブキーが取得できます。IBE では、この公開鍵を使って誰でもある ID(プリンシパルなど)宛てに暗号化できます。その ID の保有者だけが後から対応する vetKey を取得して復号できます。事前の鍵交換や受信者の立ち会いは必要ありません。

vetkd_public_key : (record {
  canister_id : opt canister_id;
  context : blob;
  key_id : record { curve : vetkd_curve; name : text };
}) -> (record { public_key : blob })
  • canister_id:省略可能です。省略して (null) 場合は、呼び出し元のカニスターの公開鍵が返されます。指定した場合は、そのカニスターの鍵が返されます。
  • context:vetkd_derive_key と同じ意味を持つドメイン分離子です。特定のコンテキストでキーが導出されるようにし、アプリやユースケース間での衝突を防ぎます。
  • key_id.curve:bls12_381_g2(サポートされている唯一の曲線)。
  • key_id.name:マスターキー名:test_key_1(ローカルおよびメインネットでのテスト用)または key_1(本番用)。

既知のメインネットのマスター公開鍵から、この公開鍵をオフラインで導出することもできます。詳しくは、以下の「公開鍵のオフライン導出」をご覧ください。

vetkd_derive_key

指定した(コンテキスト、入力)に対応する鍵素材を導出し、受信者のトランスポート公開鍵で暗号化して返します。トランスポート秘密鍵の保有者だけが復号できます。復号した素材は、ユースケースに応じて使用します(対称鍵の場合は toDerivedKeyMaterial() を使用する、IBE の復号に使用するなど)。

vetkd_derive_key : (record {
  input : blob;
  context : blob;
  transport_public_key : blob;
  key_id : record { curve : vetkd_curve; name : text };
}) -> (record { encrypted_key : blob })
  • input:鍵の識別子として使用する任意のデータです。異なる入力からは異なる導出済みキーが生成されます。ランダムである必要はなく、平文で管理カニスターに送信されます。
  • context:ドメイン分離子です。公開鍵の取得時に使用したコンテキスト(検証や IBE など)と一致させる必要があります。
  • transport_public_key:受信者の公開鍵です。安全に受け渡すため、導出済みキーはこの鍵で暗号化されます。
  • 戻り値:encrypted_key。トランスポート秘密鍵で復号して生の vetKey を取得し、必要に応じて使用します(対称鍵の導出など。生のバイト列を AES キーとして直接使用しないでください)。

マスターキー名とサイクルコストについては、「Canister IDs」のマスターキー名と API 手数料をご覧ください。

実装

Rust

Cargo.toml:

[dependencies]
candid = "0.10"
ic-cdk = "0.19"
serde = { version = "1", features = ["derive"] }
serde_bytes = "0.11"

# High-level library (recommended) — source: https://github.com/dfinity/vetkeys
ic-vetkeys = "0.6"
ic-stable-structures = "0.7"

ic-vetkeys ライブラリを使用(推奨):

use candid::Principal;
use ic_cdk::update;
use ic_stable_structures::memory_manager::{MemoryId, MemoryManager, VirtualMemory};
use ic_stable_structures::DefaultMemoryImpl;
use ic_vetkeys::key_manager::KeyManager;
use ic_vetkeys::types::{AccessRights, VetKDCurve, VetKDKeyId};

// KeyManager is generic over an AccessControl type — AccessRights is the default.
// It uses stable memory for persistent storage of access control state.
thread_local! {
    static MEMORY_MANAGER: std::cell::RefCell<MemoryManager<DefaultMemoryImpl>> =
        std::cell::RefCell::new(MemoryManager::init(DefaultMemoryImpl::default()));

    static KEY_MANAGER: std::cell::RefCell<Option<KeyManager<AccessRights>>> =
        std::cell::RefCell::new(None);
}

#[ic_cdk::init]
fn init() {
    let key_id = VetKDKeyId {
        curve: VetKDCurve::Bls12381G2,
        name: "key_1".to_string(), // "test_key_1" for local + mainnet testing
    };
    MEMORY_MANAGER.with(|mm| {
        let mm = mm.borrow();
        KEY_MANAGER.with(|km| {
            *km.borrow_mut() = Some(KeyManager::init(
                "my_app_v1",              // domain separator
                key_id,
                mm.get(MemoryId::new(0)), // config memory
                mm.get(MemoryId::new(1)), // access control memory
                mm.get(MemoryId::new(2)), // shared keys memory
            ));
        });
    });
}

#[update]
async fn get_encrypted_vetkey(subkey_id: Vec<u8>, transport_public_key: Vec<u8>) -> Vec<u8> {
    let caller = ic_cdk::caller(); // Capture BEFORE await
    let future = KEY_MANAGER.with(|km| {
        let km = km.borrow();
        let km = km.as_ref().expect("not initialized");
        km.get_encrypted_vetkey(caller, subkey_id, transport_public_key)
            .expect("access denied")
    });
    future.await
}

#[update]
async fn get_vetkey_verification_key() -> Vec<u8> {
    let future = KEY_MANAGER.with(|km| {
        let km = km.borrow();
        let km = km.as_ref().expect("not initialized");
        km.get_vetkey_verification_key()
    });
    future.await
}

管理カニスターを直接呼び出す(低レベル):

use candid::{CandidType, Deserialize, Principal};
use ic_cdk::update;

#[derive(CandidType, Deserialize)]
struct VetKdKeyId {
    curve: VetKdCurve,
    name: String,
}

#[derive(CandidType, Deserialize)]
enum VetKdCurve {
    #[serde(rename = "bls12_381_g2")]
    Bls12381G2,
}

#[derive(CandidType)]
struct VetKdPublicKeyRequest {
    canister_id: Option<Principal>,
    context: Vec<u8>,
    key_id: VetKdKeyId,
}

#[derive(CandidType, Deserialize)]
struct VetKdPublicKeyResponse {
    public_key: Vec<u8>,
}

#[derive(CandidType)]
struct VetKdDeriveKeyRequest {
    input: Vec<u8>,
    context: Vec<u8>,
    transport_public_key: Vec<u8>,
    key_id: VetKdKeyId,
}

#[derive(CandidType, Deserialize)]
struct VetKdDeriveKeyResponse {
    encrypted_key: Vec<u8>,
}

const CONTEXT: &[u8] = b"my_app_v1";

fn key_id() -> VetKdKeyId {
    VetKdKeyId {
        curve: VetKdCurve::Bls12381G2,
        // Key names: "test_key_1" for local + mainnet testing, "key_1" for production
        name: "key_1".to_string(),
    }
}

#[update]
async fn vetkd_public_key() -> Vec<u8> {
    let request = VetKdPublicKeyRequest {
        canister_id: None, // defaults to this canister
        context: CONTEXT.to_vec(),
        key_id: key_id(),
    };

    // vetkd_public_key does not require cycles (unlike vetkd_derive_key).
    let (response,): (VetKdPublicKeyResponse,) = ic_cdk::api::call::call(
        Principal::management_canister(), // aaaaa-aa
        "vetkd_public_key",
        (request,),
    )
    .await
    .expect("vetkd_public_key call failed");

    response.public_key
}

#[update]
async fn vetkd_derive_key(transport_public_key: Vec<u8>) -> Vec<u8> {
    let caller = ic_cdk::caller(); // MUST capture before await

    let request = VetKdDeriveKeyRequest {
        input: caller.as_slice().to_vec(), // derive key specific to this caller
        context: CONTEXT.to_vec(),
        transport_public_key,
        key_id: key_id(),
    };

    // key_1 costs ~26B cycles, test_key_1 costs ~10B cycles.
    let (response,): (VetKdDeriveKeyResponse,) = ic_cdk::api::call::call_with_payment128(
        Principal::management_canister(),
        "vetkd_derive_key",
        (request,),
        26_000_000_000, // cycles for key_1 (use 10_000_000_000 for test_key_1)
    )
    .await
    .expect("vetkd_derive_key call failed");

    response.encrypted_key
}
Motoko

mops.toml:

[package]
name = "my-vetkd-app"
version = "0.1.0"

[dependencies]
core = "2.0.0"

管理カニスターを直接使用:

import Blob "mo:core/Blob";
import Principal "mo:core/Principal";
import Text "mo:core/Text";

persistent actor {

  type VetKdCurve = { #bls12_381_g2 };

  type VetKdKeyId = {
    curve : VetKdCurve;
    name : Text;
  };

  type VetKdPublicKeyRequest = {
    canister_id : ?Principal;
    context : Blob;
    key_id : VetKdKeyId;
  };

  type VetKdPublicKeyResponse = {
    public_key : Blob;
  };

  type VetKdDeriveKeyRequest = {
    input : Blob;
    context : Blob;
    transport_public_key : Blob;
    key_id : VetKdKeyId;
  };

  type VetKdDeriveKeyResponse = {
    encrypted_key : Blob;
  };

  let managementCanister : actor {
    vetkd_public_key : VetKdPublicKeyRequest -> async VetKdPublicKeyResponse;
    vetkd_derive_key : VetKdDeriveKeyRequest -> async VetKdDeriveKeyResponse;
  } = actor "aaaaa-aa";

  let context : Blob = Text.encodeUtf8("my_app_v1");

  // Key names: "test_key_1" for local + mainnet testing, "key_1" for production
  func keyId() : VetKdKeyId {
    { curve = #bls12_381_g2; name = "key_1" }
  };

  public shared func getPublicKey() : async Blob {
    // vetkd_public_key does not require cycles (unlike vetkd_derive_key).
    let response = await managementCanister.vetkd_public_key({
      canister_id = null;
      context;
      key_id = keyId();
    });
    response.public_key
  };

  public shared ({ caller }) func deriveKey(transportPublicKey : Blob) : async Blob {
    // caller is captured here, before the await. vetkd_derive_key requires cycles.
    let response = await (with cycles = 26_000_000_000) managementCanister.vetkd_derive_key({
      input = Principal.toBlob(caller);
      context;
      transport_public_key = transportPublicKey;
      key_id = keyId();
    });
    response.encrypted_key
  };
};
フロントエンド(TypeScript)

フロントエンドでトランスポート鍵ペアを生成し、公開鍵をカニスターに送信します。暗号化された導出済みキーを受け取ったら、トランスポート秘密鍵で復号して vetKey(生の鍵素材)を取得し、その素材から対称鍵を導出します(AES などで使用する場合は toDerivedKeyMaterial() を使用します)。

import { TransportSecretKey, DerivedPublicKey, EncryptedVetKey } from "@dfinity/vetkeys";

// 1. Generate a transport secret key (BLS12-381)
const seed = crypto.getRandomValues(new Uint8Array(32));
const transportSecretKey = TransportSecretKey.fromSeed(seed);
const transportPublicKey = transportSecretKey.publicKey();

// 2. Request encrypted vetkey and verification key from your canister
const [encryptedKeyBytes, verificationKeyBytes] = await Promise.all([
  backendActor.get_encrypted_vetkey(subkeyId, transportPublicKey),
  backendActor.get_vetkey_verification_key(),
]);

// 3. Deserialize and decrypt
const verificationKey = DerivedPublicKey.deserialize(new Uint8Array(verificationKeyBytes));
const encryptedVetKey = EncryptedVetKey.deserialize(new Uint8Array(encryptedKeyBytes));
const vetKey = encryptedVetKey.decryptAndVerify(
  transportSecretKey,
  verificationKey,
  new Uint8Array(subkeyId),
);

// 4. Derive a symmetric key for AES-GCM
const aesKeyMaterial = vetKey.toDerivedKeyMaterial();
const aesKey = await crypto.subtle.importKey(
  "raw",
  aesKeyMaterial.data.slice(0, 32), // 256-bit AES key
  { name: "AES-GCM" },
  false,
  ["encrypt", "decrypt"],
);

// 5. Encrypt
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
  { name: "AES-GCM", iv },
  aesKey,
  new TextEncoder().encode("secret message"),
);

// 6. Decrypt
const plaintext = await crypto.subtle.decrypt(
  { name: "AES-GCM", iv },
  aesKey,
  ciphertext,
);

@dfinity/vetkeys パッケージには、サブパスを通じて、より高レベルの抽象化も用意されています。

  • @dfinity/vetkeys/key_manager -- アクセス制御されたキーを管理するための KeyManager および DefaultKeyManagerClient
  • @dfinity/vetkeys/encrypted_maps -- 暗号化されたキーと値のストレージを提供する EncryptedMaps および DefaultEncryptedMapsClient

これらは Rust の KeyManager および EncryptedMaps 型に対応しており、トランスポート鍵のフローを自動的に処理します。

オフラインでの公開鍵導出

既知のメインネットのマスター公開鍵から、指定した鍵名(例:key_1)の公開鍵を、オフラインで(canister 呼び出しなしで)導出できます。これは IBE に便利です。コンテキストに応じた canister の公開鍵を導出してから、受信者や canister がオンラインでなくても、ID(例:principal)宛てに暗号化できます。

Rust:

use ic_vetkeys::{MasterPublicKey, DerivedPublicKey};

// Start from the known mainnet master public key for key_1
let master_key = MasterPublicKey::for_mainnet_key("key_1")
    .expect("unknown key name");

// Derive the canister-level key
let canister_key = master_key.derive_canister_key(canister_id.as_slice());

// Derive a sub-key for a specific context/identity
let derived_key: DerivedPublicKey = canister_key.derive_sub_key(b"my_app_v1");

// Use derived_key for IBE encryption — no canister call needed

TypeScript:

import { MasterPublicKey, DerivedPublicKey } from "@dfinity/vetkeys";

// Start from the known mainnet master public key
const masterKey = MasterPublicKey.productionKey();

// Derive the canister-level key
const canisterKey = masterKey.deriveCanisterKey(canisterId);

// Derive a sub-key for a specific context/identity
const derivedKey: DerivedPublicKey = canisterKey.deriveSubKey(
  new TextEncoder().encode("my_app_v1"),
);

// Use derivedKey for IBE encryption — no canister call needed
ID ベース暗号化(IBE)

IBE では、canister の導出済み公開鍵だけを使って、ID(例:principal)宛てに暗号化できます。受信者はオンラインである必要も、事前に鍵を登録する必要もありません。受信者は後から canister に認証し、その ID 用に導出された vetKey を vetkd_derive_key で取得して、ローカルで復号します。

TypeScript:

import {
  TransportSecretKey, DerivedPublicKey, EncryptedVetKey,
  IbeCiphertext, IbeIdentity, IbeSeed,
} from "@dfinity/vetkeys";

// --- Encrypt (sender side, no canister call needed) ---

// Derive the recipient's public key offline (see "Offline Public Key Derivation" above)
const recipientIdentity = IbeIdentity.fromBytes(recipientPrincipalBytes);
const seed = IbeSeed.random();
const plaintext = new TextEncoder().encode("secret message");

const ciphertext = IbeCiphertext.encrypt(derivedPublicKey, recipientIdentity, plaintext, seed);
const serialized = ciphertext.serialize(); // store or transmit this

// --- Decrypt (recipient side, requires canister call to get vetKey) ---

// 1. Get the vetKey (same flow as the Frontend section above)
const transportSecretKey = TransportSecretKey.fromSeed(crypto.getRandomValues(new Uint8Array(32)));
const [encryptedKeyBytes, verificationKeyBytes] = await Promise.all([
  backendActor.get_encrypted_vetkey(subkeyId, transportSecretKey.publicKey()),
  backendActor.get_vetkey_verification_key(),
]);
const verificationKey = DerivedPublicKey.deserialize(new Uint8Array(verificationKeyBytes));
const encryptedVetKey = EncryptedVetKey.deserialize(new Uint8Array(encryptedKeyBytes));
const vetKey = encryptedVetKey.decryptAndVerify(
  transportSecretKey, verificationKey, new Uint8Array(subkeyId),
);

// 2. Decrypt the IBE ciphertext
const deserialized = IbeCiphertext.deserialize(serialized);
const decrypted = deserialized.decrypt(vetKey);
// decrypted is Uint8Array containing "secret message"

Rust(オフチェーンのクライアントまたはテスト):

use ic_vetkeys::{
    DerivedPublicKey, IbeCiphertext, IbeIdentity, IbeSeed, VetKey,
};

// --- Encrypt ---
let identity = IbeIdentity::from_bytes(recipient_principal.as_slice());
let seed = IbeSeed::new(&mut rand::rng());
let plaintext = b"secret message";

let ciphertext = IbeCiphertext::encrypt(
    &derived_public_key,
    &identity,
    plaintext,
    &seed,
);
let serialized = ciphertext.serialize();

// --- Decrypt (after obtaining the VetKey) ---
let deserialized = IbeCiphertext::deserialize(&serialized)
    .expect("invalid ciphertext");
let decrypted = deserialized.decrypt(&vet_key)
    .expect("decryption failed");
// decrypted == b"secret message"
高水準の抽象化:KeyManager と EncryptedMaps

Rust クレートと TypeScript パッケージには、トランスポート鍵のフロー、アクセス制御、暗号化ストレージを処理する、次の2つの高水準モジュールが用意されています。

  • KeyManager<T: AccessControl>(Rust)/KeyManager(TS)— 安定ストレージを使用して、アクセス制御された vetKey を管理します。canister はどのユーザーがどの鍵をリクエストできるかを適用し、ライブラリは導出リクエスト、ユーザー権限(Read、ReadWrite、ReadWriteManage)、principal 間の鍵共有を処理します。

  • EncryptedMaps<T: AccessControl>(Rust)/EncryptedMaps(TS)— KeyManager を基盤として、暗号化されたキーと値のストアを提供します。各マップはアクセス制御され、導出された vetKey で暗号化されます。値の暗号化と復号は、vetKey を使ってクライアント(フロントエンド)側で処理され、canister には暗号文のみが保存されます。

Rust では、これらは ic_vetkeys::key_manager と ic_vetkeys::encrypted_maps にあります。TypeScript では、@dfinity/vetkeys/key_manager と @dfinity/vetkeys/encrypted_maps からインポートします。完全な例については、vetkeys リポジトリを参照してください。

デプロイとテスト

ローカル開発
# Start the local network (provisions test_key_1 and key_1 automatically)
icp network start -d

# Deploy your canister
icp deploy backend

# Test public key retrieval
icp canister call backend getPublicKey '()'
# Returns: (blob "...") -- the vetKD public key

# For derive_key, you need a transport public key (generated by frontend)
# Test with a dummy 48-byte blob:
icp canister call backend deriveKey '(blob "\00\01\02\03\04\05\06\07\08\09\0a\0b\0c\0d\0e\0f\10\11\12\13\14\15\16\17\18\19\1a\1b\1c\1d\1e\1f\20\21\22\23\24\25\26\27\28\29\2a\2b\2c\2d\2e\2f")'
メインネット
# Deploy to mainnet
icp deploy backend -e ic

# Use test_key_1 for initial testing, key_1 for production
# Make sure your canister code references the correct key name

動作確認

# 1. Verify public key is returned (non-empty blob)
icp canister call backend getPublicKey '()'
# Expected: (blob "\ab\cd\ef...")  -- 48+ bytes of BLS public key data

# 2. Verify derive_key returns encrypted key (non-empty blob)
icp canister call backend deriveKey '(blob "\00\01...")'
# Expected: (blob "\12\34\56...")  -- encrypted key material

# 3. Verify determinism: same (caller, context, input) and same transport key produce same encrypted_key
# Call deriveKey twice with the same identity and transport key
# Expected: identical encrypted_key blobs both times

# 4. Verify isolation: different callers get different keys
icp identity new test-user-1 --storage-mode=plaintext
icp identity new test-user-2 --storage-mode=plaintext
icp identity default test-user-1
icp canister call backend deriveKey '(blob "\00\01...")'
# Note the output
icp identity default test-user-2
icp canister call backend deriveKey '(blob "\00\01...")'
# Expected: DIFFERENT encrypted_key (different caller = different derived key)

# 5. Frontend integration test
# Open the frontend, trigger encryption/decryption
# Verify: encrypted data can be decrypted by the same user
# Verify: encrypted data CANNOT be decrypted by a different user

コメント