关于 vetkd
vetkd 是面向 Internet Computer Canister 与前端开发者的 vetKeys 和阈值密钥派生指南,介绍加密密钥交付、实现方式及安全注意事项。
主要功能
- 01介绍阈值密钥派生
- 02涵盖 Rust、Motoko 和 TypeScript
- 03说明传输密钥加密
- 04记录 API 费用与密钥名称
- 05提示实现中的安全风险
适用场景
- 01构建链上加密存储
- 02创建私密消息系统
- 03实现基于身份的加密
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dfinity/ icskills --skill vetkd安装后在 Claude Code 中输入/vetkd确认已加载。
详细介绍
vetKeys(可验证加密门限密钥)
**注意:**vetKeys 是 IC 的一项较新功能。
ic-vetkeysRust crate 和@dfinity/vetkeysnpm package 已发布,但 API 仍可能随时间变化。 请固定依赖版本,并在升级后查看 DFINITY 论坛 中的迁移指南。
功能简介
vetKeys(可验证加密门限密钥)通过 vetKD 协议为 IC 带来链上隐私:安全、按需派生密钥,使公有区块链能够持有并处理机密数据。密钥具有以下特性:可验证(用户可以检查其正确性以及是否遭到篡改)、加密(派生密钥使用用户提供的传输密钥加密——节点和 canister 均无法看到原始密钥),以及门限(子网中的多个节点协作派生密钥;任何单方都不持有主密钥)。canister 向子网的门限基础设施请求派生密钥,并接收使用客户端传输公钥加密的密钥,只有客户端能在本地解密。这为去中心化密钥管理(DKMS)、链上加密存储、私密消息、基于身份的加密(IBE)、时间锁加密、门限 BLS 和可验证随机性等应用场景提供了支持。
前置条件
- Rust:
ic-vetkeys = "0.6"(crates.io) - Motoko:使用下方所示的原始管理 canister 方案
- 前端:
@dfinity/vetkeysv0.4.0
Canister ID
| Canister | ID | 用途 |
|---|---|---|
| 管理 Canister | aaaaa-aa | 提供 vetkd_public_key 和 vetkd_derive_key 系统 API |
| 链密钥测试 canister | vrqyr-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 name | Environment | 用途 | 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 并解密。
会导致构建失败的错误
-
未锁定依赖项版本。
ic-vetkeyscrate 和@dfinity/vetkeysnpm 包均已发布,但新版本中的 API 仍可能发生变化。请锁定版本,并在升级后重新测试。如果升级后出现问题,请查阅相关变更说明,了解发生了什么。 -
跨会话重复使用传输密钥。 每个会话都必须生成一对新的传输密钥。Rust 和 TypeScript 库支持安全生成密钥;请尽可能使用这些功能。
-
将原始
vetkd_derive_key输出用作加密密钥。 输出是一个加密数据块。必须使用传输密钥对应的私钥解密,才能获得 vetKey(原始密钥材料)。接下来的处理取决于使用场景:例如,可以通过toDerivedKeyMaterial()或同等方法派生对称密钥(例如用于 AES)。不要直接将解密后的字节用作 AES 密钥。其他用途(IBE 解密、签名等)会以各自的方式使用 vetKey;相关库都记录了对应的正确用法。 -
将 vetKD 与传统公钥密码学混为一谈。 用户没有静态密钥对。密钥是通过 vetKD 协议从子网的阈值主密钥按需派生的。相同的 (canister、context、input) 始终会产生相同的派生密钥。
-
在
input字段中放入机密数据。 input 以明文形式发送给管理 canister。它是密钥标识符,而不是加密载荷。请用它存放 ID(principal、文档 ID),切勿存放实际机密数据。 -
忘记
vetkd_derive_key是异步的 canister 间调用。 该调用会消耗 cycles,并且需要使用await。作为防御性实践,请在await之前保存caller。 -
不一致地使用
context。 如果后端将b"my_app_v1"用作 context,而前端验证时使用b"my_app",派生密钥就会不匹配,解密也会静默失败。 -
为
vetkd_derive_key附加的 cycles 不足。vetkd_derive_key会消耗 cycles;vetkd_public_key不会。对于 derive_key,key_1的成本约为 26B cycles,test_key_1的成本约为 10B cycles。 -
未经适当的授权检查就自行实现 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