certified-variables

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Certified Variables & Certified Assets

认证变量与认证资产

What This Is

这是什么

Query responses on the Internet Computer come from a single replica and are NOT verified by consensus. A malicious or faulty replica could return fabricated data. Certification solves this: the canister stores a hash in the subnet's certified state tree during update calls, and then query responses include a certificate signed by the subnet's threshold BLS key proving the data is authentic. The result is responses that are both fast (no consensus delay) AND cryptographically verified.
在Internet Computer(IC)上,查询响应来自单个副本,且未经过共识验证。恶意或故障的副本可能返回伪造的数据。认证机制解决了这个问题:在更新调用期间,canister会将一个哈希值存储在子网的认证状态树中,之后查询响应会包含由子网的门限BLS密钥签名的证书,以证明数据的真实性。这样得到的响应既快速(无共识延迟)又经过加密验证。

Prerequisites

前置条件

  • Rust:
    ic-certified-map
    crate (for Merkle tree),
    ic-cdk
    (for
    certified_data_set
    /
    data_certificate
    )
  • Motoko:
    CertifiedData
    module (included in mo:core/mo:base),
    ic-certification
    package (
    mops add ic-certification
    ) for Merkle tree with witness support
  • Frontend:
    @icp-sdk/core
    (agent, principal),
    @dfinity/certificate-verification
  • Rust:
    ic-certified-map
    crate(用于默克尔树)、
    ic-cdk
    (用于
    certified_data_set
    /
    data_certificate
  • Motoko:
    CertifiedData
    模块(包含在mo:core/mo:base中)、
    ic-certification
    包(执行
    mops add ic-certification
    安装,用于支持见证的默克尔树)
  • 前端:
    @icp-sdk/core
    (Agent、Principal)、
    @dfinity/certificate-verification

Canister IDs

Canister ID

No external canister IDs required. Certification uses the IC system API exposed through CDK wrappers:
  • ic_cdk::api::certified_data_set
    (Rust) /
    CertifiedData.set
    (Motoko) -- called during update calls to set the certified hash (max 32 bytes)
  • ic_cdk::api::data_certificate
    (Rust) /
    CertifiedData.getCertificate
    (Motoko) -- called during query calls to retrieve the subnet certificate
The IC root public key (needed for client-side verification):
  • Mainnet:
    308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c05030201036100814c0e6ec71fab583b08bd81373c255c3c371b2e84863c98a4f1e08b74235d14fb5d9c0cd546d9685f913a0c0b2cc5341583bf4b4392e467db96d65b9bb4cb717112f8472e0d5a4d14505ffd7484b01291091c5f87b98883463f98091a0baaae
  • Local: available from
    icp
    (agent handles this automatically)
无需外部Canister ID。认证功能使用通过CDK封装暴露的IC系统API:
  • ic_cdk::api::certified_data_set
    (Rust)/
    CertifiedData.set
    (Motoko)——在更新调用期间调用,用于设置认证哈希(最大32字节)
  • ic_cdk::api::data_certificate
    (Rust)/
    CertifiedData.getCertificate
    (Motoko)——在查询调用期间调用,用于获取子网证书
客户端验证所需的IC根公钥:
  • 主网:
    308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c05030201036100814c0e6ec71fab583b08bd81373c255c3c371b2e84863c98a4f1e08b74235d14fb5d9c0cd546d9685f913a0c0b2cc5341583bf4b4392e467db96d65b9bb4cb717112f8472e0d5a4d14505ffd7484b01291091c5f87b98883463f98091a0baaae
  • 本地环境:可从
    icp
    获取(Agent会自动处理)

Mistakes That Break Your Build

导致构建失败的常见错误

  1. Trying to store more than 32 bytes of certified data. The
    certified_data_set
    API accepts exactly one blob of at most 32 bytes. You cannot certify arbitrary data directly. Instead, build a Merkle tree over your data and certify only the root hash (32 bytes). The tree structure provides proofs for individual values.
  2. Calling
    certified_data_set
    in a query call.
    Certification can ONLY be set during update calls (which go through consensus). Calling it in a query traps. Pattern: set the hash during writes, read the certificate during queries.
  3. Forgetting to include the certificate in query responses. The certificate is obtained via
    data_certificate()
    during query calls. If you return data without the certificate, clients cannot verify anything. Always return a tuple of (data, certificate, witness).
  4. Not updating the certified hash after data changes. If you modify the data but forget to call
    certified_data_set
    with the new root hash, query responses will fail verification because the certificate proves a stale hash.
  5. Building the witness for the wrong key. The witness (Merkle proof) must correspond to the exact key being queried. A witness for key "users/alice" will not verify key "users/bob".
  6. Assuming
    data_certificate()
    returns a value in update calls.
    It returns
    null
    /
    None
    during update calls. Certificates are only available during query calls.
  7. Certifying data at canister init but not on upgrades. After a canister upgrade, the certified data is cleared. You must call
    certified_data_set
    in both
    #[init]
    and
    #[post_upgrade]
    (Rust) or
    system func postupgrade
    (Motoko) to re-establish certification.
  8. Not validating certificate freshness on the client. The certificate's state tree contains a
    /time
    field with the timestamp when the subnet produced it. Clients MUST check that this timestamp is recent (recommended: within 5 minutes of current time). Without this check, an attacker could replay a stale certificate with outdated data. Always verify
    certificate_time
    is within an acceptable delta before trusting the response.
  1. 尝试存储超过32字节的认证数据
    certified_data_set
    API仅接受最多32字节的单个二进制数据块,无法直接认证任意数据。正确做法是在数据之上构建默克尔树,仅认证根哈希(32字节),通过树结构为单个值提供证明。
  2. 在查询调用中调用
    certified_data_set
    。认证只能在更新调用期间设置(更新调用需经过共识),在查询中调用会触发陷阱。正确模式:写入时设置哈希,查询时读取证书。
  3. 查询响应中未包含证书。证书需在查询调用期间通过
    data_certificate()
    获取。如果返回数据时不附带证书,客户端将无法进行任何验证。务必返回(数据、证书、见证)的元组。
  4. 数据变更后未更新认证哈希。如果修改了数据但忘记使用新的根哈希调用
    certified_data_set
    ,查询响应的验证会失败,因为证书证明的是过时的哈希。
  5. 为错误的密钥构建见证。见证(默克尔证明)必须与被查询的密钥完全对应。为密钥"users/alice"生成的见证无法验证密钥"users/bob"。
  6. 假设
    data_certificate()
    在更新调用中返回有效值
    。在更新调用中它会返回
    null
    /
    None
    ,证书仅在查询调用中可用。
  7. 在Canister初始化时认证数据,但升级时未重新认证。Canister升级后,认证数据会被清除。必须在
    #[init]
    #[post_upgrade]
    (Rust)或
    system func postupgrade
    (Motoko)中调用
    certified_data_set
    ,重新建立认证。
  8. 客户端未验证证书的新鲜度。证书的状态树包含
    /time
    字段,记录子网生成证书的时间戳。客户端必须检查该时间戳是否为近期(建议:当前时间的5分钟内)。如果不进行此检查,攻击者可能会重放包含过时数据的旧证书。在信任响应前,务必验证
    certificate_time
    是否在可接受的时间差范围内。

How Certification Works

认证机制的工作原理

UPDATE CALL (goes through consensus):
  1. Canister modifies data
  2. Canister builds/updates Merkle tree
  3. Canister calls certified_data_set(root_hash)  -- 32 bytes
  4. Subnet includes root_hash in its certified state tree

QUERY CALL (single replica, no consensus):
  1. Client sends query
  2. Canister calls data_certificate() -- gets subnet BLS signature
  3. Canister builds witness (Merkle proof) for the requested key
  4. Canister returns: { data, certificate, witness }

CLIENT VERIFICATION:
  1. Verify certificate signature against IC root public key
  2. Extract root_hash from certificate's state tree
  3. Verify witness: root_hash + witness proves data is in the tree
  4. Trust the data
更新调用(需经过共识):
  1. Canister修改数据
  2. Canister构建/更新默克尔树
  3. Canister调用certified_data_set(root_hash) —— 32字节
  4. 子网将root_hash纳入其认证状态树

查询调用(单个副本,无需共识):
  1. 客户端发送查询请求
  2. Canister调用data_certificate() —— 获取子网BLS签名
  3. Canister为请求的密钥构建见证(默克尔证明)
  4. Canister返回:{ data, certificate, witness }

客户端验证:
  1. 验证证书签名是否匹配IC根公钥
  2. 从证书的状态树中提取root_hash
  3. 验证见证:root_hash + 见证证明数据存在于树中
  4. 信任该数据

Implementation

实现示例

Rust

Rust

Cargo.toml:
toml
[package]
name = "certified_vars_backend"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
candid = "0.10"
ic-cdk = "0.19"
ic-certified-map = "0.4"
serde = { version = "1", features = ["derive"] }
serde_bytes = "0.11"
ciborium = "0.2"
Complete certified key-value store:
rust
use candid::{CandidType, Deserialize};
use ic_cdk::{init, post_upgrade, query, update};
use ic_certified_map::{AsHashTree, RbTree};
use serde_bytes::ByteBuf;
use std::cell::RefCell;

thread_local! {
    // RbTree is a Merkle-tree-backed map: keys and values are byte slices
    static TREE: RefCell<RbTree<Vec<u8>, Vec<u8>>> = RefCell::new(RbTree::new());
}

// Update the certified data hash after any modification
fn update_certified_data() {
    TREE.with(|tree| {
        let tree = tree.borrow();
        // root_hash() returns a 32-byte SHA-256 hash of the entire tree
        ic_cdk::api::certified_data_set(&tree.root_hash());
    });
}

#[init]
fn init() {
    update_certified_data();
}

#[post_upgrade]
fn post_upgrade() {
    // Assumes data has already been deserialized from stable memory into the TREE.
    // CRITICAL: re-establish certification after upgrade — certified_data is cleared on upgrade.
    update_certified_data();
}

#[update]
fn set(key: String, value: String) {
    TREE.with(|tree| {
        let mut tree = tree.borrow_mut();
        tree.insert(key.as_bytes().to_vec(), value.as_bytes().to_vec());
    });
    // Must update certified hash after every data change
    update_certified_data();
}

#[update]
fn delete(key: String) {
    TREE.with(|tree| {
        let mut tree = tree.borrow_mut();
        tree.delete(key.as_bytes());
    });
    update_certified_data();
}

#[derive(CandidType, Deserialize)]
struct CertifiedResponse {
    value: Option<String>,
    certificate: ByteBuf,      // subnet BLS signature
    witness: ByteBuf,          // Merkle proof for this key
}

#[query]
fn get(key: String) -> CertifiedResponse {
    // data_certificate() is only available in query calls
    let certificate = ic_cdk::api::data_certificate()
        .expect("data_certificate only available in query calls");

    TREE.with(|tree| {
        let tree = tree.borrow();

        // Look up the value
        let value = tree.get(key.as_bytes())
            .map(|v| String::from_utf8(v.clone()).unwrap());

        // Build a witness (Merkle proof) for this specific key
        let witness = tree.witness(key.as_bytes());

        // Serialize the witness as CBOR
        let mut witness_buf = vec![];
        ciborium::into_writer(&witness, &mut witness_buf)
            .expect("Failed to serialize witness as CBOR");

        CertifiedResponse {
            value,
            certificate: ByteBuf::from(certificate),
            witness: ByteBuf::from(witness_buf),
        }
    })
}

// Batch set multiple values in one update call (more efficient)
#[update]
fn set_many(entries: Vec<(String, String)>) {
    TREE.with(|tree| {
        let mut tree = tree.borrow_mut();
        for (key, value) in entries {
            tree.insert(key.as_bytes().to_vec(), value.as_bytes().to_vec());
        }
    });
    // Single certification update for all changes
    update_certified_data();
}
Cargo.toml:
toml
[package]
name = "certified_vars_backend"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
candid = "0.10"
ic-cdk = "0.19"
ic-certified-map = "0.4"
serde = { version = "1", features = ["derive"] }
serde_bytes = "0.11"
ciborium = "0.2"
完整的认证键值存储:
rust
use candid::{CandidType, Deserialize};
use ic_cdk::{init, post_upgrade, query, update};
use ic_certified_map::{AsHashTree, RbTree};
use serde_bytes::ByteBuf;
use std::cell::RefCell;

thread_local! {
    // RbTree是基于默克尔树的映射:键和值均为字节切片
    static TREE: RefCell<RbTree<Vec<u8>, Vec<u8>>> = RefCell::new(RbTree::new());
}

// 任何修改后更新认证数据哈希
fn update_certified_data() {
    TREE.with(|tree| {
        let tree = tree.borrow();
        // root_hash()返回整个树的32字节SHA-256哈希
        ic_cdk::api::certified_data_set(&tree.root_hash());
    });
}

#[init]
fn init() {
    update_certified_data();
}

#[post_upgrade]
fn post_upgrade() {
    // 假设数据已从稳定内存反序列化到TREE中
    // 关键:升级后重新建立认证——certified_data在升级时会被清除
    update_certified_data();
}

#[update]
fn set(key: String, value: String) {
    TREE.with(|tree| {
        let mut tree = tree.borrow_mut();
        tree.insert(key.as_bytes().to_vec(), value.as_bytes().to_vec());
    });
    // 每次数据变更后必须更新认证哈希
    update_certified_data();
}

#[update]
fn delete(key: String) {
    TREE.with(|tree| {
        let mut tree = tree.borrow_mut();
        tree.delete(key.as_bytes());
    });
    update_certified_data();
}

#[derive(CandidType, Deserialize)]
struct CertifiedResponse {
    value: Option<String>,
    certificate: ByteBuf,      // 子网BLS签名
    witness: ByteBuf,          // 该密钥的默克尔证明
}

#[query]
fn get(key: String) -> CertifiedResponse {
    // data_certificate()仅在查询调用中可用
    let certificate = ic_cdk::api::data_certificate()
        .expect("data_certificate仅在查询调用中可用");

    TREE.with(|tree| {
        let tree = tree.borrow();

        // 查找值
        let value = tree.get(key.as_bytes())
            .map(|v| String::from_utf8(v.clone()).unwrap());

        // 为该特定密钥构建见证(默克尔证明)
        let witness = tree.witness(key.as_bytes());

        // 将见证序列化为CBOR
        let mut witness_buf = vec![];
        ciborium::into_writer(&witness, &mut witness_buf)
            .expect("见证序列化为CBOR失败");

        CertifiedResponse {
            value,
            certificate: ByteBuf::from(certificate),
            witness: ByteBuf::from(witness_buf),
        }
    })
}

// 在单个更新调用中批量设置多个值(更高效)
#[update]
fn set_many(entries: Vec<(String, String)>) {
    TREE.with(|tree| {
        let mut tree = tree.borrow_mut();
        for (key, value) in entries {
            tree.insert(key.as_bytes().to_vec(), value.as_bytes().to_vec());
        }
    });
    // 所有变更完成后单次更新认证
    update_certified_data();
}

HTTP Certification (v2) for Custom HTTP Canisters

自定义HTTP Canister的HTTP认证(v2)

For canisters serving HTTP responses directly (not through the asset canister), responses must be certified so the HTTP gateway can verify them.
Additional Cargo.toml dependency:
toml
[package]
name = "http_certified_backend"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
ic-http-certification = "3.1"
Certifying HTTP responses:
Note: The HTTP certification API is evolving rapidly. Verify these examples against the latest ic-http-certification docs before use.
rust
use ic_http_certification::{
    HttpCertification, HttpCertificationPath, HttpCertificationTree,
    HttpCertificationTreeEntry, HttpRequest, HttpResponse,
    DefaultCelBuilder, DefaultResponseCertification,
};
use std::cell::RefCell;

thread_local! {
    static HTTP_TREE: RefCell<HttpCertificationTree> = RefCell::new(
        HttpCertificationTree::default()
    );
}

// Define what gets certified using CEL (Common Expression Language)
fn certify_response(path: &str, request: &HttpRequest, response: &HttpResponse) {
    // Full certification: certify both request path and response body
    let cel = DefaultCelBuilder::full_certification()
        .with_response_certification(DefaultResponseCertification::certified_response_headers(
            vec!["Content-Type", "Content-Length"],
        ))
        .build();

    // Create the certification from the CEL expression, request, and response
    let certification = HttpCertification::full(&cel, request, response, None)
        .expect("Failed to create HTTP certification");

    let http_path = HttpCertificationPath::exact(path);

    HTTP_TREE.with(|tree| {
        let mut tree = tree.borrow_mut();
        let entry = HttpCertificationTreeEntry::new(http_path, certification);
        tree.insert(&entry);

        // Update canister certified data with tree root hash
        ic_cdk::api::certified_data_set(&tree.root_hash());
    });
}
对于直接提供HTTP响应的Canister(不通过资产Canister),响应必须经过认证,以便HTTP网关进行验证。
额外的Cargo.toml依赖:
toml
[package]
name = "http_certified_backend"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
ic-http-certification = "3.1"
HTTP响应认证:
注意: HTTP认证API正在快速演进。使用前请对照最新的ic-http-certification文档验证示例代码。
rust
use ic_http_certification::{
    HttpCertification, HttpCertificationPath, HttpCertificationTree,
    HttpCertificationTreeEntry, HttpRequest, HttpResponse,
    DefaultCelBuilder, DefaultResponseCertification,
};
use std::cell::RefCell;

thread_local! {
    static HTTP_TREE: RefCell<HttpCertificationTree> = RefCell::new(
        HttpCertificationTree::default()
    );
}

// 使用CEL(通用表达式语言)定义需要认证的内容
fn certify_response(path: &str, request: &HttpRequest, response: &HttpResponse) {
    // 完整认证:同时认证请求路径和响应体
    let cel = DefaultCelBuilder::full_certification()
        .with_response_certification(DefaultResponseCertification::certified_response_headers(
            vec!["Content-Type", "Content-Length"],
        ))
        .build();

    // 从CEL表达式、请求和响应创建认证信息
    let certification = HttpCertification::full(&cel, request, response, None)
        .expect("创建HTTP认证失败");

    let http_path = HttpCertificationPath::exact(path);

    HTTP_TREE.with(|tree| {
        let mut tree = tree.borrow_mut();
        let entry = HttpCertificationTreeEntry::new(http_path, certification);
        tree.insert(&entry);

        // 使用树的根哈希更新Canister认证数据
        ic_cdk::api::certified_data_set(&tree.root_hash());
    });
}

Motoko

Motoko

Using CertifiedData module:
motoko
import CertifiedData "mo:core/CertifiedData";
import Blob "mo:core/Blob";
import Nat8 "mo:core/Nat8";
import Text "mo:core/Text";
import Map "mo:core/Map";
import Array "mo:core/Array";
import Iter "mo:core/Iter";
// Requires: mops add sha2
import Sha256 "mo:sha2/Sha256";

persistent actor {

  // Simple certified single-value example:
  var certifiedValue : Text = "";

  // Set a certified value (update call only)
  public func setCertifiedValue(value : Text) : async () {
    certifiedValue := value;
    // Hash the value and set as certified data (max 32 bytes)
    let hash = Sha256.fromBlob(#sha256, Text.encodeUtf8(value));
    CertifiedData.set(hash);
  };

  // Get the certified value with its certificate (query call)
  public query func getCertifiedValue() : async {
    value : Text;
    certificate : ?Blob;
  } {
    {
      value = certifiedValue;
      certificate = CertifiedData.getCertificate();
    }
  };
};
Certified key-value store with Merkle tree (advanced):
For certifying multiple values with per-key witnesses, use the
ic-certification
mops package (
mops add ic-certification
). It provides a real Merkle tree (
CertTree
) that can generate proofs for individual keys:
motoko
import CertifiedData "mo:core/CertifiedData";
import Blob "mo:core/Blob";
import Text "mo:core/Text";
// Requires: mops add ic-certification
import CertTree "mo:ic-certification/CertTree";

persistent actor {

  // CertTree.Store is stable -- persists across upgrades
  let certStore : CertTree.Store = CertTree.newStore();
  let ct = CertTree.Ops(certStore);

  // Set certified data on init
  ct.setCertifiedData();

  // Set a key-value pair and update certification
  public func set(key : Text, value : Text) : async () {
    ct.put([Text.encodeUtf8(key)], Text.encodeUtf8(value));
    // CRITICAL: call after every mutation to update the subnet-certified root hash
    ct.setCertifiedData();
  };

  // Delete a key and update certification
  public func remove(key : Text) : async () {
    ct.delete([Text.encodeUtf8(key)]);
    ct.setCertifiedData();
  };

  // Query with certificate and Merkle witness for the requested key
  public query func get(key : Text) : async {
    value : ?Blob;
    certificate : ?Blob;
    witness : Blob;
  } {
    let path = [Text.encodeUtf8(key)];
    // reveal() generates a Merkle proof for this specific path
    let witness = ct.reveal(path);
    {
      value = ct.lookup(path);
      certificate = CertifiedData.getCertificate();
      witness = ct.encodeWitness(witness);
    }
  };

  // Re-establish certification after upgrade
  // (CertTree.Store is stable, so the tree data survives, but certified_data is cleared)
  system func postupgrade() {
    ct.setCertifiedData();
  };
};
使用CertifiedData模块:
motoko
import CertifiedData "mo:core/CertifiedData";
import Blob "mo:core/Blob";
import Nat8 "mo:core/Nat8";
import Text "mo:core/Text";
import Map "mo:core/Map";
import Array "mo:core/Array";
import Iter "mo:core/Iter";
// 依赖:mops add sha2
import Sha256 "mo:sha2/Sha256";

persistent actor {

  // 简单的单值认证示例:
  var certifiedValue : Text = "";

  // 设置认证值(仅支持更新调用)
  public func setCertifiedValue(value : Text) : async () {
    certifiedValue := value;
    // 对值进行哈希并设置为认证数据(最大32字节)
    let hash = Sha256.fromBlob(#sha256, Text.encodeUtf8(value));
    CertifiedData.set(hash);
  };

  // 获取带证书的认证值(查询调用)
  public query func getCertifiedValue() : async {
    value : Text;
    certificate : ?Blob;
  } {
    {
      value = certifiedValue;
      certificate = CertifiedData.getCertificate();
    }
  };
};
带默克尔树的认证键值存储(进阶):
如需为多个值提供单键见证的认证,使用
ic-certification
mops包(执行
mops add ic-certification
安装)。它提供了可生成单键证明的真实默克尔树(
CertTree
):
motoko
import CertifiedData "mo:core/CertifiedData";
import Blob "mo:core/Blob";
import Text "mo:core/Text";
// 依赖:mops add ic-certification
import CertTree "mo:ic-certification/CertTree";

persistent actor {

  // CertTree.Store是稳定的——可跨升级持久化
  let certStore : CertTree.Store = CertTree.newStore();
  let ct = CertTree.Ops(certStore);

  // 初始化时设置认证数据
  ct.setCertifiedData();

  // 设置键值对并更新认证
  public func set(key : Text, value : Text) : async () {
    ct.put([Text.encodeUtf8(key)], Text.encodeUtf8(value));
    // 关键:每次变更后调用以更新子网认证的根哈希
    ct.setCertifiedData();
  };

  // 删除密钥并更新认证
  public func remove(key : Text) : async () {
    ct.delete([Text.encodeUtf8(key)]);
    ct.setCertifiedData();
  };

  // 查询带证书和对应密钥默克尔见证的数据
  public query func get(key : Text) : async {
    value : ?Blob;
    certificate : ?Blob;
    witness : Blob;
  } {
    let path = [Text.encodeUtf8(key)];
    // reveal()为该特定路径生成默克尔证明
    let witness = ct.reveal(path);
    {
      value = ct.lookup(path);
      certificate = CertifiedData.getCertificate();
      witness = ct.encodeWitness(witness);
    }
  };

  // 升级后重新建立认证
  // (CertTree.Store是稳定的,树数据会保留,但certified_data会被清除)
  system func postupgrade() {
    ct.setCertifiedData();
  };
};

Frontend Verification (TypeScript)

前端验证(TypeScript)

Uses
@dfinity/certificate-verification
which handles the full 6-step verification:
  1. Verify certificate BLS signature against IC root key
  2. Validate certificate freshness (
    /time
    within
    maxCertificateTimeOffsetMs
    )
  3. CBOR-decode the witness into a HashTree
  4. Reconstruct the witness root hash
  5. Compare reconstructed root hash with
    certified_data
    from the certificate
  6. Return the verified HashTree for value lookup
typescript
import { verifyCertification } from "@dfinity/certificate-verification";
import { lookup_path, HashTree } from "@icp-sdk/core/agent";
import { Principal } from "@icp-sdk/core/principal";

const MAX_CERT_TIME_OFFSET_MS = 5 * 60 * 1000; // 5 minutes

async function getVerifiedValue(
  rootKey: ArrayBuffer,
  canisterId: string,
  key: string,
  response: { value: string | null; certificate: ArrayBuffer; witness: ArrayBuffer }
): Promise<string | null> {
  // verifyCertification performs steps 1-5:
  //  - verifies BLS signature on the certificate
  //  - checks certificate /time is within maxCertificateTimeOffsetMs
  //  - CBOR-decodes the witness into a HashTree
  //  - reconstructs root hash from the witness tree
  //  - compares it against certified_data in the certificate
  // Throws CertificateTimeError or CertificateVerificationError on failure.
  const tree: HashTree = await verifyCertification({
    canisterId: Principal.fromText(canisterId),
    encodedCertificate: response.certificate,
    encodedTree: response.witness,
    rootKey,
    maxCertificateTimeOffsetMs: MAX_CERT_TIME_OFFSET_MS,
  });

  // Step 6: Look up the specific key in the verified witness tree.
  // The path must match how the canister inserted the key (e.g., key as UTF-8 bytes).
  const leafData = lookup_path([new TextEncoder().encode(key)], tree);

  if (!leafData) {
    // Key is provably absent from the certified tree
    return null;
  }

  const verifiedValue = new TextDecoder().decode(leafData);

  // Confirm the canister-returned value matches the witness-proven value
  if (response.value !== null && response.value !== verifiedValue) {
    throw new Error(
      "Response value does not match witness — canister returned tampered data"
    );
  }

  return verifiedValue;
}
For asset canisters, the HTTP gateway (boundary node) verifies certification transparently using the HTTP Gateway Protocol -- no client-side code needed.
使用
@dfinity/certificate-verification
处理完整的6步验证:
  1. 验证证书BLS签名与IC根公钥是否匹配
  2. 验证证书新鲜度(
    /time
    maxCertificateTimeOffsetMs
    范围内)
  3. 将见证CBOR解码为HashTree
  4. 重构见证的根哈希
  5. 比较重构的根哈希与证书中的
    certified_data
  6. 返回已验证的HashTree用于值查找
typescript
import { verifyCertification } from "@dfinity/certificate-verification";
import { lookup_path, HashTree } from "@icp-sdk/core/agent";
import { Principal } from "@icp-sdk/core/principal";

const MAX_CERT_TIME_OFFSET_MS = 5 * 60 * 1000; // 5分钟

async function getVerifiedValue(
  rootKey: ArrayBuffer,
  canisterId: string,
  key: string,
  response: { value: string | null; certificate: ArrayBuffer; witness: ArrayBuffer }
): Promise<string | null> {
  // verifyCertification执行步骤1-5:
  //  - 验证证书上的BLS签名
  //  - 检查证书/time是否在maxCertificateTimeOffsetMs范围内
  //  - 将见证CBOR解码为HashTree
  //  - 从见证树重构根哈希
  //  - 与证书中的certified_data进行比较
  // 验证失败时抛出CertificateTimeError或CertificateVerificationError
  const tree: HashTree = await verifyCertification({
    canisterId: Principal.fromText(canisterId),
    encodedCertificate: response.certificate,
    encodedTree: response.witness,
    rootKey,
    maxCertificateTimeOffsetMs: MAX_CERT_TIME_OFFSET_MS,
  });

  // 步骤6:在已验证的见证树中查找特定密钥
  // 路径必须与Canister插入密钥的方式一致(例如,密钥为UTF-8字节)
  const leafData = lookup_path([new TextEncoder().encode(key)], tree);

  if (!leafData) {
    // 密钥已被证明不存在于认证树中
    return null;
  }

  const verifiedValue = new TextDecoder().decode(leafData);

  // 确认Canister返回的值与见证证明的值一致
  if (response.value !== null && response.value !== verifiedValue) {
    throw new Error(
      "响应值与见证不匹配——Canister返回了被篡改的数据"
    );
  }

  return verifiedValue;
}
对于资产Canister,HTTP网关(边界节点)会使用HTTP网关协议透明地验证认证——无需客户端代码。

Deploy & Test

部署与测试

bash
undefined
bash
undefined

Deploy the canister

部署Canister

icp deploy backend
icp deploy backend

Set a certified value (update call -- goes through consensus)

设置认证值(更新调用——需经过共识)

icp canister call backend set '("greeting", "hello world")'
icp canister call backend set '("greeting", "hello world")'

Query the certified value

查询认证值

icp canister call backend get '("greeting")'
icp canister call backend get '("greeting")'

Returns: record { value = opt "hello world"; certificate = blob "..."; witness = blob "..." }

返回:record { value = opt "hello world"; certificate = blob "..."; witness = blob "..." }

Set multiple values

设置多个值

icp canister call backend set '("name", "Alice")' icp canister call backend set '("age", "30")'
icp canister call backend set '("name", "Alice")' icp canister call backend set '("age", "30")'

Delete a value

删除值

icp canister call backend delete '("age")'
icp canister call backend delete '("age")'

Verify the root hash is being set

验证根哈希已被设置

(No direct command -- verified by the presence of a non-null certificate in query response)

(无直接命令——通过查询响应中存在非空证书来验证)

undefined
undefined

Verify It Works

验证功能正常

bash
undefined
bash
undefined

1. Verify certificate is present in query response

1. 验证查询响应中包含证书

icp canister call backend get '("greeting")'
icp canister call backend get '("greeting")'

Expected: certificate field is a non-empty blob (NOT null)

预期结果:certificate字段为非空二进制数据(而非null)

If certificate is null, you are calling from an update context (wrong)

如果certificate为null,说明你在更新上下文中调用(错误)

2. Verify data integrity after update

2. 验证更新后的数据完整性

icp canister call backend set '("key1", "value1")' icp canister call backend get '("key1")'
icp canister call backend set '("key1", "value1")' icp canister call backend get '("key1")'

Expected: value = opt "value1" with valid certificate

预期结果:value = opt "value1"且附带有效证书

3. Verify certification survives canister upgrade

3. 验证认证在Canister升级后仍保留

icp canister call backend set '("persistent", "data")' icp deploy backend # triggers upgrade icp canister call backend get '("persistent")'
icp canister call backend set '("persistent", "data")' icp deploy backend # 触发升级 icp canister call backend get '("persistent")'

Expected: certificate is still non-null (postupgrade re-established certification)

预期结果:certificate仍为非空(postupgrade重新建立了认证)

Note: data persistence depends on stable storage implementation

注意:数据持久性取决于稳定存储的实现

4. Verify non-existent key returns null value with valid certificate

4. 验证不存在的密钥返回null值和有效证书

icp canister call backend get '("nonexistent")'
icp canister call backend get '("nonexistent")'

Expected: value = null, certificate = blob "..." (certificate still valid)

预期结果:value = null,certificate = blob "..."(证书仍然有效)

5. Frontend verification test

5. 前端验证测试

Open browser developer tools, check network requests

打开浏览器开发者工具,检查网络请求

Query responses should include IC-Certificate header

查询响应应包含IC-Certificate头

The service worker (if using asset canister) validates automatically

资产Canister的Service Worker会自动验证

Console should NOT show "Certificate verification failed" errors

控制台不应显示“Certificate verification failed”错误

6. For HTTP certification (custom HTTP canister):

6. 对于HTTP认证(自定义HTTP Canister):

Expected: Response headers include IC-Certificate

预期结果:响应头包含IC-Certificate

HTTP gateway verifies the certificate before forwarding to client

HTTP网关在转发给客户端前会验证证书

undefined
undefined