mcpField
V
vetkd

vetkd

github.com/dfinity安全39198 次安装今天更新
代码仓库

关于 vetkd

vetkd 是面向 Internet Computer Canister 与前端开发者的 vetKeys 和阈值密钥派生指南,介绍加密密钥交付、实现方式及安全注意事项。

主要功能

  1. 01介绍阈值密钥派生
  2. 02涵盖 Rust、Motoko 和 TypeScript
  3. 03说明传输密钥加密
  4. 04记录 API 费用与密钥名称
  5. 05提示实现中的安全风险

适用场景

  1. 01构建链上加密存储
  2. 02创建私密消息系统
  3. 03实现基于身份的加密

安装

复制下面的命令,在终端中运行。

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

安装后在 Claude Code 中输入/vetkd确认已加载。

详细介绍

vetKeys(可验证加密门限密钥)

**注意:**vetKeys 是 IC 的一项较新功能。ic-vetkeys Rust crate 和 @dfinity/vetkeys npm package 已发布,但 API 仍可能随时间变化。 请固定依赖版本,并在升级后查看 DFINITY 论坛 中的迁移指南。

功能简介

vetKeys(可验证加密门限密钥)通过 vetKD 协议为 IC 带来链上隐私:安全、按需派生密钥,使公有区块链能够持有并处理机密数据。密钥具有以下特性:可验证(用户可以检查其正确性以及是否遭到篡改)、加密(派生密钥使用用户提供的传输密钥加密——节点和 canister 均无法看到原始密钥),以及门限(子网中的多个节点协作派生密钥;任何单方都不持有主密钥)。canister 向子网的门限基础设施请求派生密钥,并接收使用客户端传输公钥加密的密钥,只有客户端能在本地解密。这为去中心化密钥管理(DKMS)、链上加密存储、私密消息、基于身份的加密(IBE)、时间锁加密、门限 BLS 和可验证随机性等应用场景提供了支持。

前置条件

  • Rust:ic-vetkeys = "0.6"(crates.io)
  • Motoko:使用下方所示的原始管理 canister 方案
  • 前端:@dfinity/vetkeys v0.4.0

Canister ID

CanisterID用途
管理 Canisteraaaaa-aa提供 vetkd_public_key 和 vetkd_derive_key 系统 API
链密钥测试 canistervrqyr-saaaa-aaaan-qzn4q-cai**仅用于测试:**用于测试密钥派生的虚假 vetKD 实现,无需支付生产 API 费用。不安全,请勿用于生产环境。

管理 canister 并非真正的 canister,而是系统级 API 端点。对 aaaaa-aa 的调用会由系统路由到启用了 vetKD、且持有 key_id 中指定主密钥的子网;该子网的节点会运行门限密钥派生。你的 canister 可以从任意子网发起调用。

测试 canister:链密钥测试 canister 已部署在主网上,提供虚假的 vetKD 实现(密钥硬编码、不使用门限),让你无需支付生产环境的 cycles 费用即可测试密钥派生。请使用密钥名称 insecure_test_key_1。**不安全,仅供测试:**切勿在生产环境中或使用敏感数据时使用。你也可以从仓库部署自己的实例。

主密钥名称与 API 费用

IC 上的任何 canister 都可以使用任何可用的主密钥,无论 canister 或密钥位于哪个子网;管理 canister 会将调用路由到持有该主密钥的子网。

Key nameEnvironment用途Cycles(约)备注
test_key_1本地 + 主网开发与测试10_000_000_000(主网)可在本地和主网上使用。用于开发和测试。
key_1主网生产环境26_153_846_153子网 pzp6e(在 uzr34 上备份)

费用取决于主密钥所在的子网(及其大小),而非发起调用的 canister 所在子网。如果 canister 可能会被黑洞化或供其他 canister 使用,请发送多于当前费用的 cycles,以免子网未来扩容导致调用失败;未使用的 cycles 会退还。当前美元估算请参阅 vetKD API — API 费用。

核心概念

  • vetKey:由 (canister_id, context, input) 确定性派生的密钥材料。相同输入始终会产生相同的密钥。在本地解密之前,密钥始终使用客户端的传输密钥加密,因此 canister 和任何子网节点都无法看到原始密钥。
  • 传输密钥:由客户端生成的一对临时密钥。客户端会将公钥发送给 canister,以便 IC 加密派生出的密钥并将其交付。只有持有对应私钥的客户端才能解密结果。
  • Context:域分隔数据块。它会按使用场景(例如按功能或密钥用途)隔离派生子密钥,并防止同一 canister 内发生密钥冲突。可以将它视为命名空间。
  • Input:由应用定义、用于标识要派生的密钥的数据(例如用户 principal、文件 ID、聊天室 ID)。它以明文形式发送给管理 canister。仅将其用作标识符,切勿存放机密数据。
  • IBE(基于身份的加密):使用派生公钥向某个身份(例如 principal)加密的方案。vetKeys 让 IC 支持 IBE:任何人都可以使用 canister 的派生公钥加密发给某个 principal 的内容;只有该 principal 才能获取匹配的 vetKey 并解密。

