关于 vetkeys
这份开发参考介绍如何在 IC 上使用 vetKeys 与 vetKD 阈值密钥派生协议,面向 Rust、Motoko 和前端开发者。内容涵盖加密密钥交付、对称派生、IBE、BLS 签名及可验证随机数,并说明库的边界与常见问题。
主要功能
- 01加密阈值密钥交付
- 02对称密钥派生
- 03IBE 与离线公钥派生
- 04阈值 BLS 签名
- 05基于 VRF 的可验证随机数
- 06Rust、Motoko 与前端指南
适用场景
- 01派生用户或资源专属加密密钥
- 02为指定 Principal 加密消息
- 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 带来链上隐私:canister 请求由子网阈值密钥派生基础设施派生的密钥,并接收一个由客户端提供的传输密钥加密的密钥,只有客户端会在本地解密。子网节点永远不会看到原始密钥;在这种标准的客户端交付模式中,canister 也不会看到——它只会将仍处于加密状态的密钥转发给客户端。(有些流程会有意让 canister 自行获取密钥材料:阈值 BLS 签名和 canister 内的时间锁解密——详见相应章节。)派生是确定性的:相同的 (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 | — |
| 底层原语 | crate 根目录(ic_vetkeys::…) | —(不可用,见下文) | 包根目录(@icp-sdk/vetkeys) |
@dfinity/vetkeys已过时(冻结于 0.4.0)。该包在 0.5.0 时更名为@icp-sdk/vetkeys。前端 Agent/身份类型来自@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 完全相同。
另外还需要: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。
使用哪个 skill / 哪项功能
| 你想要…… | 使用 |
|---|---|
| 存储和共享加密的键值数据(密码管理器、笔记、保险库) | encrypted-maps skill(更高层级,从这里开始) |
| 加密给某个 principal,使得只有该 principal 能解密(消息传递) | IBE → references/ibe.md |
| 仅在截止时间之后公开数据(密封投标拍卖、时间锁) | 时间锁 IBE → references/ibe.md |
| 让 canister 生成任何人都能验证的签名 | 阈值 BLS → references/bls-signing.md |
| 派生每个用户/资源专用的对称(AES)密钥 | 对称派生 → 本文件 |
| 无需调用 canister 即可加密给某个 principal | 离线公钥派生 → 本文件 |
| 生成可在链上验证的随机数 | 可验证随机数(VRF) → 本文件 |
| 验证用户身份 / 登录 | 不使用 vetKeys——请使用 internet-identity skill |
核心概念
- context —— 域分隔数据块,用于在 canister 内为派生密钥创建命名空间(例如
"my_app",或"symmetric_key"这类按用途区分的值)。公钥调用、派生调用以及客户端验证/解密时的值必须完全相同,否则密钥将不匹配,decryptAndVerify会抛出异常(Rust API 会返回错误);因此应处理此失败情况,而不要假定操作一定成功。 - input —— 用于标识要派生哪一个密钥的应用数据(例如调用者 principal、文档 ID)。该数据会以明文发送给管理 canister,因此只能将其用作标识符,切勿用于存放机密数据。
- 传输密钥 —— 客户端为每个请求生成的一对临时密钥。公钥部分会发送出去,以便子网加密派生密钥并传送;只有持有私钥部分的一方才能解密。每次请求都要生成一个全新的密钥(
TransportSecretKey.random())。 - 加密与未加密的 vetKey —— IBE 和对称密钥派生使用加密传送流程(传输密钥 →
decryptAndVerify→VetKey)。阈值 BLS 则直接使用未加密的 vetKey;库中的sign_with_bls/signWithBls会处理这一过程——切勿将加密的 vetKey 输入 BLS。 - Motoko 的不对称之处 —— Motoko 的
ic-vetkeys库仅提供管理 API 和KeyManager/EncryptedMaps。它不提供 IBE、传输密钥、MasterPublicKey/DerivedPublicKey或 vetKey 解密功能。在 Motoko 应用中,canister 会返回加密的 vetKey,而前端(@icp-sdk/vetkeys)负责生成传输密钥、执行decryptAndVerify,以及进行 IBE 和对称密钥派生。这些原语仅在 Rust 和 TypeScript 中提供。
密钥名称与 cycles
| 密钥名称 | 位置 | vetkd_derive_key 费用 |
|---|---|---|
test_key_1 | 本地 + 主网(测试) | 10_000_000_000 |
key_1 | 本地 + 主网(生产) | 26_153_846_153 |
vetkd_public_key 免费;vetkd_derive_key 会消耗 cycles。test_key_1 和 key_1 在本地和主网上的行为相同。费用金额交由辅助函数处理:Rust 绑定会计算准确费用,Motoko 的 ManagementCanister 会附加 26_153_846_153,并退还任何多余部分——你只需确保 canister 有足够的 cycles。管理 canister 为 aaaaa-aa;调用会路由到持有主密钥的子网。
- Rust 从
#[init]参数中读取密钥名称(通过icp.yaml中的init_args传入)。 - Motoko 从 canister 环境变量
VETKD_KEY_NAME中读取密钥名称,默认为test_key_1。该名称会在首次安装时写入稳定状态,并在 canister 数据的整个生命周期内保持不可变——之后更改将被静默忽略(只有会清除状态的reinstall才会切换密钥)。由于test_key_1也是有效的主网密钥,如果生产部署时忘记设置VETKD_KEY_NAME,系统会静默使用该密钥——如有需要,请在部署时断言所用密钥符合预期。
vetKD 管理 API(基础功能 + 对称加密)
管理 API 有两个端点:vetkd_public_key(用于验证/离线加密的公钥)和 vetkd_derive_key(调用者的加密密钥)。它是对称加密、IBE 和 BLS 的基础。请通过库辅助函数调用,以确保 Candid 类型和 cycles 正确。
后端 — 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)
canister 会返回加密的 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"
离线公钥派生
从已知的主网主公钥开始,无需调用任何 canister即可派生 canister 在指定 context 下的公钥。可用于在接收方和 canister 均不在线时,对目标 principal 进行加密(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 canister 调用 ic_vetkeys::management_canister::compute_vrf(input, context, key_id) -> VrfOutput(将 input/context 限定到具体抽选,例如一次彩票抽奖或一次领导者选举),前端则使用 @icp-sdk/vetkeys 中的 VrfOutput.deserialize(...) 验证证明。目前还没有随附标准的端到端示例。(Motoko 库不支持 — 请通过 Rust canister 派生。)
注意事项
-
包或导入路径错误。 使用
@icp-sdk/vetkeys(≥0.7),不要使用@dfinity/vetkeys(版本冻结在 0.4)。从@icp-sdk/core导入 agent/identity(@icp-sdk/core/agent、@icp-sdk/core/principal),并通过await HttpAgent.create({ identity, host, rootKey })构建 agent — 客户端类接收的是已就绪的HttpAgent,而不是选项。从safeGetCanisterEnv()(@icp-sdk/core/agent/canister-env)获取rootKey;已发布的代码中绝不要调用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 类型,并会自动附加正确的 cycles。手动声明actor "aaaaa-aa"和vetkd_*记录既无必要,又容易出错。 -
Motoko 不支持底层加密。 Motoko 库不支持 IBE、传输密钥、
MasterPublicKey/DerivedPublicKey或 vetKey 解密。Motoko canister 返回加密后的 vetKey;其余工作由前端@icp-sdk/vetkeys(或 Rust 链下客户端)完成。 -
确保 canister 有足够的 cycles 用于派生。
vetkd_derive_key需要 cycles(key_1= 26_153_846_153,test_key_1= 10_000_000_000 — 本地和主网的费用相同);vetkd_public_key免费。让辅助函数自动附加费用 — Rust 绑定会计算精确费用,Motoko 辅助函数会附加 26_153_846_153,多余部分会退还 — 并确保 canister 保持足够余额。 -
context和input必须在整个流程中保持一致。 不同的context(或不同的input)会产生不同的密钥;此时decryptAndVerify会抛出异常(Rust API 会返回错误),而不是返回错误明文 — 请处理这种情况,并确保公开密钥、派生以及客户端验证/解密使用的context/input字节完全一致。 -
input是明文。 它是发送给管理 canister 的密钥标识符 — 请使用 ID(principal、文档 ID),绝不要使用秘密数据。 -
在
await之前捕获调用者。 在异步派生调用之前,先获取ic_cdk::api::msg_caller()(Rust)/解构({ caller })(Motoko)。 -
手动实现派生时强制执行授权。 如果直接调用
vetkd_derive_key(而不是通过KeyManager/EncryptedMaps),canister 必须确保只为调用者有权使用的input派生密钥(例如调用者自己的 principal)。否则,任何调用者都能获取任何人的密钥。 -
BLS 使用未加密的密钥。 使用
sign_with_bls/signWithBls;不要将加密后的 vetKey 传入 BLS,也不要在 IBE 和 BLS 中重复使用相同的context/input。 -
Wasm 上的 Rust 随机性。 IBE 种子需要随机性;请添加
ic-dummy-getrandom-for-wasm(或getrandomwasm shim),否则 canister 会触发陷阱。(前端使用 WebCrypto,没有问题。) -
使用现代 ic-cdk 调用 API。 使用
ic_cdk::api::msg_caller()(而不是ic_cdk::caller())。如果确实需要在不使用绑定的情况下调用管理 canister,请使用ic_cdk::call::Call::unbounded_wait(..).with_cycles(..)— 旧版ic_cdk::api::call::call*API 已在 ic-cdk 0.20+ 中移除。
功能指南
- 基于身份的加密(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