Skip to content

Commit ede16c0

Browse files
authored
fix(ic-asset-certification): fix range response certification (#420)
add additional error types to ic-response-verification crate
1 parent d53a44e commit ede16c0

18 files changed

+676
-277
lines changed

Cargo.lock

+4
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
@@ -94,6 +94,7 @@ wasm-bindgen-console-logger = "0.1"
9494
rand = "0.8"
9595
getrandom = { version = "0.2", features = ["js"] }
9696
rand_chacha = "0.3"
97+
once_cell = "1"
9798

9899
ic-asset-certification = { path = "./packages/ic-asset-certification", version = "3.0.2" }
99100
ic-certification = { path = "./packages/ic-certification", default-features = false, version = "3.0.2" }

packages/ic-asset-certification/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ rstest.workspace = true
2727
assert_matches.workspace = true
2828
ic-response-verification.workspace = true
2929
ic-response-verification-test-utils.workspace = true
30+
ic-certification-testing.workspace = true
31+
once_cell.workspace = true

packages/ic-asset-certification/src/asset_router.rs

+14-9
Original file line numberDiff line numberDiff line change
@@ -908,10 +908,15 @@ impl<'content> AssetRouter<'content> {
908908
http::header::CONTENT_RANGE.to_string(),
909909
format!("bytes {range_begin}-{range_end}/{total_length}"),
910910
));
911-
request_headers.push((
912-
http::header::RANGE.to_string(),
913-
format!("bytes={range_begin}-"),
914-
));
911+
912+
// The `Range` request header will not be sent with the first request,
913+
// so we don't include it in certification for the first chunk.
914+
if range_begin != 0 {
915+
request_headers.push((
916+
http::header::RANGE.to_string(),
917+
format!("bytes={range_begin}-"),
918+
));
919+
}
915920
};
916921

