Skip to content

Commit

Permalink
feat: move request and response hashing from ic-response-verification…
Browse files Browse the repository at this point in the history
… to ic-http-certification

BREAKING CHANGE:
  • Loading branch information
nathanosdev committed Dec 7, 2023
1 parent ddc393d commit d4a5506
Show file tree
Hide file tree
Showing 21 changed files with 380 additions and 426 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions packages/ic-http-certification/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ candid.workspace = true
serde.workspace = true
http.workspace = true
urlencoding.workspace = true
ic-representation-independent-hash.workspace = true
ic-certification.workspace = true
thiserror.workspace = true

[dev-dependencies]
rstest.workspace = true
hex.workspace = true
8 changes: 4 additions & 4 deletions packages/ic-http-certification/src/cel/cel_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::borrow::Cow;
/// A certification CEL expression defintion.
/// Contains an enum variant for each CEL function supported for certification.
/// Currently only one variant is supported: [CelExpression::DefaultCertification].
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CelExpression<'a> {
/// A certification CEL expression definition that uses the `default_certification` function.
/// This is currently the only supported function.
Expand All @@ -27,7 +27,7 @@ impl<'a> CelExpression<'a> {
///
/// [request_certification](DefaultCertification::request_certification) is used for configuring request certification, and
/// [response_certification](DefaultCertification::response_certification) is used for configuring response certification.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DefaultCertification<'a> {
/// Options for configuring certification of a request.
///
Expand All @@ -48,7 +48,7 @@ pub struct DefaultCertification<'a> {
/// The request method and body are always certified, but this struct allows configuring the
/// certification of request [headers](DefaultRequestCertification::headers) and
/// [query parameters](DefaultRequestCertification::query_parameters).
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DefaultRequestCertification<'a> {
/// A list of request headers to include in certification.
///
Expand All @@ -70,7 +70,7 @@ pub struct DefaultRequestCertification<'a> {
/// [CertifiedResponseHeaders](DefaultResponseCertification::CertifiedResponseHeaders) variant,
/// and response headers may be excluded using the
/// [ResponseHeaderExclusions](DefaultResponseCertification::ResponseHeaderExclusions) variant.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DefaultResponseCertification<'a> {
/// A list of response headers to include in certification.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ pub use request_hash::*;

mod response_hash;
pub use response_hash::*;

/// Sha256 Digest: 32 bytes
pub type Hash = [u8; 32];
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
use crate::error::ResponseVerificationResult;
use crate::types::RequestCertification;
use ic_certification::hash_tree::Hash;
use ic_http_certification::HttpRequest;
use super::Hash;
use crate::{cel::DefaultRequestCertification, HttpCertificationResult, HttpRequest};
use ic_representation_independent_hash::{hash, representation_independent_hash, Value};

/// Calculates the
/// [Representation Independent Hash](https://internetcomputer.org/docs/current/references/ic-interface-spec/#hash-of-map)
/// of [crate::types::Request] according to [crate::types::RequestCertification] returned from
/// [crate::cel::cel_to_certification].
pub fn request_hash(
request: &HttpRequest,
request_certification: &RequestCertification,
) -> ResponseVerificationResult<Hash> {
pub fn request_hash<'a>(
request: &'a HttpRequest,
request_certification: &'a DefaultRequestCertification,
) -> HttpCertificationResult<Hash> {
let mut filtered_headers = get_filtered_headers(&request.headers, request_certification);

filtered_headers.push((
Expand All @@ -37,14 +35,14 @@ pub fn request_hash(

fn get_filtered_headers(
headers: &[(String, String)],
request_certification: &RequestCertification,
request_certification: &DefaultRequestCertification,
) -> Vec<(String, Value)> {
headers
.iter()
.filter_map(|(header_name, header_value)| {
let is_header_included =
request_certification
.certified_request_headers
.headers
.iter()
.any(|header_to_include| {
header_to_include.eq_ignore_ascii_case(&header_name.to_string())
Expand All @@ -62,7 +60,7 @@ fn get_filtered_headers(
.collect()
}

fn get_filtered_query(query: &str, request_certification: &RequestCertification) -> String {
fn get_filtered_query(query: &str, request_certification: &DefaultRequestCertification) -> String {
let filtered_query_string = query
.split('&')
.filter(|query_fragment| {
Expand All @@ -71,11 +69,12 @@ fn get_filtered_query(query: &str, request_certification: &RequestCertification)

query_param_name
.map(|query_param_name| {
request_certification.certified_query_parameters.iter().any(
|query_param_to_include| {
request_certification
.query_parameters
.iter()
.any(|query_param_to_include| {
query_param_to_include.eq_ignore_ascii_case(query_param_name)
},
)
})
})
.unwrap_or(false)
})
Expand All @@ -88,12 +87,13 @@ fn get_filtered_query(query: &str, request_certification: &RequestCertification)
#[cfg(test)]
mod tests {
use super::*;
use std::borrow::Cow;

#[test]
fn request_hash_without_query() {
let request_certification = RequestCertification {
certified_request_headers: vec!["host".into()],
certified_query_parameters: vec![],
let request_certification = DefaultRequestCertification {
headers: Cow::Borrowed(&["host"]),
query_parameters: Cow::Borrowed(&[]),
};
let request = create_request("https://ic0.app");
let expected_hash =
Expand All @@ -107,9 +107,9 @@ mod tests {

#[test]
fn request_hash_with_query() {
let request_certification = RequestCertification {
certified_request_headers: vec!["host".into()],
certified_query_parameters: vec!["q".into(), "name".into()],
let request_certification = DefaultRequestCertification {
headers: Cow::Borrowed(&["host"]),
query_parameters: Cow::Borrowed(&["q", "name"]),
};
let request =
create_request("https://ic0.app?q=hello+world&name=foo&name=bar&color=purple");
Expand All @@ -124,9 +124,9 @@ mod tests {

#[test]
fn request_hash_query_order_matters() {
let request_certification = RequestCertification {
certified_request_headers: vec!["host".into()],
certified_query_parameters: vec!["q".into(), "name".into()],
let request_certification = DefaultRequestCertification {
headers: Cow::Borrowed(&["host"]),
query_parameters: Cow::Borrowed(&["q", "name"]),
};
let request =
create_request("https://ic0.app?q=hello+world&name=foo&name=bar&color=purple");
Expand All @@ -141,9 +141,9 @@ mod tests {

#[test]
fn request_hash_query_with_fragment_does_not_change() {
let request_certification = RequestCertification {
certified_request_headers: vec!["host".into()],
certified_query_parameters: vec!["q".into(), "name".into()],
let request_certification = DefaultRequestCertification {
headers: Cow::Borrowed(&["host"]),
query_parameters: Cow::Borrowed(&["q", "name"]),
};
let request =
create_request("https://ic0.app?q=hello+world&name=foo&name=bar&color=purple");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use crate::types::ResponseCertification;
use ic_certification::hash_tree::Hash;
use ic_http_certification::HttpResponse;
use super::Hash;
use crate::{DefaultResponseCertification, HttpResponse};
use ic_representation_independent_hash::{hash, representation_independent_hash, Value};

const CERTIFICATE_HEADER_NAME: &str = "IC-Certificate";
Expand All @@ -20,19 +19,19 @@ pub struct ResponseHeaders {

/// Filters headers of [crate::types::Response] according to [crate::types::ResponseCertification]
/// returned from [crate::cel::cel_to_certification].
pub fn filter_response_headers(
pub fn filter_response_headers<'a>(
response: &HttpResponse,
response_certification: &ResponseCertification,
response_certification: &DefaultResponseCertification<'a>,
) -> ResponseHeaders {
let headers_filter: Box<dyn Fn(_) -> _> = match response_certification {
ResponseCertification::CertifiedHeaders(headers_to_include) => {
DefaultResponseCertification::CertifiedResponseHeaders(headers_to_include) => {
Box::new(move |header_name: &String| {
headers_to_include.iter().any(|header_to_include| {
header_to_include.eq_ignore_ascii_case(&header_name.to_string())
})
})
}
ResponseCertification::HeaderExclusions(headers_to_exclude) => {
DefaultResponseCertification::ResponseHeaderExclusions(headers_to_exclude) => {
Box::new(move |header_name: &String| {
!headers_to_exclude.iter().any(|header_to_exclude| {
header_to_exclude.eq_ignore_ascii_case(&header_name.to_string())
Expand Down Expand Up @@ -116,7 +115,7 @@ pub fn response_headers_hash(status_code: &u64, response_headers: &ResponseHeade
/// [crate::cel::cel_to_certification].
pub fn response_hash(
response: &HttpResponse,
response_certification: &ResponseCertification,
response_certification: &DefaultResponseCertification,
) -> Hash {
let filtered_headers = filter_response_headers(response, response_certification);
let concatenated_hashes = [
Expand All @@ -131,7 +130,6 @@ pub fn response_hash(
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::test_utils::remove_whitespace;

const HELLO_WORLD_BODY: &[u8] = &[72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33];
const CERTIFICATE: &str = "certificate=:SGVsbG8gQ2VydGlmaWNhdGUh:,tree=:SGVsbG8gVHJlZSE=:";
Expand Down Expand Up @@ -167,7 +165,7 @@ mod tests {
#[test]
fn response_with_certified_headers_without_excluded_headers() {
let response_certification =
ResponseCertification::CertifiedHeaders(vec!["Accept-Encoding".into()]);
DefaultResponseCertification::certified_response_headers(&["Accept-Encoding"]);
let response = create_response(CERTIFIED_HEADERS_CEL_EXPRESSION);
let response_headers = filter_response_headers(&response, &response_certification);

Expand All @@ -179,9 +177,9 @@ mod tests {

#[test]
fn response_with_certified_headers() {
let response_certification = ResponseCertification::CertifiedHeaders(vec![
"Accept-Encoding".into(),
"Cache-Control".into(),
let response_certification = DefaultResponseCertification::certified_response_headers(&[
"Accept-Encoding",
"Cache-Control",
]);
let response = create_response(CERTIFIED_HEADERS_CEL_EXPRESSION);
let response_headers = filter_response_headers(&response, &response_certification);
Expand All @@ -198,9 +196,9 @@ mod tests {

#[test]
fn response_hash_with_certified_headers() {
let response_certification = ResponseCertification::CertifiedHeaders(vec![
"Accept-Encoding".into(),
"Cache-Control".into(),
let response_certification = DefaultResponseCertification::certified_response_headers(&[
"Accept-Encoding",
"Cache-Control",
]);
let response = create_response(CERTIFIED_HEADERS_CEL_EXPRESSION);
let expected_hash =
Expand All @@ -215,7 +213,7 @@ mod tests {
#[test]
fn response_hash_with_certified_headers_without_excluded_headers() {
let response_certification =
ResponseCertification::CertifiedHeaders(vec!["Accept-Encoding".into()]);
DefaultResponseCertification::certified_response_headers(&["Accept-Encoding"]);
let response = create_response(CERTIFIED_HEADERS_CEL_EXPRESSION);
let response_without_excluded_headers = HttpResponse {
status_code: 200,
Expand All @@ -238,9 +236,9 @@ mod tests {

#[test]
fn response_hash_with_header_exclusions() {
let response_certification = ResponseCertification::HeaderExclusions(vec![
"Accept-Encoding".into(),
"Cache-Control".into(),
let response_certification = DefaultResponseCertification::response_header_exclusions(&[
"Accept-Encoding",
"Cache-Control",
]);
let response = create_response(HEADER_EXCLUSIONS_CEL_EXPRESSION);
let expected_hash =
Expand All @@ -255,7 +253,7 @@ mod tests {
#[test]
fn response_hash_with_header_exclusions_without_excluded_headers() {
let response_certification =
ResponseCertification::HeaderExclusions(vec!["Content-Security-Policy".into()]);
DefaultResponseCertification::response_header_exclusions(&["Content-Security-Policy"]);
let response = create_response(HEADER_EXCLUSIONS_CEL_EXPRESSION);
let response_without_excluded_headers = HttpResponse {
status_code: 200,
Expand All @@ -280,9 +278,9 @@ mod tests {

#[test]
fn response_headers_hash_with_certified_headers() {
let response_certification = ResponseCertification::CertifiedHeaders(vec![
"Accept-Encoding".into(),
"Cache-Control".into(),
let response_certification = DefaultResponseCertification::certified_response_headers(&[
"Accept-Encoding",
"Cache-Control",
]);
let response = create_response(CERTIFIED_HEADERS_CEL_EXPRESSION);
let expected_hash =
Expand All @@ -298,7 +296,7 @@ mod tests {
#[test]
fn response_headers_hash_with_certified_headers_without_excluded_headers() {
let response_certification =
ResponseCertification::CertifiedHeaders(vec!["Accept-Encoding".into()]);
DefaultResponseCertification::certified_response_headers(&["Accept-Encoding"]);
let response = create_response(CERTIFIED_HEADERS_CEL_EXPRESSION);
let response_without_excluded_headers = HttpResponse {
status_code: 200,
Expand Down Expand Up @@ -327,9 +325,9 @@ mod tests {

#[test]
fn response_headers_hash_with_header_exclusions() {
let response_certification = ResponseCertification::HeaderExclusions(vec![
"Accept-Encoding".into(),
"Cache-Control".into(),
let response_certification = DefaultResponseCertification::response_header_exclusions(&[
"Accept-Encoding",
"Cache-Control",
]);
let response = create_response(HEADER_EXCLUSIONS_CEL_EXPRESSION);
let expected_hash =
Expand All @@ -345,7 +343,7 @@ mod tests {
#[test]
fn response_headers_hash_with_header_exclusions_without_excluded_headers() {
let response_certification =
ResponseCertification::HeaderExclusions(vec!["Content-Security-Policy".into()]);
DefaultResponseCertification::response_header_exclusions(&["Content-Security-Policy"]);
let response = create_response(HEADER_EXCLUSIONS_CEL_EXPRESSION);
let response_without_excluded_headers = HttpResponse {
status_code: 200,
Expand Down Expand Up @@ -399,4 +397,8 @@ mod tests {
body: HELLO_WORLD_BODY.into(),
}
}

fn remove_whitespace<'a>(s: &'a str) -> String {
s.chars().filter(|c| !c.is_whitespace()).collect()
}
}
4 changes: 3 additions & 1 deletion packages/ic-http-certification/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,9 @@ Typically these requests have been routed through `raw` Internet Computer URLs i
)]

pub mod cel;
pub use cel::{DefaultCelBuilder, DefaultResponseCertification};
pub use cel::{CelExpression, DefaultCelBuilder, DefaultResponseCertification};
pub mod hash;
pub use hash::*;
pub mod error;
pub use error::*;
pub mod http;
Expand Down
3 changes: 2 additions & 1 deletion packages/ic-response-verification-wasm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"browser": "./dist/web/web.js",
"types": "./dist/web/web.d.ts",
"scripts": {
"build": "../../scripts/package.sh . ./dist"
"build": "../../scripts/package.sh . ./dist",
"test": " wasm-pack test --node"
}
}
2 changes: 1 addition & 1 deletion packages/ic-response-verification-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub fn get_max_verification_version() -> u8 {
}

#[wasm_bindgen(start)]
pub fn main() {
pub fn main_js() {
console_error_panic_hook::set_once();
log::set_logger(&wasm_bindgen_console_logger::DEFAULT_LOGGER).unwrap();
log::set_max_level(log::LevelFilter::Trace);
Expand Down
Loading

0 comments on commit d4a5506

Please sign in to comment.