Skip to content

Commit dd7d8d6

Browse files
authored
docs: add example of skipping certification with ic-asset-certification (#395)
1 parent 3644d3f commit dd7d8d6

File tree

13 files changed

+354
-15
lines changed

13 files changed

+354
-15
lines changed

Cargo.lock

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ assert_matches = "1.5"
7272

7373
serde_bytes = "0.11"
7474
serde_cbor = "0.11"
75+
serde_json = "1.0"
7576

7677
thiserror = "1.0"
7778
anyhow = "1.0"

examples/http-certification/assets/README.md

+79-8
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,19 @@ fn post_upgrade() {
7676

7777
## Canister endpoints
7878

79-
There is only one canister endpoint in this example to serve assets, `http_request` query endpoint. The `serve_asset` function will be covered in a later section.
79+
There is only one canister endpoint in this example to serve assets, the `http_request` query endpoint. The `http_request` handler uses two auxiliary functions, `serve_metrics` and `serve_asset`, which are covered in a later section.
8080

8181
```rust
8282
#[query]
8383
fn http_request(req: HttpRequest) -> HttpResponse {
84+
let path = req.get_path().expect("Failed to parse request path");
85+
86+
// if the request is for the metrics endpoint, serve the metrics
87+
if path == "/metrics" {
88+
return serve_metrics();
89+
}
90+
91+
// otherwise, serve the requested asset
8492
serve_asset(&req)
8593
}
8694
```
@@ -128,7 +136,7 @@ fn get_asset_headers(additional_headers: Vec<HeaderField>) -> Vec<HeaderField> {
128136
("strict-transport-security".to_string(), "max-age=31536000; includeSubDomains".to_string()),
129137
("x-frame-options".to_string(), "DENY".to_string()),
130138
("x-content-type-options".to_string(), "nosniff".to_string()),
131-
("content-security-policy".to_string(), "default-src 'self'; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content".to_string()),
139+
("content-security-policy".to_string(), "default-src 'self'; img-src 'self' data:; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content".to_string()),
132140
("referrer-policy".to_string(), "no-referrer".to_string()),
133141
("permissions-policy".to_string(), "accelerometer=(),ambient-light-sensor=(),autoplay=(),battery=(),camera=(),display-capture=(),document-domain=(),encrypted-media=(),fullscreen=(),gamepad=(),geolocation=(),gyroscope=(),layout-animations=(self),legacy-image-formats=(self),magnetometer=(),microphone=(),midi=(),oversized-images=(self),payment=(),picture-in-picture=(),publickey-credentials-get=(),speaker-selection=(),sync-xhr=(self),unoptimized-images=(self),unsized-media=(self),usb=(),screen-wake-lock=(),web-share=(),xr-spatial-tracking=()".to_string()),
134142
("cross-origin-embedder-policy".to_string(), "require-corp".to_string()),
@@ -148,17 +156,21 @@ The `certify_all_assets` function performs the following steps:
148156

149157
1. Define the asset certification configurations.
150158
2. Collect all assets from the frontend build directory.
151-
3. Certify the assets using the `certify_assets` function from the `ic-asset-certification` crate.
152-
4. Set the canister's certified data.
159+
3. Skip certification for the `/metrics` endpoint.
160+
4. Certify the assets using the `certify_assets` function from the `ic-asset-certification` crate.
161+
5. Set the canister's certified data.
153162

154163
```rust
155164
thread_local! {
156165
static HTTP_TREE: Rc<RefCell<HttpCertificationTree>> = Default::default();
157166

167+
static ASSET_ROUTER: RefCell<AssetRouter<'static>> = RefCell::new(AssetRouter::with_tree(HTTP_TREE.with(|tree| tree.clone())));
168+
158169
// initializing the asset router with an HTTP certification tree is optional.
159170
// if direct access to the HTTP certification tree is not needed for certifying
160-
// requests and responses outside of the asset router, then this step can be skipped.
161-
static ASSET_ROUTER: RefCell<AssetRouter<'static>> = RefCell::new(AssetRouter::with_tree(HTTP_TREE.with(|tree| tree.clone())));
171+
// requests and responses outside of the asset router, then this step can be skipped
172+
// and the asset router can be initialized like so:
173+
static ASSET_ROUTER: RefCell<AssetRouter<'static>> = Default::default();
162174
}
163175

164176
const IMMUTABLE_ASSET_CACHE_CONTROL: &str = "public, max-age=31536000, immutable";
@@ -232,13 +244,25 @@ fn certify_all_assets() {
232244
let mut assets = Vec::new();
233245
collect_assets(&ASSETS_DIR, &mut assets);
234246

247+
// 3. Skip certification for the metrics endpoint.
248+
HTTP_TREE.with(|tree| {
249+
let mut tree = tree.borrow_mut();
250+
251+
let metrics_tree_path = HttpCertificationPath::exact("/metrics");
252+
let metrics_certification = HttpCertification::skip();
253+
let metrics_tree_entry =
254+
HttpCertificationTreeEntry::new(metrics_tree_path, metrics_certification);
255+
256+
tree.insert(&metrics_tree_entry);
257+
});
258+
235259
ASSET_ROUTER.with_borrow_mut(|asset_router| {
236-
// 3. Certify the assets using the `certify_assets` function from the `ic-asset-certification` crate.
260+
// 4. Certify the assets using the `certify_assets` function from the `ic-asset-certification` crate.
237261
if let Err(err) = asset_router.certify_assets(assets, asset_configs) {
238262
ic_cdk::trap(&format!("Failed to certify assets: {}", err));
239263
}
240264

241-
// 4. Set the canister's certified data.
265+
// 5. Set the canister's certified data.
242266
set_certified_data(&asset_router.root_hash());
243267
});
244268
}
@@ -262,3 +286,50 @@ fn serve_asset(req: &HttpRequest) -> HttpResponse<'static> {
262286
})
263287
}
264288
```
289+
290+
## Serving metrics
291+
292+
The `serve_metrics` function is responsible for serving metrics. Since metrics are not certified, this procedure is a bit more involved compared to serving assets, which is handled entirely by the `asset_router`.
293+
294+
It's important to determine whether skipping certification is appropriate for the use case. In this example, metrics are not sensitive data and are not used to make decisions that could affect the canister's security. Therefore, it's determined to be acceptable to skip certification for this use case, but that may not be the case for every canister. The important takeaway from this example is to learn how to skip certification, when it is necessary and safe to do so.
295+
296+
The `Metrics` struct is used to collect the number of assets, number of fallback assets, and the cycle balance and serialize this into JSON. The `add_v2_certificate_header` function from the `ic-http-certification` library is used to add the `IC-Certificate` header to the response and then the `IC-Certificate-Expression` header is added too. The `get_asset_headers` function is used to get the same headers for the response that are used for asset responses.
297+
298+
```rust
299+
fn serve_metrics() -> HttpResponse<'static> {
300+
ASSET_ROUTER.with_borrow(|asset_router| {
301+
let metrics = Metrics {
302+
num_assets: asset_router.get_assets().len(),
303+
num_fallback_assets: asset_router.get_fallback_assets().len(),
304+
cycle_balance: canister_balance(),
305+
};
306+
let body = serde_json::to_vec(&metrics).expect("Failed to serialize metrics");
307+
let mut response = HttpResponse::builder()
308+
.with_status_code(200)
309+
.with_body(body)
310+
.build();
311+
312+
HTTP_TREE.with(|tree| {
313+
let tree = tree.borrow();
314+
315+
let metrics_tree_path = HttpCertificationPath::exact("/metrics");
316+
let metrics_certification = HttpCertification::skip();
317+
let metrics_tree_entry =
318+
HttpCertificationTreeEntry::new(&metrics_tree_path, metrics_certification);
319+
add_v2_certificate_header(
320+
&data_certificate().expect("No data certificate available"),
321+
&mut response,
322+
&tree.witness(&metrics_tree_entry, "/metrics").unwrap(),
323+
&metrics_tree_path.to_expr_path(),
324+
);
325+
326+
let headers = get_asset_headers(vec![(
327+
CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
328+
DefaultCelBuilder::skip_certification().to_string(),
329+
)]);
330+
response.headers_mut().extend_from_slice(&headers);
331+
response
332+
})
333+
})
334+
}
335+
```

examples/http-certification/assets/src/backend/Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@ ic-cdk.workspace = true
1212
ic-http-certification.workspace = true
1313
ic-asset-certification.workspace = true
1414
include_dir = { version = "0.7", features = ["glob"] }
15+
16+
# The following dependencies are only necessary for JSON serialization of metrics
17+
serde.workspace = true
18+
serde_json.workspace = true

examples/http-certification/assets/src/backend/src/lib.rs

+79-6
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
1+
use api::canister_balance;
12
use ic_asset_certification::{
2-
Asset, AssetConfig, AssetEncoding, AssetFallbackConfig, AssetRedirectKind, AssetRouter,
3+
Asset, AssetConfig, AssetEncoding, AssetFallbackConfig, AssetMap, AssetRedirectKind,
4+
AssetRouter,
35
};
46
use ic_cdk::{
57
api::{data_certificate, set_certified_data},
68
*,
79
};
810
use ic_http_certification::{
9-
HeaderField, HttpCertificationTree, HttpRequest, HttpResponse, StatusCode,
11+
utils::add_v2_certificate_header, DefaultCelBuilder, HeaderField, HttpCertification,
12+
HttpCertificationPath, HttpCertificationTree, HttpCertificationTreeEntry, HttpRequest,
13+
HttpResponse, StatusCode, CERTIFICATE_EXPRESSION_HEADER_NAME,
1014
};
1115
use include_dir::{include_dir, Dir};
16+
use serde::Serialize;
1217
use std::{cell::RefCell, rc::Rc};
1318

19+
#[derive(Debug, Clone, Serialize)]
20+
pub struct Metrics {
21+
pub num_assets: usize,
22+
pub num_fallback_assets: usize,
23+
pub cycle_balance: u64,
24+
}
25+
1426
// Public methods
1527
#[init]
1628
fn init() {
@@ -24,6 +36,14 @@ fn post_upgrade() {
2436

2537
#[query]
2638
fn http_request(req: HttpRequest) -> HttpResponse {
39+
let path = req.get_path().expect("Failed to parse request path");
40+
41+
// if the request is for the metrics endpoint, serve the metrics
42+
if path == "/metrics" {
43+
return serve_metrics();
44+
}
45+
46+
// otherwise, serve the requested asset
2747
serve_asset(&req)
2848
}
2949

@@ -32,7 +52,11 @@ thread_local! {
3252

3353
// initializing the asset router with an HTTP certification tree is optional.
3454
// if direct access to the HTTP certification tree is not needed for certifying
35-
// requests and responses outside of the asset router, then this step can be skipped.
55+
// requests and responses outside of the asset router, then this step can be skipped
56+
// and the asset router can be initialized like so:
57+
// ```
58+
// static ASSET_ROUTER: RefCell<AssetRouter<'static>> = Default::default();
59+
// ```
3660
static ASSET_ROUTER: RefCell<AssetRouter<'static>> = RefCell::new(AssetRouter::with_tree(HTTP_TREE.with(|tree| tree.clone())));
3761
}
3862

@@ -123,18 +147,67 @@ fn certify_all_assets() {
123147
let mut assets = Vec::new();
124148
collect_assets(&ASSETS_DIR, &mut assets);
125149

150+
// 3. Skip certification for the metrics endpoint.
151+
HTTP_TREE.with(|tree| {
152+
let mut tree = tree.borrow_mut();
153+
154+
let metrics_tree_path = HttpCertificationPath::exact("/metrics");
155+
let metrics_certification = HttpCertification::skip();
156+
let metrics_tree_entry =
157+
HttpCertificationTreeEntry::new(metrics_tree_path, metrics_certification);
158+
159+
tree.insert(&metrics_tree_entry);
160+
});
161+
126162
ASSET_ROUTER.with_borrow_mut(|asset_router| {
127-
// 3. Certify the assets using the `certify_assets` function from the `ic-asset-certification` crate.
163+
// 4. Certify the assets using the `certify_assets` function from the `ic-asset-certification` crate.
128164
if let Err(err) = asset_router.certify_assets(assets, asset_configs) {
129165
ic_cdk::trap(&format!("Failed to certify assets: {}", err));
130166
}
131167

132-
// 4. Set the canister's certified data.
168+
// 5. Set the canister's certified data.
133169
set_certified_data(&asset_router.root_hash());
134170
});
135171
}
136172

137173
// Handlers
174+
fn serve_metrics() -> HttpResponse<'static> {
175+
ASSET_ROUTER.with_borrow(|asset_router| {
176+
let metrics = Metrics {
177+
num_assets: asset_router.get_assets().len(),
178+
num_fallback_assets: asset_router.get_fallback_assets().len(),
179+
cycle_balance: canister_balance(),
180+
};
181+
let body = serde_json::to_vec(&metrics).expect("Failed to serialize metrics");
182+
let mut response = HttpResponse::builder()
183+
.with_status_code(StatusCode::OK)
184+
.with_body(body)
185+
.build();
186+
187+
HTTP_TREE.with(|tree| {
188+
let tree = tree.borrow();
189+
190+
let metrics_tree_path = HttpCertificationPath::exact("/metrics");
191+
let metrics_certification = HttpCertification::skip();
192+
let metrics_tree_entry =
193+
HttpCertificationTreeEntry::new(&metrics_tree_path, metrics_certification);
194+
add_v2_certificate_header(
195+
&data_certificate().expect("No data certificate available"),
196+
&mut response,
197+
&tree.witness(&metrics_tree_entry, "/metrics").unwrap(),
198+
&metrics_tree_path.to_expr_path(),
199+
);
200+
201+
let headers = get_asset_headers(vec![(
202+
CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
203+
DefaultCelBuilder::skip_certification().to_string(),
204+
)]);
205+
response.headers_mut().extend_from_slice(&headers);
206+
response
207+
})
208+
})
209+
}
210+
138211
fn serve_asset(req: &HttpRequest) -> HttpResponse<'static> {
139212
ASSET_ROUTER.with_borrow(|asset_router| {
140213
if let Ok(response) = asset_router.serve_asset(
@@ -154,7 +227,7 @@ fn get_asset_headers(additional_headers: Vec<HeaderField>) -> Vec<HeaderField> {
154227
("strict-transport-security".to_string(), "max-age=31536000; includeSubDomains".to_string()),
155228
("x-frame-options".to_string(), "DENY".to_string()),
156229
("x-content-type-options".to_string(), "nosniff".to_string()),
157-
("content-security-policy".to_string(), "default-src 'self'; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content".to_string()),
230+
("content-security-policy".to_string(), "default-src 'self'; img-src 'self' data:; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content".to_string()),
158231
("referrer-policy".to_string(), "no-referrer".to_string()),
159232
("permissions-policy".to_string(), "accelerometer=(),ambient-light-sensor=(),autoplay=(),battery=(),camera=(),display-capture=(),document-domain=(),encrypted-media=(),fullscreen=(),gamepad=(),geolocation=(),gyroscope=(),layout-animations=(self),legacy-image-formats=(self),magnetometer=(),microphone=(),midi=(),oversized-images=(self),payment=(),picture-in-picture=(),publickey-credentials-get=(),speaker-selection=(),sync-xhr=(self),unoptimized-images=(self),unsized-media=(self),usb=(),screen-wake-lock=(),web-share=(),xr-spatial-tracking=()".to_string()),
160233
("cross-origin-embedder-policy".to_string(), "require-corp".to_string()),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { Config } from 'jest';
2+
3+
const config: Config = {
4+
watch: false,
5+
preset: 'ts-jest/presets/js-with-ts',
6+
testEnvironment: 'node',
7+
verbose: true,
8+
};
9+
10+
export default config;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "http-certification-assets-tests",
3+
"private": true,
4+
"scripts": {
5+
"test": "jest"
6+
},
7+
"devDependencies": {
8+
"@dfinity/response-verification": "workspace:*"
9+
}
10+
}

0 commit comments

Comments
 (0)