Loading...
Loading...
Serve cryptographically verified responses from query calls using Merkle trees and subnet BLS signatures. Covers certified data API, RbTree/CertTree construction, witness generation, and frontend certificate validation. Use when query responses need verification, certified data, or response authenticity proofs.
npx skill4agent add dfinity/icskills certified-variablesic-certified-mapic-cdkcertified_data_setdata_certificateCertifiedDataic-certificationmops add ic-certification@icp-sdk/core@dfinity/certificate-verificationic_cdk::api::certified_data_setCertifiedData.setic_cdk::api::data_certificateCertifiedData.getCertificate308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c05030201036100814c0e6ec71fab583b08bd81373c255c3c371b2e84863c98a4f1e08b74235d14fb5d9c0cd546d9685f913a0c0b2cc5341583bf4b4392e467db96d65b9bb4cb717112f8472e0d5a4d14505ffd7484b01291091c5f87b98883463f98091a0baaaeicpcertified_data_setcertified_data_setdata_certificate()certified_data_setdata_certificate()nullNonecertified_data_set#[init]#[post_upgrade]system func postupgrade/timecertificate_timeUPDATE 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[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"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();
}[package]
name = "http_certified_backend"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
ic-http-certification = "3.1"Note: The HTTP certification API is evolving rapidly. Verify these examples against the latest ic-http-certification docs before use.
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());
});
}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();
}
};
};ic-certificationmops add ic-certificationCertTreeimport 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();
};
};@dfinity/certificate-verification/timemaxCertificateTimeOffsetMscertified_dataimport { 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;
}# Deploy the canister
icp deploy backend
# Set a certified value (update call -- goes through consensus)
icp canister call backend set '("greeting", "hello world")'
# Query the certified value
icp canister call backend get '("greeting")'
# Returns: 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")'
# Delete a value
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)# 1. Verify certificate is present in query response
icp canister call backend get '("greeting")'
# Expected: certificate field is a non-empty blob (NOT null)
# If certificate is null, you are calling from an update context (wrong)
# 2. Verify data integrity after update
icp canister call backend set '("key1", "value1")'
icp canister call backend get '("key1")'
# Expected: value = opt "value1" with valid certificate
# 3. Verify certification survives canister upgrade
icp canister call backend set '("persistent", "data")'
icp deploy backend # triggers upgrade
icp canister call backend get '("persistent")'
# Expected: certificate is still non-null (postupgrade re-established certification)
# Note: data persistence depends on stable storage implementation
# 4. Verify non-existent key returns null value with valid certificate
icp canister call backend get '("nonexistent")'
# Expected: value = null, certificate = blob "..." (certificate still valid)
# 5. Frontend verification test
# Open browser developer tools, check network requests
# Query responses should include IC-Certificate header
# The service worker (if using asset canister) validates automatically
# Console should NOT show "Certificate verification failed" errors
# 6. For HTTP certification (custom HTTP canister):
curl -v https://CANISTER_ID.ic0.app/path
# Expected: Response headers include IC-Certificate
# HTTP gateway verifies the certificate before forwarding to client