Skip to content

Commit

Permalink
fix(ic-asset-certification): fix range response certification (#420)
Browse files Browse the repository at this point in the history
add additional error types to ic-response-verification crate
  • Loading branch information
nathanosdev authored Jan 23, 2025
1 parent d53a44e commit ede16c0
Show file tree
Hide file tree
Showing 18 changed files with 676 additions and 277 deletions.
4 changes: 4 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 @@ -94,6 +94,7 @@ wasm-bindgen-console-logger = "0.1"
rand = "0.8"
getrandom = { version = "0.2", features = ["js"] }
rand_chacha = "0.3"
once_cell = "1"

ic-asset-certification = { path = "./packages/ic-asset-certification", version = "3.0.2" }
ic-certification = { path = "./packages/ic-certification", default-features = false, version = "3.0.2" }
Expand Down
2 changes: 2 additions & 0 deletions packages/ic-asset-certification/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ rstest.workspace = true
assert_matches.workspace = true
ic-response-verification.workspace = true
ic-response-verification-test-utils.workspace = true
ic-certification-testing.workspace = true
once_cell.workspace = true
23 changes: 14 additions & 9 deletions packages/ic-asset-certification/src/asset_router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -908,10 +908,15 @@ impl<'content> AssetRouter<'content> {
http::header::CONTENT_RANGE.to_string(),
format!("bytes {range_begin}-{range_end}/{total_length}"),
));
request_headers.push((
http::header::RANGE.to_string(),
format!("bytes={range_begin}-"),
));

// The `Range` request header will not be sent with the first request,
// so we don't include it in certification for the first chunk.
if range_begin != 0 {
request_headers.push((
http::header::RANGE.to_string(),
format!("bytes={range_begin}-"),
));
}
};

