Install
$ agentstack add skill-dfinity-icskills-vetkd ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
vetKeys (Verifiable Encrypted Threshold Keys)
> Note: vetKeys is a newer feature of the IC. The ic-vetkeys Rust crate and @dfinity/vetkeys > npm package are published, but the APIs may still change over time. > Pin your dependency versions and check the DFINITY forum for any migration guides after upgrades.
What This Is
vetKeys (verifiably encrypted threshold keys) bring on-chain privacy to the IC via the vetKD protocol: secure, on-demand key derivation so that a public blockchain can hold and work with secret data. Keys are verifiable (users can check correctness and lack of tampering), encrypted (derived keys are encrypted under a user-supplied transport key—no node or canister ever sees the raw key), and threshold (a quorum of subnet nodes cooperates to derive keys; no single party has the master key). A canister requests a derived key from the subnet’s threshold infrastructure, receives it encrypted under the client’s transport public key, and only the client decrypts it locally. This unlocks decentralized key management (DKMS), encrypted on-chain storage, private messaging, identity-based encryption (IBE), timelock encryption, threshold BLS, and verifiable randomness—use cases.
Prerequisites
- Rust:
ic-vetkeys = "0.6"(crates.io) - Motoko: Use the raw management canister approach shown below
- Frontend:
@dfinity/vetkeysv0.4.0
Canister IDs
| Canister | ID | Purpose | |----------|-----|---------| | Management Canister | aaaaa-aa | Exposes vetkd_public_key and vetkd_derive_key system APIs | | Chain-key testing canister | vrqyr-saaaa-aaaan-qzn4q-cai | Testing only: fake vetKD implementation to test key derivation without paying production API fees. Insecure, do not use in production. |
The management canister is not a real canister, it is a system-level API endpoint. Calls to aaaaa-aa are routed by the system to the vetKD-enabled subnet that holds the master key specified in key_id; that subnet's nodes run the threshold key derivation. Your canister can call from any subnet.
Testing canister: The chain-key testing canister is deployed on mainnet and provides a fake vetKD implementation (hard-coded keys, no threshold) so you can exercise key derivation without production cycle costs. Use key name insecure_test_key_1. Insecure, for testing only: never use it in production or with sensitive data. You can also deploy your own instance from the repo.
Master Key Names and API Fees
Any canister on the IC can use any available master key regardless of which subnet the canister or the key resides on; the management canister routes calls to the subnet that holds the master key.
| Key name | Environment | Purpose | Cycles (approx.) | Notes | |----------------|------------------|-------------------|--------------------|-------| | test_key_1 | Local + Mainnet | Development & testing | 10000000000 (mainnet) | Works both locally and on mainnet. Use for development and testing. | | key_1 | Mainnet | Production | 26153846153 | Subnet pzp6e (backed up on uzr34) |
Fees depend on the subnet where the master key resides (and its size), not on the calling canister's subnet. If the canister may be blackholed or used by other canisters, send more cycles than the current cost so that future subnet size increases do not cause calls to fail; unused cycles are refunded. See vetKD API — API fees for current USD estimates.
Key Concepts
- vetKey: Key material derived deterministically from
(canister_id, context, input). Same inputs always produce the same key. Neither the canister nor any subnet node ever sees the raw key, as it is encrypted under the client's transport key until decrypted locally. - Transport key: An ephemeral key pair generated by the client. The public key is sent to the canister so the IC can encrypt the derived key for delivery. Only the client holding the corresponding private key can decrypt the result.
- Context: A domain separator blob. Isolates derived subkeys per use case (e.g. per feature or key purpose) and prevents key collisions within the same canister. Think of it as a namespace.
- Input: Application-defined data that identifies which key to derive (e.g. user principal, file ID, chat room ID). It is sent in plaintext to the management canister. Use it only as an identifier, never for secret data.
- IBE (Identity-Based Encryption): A scheme where you encrypt to an identity (e.g. a principal) using a derived public key. vetKeys enables IBE on the IC: anyone can encrypt to a principal using the canister's derived public key; only that principal can obtain the matching vetKey and decrypt.
Mistakes That Break Your Build
- Not pinning dependency versions. The
ic-vetkeyscrate and@dfinity/vetkeysnpm package are published, but the APIs may still change in new releases. Pin your versions and re-test after upgrades. If something stops working after an upgrade, consult the relevant change notes to understand what happened.
- Reusing transport keys across sessions. Each session must generate a fresh transport key pair. The Rust and TypeScript libraries include support for generating keys safely; use them if at all possible.
- Using raw
vetkd_derive_keyoutput as an encryption key. The output is an encrypted blob. You must decrypt it with the transport secret to get the vetKey (raw key material). What you do next depends on your use case: for example, you might derive a symmetric key (e.g. for AES) viatoDerivedKeyMaterial()or the equivalent. Do not use the decrypted bytes directly as an AES key. Other uses (IBE decryption, signing, etc.) consume the vetKey in their own way; the libraries document the right pattern for each.
- Confusing vetKD with traditional public-key crypto. There are no static key pairs per user. Keys are derived on-demand from the subnet's threshold master key (via the vetKD protocol). The same (canister, context, input) always yields the same derived key.
- Putting secret data in the
inputfield. The input is sent to the management canister in plaintext. It is a key identifier, not encrypted payload. Use it for IDs (principal, document ID), never for the actual secret data.
- Forgetting that
vetkd_derive_keyis an async inter-canister call. It costs cycles and requiresawait. Capturecallerbefore the await as defensive practice.
- Using
contextinconsistently. If the backend usesb"my_app_v1"as context but the frontend verification usesb"my_app", the derived keys will not match and decryption will silently fail.
- Not attaching enough cycles to
vetkd_derive_key.vetkd_derive_keyconsumes cycles;vetkd_public_keydoes not. For derive_key,key_1costs ~26B cycles andtest_key_1costs ~10B cycles.
- Rolling your own IBE without proper authorization checks. If you implement IBE manually (bypassing
KeyManager/EncryptedMaps), your canister must enforce thatvetkd_derive_keyonly returns the derived key to the authorized caller — e.g. the principal whose identity was used as theinput. Without this check, any caller can request any derived key and decrypt messages meant for someone else. The providedic-vetkeys/@dfinity/vetkeyslibraries handle this correctly; prefer them over a custom implementation.
System API (Candid)
The vetKD API lets canisters request vetKeys derived by the threshold protocol. Derivation is deterministic: the same inputs always produce the same key, so keys can be retrieved reliably. Different inputs yield different keys—canisters can derive an unlimited number of unique keys. Summary below; full spec: vetKD API and the IC interface specification.
vetkdpublickey
Returns a public key used to verify keys derived with vetkd_derive_key. With an empty context you get the canister-level master public key; with a non-empty context you get the derived subkey for that context. In IBE, this public key lets anyone encrypt to an identity (e.g. a principal); only the holder of that identity can later obtain the matching vetKey and decrypt—no prior key exchange or recipient presence required.
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: Optional. If omitted (null), the public key for the calling canister is returned; if provided, the key for that canister is returned.context: Domain separator which has the same meaning as invetkd_derive_key. Ensures keys are derived in a specific context and avoids collisions across apps or use cases.key_id.curve:bls12_381_g2(only supported curve).key_id.name: Master key name:test_key_1(local + mainnet testing) orkey_1(production).
You can also derive this public key offline from the known mainnet master public key; see "Offline Public Key Derivation" below.
vetkdderivekey
Derives key material for the given (context, input) and returns it encrypted under the recipient's transport public key. Only the holder of the transport secret can decrypt. The decrypted material is then used according to your use case (e.g. via toDerivedKeyMaterial() for symmetric keys, or for IBE decryption).
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: Arbitrary data used as the key identifier—different inputs yield different derived keys. Does not need to be random; sent in plaintext to the management canister.context: Domain separator; must match the context used when obtaining the public key (e.g. for verification or IBE).transport_public_key: The recipient's public key; the derived key is encrypted under this for secure delivery.- Returns:
encrypted_key. Decrypt with the transport secret to get the raw vetKey, then use it as required (e.g. derive a symmetric key; do not use raw bytes directly as an AES key).
Master key names and cycle costs are in Master Key Names and API Fees under Canister IDs.
Implementation
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"
Using ic-vetkeys library (recommended):
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> =
std::cell::RefCell::new(MemoryManager::init(DefaultMemoryImpl::default()));
static KEY_MANAGER: std::cell::RefCell>> =
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, transport_public_key: Vec) -> Vec {
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 {
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
}
Calling management canister directly (lower level):
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,
context: Vec,
key_id: VetKdKeyId,
}
#[derive(CandidType, Deserialize)]
struct VetKdPublicKeyResponse {
public_key: Vec,
}
#[derive(CandidType)]
struct VetKdDeriveKeyRequest {
input: Vec,
context: Vec,
transport_public_key: Vec,
key_id: VetKdKeyId,
}
#[derive(CandidType, Deserialize)]
struct VetKdDeriveKeyResponse {
encrypted_key: Vec,
}
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 {
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) -> Vec {
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"
Using the management canister directly:
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 = {
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [dfinity](https://github.com/dfinity)
- **Source:** [dfinity/icskills](https://github.com/dfinity/icskills)
- **License:** Apache-2.0
- **Homepage:** https://skills.internetcomputer.org
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.