Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: add example of skipping certification with ic-asset-certification #395

Merged
merged 2 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ assert_matches = "1.5"

serde_bytes = "0.11"
serde_cbor = "0.11"
serde_json = "1.0"

thiserror = "1.0"
anyhow = "1.0"
Expand Down
87 changes: 79 additions & 8 deletions examples/http-certification/assets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,19 @@ fn post_upgrade() {

## Canister endpoints

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.
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.

```rust
#[query]
fn http_request(req: HttpRequest) -> HttpResponse {
let path = req.get_path().expect("Failed to parse request path");

// if the request is for the metrics endpoint, serve the metrics
if path == "/metrics" {
return serve_metrics();
}

// otherwise, serve the requested asset
serve_asset(&req)
}
```
Expand Down Expand Up @@ -128,7 +136,7 @@ fn get_asset_headers(additional_headers: Vec<HeaderField>) -> Vec<HeaderField> {
("strict-transport-security".to_string(), "max-age=31536000; includeSubDomains".to_string()),
("x-frame-options".to_string(), "DENY".to_string()),
("x-content-type-options".to_string(), "nosniff".to_string()),
("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()),
("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()),
("referrer-policy".to_string(), "no-referrer".to_string()),
("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()),
("cross-origin-embedder-policy".to_string(), "require-corp".to_string()),
Expand All @@ -148,17 +156,21 @@ The `certify_all_assets` function performs the following steps:

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

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

static ASSET_ROUTER: RefCell<AssetRouter<'static>> = RefCell::new(AssetRouter::with_tree(HTTP_TREE.with(|tree| tree.clone())));

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

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

// 3. Skip certification for the metrics endpoint.
HTTP_TREE.with(|tree| {
let mut tree = tree.borrow_mut();

let metrics_tree_path = HttpCertificationPath::exact("/metrics");
let metrics_certification = HttpCertification::skip();
let metrics_tree_entry =
HttpCertificationTreeEntry::new(metrics_tree_path, metrics_certification);

tree.insert(&metrics_tree_entry);
});

ASSET_ROUTER.with_borrow_mut(|asset_router| {
// 3. Certify the assets using the `certify_assets` function from the `ic-asset-certification` crate.
// 4. Certify the assets using the `certify_assets` function from the `ic-asset-certification` crate.
if let Err(err) = asset_router.certify_assets(assets, asset_configs) {
ic_cdk::trap(&format!("Failed to certify assets: {}", err));
}

// 4. Set the canister's certified data.
// 5. Set the canister's certified data.
set_certified_data(&asset_router.root_hash());
});
}
Expand All @@ -262,3 +286,50 @@ fn serve_asset(req: &HttpRequest) -> HttpResponse<'static> {
})
}
```

## Serving metrics

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`.

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.

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.

```rust
fn serve_metrics() -> HttpResponse<'static> {
ASSET_ROUTER.with_borrow(|asset_router| {
let metrics = Metrics {
num_assets: asset_router.get_assets().len(),
num_fallback_assets: asset_router.get_fallback_assets().len(),
cycle_balance: canister_balance(),
};
let body = serde_json::to_vec(&metrics).expect("Failed to serialize metrics");
let mut response = HttpResponse::builder()
.with_status_code(200)
.with_body(body)
.build();

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

let metrics_tree_path = HttpCertificationPath::exact("/metrics");
let metrics_certification = HttpCertification::skip();
let metrics_tree_entry =
HttpCertificationTreeEntry::new(&metrics_tree_path, metrics_certification);
add_v2_certificate_header(
&data_certificate().expect("No data certificate available"),
&mut response,
&tree.witness(&metrics_tree_entry, "/metrics").unwrap(),
&metrics_tree_path.to_expr_path(),
);

let headers = get_asset_headers(vec![(
CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
DefaultCelBuilder::skip_certification().to_string(),
)]);
response.headers_mut().extend_from_slice(&headers);
response
})
})
}
```
4 changes: 4 additions & 0 deletions examples/http-certification/assets/src/backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ ic-cdk.workspace = true
ic-http-certification.workspace = true
ic-asset-certification.workspace = true
include_dir = { version = "0.7", features = ["glob"] }

# The following dependencies are only necessary for JSON serialization of metrics
serde.workspace = true
serde_json.workspace = true
85 changes: 79 additions & 6 deletions examples/http-certification/assets/src/backend/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
use api::canister_balance;
use ic_asset_certification::{
Asset, AssetConfig, AssetEncoding, AssetFallbackConfig, AssetRedirectKind, AssetRouter,
Asset, AssetConfig, AssetEncoding, AssetFallbackConfig, AssetMap, AssetRedirectKind,
AssetRouter,
};
use ic_cdk::{
api::{data_certificate, set_certified_data},
*,
};
use ic_http_certification::{
HeaderField, HttpCertificationTree, HttpRequest, HttpResponse, StatusCode,
utils::add_v2_certificate_header, DefaultCelBuilder, HeaderField, HttpCertification,
HttpCertificationPath, HttpCertificationTree, HttpCertificationTreeEntry, HttpRequest,
HttpResponse, StatusCode, CERTIFICATE_EXPRESSION_HEADER_NAME,
};
use include_dir::{include_dir, Dir};
use serde::Serialize;
use std::{cell::RefCell, rc::Rc};

#[derive(Debug, Clone, Serialize)]
pub struct Metrics {
pub num_assets: usize,
pub num_fallback_assets: usize,
pub cycle_balance: u64,
}

// Public methods
#[init]
fn init() {
Expand All @@ -24,6 +36,14 @@ fn post_upgrade() {

#[query]
fn http_request(req: HttpRequest) -> HttpResponse {
let path = req.get_path().expect("Failed to parse request path");

// if the request is for the metrics endpoint, serve the metrics
if path == "/metrics" {
return serve_metrics();
}

// otherwise, serve the requested asset
serve_asset(&req)
}

Expand All @@ -32,7 +52,11 @@ thread_local! {

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

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

// 3. Skip certification for the metrics endpoint.
HTTP_TREE.with(|tree| {
let mut tree = tree.borrow_mut();

let metrics_tree_path = HttpCertificationPath::exact("/metrics");
let metrics_certification = HttpCertification::skip();
let metrics_tree_entry =
HttpCertificationTreeEntry::new(metrics_tree_path, metrics_certification);

tree.insert(&metrics_tree_entry);
});

ASSET_ROUTER.with_borrow_mut(|asset_router| {
// 3. Certify the assets using the `certify_assets` function from the `ic-asset-certification` crate.
// 4. Certify the assets using the `certify_assets` function from the `ic-asset-certification` crate.
if let Err(err) = asset_router.certify_assets(assets, asset_configs) {
ic_cdk::trap(&format!("Failed to certify assets: {}", err));
}

// 4. Set the canister's certified data.
// 5. Set the canister's certified data.
set_certified_data(&asset_router.root_hash());
});
}

// Handlers
fn serve_metrics() -> HttpResponse<'static> {
ASSET_ROUTER.with_borrow(|asset_router| {
let metrics = Metrics {
num_assets: asset_router.get_assets().len(),
num_fallback_assets: asset_router.get_fallback_assets().len(),
cycle_balance: canister_balance(),
};
let body = serde_json::to_vec(&metrics).expect("Failed to serialize metrics");
let mut response = HttpResponse::builder()
.with_status_code(StatusCode::OK)
.with_body(body)
.build();

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

let metrics_tree_path = HttpCertificationPath::exact("/metrics");
let metrics_certification = HttpCertification::skip();
let metrics_tree_entry =
HttpCertificationTreeEntry::new(&metrics_tree_path, metrics_certification);
add_v2_certificate_header(
&data_certificate().expect("No data certificate available"),
&mut response,
&tree.witness(&metrics_tree_entry, "/metrics").unwrap(),
&metrics_tree_path.to_expr_path(),
);

let headers = get_asset_headers(vec![(
CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
DefaultCelBuilder::skip_certification().to_string(),
)]);
response.headers_mut().extend_from_slice(&headers);
response
})
})
}