会导致构建失败的错误

  1. 未锁定依赖项版本。 ic-vetkeys crate 和 @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(principal、文档 ID),切勿存放实际机密数据。

  6. 忘记 vetkd_derive_key 是异步的 canister 间调用。 该调用会消耗 cycles,并且需要使用 await。作为防御性实践,请在 await 之前保存 caller。

  7. 不一致地使用 context。 如果后端将 b"my_app_v1" 用作 context,而前端验证时使用 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 的 principal。若缺少此检查,任何调用方都可以请求任意派生密钥,并解密发给他人的消息。提供的 ic-vetkeys / @dfinity/vetkeys 库已正确处理此问题;请优先使用这些库,而不要自定义实现。

系统 API(Candid)

vetKD API 允许 canister 请求通过阈值协议派生的 vetKeys。派生过程是确定性的:相同的输入始终生成相同的密钥,因此可以可靠地获取密钥。不同的输入会生成不同的密钥,因此 canister 可以派生数量不限的唯一密钥。以下为概要;完整规范请参阅:vetKD API 和 IC 接口规范。

vetkd_public_key

返回用于验证通过 vetkd_derive_key 派生的密钥的公钥。使用空 context 时,会获得 canister 级别的主公钥;使用非空 context 时,会获得该 context 对应的派生子密钥。在 IBE 中,任何人都可以使用此公钥加密给某个身份(例如 principal);只有该身份的持有者之后才能获取匹配的 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),则返回调用方 canister的公钥;如果提供,则返回该 canister 的公钥。
  • context:域分隔符,与 vetkd_derive_key 中的含义相同。确保密钥在特定 context 中派生,避免不同应用或用例之间发生冲突。
  • key_id.curve:bls12_381_g2(唯一受支持的曲线)。
  • key_id.name:主密钥名称:test_key_1(本地测试 + 主网测试)或 key_1(生产环境)。

你也可以根据已知的主网主公钥离线派生此公钥;请参阅下文的“离线公钥派生”。

vetkd_derive_key

为给定的(context,input)派生密钥材料,并将其加密后返回,使用接收方的传输公钥进行加密。只有传输私钥的持有者才能解密。解密后的材料可根据你的用例使用(例如,通过 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:用作密钥标识符的任意数据;不同输入会生成不同的派生密钥。无需随机生成;会以明文形式发送给管理 canister。
  • context:域分隔符;必须与获取公钥时使用的 context 匹配(例如,用于验证或 IBE)。
  • transport_public_key:接收方的公钥;派生密钥会使用此公钥加密,以便安全传输。
  • 返回值:encrypted_key。使用传输私钥解密以获取原始 vetKey,然后按需使用(例如派生对称密钥;不要直接将原始字节用作 AES 密钥)。

主密钥名称和 cycle 费用请参阅 Canister ID 下的主密钥名称和 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
}

直接调用管理 canister(较底层):

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"

直接使用管理 canister:

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)

前端会生成一对传输密钥,将公钥部分发送给 canister,接收加密后的派生密钥,再使用传输私钥解密以获取 vetKey(原始密钥材料),然后从该材料派生对称密钥(例如,通过 toDerivedKeyMaterial()),用于 AES 或其他用途。

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 类型相对应,并会自动处理传输密钥流程。

离线公钥派生

你可以根据已知的 mainnet 主公钥,离线派生指定密钥名称(例如 key_1)的公钥(无需调用任何 canister)。这对 IBE 很有用:你可以为自己的上下文派生 canister 的公钥,然后加密给某个身份(例如一个 principal),而无需接收方或 canister 在线。

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
基于身份的加密(IBE)

IBE 允许你仅使用 canister 派生的公钥加密给某个身份(例如一个 principal),接收方无需在线,也无需事先注册密钥。接收方之后向 canister 验证身份,通过 vetkd_derive_key 获取为该身份派生的 vetKey,并在本地解密。

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 crate 和 TypeScript package 都提供了两个更高层的模块,为你处理传输密钥流程、访问控制和加密存储:

  • KeyManager<T: AccessControl>(Rust)/ KeyManager(TS)——管理具有稳定存储的访问控制型 vetKeys。canister 会强制执行谁可以请求哪些密钥;该库负责处理派生请求、用户权限(Read、ReadWrite、ReadWriteManage),以及 principal 之间的密钥共享。

  • EncryptedMaps<T: AccessControl>(Rust)/ EncryptedMaps(TS)——基于 KeyManager 构建,提供加密的键值存储。每个 map 都受访问控制,并使用派生的 vetKey 加密。值的加密和解密由客户端(前端)使用 vetKeys 处理;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")'
Mainnet
# 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

评论

支持 Markdown