mcpField
V
vetkeys

vetkeys

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

关于 vetkeys

这份开发参考介绍如何在 IC 上使用 vetKeys 与 vetKD 阈值密钥派生协议,面向 Rust、Motoko 和前端开发者。内容涵盖加密密钥交付、对称派生、IBE、BLS 签名及可验证随机数,并说明库的边界与常见问题。

主要功能

  1. 01加密阈值密钥交付
  2. 02对称密钥派生
  3. 03IBE 与离线公钥派生
  4. 04阈值 BLS 签名
  5. 05基于 VRF 的可验证随机数
  6. 06Rust、Motoko 与前端指南

适用场景

  1. 01派生用户或资源专属加密密钥
  2. 02为指定 Principal 加密消息
  3. 03实现截止时间后揭示数据
  4. 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 接口:

层RustMotoko前端
包ic-vetkeys 0.9 (crates.io)ic-vetkeys 0.6 (mops)@icp-sdk/vetkeys 0.7 (npm)
管理 APIic-cdk-management-canister、ic_vetkeys::management_canistermo: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 派生。)

注意事项

  1. 包或导入路径错误。 使用 @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 技能)。

  2. 不存在 toDerivedKeyMaterial()。 对于对称加密:使用 const dkm = await vetKey.asDerivedKeyMaterial(),然后调用 await dkm.encryptMessage(msg, domainSep, associatedData) / await dkm.decryptMessage(ct, domainSep, associatedData)(均为异步操作)。绝不要直接将解密后的原始 vetKey 字节用作 AES 密钥。

  3. 不要手动编写管理接口。 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_* 记录既无必要,又容易出错。

  4. Motoko 不支持底层加密。 Motoko 库不支持 IBE、传输密钥、MasterPublicKey/DerivedPublicKey 或 vetKey 解密。Motoko canister 返回加密后的 vetKey;其余工作由前端 @icp-sdk/vetkeys(或 Rust 链下客户端)完成。

  5. 确保 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 保持足够余额。

  6. context 和 input 必须在整个流程中保持一致。 不同的 context(或不同的 input)会产生不同的密钥;此时 decryptAndVerify 会抛出异常(Rust API 会返回错误),而不是返回错误明文 — 请处理这种情况,并确保公开密钥、派生以及客户端验证/解密使用的 context/input 字节完全一致。

  7. input 是明文。 它是发送给管理 canister 的密钥标识符 — 请使用 ID(principal、文档 ID),绝不要使用秘密数据。

  8. 在 await 之前捕获调用者。 在异步派生调用之前,先获取 ic_cdk::api::msg_caller()(Rust)/解构 ({ caller })(Motoko)。

  9. 手动实现派生时强制执行授权。 如果直接调用 vetkd_derive_key(而不是通过 KeyManager/EncryptedMaps),canister 必须确保只为调用者有权使用的 input 派生密钥(例如调用者自己的 principal)。否则,任何调用者都能获取任何人的密钥。

  10. BLS 使用未加密的密钥。 使用 sign_with_bls/signWithBls;不要将加密后的 vetKey 传入 BLS,也不要在 IBE 和 BLS 中重复使用相同的 context/input。

  11. Wasm 上的 Rust 随机性。 IBE 种子需要随机性;请添加 ic-dummy-getrandom-for-wasm(或 getrandom wasm shim),否则 canister 会触发陷阱。(前端使用 WebCrypto,没有问题。)

  12. 使用现代 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

评论

支持 Markdown