917922
Self::prepare_response_and_certification(
@@ -1260,7 +1265,7 @@ mod tests {
12601265
let request = HttpRequest::get(&req_url).build();
12611266
let mut expected_response = build_206_response(
12621267
asset_body[0..ASSET_CHUNK_SIZE].to_vec(),
1263-
asset_range_chunk_cel_expr(),
1268+
asset_cel_expr(),
12641269
vec![
12651270
(
12661271
"cache-control".to_string(),
@@ -1460,7 +1465,7 @@ mod tests {
14601465
.build();
14611466
let mut expected_response = build_206_response(
14621467
asset_body[0..ASSET_CHUNK_SIZE].to_vec(),
1463-
encoded_range_chunk_asset_cel_expr(),
1468+
asset_cel_expr(),
14641469
vec![
14651470
(
14661471
"cache-control".to_string(),
@@ -3085,7 +3090,7 @@ mod tests {
30853090
.get(format!("/{}", TWO_CHUNKS_ASSET_NAME), None, Some(0));
30863091
let expected_first_chunk_response = build_206_response(
30873092
first_chunk_body.to_vec(),
3088-
asset_range_chunk_cel_expr(),
3093+
asset_cel_expr(),
30893094
vec![
30903095
(
30913096
"cache-control".to_string(),
@@ -3111,7 +3116,7 @@ mod tests {
31113116
);
31123117
let expected_first_chunk_gzip_response = build_206_response(
31133118
first_chunk_gzip_body.to_vec(),
3114-
asset_range_chunk_cel_expr(),
3119+
encoded_asset_cel_expr(),
31153120
vec![
31163121
(
31173122
"cache-control".to_string(),
@@ -3251,7 +3256,7 @@ mod tests {
32513256
let first_chunk_body = &full_body[0..ASSET_CHUNK_SIZE];
32523257
let expected_first_chunk_response = build_206_response(
32533258
first_chunk_body.to_vec(),
3254-
asset_range_chunk_cel_expr(),
3259+
asset_cel_expr(),
32553260
vec![
32563261
(
32573262
"cache-control".to_string(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use ic_asset_certification::ASSET_CHUNK_SIZE;
2+
use ic_response_verification_test_utils::hash;
3+
use rand_chacha::{
4+
rand_core::{RngCore, SeedableRng},
5+
ChaCha20Rng,
6+
};
7+
8+
pub fn asset_chunk(asset_body: &[u8], chunk_number: usize) -> &[u8] {
9+
let start = chunk_number * ASSET_CHUNK_SIZE;
10+
let end = start + ASSET_CHUNK_SIZE;
11+
&asset_body[start..end.min(asset_body.len())]
12+
}
13+
14+
pub fn asset_body(asset_name: &str, asset_size: usize) -> Vec<u8> {
15+
let mut rng = ChaCha20Rng::from_seed(hash(asset_name));
16+
let mut body = vec![0u8; asset_size];
17+
rng.fill_bytes(&mut body);
18+
19+
body
20+
}
21+
22+
#[macro_export]
23+
macro_rules! assert_contains {
24+
($vec:expr, $elems:expr) => {
25+
for elem in $elems {
26+
assert!(
27+
$vec.contains(&elem),
28+
"assertion failed: Expected vector {:?} to contain element {:?}",
29+
$vec,
30+
elem
31+
);
32+
}
33+
};
34+
}
35+
36+
#[macro_export]
37+
macro_rules! assert_response_eq {
38+
($actual:expr, $expected:expr) => {
39+
let actual: &ic_http_certification::HttpResponse = &$actual;
40+
let expected: &ic_http_certification::HttpResponse = &$expected;
41+
42+
assert_eq!(actual.status_code(), expected.status_code());
43+
assert_eq!(actual.body(), expected.body());
44+
assert_contains!(actual.headers(), expected.headers());
45+
};
46+
}
47+
48+
#[macro_export]
49+
macro_rules! assert_verified_response_eq {
50+
($actual:expr, $expected:expr) => {
51+
let actual: &ic_response_verification::types::VerifiedResponse = &$actual;
52+
let expected: &ic_http_certification::HttpResponse = &$expected;
53+
54+
assert_eq!(actual.status_code, Some(expected.status_code().as_u16()));
55+
assert_eq!(actual.body, expected.body());
56+
assert_contains!(actual.headers, expected.headers());
57+
};
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
use http::StatusCode;
2+
use ic_asset_certification::{Asset, AssetConfig, AssetEncoding, AssetRouter, ASSET_CHUNK_SIZE};
3+
use ic_certification_testing::{CertificateBuilder, CertificateData};
4+
use ic_http_certification::{
5+
DefaultCelBuilder, DefaultResponseCertification, HeaderField, HttpRequest, HttpResponse,
6+
};
7+
use ic_response_verification::verify_request_response_pair;
8+
use ic_response_verification_test_utils::{create_canister_id, get_current_timestamp};
9+
use once_cell::sync::OnceCell;
10+
use rstest::*;
11+
12+
mod common;
13+
use common::*;
14+
15+
const ASSET_ONE_NAME: &str = "asset_one";
16+
const ASSET_ONE_SIZE: usize = ASSET_CHUNK_SIZE + 1;
17+
18+
const MAX_CERT_TIME_OFFSET_NS: u128 = 300_000_000_000;
19+
const MIN_REQUESTED_VERIFICATION_VERSION: u8 = 2;
20+
21+
#[rstest]
22+
fn should_certify_long_asset_chunkwise(
23+
asset_one_body: &'static [u8],
24+
asset_one_chunk_one: &'static [u8],
25+
asset_one_chunk_two: &'static [u8],
26+
) {
27+
let current_time = get_current_timestamp();
28+
let canister_id = create_canister_id("rdmx6-jaaaa-aaaaa-aaadq-cai");
29+
let req_url = format!("/{}", ASSET_ONE_NAME);
30+
31+
let mut asset_router = AssetRouter::default();
32+
let assets = [Asset::new(ASSET_ONE_NAME, asset_one_body)];
33+
let asset_configs = [asset_config(ASSET_ONE_NAME.to_string(), vec![])];
34+
asset_router.certify_assets(assets, asset_configs).unwrap();
35+
36+
let certified_data = asset_router.root_hash();
37+
let CertificateData {
38+
cbor_encoded_certificate,
39+
certificate: _,
40+
root_key,
41+
} = CertificateBuilder::new(&canister_id.to_string(), &certified_data)
42+
.expect("Failed to create CertificateBuilder")
43+
.with_time(current_time)
44+
.build()
45+
.expect("Failed to create CertificateData from CertificateBuilder");
46+
47+
let mut expected_headers = common_asset_headers();
48+
expected_headers.extend(vec![
49+
("content-type".to_string(), "text/html".to_string()),
50+
("content-length".to_string(), ASSET_CHUNK_SIZE.to_string()),
51+
(
52+
"content-range".to_string(),
53+
format!("bytes 0-{}/{}", ASSET_CHUNK_SIZE - 1, ASSET_ONE_SIZE),
54+
),
55+
]);
56+
let expected_chunk_one_res = HttpResponse::builder()
57+
.with_status_code(StatusCode::PARTIAL_CONTENT)
58+
.with_headers(expected_headers)
59+
.with_body(asset_one_chunk_one)
60+
.build();
61+
62+
let chunk_one_req = HttpRequest::get(&req_url).build();
63+
let chunk_one_res = asset_router
64+
.serve_asset(&cbor_encoded_certificate, &chunk_one_req)
65+
.unwrap();
66+
assert_response_eq!(chunk_one_res, expected_chunk_one_res);
67+
68+
let chunk_one_verification = verify_request_response_pair(
69+
chunk_one_req,
70+
chunk_one_res,
71+
canister_id.as_ref(),
72+
current_time,
73+
MAX_CERT_TIME_OFFSET_NS,
74+
&root_key,
75+
MIN_REQUESTED_VERIFICATION_VERSION,
76+
)
77+
.unwrap();
78+
assert_eq!(chunk_one_verification.verification_version, 2);
79+
assert_verified_response_eq!(
80+
chunk_one_verification.response.unwrap(),
81+
expected_chunk_one_res
82+
);
83+
84+
let mut expected_headers = common_asset_headers();
85+
expected_headers.extend(vec![
86+
("content-type".to_string(), "text/html".to_string()),
87+
(
88+
"content-length".to_string(),
89+
(ASSET_ONE_SIZE - ASSET_CHUNK_SIZE).to_string(),
90+
),
91+
(
92+
"content-range".to_string(),
93+
format!(
94+
"bytes {}-{}/{}",
95+
ASSET_CHUNK_SIZE,
96+
ASSET_ONE_SIZE - 1,
97+
ASSET_ONE_SIZE
98+
),
99+
),
100+
]);
101+
let expected_chunk_two_res = HttpResponse::builder()
102+
.with_status_code(StatusCode::PARTIAL_CONTENT)
103+
.with_headers(expected_headers)
104+
.with_body(asset_one_chunk_two)
105+
.build();
106+
107+
let chunk_two_req = HttpRequest::get(&req_url)
108+
.with_headers(vec![(
109+
"range".to_string(),
110+
format!("bytes={}-", ASSET_CHUNK_SIZE),
111+
)])
112+
.build();
113+
let chunk_two_res = asset_router
114+
.serve_asset(&cbor_encoded_certificate, &chunk_two_req)
115+
.unwrap();
116+
assert_response_eq!(chunk_two_res, expected_chunk_two_res);
117+
118+
let chunk_two_verification = verify_request_response_pair(
119+
chunk_two_req,
120+
chunk_two_res,
121+
canister_id.as_ref(),
122+
current_time,
123+
MAX_CERT_TIME_OFFSET_NS,
124+
&root_key,
125+
MIN_REQUESTED_VERIFICATION_VERSION,
126+
)
127+
.unwrap();
128+
assert_eq!(chunk_two_verification.verification_version, 2);
129+
assert_verified_response_eq!(
130+
chunk_two_verification.response.unwrap(),
131+
expected_chunk_two_res
132+
);
133+
}
134+
135+
#[fixture]
136+
fn asset_cel_expr() -> String {
137+
DefaultCelBuilder::full_certification()
138+
.with_response_certification(DefaultResponseCertification::response_header_exclusions(
139+
vec![],
140+
))
141+
.build()
142+
.to_string()
143+
}
144+
145+
#[fixture]
146+
fn asset_range_cel_expr() -> String {
147+
DefaultCelBuilder::full_certification()
148+
.with_request_headers(vec!["range"])
149+
.with_response_certification(DefaultResponseCertification::response_header_exclusions(
150+
vec![],
151+
))
152+
.build()
153+
.to_string()
154+
}
155+
156+
#[fixture]
157+
fn asset_one_body() -> &'static [u8] {
158+
static ASSET_ONE_BODY: OnceCell<Vec<u8>> = OnceCell::new();
159+
160+
ASSET_ONE_BODY.get_or_init(|| asset_body(ASSET_ONE_NAME, ASSET_ONE_SIZE))
161+
}
162+
163+
#[fixture]
164+
fn asset_one_chunk_one(asset_one_body: &'static [u8]) -> &'static [u8] {
165+
static ASSET_ONE_CHUNK: OnceCell<&[u8]> = OnceCell::new();
166+
167+
ASSET_ONE_CHUNK.get_or_init(|| asset_chunk(asset_one_body, 0))
168+
}
169+
170+
#[fixture]
171+
fn asset_one_chunk_two(asset_one_body: &'static [u8]) -> &'static [u8] {
172+
static ASSET_ONE_CHUNK: OnceCell<&[u8]> = OnceCell::new();
173+
174+
ASSET_ONE_CHUNK.get_or_init(|| asset_chunk(asset_one_body, 1))
175+
}
176+
177+
fn asset_config(path: String, encodings: Vec<(AssetEncoding, String)>) -> AssetConfig {
178+
AssetConfig::File {
179+
path,
180+
content_type: Some("text/html".to_string()),
181+
headers: common_asset_headers(),
182+
fallback_for: vec![],
183+
aliased_by: vec![],
184+
encodings,
185+
}
186+
}
187+
188+
fn common_asset_headers() -> Vec<HeaderField> {
189+
vec![(
190+
"cache-control".to_string(),
191+
"public, no-cache, no-store".to_string(),
192+
)]
193+
}

packages/ic-http-certification-tests/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ ic-types.workspace = true
2222
candid.workspace = true
2323
hex.workspace = true
2424
rstest.workspace = true
25+
assert_matches.workspace = true

0 commit comments

Comments
 (0)