Self::prepare_response_and_certification(
Expand Down Expand Up @@ -1260,7 +1265,7 @@ mod tests {
let request = HttpRequest::get(&req_url).build();
let mut expected_response = build_206_response(
asset_body[0..ASSET_CHUNK_SIZE].to_vec(),
asset_range_chunk_cel_expr(),
asset_cel_expr(),
vec![
(
"cache-control".to_string(),
Expand Down Expand Up @@ -1460,7 +1465,7 @@ mod tests {
.build();
let mut expected_response = build_206_response(
asset_body[0..ASSET_CHUNK_SIZE].to_vec(),
encoded_range_chunk_asset_cel_expr(),
asset_cel_expr(),
vec![
(
"cache-control".to_string(),
Expand Down Expand Up @@ -3085,7 +3090,7 @@ mod tests {
.get(format!("/{}", TWO_CHUNKS_ASSET_NAME), None, Some(0));
let expected_first_chunk_response = build_206_response(
first_chunk_body.to_vec(),
asset_range_chunk_cel_expr(),
asset_cel_expr(),
vec![
(
"cache-control".to_string(),
Expand All @@ -3111,7 +3116,7 @@ mod tests {
);
let expected_first_chunk_gzip_response = build_206_response(
first_chunk_gzip_body.to_vec(),
asset_range_chunk_cel_expr(),
encoded_asset_cel_expr(),
vec![
(
"cache-control".to_string(),
Expand Down Expand Up @@ -3251,7 +3256,7 @@ mod tests {
let first_chunk_body = &full_body[0..ASSET_CHUNK_SIZE];
let expected_first_chunk_response = build_206_response(
first_chunk_body.to_vec(),
asset_range_chunk_cel_expr(),
asset_cel_expr(),
vec![
(
"cache-control".to_string(),
Expand Down
58 changes: 58 additions & 0 deletions packages/ic-asset-certification/tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use ic_asset_certification::ASSET_CHUNK_SIZE;
use ic_response_verification_test_utils::hash;
use rand_chacha::{
rand_core::{RngCore, SeedableRng},
ChaCha20Rng,
};

pub fn asset_chunk(asset_body: &[u8], chunk_number: usize) -> &[u8] {
let start = chunk_number * ASSET_CHUNK_SIZE;
let end = start + ASSET_CHUNK_SIZE;
&asset_body[start..end.min(asset_body.len())]
}

pub fn asset_body(asset_name: &str, asset_size: usize) -> Vec<u8> {
let mut rng = ChaCha20Rng::from_seed(hash(asset_name));
let mut body = vec![0u8; asset_size];
rng.fill_bytes(&mut body);

body
}

#[macro_export]
macro_rules! assert_contains {
($vec:expr, $elems:expr) => {
for elem in $elems {
assert!(
$vec.contains(&elem),
"assertion failed: Expected vector {:?} to contain element {:?}",
$vec,
elem
);
}
};
}

#[macro_export]
macro_rules! assert_response_eq {
($actual:expr, $expected:expr) => {
let actual: &ic_http_certification::HttpResponse = &$actual;
let expected: &ic_http_certification::HttpResponse = &$expected;

assert_eq!(actual.status_code(), expected.status_code());
assert_eq!(actual.body(), expected.body());
assert_contains!(actual.headers(), expected.headers());
};
}

#[macro_export]
macro_rules! assert_verified_response_eq {
($actual:expr, $expected:expr) => {
let actual: &ic_response_verification::types::VerifiedResponse = &$actual;
let expected: &ic_http_certification::HttpResponse = &$expected;

assert_eq!(actual.status_code, Some(expected.status_code().as_u16()));
assert_eq!(actual.body, expected.body());
assert_contains!(actual.headers, expected.headers());
};
}
193 changes: 193 additions & 0 deletions packages/ic-asset-certification/tests/large_assets.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
use http::StatusCode;
use ic_asset_certification::{Asset, AssetConfig, AssetEncoding, AssetRouter, ASSET_CHUNK_SIZE};
use ic_certification_testing::{CertificateBuilder, CertificateData};
use ic_http_certification::{
DefaultCelBuilder, DefaultResponseCertification, HeaderField, HttpRequest, HttpResponse,
};
use ic_response_verification::verify_request_response_pair;
use ic_response_verification_test_utils::{create_canister_id, get_current_timestamp};
use once_cell::sync::OnceCell;
use rstest::*;

mod common;
use common::*;

const ASSET_ONE_NAME: &str = "asset_one";
const ASSET_ONE_SIZE: usize = ASSET_CHUNK_SIZE + 1;

const MAX_CERT_TIME_OFFSET_NS: u128 = 300_000_000_000;
const MIN_REQUESTED_VERIFICATION_VERSION: u8 = 2;

#[rstest]
fn should_certify_long_asset_chunkwise(
asset_one_body: &'static [u8],
asset_one_chunk_one: &'static [u8],
asset_one_chunk_two: &'static [u8],
) {
let current_time = get_current_timestamp();
let canister_id = create_canister_id("rdmx6-jaaaa-aaaaa-aaadq-cai");
let req_url = format!("/{}", ASSET_ONE_NAME);

let mut asset_router = AssetRouter::default();
let assets = [Asset::new(ASSET_ONE_NAME, asset_one_body)];
let asset_configs = [asset_config(ASSET_ONE_NAME.to_string(), vec![])];
asset_router.certify_assets(assets, asset_configs).unwrap();

let certified_data = asset_router.root_hash();
let CertificateData {
cbor_encoded_certificate,
certificate: _,
root_key,
} = CertificateBuilder::new(&canister_id.to_string(), &certified_data)
.expect("Failed to create CertificateBuilder")
.with_time(current_time)
.build()
.expect("Failed to create CertificateData from CertificateBuilder");

let mut expected_headers = common_asset_headers();
expected_headers.extend(vec![
("content-type".to_string(), "text/html".to_string()),
("content-length".to_string(), ASSET_CHUNK_SIZE.to_string()),
(
"content-range".to_string(),
format!("bytes 0-{}/{}", ASSET_CHUNK_SIZE - 1, ASSET_ONE_SIZE),
),
]);
let expected_chunk_one_res = HttpResponse::builder()
.with_status_code(StatusCode::PARTIAL_CONTENT)
.with_headers(expected_headers)
.with_body(asset_one_chunk_one)
.build();

let chunk_one_req = HttpRequest::get(&req_url).build();
let chunk_one_res = asset_router
.serve_asset(&cbor_encoded_certificate, &chunk_one_req)
.unwrap();
assert_response_eq!(chunk_one_res, expected_chunk_one_res);

let chunk_one_verification = verify_request_response_pair(
chunk_one_req,
chunk_one_res,
canister_id.as_ref(),
current_time,
MAX_CERT_TIME_OFFSET_NS,
&root_key,
MIN_REQUESTED_VERIFICATION_VERSION,
)
.unwrap();
assert_eq!(chunk_one_verification.verification_version, 2);
assert_verified_response_eq!(
chunk_one_verification.response.unwrap(),
expected_chunk_one_res
);

let mut expected_headers = common_asset_headers();
expected_headers.extend(vec![
("content-type".to_string(), "text/html".to_string()),
(
"content-length".to_string(),
(ASSET_ONE_SIZE - ASSET_CHUNK_SIZE).to_string(),
),
(
"content-range".to_string(),
format!(
"bytes {}-{}/{}",
ASSET_CHUNK_SIZE,
ASSET_ONE_SIZE - 1,
ASSET_ONE_SIZE
),
),
]);
let expected_chunk_two_res = HttpResponse::builder()
.with_status_code(StatusCode::PARTIAL_CONTENT)
.with_headers(expected_headers)
.with_body(asset_one_chunk_two)
.build();

let chunk_two_req = HttpRequest::get(&req_url)
.with_headers(vec![(
"range".to_string(),
format!("bytes={}-", ASSET_CHUNK_SIZE),
)])
.build();
let chunk_two_res = asset_router
.serve_asset(&cbor_encoded_certificate, &chunk_two_req)
.unwrap();
assert_response_eq!(chunk_two_res, expected_chunk_two_res);

let chunk_two_verification = verify_request_response_pair(
chunk_two_req,
chunk_two_res,
canister_id.as_ref(),
current_time,
MAX_CERT_TIME_OFFSET_NS,
&root_key,
MIN_REQUESTED_VERIFICATION_VERSION,
)
.unwrap();
assert_eq!(chunk_two_verification.verification_version, 2);
assert_verified_response_eq!(
chunk_two_verification.response.unwrap(),
expected_chunk_two_res
);
}

#[fixture]
fn asset_cel_expr() -> String {
DefaultCelBuilder::full_certification()
.with_response_certification(DefaultResponseCertification::response_header_exclusions(
vec![],
))
.build()
.to_string()
}

#[fixture]
fn asset_range_cel_expr() -> String {
DefaultCelBuilder::full_certification()
.with_request_headers(vec!["range"])
.with_response_certification(DefaultResponseCertification::response_header_exclusions(
vec![],
))
.build()
.to_string()
}

#[fixture]
fn asset_one_body() -> &'static [u8] {
static ASSET_ONE_BODY: OnceCell<Vec<u8>> = OnceCell::new();

ASSET_ONE_BODY.get_or_init(|| asset_body(ASSET_ONE_NAME, ASSET_ONE_SIZE))
}

#[fixture]
fn asset_one_chunk_one(asset_one_body: &'static [u8]) -> &'static [u8] {
static ASSET_ONE_CHUNK: OnceCell<&[u8]> = OnceCell::new();

ASSET_ONE_CHUNK.get_or_init(|| asset_chunk(asset_one_body, 0))
}

#[fixture]
fn asset_one_chunk_two(asset_one_body: &'static [u8]) -> &'static [u8] {
static ASSET_ONE_CHUNK: OnceCell<&[u8]> = OnceCell::new();

ASSET_ONE_CHUNK.get_or_init(|| asset_chunk(asset_one_body, 1))
}

fn asset_config(path: String, encodings: Vec<(AssetEncoding, String)>) -> AssetConfig {
AssetConfig::File {
path,
content_type: Some("text/html".to_string()),
headers: common_asset_headers(),
fallback_for: vec![],
aliased_by: vec![],
encodings,
}
}

fn common_asset_headers() -> Vec<HeaderField> {
vec![(
"cache-control".to_string(),
"public, no-cache, no-store".to_string(),
)]
}
1 change: 1 addition & 0 deletions packages/ic-http-certification-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ ic-types.workspace = true
candid.workspace = true
hex.workspace = true
rstest.workspace = true
assert_matches.workspace = true
Loading

0 comments on commit ede16c0

Please sign in to comment.