fn serve_asset(req: &HttpRequest) -> HttpResponse<'static> {
ASSET_ROUTER.with_borrow(|asset_router| {
if let Ok(response) = asset_router.serve_asset(
Expand All @@ -154,7 +227,7 @@ fn get_asset_headers(additional_headers: Vec<HeaderField>) -> Vec<HeaderField> {
("strict-transport-security".to_string(), "max-age=31536000; includeSubDomains".to_string()),
("x-frame-options".to_string(), "DENY".to_string()),
("x-content-type-options".to_string(), "nosniff".to_string()),
("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()),
("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()),
("referrer-policy".to_string(), "no-referrer".to_string()),
("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()),
("cross-origin-embedder-policy".to_string(), "require-corp".to_string()),
Expand Down
10 changes: 10 additions & 0 deletions examples/http-certification/assets/src/tests/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Config } from 'jest';

const config: Config = {
watch: false,
preset: 'ts-jest/presets/js-with-ts',
testEnvironment: 'node',
verbose: true,
};

export default config;
10 changes: 10 additions & 0 deletions examples/http-certification/assets/src/tests/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "http-certification-assets-tests",
"private": true,
"scripts": {
"test": "jest"
},
"devDependencies": {
"@dfinity/response-verification": "workspace:*"
}
}
Loading
Loading