From d5457e96b8e723a603d08056d10ed2cf70826d7a Mon Sep 17 00:00:00 2001 From: NathanosDev Date: Wed, 3 Jan 2024 17:22:58 +0100 Subject: [PATCH] feat(ic-http-certification): add certification builder improve underlying cel structures to prevent invalid cel expressions from being created --- .vscode/settings.json | 6 - Cargo.lock | 34 +-- packages/ic-http-certification/README.md | 242 +++++++++++----- .../src/cel/cel_builder.rs | 52 ++-- .../src/cel/cel_types.rs | 102 +++++-- .../src/cel/create_cel_expr.rs | 220 ++++++++------ .../ic-http-certification/src/hash/mod.rs | 2 +- .../src/hash/request_hash.rs | 3 +- .../src/hash/response_hash.rs | 67 +++-- packages/ic-http-certification/src/lib.rs | 272 ++++++++++++------ .../src/tree/certification.rs | 265 +++++++++++++++++ .../ic-http-certification/src/tree/mod.rs | 8 + .../src/cel/ast_mapping.rs | 25 +- .../ic-response-verification/src/cel/tests.rs | 53 ++-- .../src/validation/v2_validation.rs | 29 +- .../verify_request_response_pair.rs | 38 ++- ...se_verification_certification_scenarios.rs | 73 ++--- .../v2_response_verification_happy_path.rs | 29 +- .../v2_response_verification_sad_path.rs | 36 +-- 19 files changed, 1076 insertions(+), 480 deletions(-) delete mode 100644 .vscode/settings.json create mode 100644 packages/ic-http-certification/src/tree/certification.rs create mode 100644 packages/ic-http-certification/src/tree/mod.rs diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 835168e0..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "rust-analyzer.cargo.extraEnv": { - "CARGO_PROFILE_RUST_ANALYZER_INHERITS": "dev" - }, - "rust-analyzer.cargo.extraArgs": ["--profile", "rust-analyzer"] -} diff --git a/Cargo.lock b/Cargo.lock index 534726e5..549ff595 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -375,7 +375,7 @@ dependencies = [ "candid 0.9.11", "ic-cdk", "ic-cdk-macros", - "ic-certification 2.0.0", + "ic-certification 2.0.1", "serde", "serde_cbor", "sha2 0.10.8", @@ -1226,10 +1226,10 @@ dependencies = [ [[package]] name = "ic-cbor" -version = "2.0.0" +version = "2.0.1" dependencies = [ "candid 0.9.11", - "ic-certification 2.0.0", + "ic-certification 2.0.1", "ic-response-verification-test-utils", "leb128", "nom", @@ -1266,11 +1266,11 @@ dependencies = [ [[package]] name = "ic-certificate-verification" -version = "2.0.0" +version = "2.0.1" dependencies = [ "candid 0.9.11", "ic-cbor", - "ic-certification 2.0.0", + "ic-certification 2.0.1", "ic-certification-testing", "ic-response-verification-test-utils", "leb128", @@ -1293,7 +1293,7 @@ dependencies = [ [[package]] name = "ic-certification" -version = "2.0.0" +version = "2.0.1" dependencies = [ "hex", "serde", @@ -1304,7 +1304,7 @@ dependencies = [ [[package]] name = "ic-certification-testing" -version = "2.0.0" +version = "2.0.1" dependencies = [ "console_error_panic_hook", "getrandom", @@ -1327,7 +1327,7 @@ dependencies = [ [[package]] name = "ic-certification-testing-wasm" -version = "2.0.0" +version = "2.0.1" dependencies = [ "console_error_panic_hook", "ic-certification-testing", @@ -1489,12 +1489,12 @@ dependencies = [ [[package]] name = "ic-http-certification" -version = "2.0.0" +version = "2.0.1" dependencies = [ "candid 0.9.11", "hex", "http", - "ic-certification 2.0.0", + "ic-certification 2.0.1", "ic-representation-independent-hash", "rstest", "serde", @@ -1539,7 +1539,7 @@ dependencies = [ [[package]] name = "ic-representation-independent-hash" -version = "2.0.0" +version = "2.0.1" dependencies = [ "hex", "leb128", @@ -1548,7 +1548,7 @@ dependencies = [ [[package]] name = "ic-response-verification" -version = "2.0.0" +version = "2.0.1" dependencies = [ "base64 0.21.4", "candid 0.9.11", @@ -1557,7 +1557,7 @@ dependencies = [ "http", "ic-cbor", "ic-certificate-verification", - "ic-certification 2.0.0", + "ic-certification 2.0.1", "ic-certification-testing", "ic-crypto-tree-hash", "ic-http-certification", @@ -1580,12 +1580,12 @@ dependencies = [ [[package]] name = "ic-response-verification-test-utils" -version = "2.0.0" +version = "2.0.1" dependencies = [ "base64 0.21.4", "flate2", "hex", - "ic-certification 2.0.0", + "ic-certification 2.0.1", "ic-certification-testing", "ic-types", "leb128", @@ -1596,7 +1596,7 @@ dependencies = [ [[package]] name = "ic-response-verification-tests" -version = "2.0.0" +version = "2.0.1" dependencies = [ "anyhow", "hex", @@ -1610,7 +1610,7 @@ dependencies = [ [[package]] name = "ic-response-verification-wasm" -version = "2.0.0" +version = "2.0.1" dependencies = [ "base64 0.21.4", "console_error_panic_hook", diff --git a/packages/ic-http-certification/README.md b/packages/ic-http-certification/README.md index e5d7bfa5..c34c9e69 100644 --- a/packages/ic-http-certification/README.md +++ b/packages/ic-http-certification/README.md @@ -1,5 +1,13 @@ # Internet Computer HTTP Certification +HTTP Certification is a sub-protocol of the [Internet Computer](https://internetcomputer.org/) [HTTP Gateway Protocol](https://internetcomputer.org/docs/current/references/http-gateway-protocol-spec). It is used to verify HTTP responses received by an HTTP Gateway from a [canister](https://internetcomputer.org/how-it-works/canister-lifecycle/), with respect to the corresponding HTTP request sent by the HTTP Gateway to the canister. This allows HTTP Gateways to verify that the responses they receive from canisters are authentic and have not been tampered with by a malicious replica. + +This crate provides a foundation for implementing the HTTP Certification protocol in Rust canisters. Certification is implemented in a number of steps: + +1. [Defining CEL expressions](#defining-cel-expressions) +2. [Creating certifications](#creating-certifications) +3. ...coming soon!!! + ## Defining CEL Expressions [CEL](https://github.com/google/cel-spec) (Common Expression Language) is a portable expression language that can be used to enable different applications to more easily interoperate. It can be seen as the computation or expression counterpart to [Protocol Buffers](https://github.com/protocolbuffers/protobuf). @@ -13,27 +21,26 @@ CEL expressions can be created in two ways, by using the [CEL builder](#using-th Note that the `CelExpression` enum is not a CEL expression itself, but rather a Rust representation of a CEL expression. To convert a `CelExpression` into its `String` representation, use `CelExpression.to_string` or `create_cel_expr`. This applies to CEL expressions created both by the [CEL builder](#using-the-cel-builder) and [directly](#directly-creating-a-cel-expression). ```rust -use ic_http_certification::cel::CelExpression; +use ic_http_certification::cel::{CelExpression, DefaultCelExpression}; -let cel_expr = CelExpression::DefaultCertification(None).to_string(); +let cel_expr = CelExpression::Default(DefaultCelExpression::Skip).to_string(); ``` Alternatively: ```rust -use ic_http_certification::cel::{CelExpression, create_cel_expr}; +use ic_http_certification::cel::{CelExpression, DefaultCelExpression, create_cel_expr}; -let certification = CelExpression::DefaultCertification(None); -let cel_expr = create_cel_expr(&certification); +let certification = CelExpression::Default(DefaultCelExpression::Skip); ``` ### Using the CEL builder -The CEL builder interface is provided to ease the creation of CEL expressions through an ergonmic interface. If this interface does not meet your needs, you can also [create CEL expressions directly](#directly-creating-a-cel-expression). To define a CEL expression, start with `DefaultCelBuilder`. This struct provides a set of methods that can be used to define how your request and response pair should be certified. +The CEL builder interface is provided to ease the creation of CEL expressions through an ergonmic interface. If this interface does not meet your needs, you can also [create CEL expressions directly](#directly-creating-a-cel-expression). To define a CEL expression, start with `DefaultCelBuilder`. This struct provides a set of associated functions that can be used to define how your request and response pair should be certified. -When certifying requests, the request body and method are always certified. To additionally certify request headers and query parameters, use `with_request_headers` and `with_request_query_parameters` respectively. Both methods take a `str` slice as an argument. +When certifying requests, the request body and method are always certified. To additionally certify request headers and query parameters, use `with_request_headers` and `with_request_query_parameters` respectively. Both associated functions take a `str` slice as an argument. -When certifying a response, the response body and status code are always certified. To additionally certify response headers, use `with_response_certification`. This method takes the `DefaultResponseCertification` enum as an argument. To specify header inclusions, use the `certified_response_headers` function of the `DefaultResponseCertification` enum. Or to certify all response headers, with some exclusions, use the `response_header_exclusions` function of the `DefaultResponseCertification` enum. Both functions take a `str` slice as an argument. +When certifying a response, the response body and status code are always certified. To additionally certify response headers, use `with_response_certification`. This associated function takes the `DefaultResponseCertification` enum as an argument. To specify header inclusions, use the `certified_response_headers` associated function of the `DefaultResponseCertification` enum. Or to certify all response headers, with some exclusions, use the `response_header_exclusions` associated function of the `DefaultResponseCertification` enum. Both functions take a `str` slice as an argument. #### Fully certified request / response pair @@ -43,7 +50,7 @@ To define a fully certified request and response pair, including request headers use ic_http_certification::{DefaultCelBuilder, DefaultResponseCertification}; let cel_expr = DefaultCelBuilder::full_certification() - .with_request_headers(&["Accept", "Accept-Encoding", "If-Match"]) + .with_request_headers(&["Accept", "Accept-Encoding", "If-None-Match"]) .with_request_query_parameters(&["foo", "bar", "baz"]) .with_response_certification(DefaultResponseCertification::certified_response_headers(&[ "Cache-Control", @@ -86,12 +93,12 @@ let cel_expr = DefaultCelBuilder::full_certification() #### Skipping request certification -Request certification can be skipped entirely by using `DefaultCelBuilder::response_certification` instead of `DefaultCelBuilder::full_certification`. For example: +Request certification can be skipped entirely by using `DefaultCelBuilder::response_only_certification` instead of `DefaultCelBuilder::full_certification`. For example: ```rust use ic_http_certification::{DefaultCelBuilder, DefaultResponseCertification}; -let cel_expr = DefaultCelBuilder::response_certification() +let cel_expr = DefaultCelBuilder::response_only_certification() .with_response_certification(DefaultResponseCertification::response_header_exclusions(&[ "Date", "Cookie", @@ -102,14 +109,14 @@ let cel_expr = DefaultCelBuilder::response_certification() #### Partially certified response -Similiarly to request certification, any number of response headers can be provided via the `certified_response_headers` function of the `DefaultResponseCertification` enum when calling `with_response_certification`. The provided array can also be an empty. If the array is empty, or the method is not called, then no response headers will be certified. +Similiarly to request certification, any number of response headers can be provided via the `certified_response_headers` associated function of the `DefaultResponseCertification` enum when calling `with_response_certification`. The provided array can also be an empty. If the array is empty, or the associated function is not called, then no response headers will be certified. For example, to certify only the response body and status code: ```rust use ic_http_certification::DefaultCelBuilder; -let cel_expr = DefaultCelBuilder::response_certification().build(); +let cel_expr = DefaultCelBuilder::response_only_certification().build(); ``` This can also be done more explicitly: @@ -117,18 +124,18 @@ This can also be done more explicitly: ```rust use ic_http_certification::{DefaultCelBuilder, DefaultResponseCertification}; -let cel_expr = DefaultCelBuilder::response_certification() +let cel_expr = DefaultCelBuilder::response_only_certification() .with_response_certification(DefaultResponseCertification::certified_response_headers(&[])) .build(); ``` -The same applies when both when using `DefaultCelBuilder::response_certification` and `DefaultCelBuilder::full_certification`. +The same applies when both when using `DefaultCelBuilder::response_only_certification` and `DefaultCelBuilder::full_certification`. ```rust use ic_http_certification::DefaultCelBuilder; let cel_expr = DefaultCelBuilder::full_certification() - .with_request_headers(&["Accept", "Accept-Encoding", "If-Match"]) + .with_request_headers(&["Accept", "Accept-Encoding", "If-None-Match"]) .with_request_query_parameters(&["foo", "bar", "baz"]) .build(); ``` @@ -149,34 +156,118 @@ Skipping certification may seem counter-intuitive at first, but it is not always Typically these requests have been routed through `raw` Internet Computer URLs in the past, but this is dangerous because `raw` URLs allow any responding replica to decide whether or not certification is required. In contrast, by skipping certification using the above method with a non-`raw` URL, a replica will no longer be able to decide whether or not certification is required and instead this decision will be made by the canister itself and the result will go through consensus. -### Directly creating a CEL expression +## Creating certifications + +Once a CEL expression has been defined, it can be used in conjunction with an `HttpRequest` and `HttpResponse` to create a [Certification]. The [Certification] enum has three variants, each with a corresponding associated function used to create that particular variant: + +- The `Full` variant is used to include both the `HttpRequest` and the corresponding `HttpResponse` in certification. +- The `ResponseOnly` variant is used to include only the `HttpResponse` in certification and exclude the corresponding `HttpRequest` from certification. +- The `Skip` variant is used to skip certification entirely. + +### Full certification + +To perform a full certification, a CEL expression created from `DefaultCelBuilder::full_certification` is required, along with an `HttpRequest` and `HttpResponse` and optionally, a pre-calculated response body hash. For example: + +```rust +use ic_http_certification::{Certification, HttpRequest, HttpResponse, DefaultCelBuilder, DefaultResponseCertification}; + +let cel_expr = DefaultCelBuilder::full_certification() + .with_request_headers(&["Accept", "Accept-Encoding", "If-None-Match"]) + .with_request_query_parameters(&["foo", "bar", "baz"]) + .with_response_certification(DefaultResponseCertification::certified_response_headers(&[ + "Cache-Control", + "ETag", + ])) + .build(); -To define a CEL expression, start with the `CelExpression` enum. This enum provides a set of variants that can be used to define different types of CEL expressions supported by Internet Computer HTTP Gateways. Currently only one variant is supported, known as the "default" certification expression, but more may be added in the future as HTTP certification evolves over time. +let request = HttpRequest { + method: "GET".to_string(), + url: "/index.html?foo=a&bar=b&baz=c".to_string(), + headers: vec![ + ("Accept".to_string(), "application/json".to_string()), + ("Accept-Encoding".to_string(), "gzip".to_string()), + ("If-None-Match".to_string(), "987654321".to_string()), + ], + body: vec![], +}; + +let response = HttpResponse { + status_code: 200, + headers: vec![ + ("Cache-Control".to_string(), "no-cache".to_string()), + ("ETag".to_string(), "123456789".to_string()), + ], + body: vec![1, 2, 3, 4, 5, 6], +}; + +let certification = Certification::full(&cel_expr, &request, &response, None); +``` -When certifying requests, the request body and method are always certified. To additionally certify request headers and query parameters, use the `headers` and `query_paramters` of `DefaultRequestCertification` struct. Both properties take a `str` slice as an argument. +### Response-only certification -When certifying a response, the response body and status code are always certified. To additionally certify response headers, use the `certified_response_headers` function of the `DefaultResponseCertification` enum. Or to certify all response headers, with some exclusions, use the `response_header_exclusions` function of the `DefaultResponseCertification` enum. Both functions take a `str` slice as an argument. +To perform a response-only certification, a CEL expression created from `DefaultCelBuilder::response_only_certification` is required, along with an `HttpResponse` and optionally, a pre-calculated response body hash. For example: + +```rust +use ic_http_certification::{Certification, HttpResponse, DefaultCelBuilder, DefaultResponseCertification}; + +let cel_expr = DefaultCelBuilder::response_only_certification() + .with_response_certification(DefaultResponseCertification::certified_response_headers(&[ + "Cache-Control", + "ETag", + ])) + .build(); + +let response = HttpResponse { + status_code: 200, + headers: vec![ + ("Cache-Control".to_string(), "no-cache".to_string()), + ("ETag".to_string(), "123456789".to_string()), + ], + body: vec![1, 2, 3, 4, 5, 6], +}; + +let certification = Certification::response_only(&cel_expr, &response, None); +``` + +### Skipping certification + +Skipping certification does not need an explicit CEL expression to be defined since it's always the same. For example: + +```rust +use ic_http_certification::Certification; + +let certification = Certification::skip(); +``` + +## Directly creating a CEL expression + +To define a CEL expression, start with the `CelExpression` enum. This enum provides a set of variants that can be used to define different types of CEL expressions supported by Internet Computer HTTP Gateways. Currently only one variant is supported, known as the "default" certification expression, but more may be added in the future as the HTTP certification protocol evolves over time. + +When certifying requests, the request body and method are always certified. To additionally certify request headers and query parameters, use the `headers` and `query_paramters` fiels of the `DefaultRequestCertification` struct. Both fields take a `str` slice as an argument. + +When certifying a response, the response body and status code are always certified. To additionally certify response headers, use the `certified_response_headers` associated function of the `DefaultResponseCertification` enum. Or to certify all response headers, with some exclusions, use the `response_header_exclusions` associated function of the `DefaultResponseCertification` enum. Both associated functions take a `str` slice as an argument. Note that the example CEL expressions provided below are formatted for readability. The actual CEL expressions produced by `CelExpression::to_string` and `create_cel_expr` are minified. The minified CEL expression is preferred because it is more compact, resulting in a smaller payload and a faster evaluation time for the HTTP Gateway that is verifying the certification, but the formatted versions are also accepted. -#### Fully certified request / response pair +### Fully certified request / response pair To define a fully certified request and response pair, including request headers, query parameters, and response headers: ```rust use std::borrow::Cow; -use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultRequestCertification, DefaultResponseCertification}; +use ic_http_certification::cel::{CelExpression, DefaultCelExpression, DefaultFullCelExpression, DefaultRequestCertification, DefaultResponseCertification}; -let cel_expr = CelExpression::DefaultCertification(Some(DefaultCertification { - request_certification: Some(DefaultRequestCertification { - headers: Cow::Borrowed(&["Accept", "Accept-Encoding", "If-Match"]), - query_parameters: Cow::Borrowed(&["foo", "bar", "baz"]), - }), - response_certification: DefaultResponseCertification::certified_response_headers(&[ - "ETag", - "Cache-Control", - ]), -})); +let cel_expr = CelExpression::Default(DefaultCelExpression::Full( + DefaultFullCelExpression { + request: DefaultRequestCertification { + headers: Cow::Borrowed(&["Accept", "Accept-Encoding", "If-None-Match"]), + query_parameters: Cow::Borrowed(&["foo", "bar", "baz"]), + }, + response: DefaultResponseCertification::certified_response_headers(&[ + "ETag", + "Cache-Control", + ]), + })); ``` This will produce the following CEL expression: @@ -185,7 +276,7 @@ This will produce the following CEL expression: default_certification ( ValidationArgs { request_certification: RequestCertification { - certified_request_headers: ["Accept", "Accept-Encoding", "If-Match"], + certified_request_headers: ["Accept", "Accept-Encoding", "If-None-Match"], certified_query_parameters: ["foo", "bar", "baz"] }, response_certification: ResponseCertification { @@ -200,26 +291,27 @@ default_certification ( ) ``` -#### Partially certified request +### Partially certified request -Any number of request headers or query parameters can be provided via the `headers` and `query_parameters` properties of the `DefaultRequestCertification` struct, and both can be an empty array. If the `headers` property is empty, no request headers will be certified. Likewise for the `query_parameters` property, if it is empty then no query parameters will be certified. If both are empty, only the request body and method will be certified. +Any number of request headers or query parameters can be provided via the `headers` and `query_parameters` fields of the `DefaultRequestCertification` struct, and both can be an empty array. If the `headers` field is empty, no request headers will be certified. Likewise for the `query_parameters` field, if it is empty then no query parameters will be certified. If both are empty, only the request body and method will be certified. For example, to certify only the request body and method: ```rust use std::borrow::Cow; -use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultRequestCertification, DefaultResponseCertification}; +use ic_http_certification::cel::{CelExpression, DefaultCelExpression, DefaultFullCelExpression, DefaultRequestCertification, DefaultResponseCertification}; -let cel_expr = CelExpression::DefaultCertification(Some(DefaultCertification { - request_certification: Some(DefaultRequestCertification { - headers: Cow::Borrowed(&["Accept", "Accept-Encoding", "If-Match"]), - query_parameters: Cow::Borrowed(&["foo", "bar", "baz"]), - }), - response_certification: DefaultResponseCertification::certified_response_headers(&[ - "ETag", - "Cache-Control", - ]), -})); +let cel_expr = CelExpression::Default(DefaultCelExpression::Full( + DefaultFullCelExpression { + request: DefaultRequestCertification { + headers: Cow::Borrowed(&[]), + query_parameters: Cow::Borrowed(&[]), + }, + response: DefaultResponseCertification::certified_response_headers(&[ + "ETag", + "Cache-Control", + ]), + })); ``` This will produce the following CEL expression: @@ -243,20 +335,21 @@ default_certification ( ) ``` -#### Skipping request certification +### Skipping request certification -Request certification can be skipped entirely by setting the `request_certification` property of the `DefaultCertification` struct to `None`. For example: +Request certification can be skipped entirely by using the `ResponseOnly` variant of the `DefaultCelExpression` struct. For example: ```rust -use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultResponseCertification}; - -let cel_expr = CelExpression::DefaultCertification(Some(DefaultCertification { - request_certification: None, - response_certification: DefaultResponseCertification::certified_response_headers(&[ - "ETag", - "Cache-Control", - ]), -})); +use std::borrow::Cow; +use ic_http_certification::cel::{CelExpression, DefaultCelExpression, DefaultResponseOnlyCelExpression, DefaultResponseCertification}; + +let cel_expr = CelExpression::Default(DefaultCelExpression::ResponseOnly( + DefaultResponseOnlyCelExpression { + response: DefaultResponseCertification::certified_response_headers(&[ + "ETag", + "Cache-Control", + ]), + })); ``` This will produce the following CEL expression: @@ -277,9 +370,9 @@ default_certification ( ) ``` -#### Partially certified response +### Partially certified response -Similiarly to request certification, any number of response headers can be provided via the `certified_response_headers` variant of the `DefaultResponseCertification` enum, and it can also be an empty array. If the array is empty, no response headers will be certified. For example: +Similiarly to request certification, any number of response headers can be provided via the `certified_response_headers` associated function of the `DefaultResponseCertification` enum, and it can also be an empty array. If the array is empty, no response headers will be certified. For example: ```rust use std::borrow::Cow; @@ -287,7 +380,7 @@ use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultReq let cel_expr = CelExpression::DefaultCertification(Some(DefaultCertification { request_certification: Some(DefaultRequestCertification { - headers: Cow::Borrowed(&["Accept", "Accept-Encoding", "If-Match"]), + headers: Cow::Borrowed(&["Accept", "Accept-Encoding", "If-None-Match"]), query_parameters: Cow::Borrowed(&["foo", "bar", "baz"]), }), response_certification: DefaultResponseCertification::certified_response_headers(&[]), @@ -300,7 +393,7 @@ This will produce the following CEL expression: default_certification ( ValidationArgs { request_certification: RequestCertification { - certified_request_headers: ["Accept", "Accept-Encoding", "If-Match"], + certified_request_headers: ["Accept", "Accept-Encoding", "If-None-Match"], certified_query_parameters: ["foo", "bar", "baz"] }, response_certification: ResponseCertification { @@ -312,19 +405,20 @@ default_certification ( ) ``` -If the `response_header_exclusions` function is used, an empty array will certify _all_ response headers. For example: +If the `response_header_exclusions` associated function is used, an empty array will certify _all_ response headers. For example: ```rust use std::borrow::Cow; -use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultRequestCertification, DefaultResponseCertification}; +use ic_http_certification::cel::{CelExpression, DefaultCelExpression, DefaultFullCelExpression, DefaultRequestCertification, DefaultResponseCertification}; -let cel_expr = CelExpression::DefaultCertification(Some(DefaultCertification { - request_certification: Some(DefaultRequestCertification { - headers: Cow::Borrowed(&["Accept", "Accept-Encoding", "If-Match"]), - query_parameters: Cow::Borrowed(&["foo", "bar", "baz"]), - }), - response_certification: DefaultResponseCertification::response_header_exclusions(&[]), -})); +let cel_expr = CelExpression::Default(DefaultCelExpression::Full( + DefaultFullCelExpression { + request: DefaultRequestCertification { + headers: Cow::Borrowed(&["Accept", "Accept-Encoding", "If-None-Match"]), + query_parameters: Cow::Borrowed(&["foo", "bar", "baz"]), + }, + response: DefaultResponseCertification::response_header_exclusions(&[]), + })); ``` This will produce the following CEL expression: @@ -333,7 +427,7 @@ This will produce the following CEL expression: default_certification ( ValidationArgs { request_certification: RequestCertification { - certified_request_headers: ["Accept", "Accept-Encoding", "If-Match"], + certified_request_headers: ["Accept", "Accept-Encoding", "If-None-Match"], certified_query_parameters: ["foo", "bar", "baz"] }, response_certification: ResponseCertification { @@ -347,14 +441,14 @@ default_certification ( To skip response certification completely, then certification overall must be skipped completely. It wouldn't be useful to certify a request without certifying a response. So if anything is certified, then it must at least include the response. See the next section for more details on skipping certification entirely. -#### Skipping certification +### Skipping certification To skip certification entirely: ```rust -use ic_http_certification::cel::{CelExpression, DefaultCertification}; +use ic_http_certification::cel::{CelExpression, DefaultCelExpression}; -let cel_expr = CelExpression::DefaultCertification(None); +let cel_expr = CelExpression::Default(DefaultCelExpression::Skip); ``` This will produce the following CEL expression: diff --git a/packages/ic-http-certification/src/cel/cel_builder.rs b/packages/ic-http-certification/src/cel/cel_builder.rs index 0e16bd0f..583a72d5 100644 --- a/packages/ic-http-certification/src/cel/cel_builder.rs +++ b/packages/ic-http-certification/src/cel/cel_builder.rs @@ -1,5 +1,6 @@ use super::{ - CelExpression, DefaultCertification, DefaultRequestCertification, DefaultResponseCertification, + CelExpression, DefaultCelExpression, DefaultFullCelExpression, DefaultRequestCertification, + DefaultResponseCertification, DefaultResponseOnlyCelExpression, }; use std::borrow::Cow; @@ -10,20 +11,20 @@ pub struct DefaultCelBuilder {} impl DefaultCelBuilder { /// Create a CEL expression that skips certification entirely. pub fn skip_certification<'a>() -> CelExpression<'a> { - CelExpression::DefaultCertification(None) + CelExpression::Default(DefaultCelExpression::Skip) } /// Creates a builder for a CEL expression that will only certify a response. /// Request certification will not be included with this builder. - /// See [DefaultResponseCelBuilder] for more details on this builder's interface. + /// See [DefaultResponseOnlyCelBuilder] for more details on this builder's interface. /// See [full_certification](DefaultCelBuilder::full_certification()) for a builder that will certify both the request and response. - pub fn response_certification<'a>() -> DefaultResponseCelBuilder<'a> { + pub fn response_only_certification<'a>() -> DefaultResponseOnlyCelBuilder<'a> { Default::default() } /// Creates a builder for a CEL expression that will certify both the request and response. /// See [DefaultFullCelExpressionBuilder] for more details on this builder's interface. - /// See [response_certification](DefaultCelBuilder::response_certification()) for a builder that will only certify the response. + /// See [response_only_certification](DefaultCelBuilder::response_only_certification()) for a builder that will only certify the response. pub fn full_certification<'a>() -> DefaultFullCelExpressionBuilder<'a> { Default::default() } @@ -32,11 +33,11 @@ impl DefaultCelBuilder { /// A CEL expression builder for creating expressions that will only certify a response. /// To create an expression that certifies both the request and response, see [DefaultFullCelExpressionBuilder]. #[derive(Debug, Clone, Default)] -pub struct DefaultResponseCelBuilder<'a> { +pub struct DefaultResponseOnlyCelBuilder<'a> { response_certification: DefaultResponseCertification<'a>, } -impl<'a> DefaultResponseCelBuilder<'a> { +impl<'a> DefaultResponseOnlyCelBuilder<'a> { /// Configure the response headers that will be included in certification. /// /// See [DefaultResponseCertification] for details on how to configure this. @@ -51,16 +52,15 @@ impl<'a> DefaultResponseCelBuilder<'a> { } /// Build the CEL expression, consuming the builder. - pub fn build(self) -> CelExpression<'a> { - CelExpression::DefaultCertification(Some(DefaultCertification { - request_certification: None, - response_certification: self.response_certification, - })) + pub fn build(self) -> DefaultResponseOnlyCelExpression<'a> { + DefaultResponseOnlyCelExpression { + response: self.response_certification, + } } } /// A CEL expression builder for creating expressions that will certify both the request and response. -/// To create an expression that only certifies the response, see [DefaultResponseCelBuilder]. +/// To create an expression that only certifies the response, see [DefaultResponseOnlyCelBuilder]. #[derive(Debug, Clone, Default)] pub struct DefaultFullCelExpressionBuilder<'a> { request_headers: &'a [&'a str], @@ -103,16 +103,16 @@ impl<'a> DefaultFullCelExpressionBuilder<'a> { } /// Build the CEL expression, consuming the builder. - pub fn build(self) -> CelExpression<'a> { - let request_certification = Some(DefaultRequestCertification { + pub fn build(self) -> DefaultFullCelExpression<'a> { + let request_certification = DefaultRequestCertification { headers: Cow::Borrowed(self.request_headers), query_parameters: Cow::Borrowed(self.request_query_parameters), - }); + }; - CelExpression::DefaultCertification(Some(DefaultCertification { - request_certification, - response_certification: self.response_certification, - })) + DefaultFullCelExpression { + request: request_certification, + response: self.response_certification, + } } } @@ -131,7 +131,7 @@ mod tests { #[rstest] fn no_request_response_inclusions(no_request_response_inclusions_cel: String) { - let cel_expr = DefaultCelBuilder::response_certification() + let cel_expr = DefaultCelBuilder::response_only_certification() .with_response_certification(DefaultResponseCertification::certified_response_headers( &[ "Cache-Control", @@ -149,7 +149,7 @@ mod tests { #[rstest] fn no_request_response_exclusions(no_request_response_exclusions_cel: String) { - let cel_expr = DefaultCelBuilder::response_certification() + let cel_expr = DefaultCelBuilder::response_only_certification() .with_response_certification(DefaultResponseCertification::response_header_exclusions( &["Date", "Cookie", "Set-Cookie"], )) @@ -161,16 +161,16 @@ mod tests { #[rstest] fn no_request_empty_response_inclusions(no_request_empty_response_inclusions_cel: String) { - let implicit_cel_expr = DefaultCelBuilder::response_certification() + let implicit_cel_expr = DefaultCelBuilder::response_only_certification() .build() .to_string(); - let explicit_cel_expr = DefaultCelBuilder::response_certification() + let explicit_cel_expr = DefaultCelBuilder::response_only_certification() .with_response_certification(DefaultResponseCertification::certified_response_headers( &[], )) .build() .to_string(); - let default_cel_expr = DefaultCelBuilder::response_certification() + let default_cel_expr = DefaultCelBuilder::response_only_certification() .with_response_certification(DefaultResponseCertification::default()) .build() .to_string(); @@ -182,7 +182,7 @@ mod tests { #[rstest] fn no_request_empty_response_exclusions(no_request_empty_response_exclusions_cel: String) { - let cel_expr = DefaultCelBuilder::response_certification() + let cel_expr = DefaultCelBuilder::response_only_certification() .with_response_certification(DefaultResponseCertification::response_header_exclusions( &[], )) diff --git a/packages/ic-http-certification/src/cel/cel_types.rs b/packages/ic-http-certification/src/cel/cel_types.rs index 86b0c38c..8373cba4 100644 --- a/packages/ic-http-certification/src/cel/cel_types.rs +++ b/packages/ic-http-certification/src/cel/cel_types.rs @@ -1,9 +1,12 @@ -use super::create_cel_expr; +use super::{ + create_cel_expr, create_default_cel_expr, create_default_full_cel_expr, + create_default_response_only_cel_expr, +}; 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]. +/// Currently only one variant is supported: [CelExpression::Default]. #[derive(Debug, Clone, PartialEq, Eq)] pub enum CelExpression<'a> { /// A certification CEL expression definition that uses the `default_certification` function. @@ -11,8 +14,8 @@ pub enum CelExpression<'a> { /// /// The enum's inner value is an [Option] to allow for opting in, or out of certification. /// Providing [None] will opt out of certification, while providing [Some] will opt in to certification. - /// See [DefaultCertification] for more details on its available parameters. - DefaultCertification(Option>), + /// See [DefaultCelExpression] for more details on its available parameters. + Default(DefaultCelExpression<'a>), } impl<'a> CelExpression<'a> { @@ -23,24 +26,83 @@ impl<'a> CelExpression<'a> { } } -/// A certification CEL expression definition that uses the `default_certification` function. +/// A certification CEL expression definition that uses the default CEL function. /// -/// [request_certification](DefaultCertification::request_certification) is used for configuring request certification, and -/// [response_certification](DefaultCertification::response_certification) is used for configuring response certification. +/// This enum has three variants: +/// +/// - The [Full](DefaultCelExpression::Full) variant includes both the [HTTP request](crate::HttpRequest) and the +/// corresponding [HTTP response](crate::HttpResponse) in certification. See the [DefaultFullCelExpression] struct +/// for details on how to configure this variant. +/// +/// - The [ResponseOnly](DefaultCelExpression::ResponseOnly) variant includes the +/// [HTTP response](crate::HttpResponse) in certification, but excludes the corresponding +/// [HTTP request](crate::HttpRequest) from certification. See the [DefaultResponseOnlyCelExpression] struct for +/// details on how to configure this variant. +/// +/// - The [Skip](DefaultCelExpression::Skip) variant excludes both the [HTTP request](crate::HttpRequest) and the +/// corresponding [HTTP response](crate::HttpResponse) from certification. This variant does not require any +/// configuration. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct DefaultCertification<'a> { - /// Options for configuring certification of a request. +pub enum DefaultCelExpression<'a> { + /// Includes both the [HTTP request](crate::HttpRequest) and the corresponding + /// [HTTP response](crate::HttpResponse) in certification. + Full(DefaultFullCelExpression<'a>), + + /// Includes an [HTTP response](crate::HttpResponse) in certification, but excludes the corresponding + /// [HTTP request](crate::HttpRequest) from certification. + ResponseOnly(DefaultResponseOnlyCelExpression<'a>), + + /// Skips certification entirely by excluding both the [HTTP request](crate::HttpRequest) and + /// [HTTP response](crate::HttpResponse) from certification. + Skip, +} + +impl<'a> DefaultCelExpression<'a> { + /// Converts a [DefaultCelExpression] object into it's [String] representation. /// - /// This is an [Option] to allow for opting in, or out of request certification. - /// See [DefaultRequestCertification] for more details on its available parameters. - pub request_certification: Option>, + /// Alias of [create_default_cel_expr](create_default_cel_expr()). + pub fn to_string(&self) -> String { + create_default_cel_expr(self) + } +} + +/// Options for configuring a CEL expression that includes only the [HTTP response](crate::HttpResponse) in +/// certification and excludes the [HTTP request](crate::HttpRequest) from certification. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DefaultResponseOnlyCelExpression<'a> { + /// Options for configuring response certification for this CEL expression. + /// See [DefaultResponseCertification] for details on how to configure response certification. + pub response: DefaultResponseCertification<'a>, +} - /// Options for configuring certification of a response. +impl<'a> DefaultResponseOnlyCelExpression<'a> { + /// Converts a [DefaultResponseOnlyCelExpression] object into it's [String] representation. /// - /// This is not an [Option] because response certification is the minimum required - /// when certifying a request and response pair. - /// See [DefaultResponseCertification] for more details on its available parameters. - pub response_certification: DefaultResponseCertification<'a>, + /// Alias of [create_default_response_only_cel_expr](create_default_response_only_cel_expr()). + pub fn to_string(&self) -> String { + create_default_response_only_cel_expr(self) + } +} + +/// Options for configuring a CEL expression that includes both the [HTTP response](crate::HttpResponse) and +/// [HTTP request](crate::HttpRequest) in certification. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DefaultFullCelExpression<'a> { + /// Options for configuring request certification for this CEL expression. + /// See [DefaultRequestCertification] for details on how to configure request certification. + pub request: DefaultRequestCertification<'a>, + + /// Options for configuring response certification for this CEL expression. + /// See [DefaultResponseCertification] for details on how to configure response certification. + pub response: DefaultResponseCertification<'a>, +} + +impl<'a> DefaultFullCelExpression<'a> { + /// Converts a [DefaultFullCelExpression] object into it's [String] representation. + /// Alias of [create_default_full_cel_expr](create_default_full_cel_expr()). + pub fn to_string(&self) -> String { + create_default_full_cel_expr(self) + } } /// Options for configuring certification of a request. @@ -76,12 +138,18 @@ pub enum DefaultResponseCertification<'a> { /// /// As many or as little headers can be provided as desired. /// Providing an empty list will result in no response headers being certified. + /// + /// See [certified_response_headers](DefaultResponseCertification::certified_response_headers()) + /// for a more ergonomic way of doing this. CertifiedResponseHeaders(Cow<'a, [&'a str]>), /// A list of response headers to exclude from certification. /// /// As many or as little headers can be provided as desired. /// Providing an empty list will result in all response headers being certified. + /// + /// See [response_header_exclusions](DefaultResponseCertification::response_header_exclusions()) + /// for a more ergonomic way of doing this. ResponseHeaderExclusions(Cow<'a, [&'a str]>), } diff --git a/packages/ic-http-certification/src/cel/create_cel_expr.rs b/packages/ic-http-certification/src/cel/create_cel_expr.rs index abe7ce33..f00b5ff6 100644 --- a/packages/ic-http-certification/src/cel/create_cel_expr.rs +++ b/packages/ic-http-certification/src/cel/create_cel_expr.rs @@ -1,29 +1,75 @@ use super::{ - CelExpression, DefaultCertification, DefaultRequestCertification, DefaultResponseCertification, + CelExpression, DefaultCelExpression, DefaultFullCelExpression, DefaultRequestCertification, + DefaultResponseCertification, DefaultResponseOnlyCelExpression, }; -/// Converts a CEL expression from a [CelExpression] object into it's [String] representation. -/// [CelExpression::to_string](CelExpression::to_string()) is an alias of this method and can be used for ergonomics. +/// Converts a CEL expression from a [CelExpression] struct into it's [String] representation. +/// +/// [CelExpression::to_string](CelExpression::to_string()) is an alias of this function and can be used +/// for ergonomics. pub fn create_cel_expr(certification: &CelExpression) -> String { match certification { - CelExpression::DefaultCertification(certification) => { - create_default_cel_expr(certification) - } + CelExpression::Default(certification) => create_default_cel_expr(certification), } } -fn create_default_cel_expr(certification: &Option) -> String { - let mut cel_expr = String::from("default_certification(ValidationArgs{"); - +/// Converts a CEL expression from a [DefaultCelExpression] struct into it's [String] representation. +/// +/// [DefaultCelExpression::to_string](DefaultCelExpression::to_string()) is an alias of this function and +/// can be used for ergonomics. +pub fn create_default_cel_expr(certification: &DefaultCelExpression) -> String { match certification { - None => cel_expr.push_str("no_certification:Empty{}"), - Some(certification) => { - cel_expr.push_str("certification:Certification{"); - create_request_cel_expr(&mut cel_expr, certification.request_certification.as_ref()); - create_response_cel_expr(&mut cel_expr, &certification.response_certification); - cel_expr.push_str("}"); + DefaultCelExpression::Skip => create_default_skip_cel_expr(), + DefaultCelExpression::ResponseOnly(certification) => { + create_default_response_only_cel_expr(certification) } + DefaultCelExpression::Full(certification) => create_default_full_cel_expr(certification), } +} + +/// Creates the [String] representation of a CEL expression that skips certification entirely. +pub fn create_default_skip_cel_expr() -> String { + let mut cel_expr = String::from("default_certification(ValidationArgs{"); + cel_expr.push_str("no_certification:Empty{}"); + cel_expr.push_str("})"); + cel_expr +} + +/// Converts a CEL expression that only certifies the [HTTP response](crate::HttpResponse), excluding the +/// [HTTP request](crate::HttpRequest) from certification, from a [DefaultResponseOnlyCelExpression] struct into +/// it's [String] representation. +/// +/// [DefaultResponseOnlyCelExpression::to_string](DefaultResponseOnlyCelExpression::to_string()) is an +/// alias of this method and can be used for ergonomics. +pub fn create_default_response_only_cel_expr( + certification: &DefaultResponseOnlyCelExpression, +) -> String { + let mut cel_expr = String::from("default_certification(ValidationArgs{"); + + cel_expr.push_str("certification:Certification{"); + cel_expr.push_str("no_request_certification:Empty{},"); + + create_response_cel_expr(&mut cel_expr, &certification.response); + + cel_expr.push_str("}"); + + cel_expr.push_str("})"); + cel_expr +} + +/// Converts a CEL expression that certifies both the [HTTP request](crate::HttpRequest) and +/// [HTTP response](crate::HttpResponse), from a [DefaultFullCelExpression] struct into it's [String] representation. +/// [DefaultFullCelExpression::to_string](DefaultFullCelExpression::to_string()) is an alias of this method and can +/// be used for ergonomics. +pub fn create_default_full_cel_expr(certification: &DefaultFullCelExpression) -> String { + let mut cel_expr = String::from("default_certification(ValidationArgs{"); + + cel_expr.push_str("certification:Certification{"); + + create_request_cel_expr(&mut cel_expr, &certification.request); + create_response_cel_expr(&mut cel_expr, &certification.response); + + cel_expr.push_str("}"); cel_expr.push_str("})"); cel_expr @@ -31,30 +77,24 @@ fn create_default_cel_expr(certification: &Option) -> Stri fn create_request_cel_expr( cel_expr: &mut String, - request_certification: Option<&DefaultRequestCertification>, + request_certification: &DefaultRequestCertification, ) { - match request_certification { - None => cel_expr.push_str("no_request_certification:Empty{},"), - Some(request_certification) => { - cel_expr - .push_str("request_certification:RequestCertification{certified_request_headers:["); - - if !request_certification.headers.is_empty() { - cel_expr.push('"'); - cel_expr.push_str(&request_certification.headers.join(r#"",""#)); - cel_expr.push('"'); - } - - cel_expr.push_str("],certified_query_parameters:["); - if !request_certification.query_parameters.is_empty() { - cel_expr.push('"'); - cel_expr.push_str(&request_certification.query_parameters.join(r#"",""#)); - cel_expr.push('"'); - } - - cel_expr.push_str("]},"); - } + cel_expr.push_str("request_certification:RequestCertification{certified_request_headers:["); + + if !request_certification.headers.is_empty() { + cel_expr.push('"'); + cel_expr.push_str(&request_certification.headers.join(r#"",""#)); + cel_expr.push('"'); + } + + cel_expr.push_str("],certified_query_parameters:["); + if !request_certification.query_parameters.is_empty() { + cel_expr.push('"'); + cel_expr.push_str(&request_certification.query_parameters.join(r#"",""#)); + cel_expr.push('"'); } + + cel_expr.push_str("]},"); } fn create_response_cel_expr( @@ -139,54 +179,58 @@ mod tests { } fn no_certification() -> CelExpression<'static> { - CelExpression::DefaultCertification(None) + CelExpression::Default(DefaultCelExpression::Skip) } fn no_request_response_inclusions() -> CelExpression<'static> { - CelExpression::DefaultCertification(Some(DefaultCertification { - request_certification: None, - response_certification: DefaultResponseCertification::certified_response_headers(&[ - "Cache-Control", - "ETag", - "Content-Length", - "Content-Type", - "Content-Encoding", - ]), - })) + CelExpression::Default(DefaultCelExpression::ResponseOnly( + DefaultResponseOnlyCelExpression { + response: DefaultResponseCertification::certified_response_headers(&[ + "Cache-Control", + "ETag", + "Content-Length", + "Content-Type", + "Content-Encoding", + ]), + }, + )) } fn no_request_response_exclusions() -> CelExpression<'static> { - CelExpression::DefaultCertification(Some(DefaultCertification { - request_certification: None, - response_certification: DefaultResponseCertification::response_header_exclusions(&[ - "Date", - "Cookie", - "Set-Cookie", - ]), - })) + CelExpression::Default(DefaultCelExpression::ResponseOnly( + DefaultResponseOnlyCelExpression { + response: DefaultResponseCertification::response_header_exclusions(&[ + "Date", + "Cookie", + "Set-Cookie", + ]), + }, + )) } fn no_request_empty_response_inclusions() -> CelExpression<'static> { - CelExpression::DefaultCertification(Some(DefaultCertification { - request_certification: None, - response_certification: DefaultResponseCertification::certified_response_headers(&[]), - })) + CelExpression::Default(DefaultCelExpression::ResponseOnly( + DefaultResponseOnlyCelExpression { + response: DefaultResponseCertification::certified_response_headers(&[]), + }, + )) } fn no_request_empty_response_exclusions() -> CelExpression<'static> { - CelExpression::DefaultCertification(Some(DefaultCertification { - request_certification: None, - response_certification: DefaultResponseCertification::response_header_exclusions(&[]), - })) + CelExpression::Default(DefaultCelExpression::ResponseOnly( + DefaultResponseOnlyCelExpression { + response: DefaultResponseCertification::response_header_exclusions(&[]), + }, + )) } fn include_request_response_header_inclusions() -> CelExpression<'static> { - CelExpression::DefaultCertification(Some(DefaultCertification { - request_certification: Some(DefaultRequestCertification { + CelExpression::Default(DefaultCelExpression::Full(DefaultFullCelExpression { + request: DefaultRequestCertification { headers: Cow::Borrowed(&["Accept", "Accept-Encoding", "If-Match"]), query_parameters: Cow::Borrowed(&["foo", "bar", "baz"]), - }), - response_certification: DefaultResponseCertification::certified_response_headers(&[ + }, + response: DefaultResponseCertification::certified_response_headers(&[ "Cache-Control", "ETag", "Content-Length", @@ -197,12 +241,12 @@ mod tests { } fn include_request_response_header_exclusions() -> CelExpression<'static> { - CelExpression::DefaultCertification(Some(DefaultCertification { - request_certification: Some(DefaultRequestCertification { + CelExpression::Default(DefaultCelExpression::Full(DefaultFullCelExpression { + request: DefaultRequestCertification { headers: Cow::Borrowed(&["Accept", "Accept-Encoding", "If-Match"]), query_parameters: Cow::Borrowed(&["foo", "bar", "baz"]), - }), - response_certification: DefaultResponseCertification::response_header_exclusions(&[ + }, + response: DefaultResponseCertification::response_header_exclusions(&[ "Date", "Cookie", "Set-Cookie", @@ -211,42 +255,42 @@ mod tests { } fn include_request_empty_response_inclusions() -> CelExpression<'static> { - CelExpression::DefaultCertification(Some(DefaultCertification { - request_certification: Some(DefaultRequestCertification { + CelExpression::Default(DefaultCelExpression::Full(DefaultFullCelExpression { + request: DefaultRequestCertification { headers: Cow::Borrowed(&["Accept", "Accept-Encoding", "If-Match"]), query_parameters: Cow::Borrowed(&["foo", "bar", "baz"]), - }), - response_certification: DefaultResponseCertification::certified_response_headers(&[]), + }, + response: DefaultResponseCertification::certified_response_headers(&[]), })) } fn include_request_empty_response_exclusions() -> CelExpression<'static> { - CelExpression::DefaultCertification(Some(DefaultCertification { - request_certification: Some(DefaultRequestCertification { + CelExpression::Default(DefaultCelExpression::Full(DefaultFullCelExpression { + request: DefaultRequestCertification { headers: Cow::Borrowed(&["Accept", "Accept-Encoding", "If-Match"]), query_parameters: Cow::Borrowed(&["foo", "bar", "baz"]), - }), - response_certification: DefaultResponseCertification::response_header_exclusions(&[]), + }, + response: DefaultResponseCertification::response_header_exclusions(&[]), })) } fn empty_request_response_inclusions() -> CelExpression<'static> { - CelExpression::DefaultCertification(Some(DefaultCertification { - request_certification: Some(DefaultRequestCertification { + CelExpression::Default(DefaultCelExpression::Full(DefaultFullCelExpression { + request: DefaultRequestCertification { headers: Cow::Borrowed(&[]), query_parameters: Cow::Borrowed(&[]), - }), - response_certification: DefaultResponseCertification::certified_response_headers(&[]), + }, + response: DefaultResponseCertification::certified_response_headers(&[]), })) } fn empty_request_response_exclusions() -> CelExpression<'static> { - CelExpression::DefaultCertification(Some(DefaultCertification { - request_certification: Some(DefaultRequestCertification { + CelExpression::Default(DefaultCelExpression::Full(DefaultFullCelExpression { + request: DefaultRequestCertification { headers: Cow::Borrowed(&[]), query_parameters: Cow::Borrowed(&[]), - }), - response_certification: DefaultResponseCertification::response_header_exclusions(&[]), + }, + response: DefaultResponseCertification::response_header_exclusions(&[]), })) } } diff --git a/packages/ic-http-certification/src/hash/mod.rs b/packages/ic-http-certification/src/hash/mod.rs index 50add5ef..6d30bb3e 100644 --- a/packages/ic-http-certification/src/hash/mod.rs +++ b/packages/ic-http-certification/src/hash/mod.rs @@ -1,6 +1,6 @@ //! Utilities for calculating //! [Representation Independent Hashes](https://internetcomputer.org/docs/current/references/ic-interface-spec/#hash-of-map) -//! of [crate::Request] and [crate::Response] objects. +//! of [crate::HttpRequest] and [crate::HttpRequest] objects. mod request_hash; pub use request_hash::*; diff --git a/packages/ic-http-certification/src/hash/request_hash.rs b/packages/ic-http-certification/src/hash/request_hash.rs index 85a17976..3a662fce 100644 --- a/packages/ic-http-certification/src/hash/request_hash.rs +++ b/packages/ic-http-certification/src/hash/request_hash.rs @@ -4,8 +4,7 @@ use ic_representation_independent_hash::{hash, representation_independent_hash, /// 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]. +/// of an [HttpRequest] according to a CEL expression defined by [DefaultRequestCertification]. pub fn request_hash<'a>( request: &'a HttpRequest, request_certification: &'a DefaultRequestCertification, diff --git a/packages/ic-http-certification/src/hash/response_hash.rs b/packages/ic-http-certification/src/hash/response_hash.rs index 4d691200..3071a403 100644 --- a/packages/ic-http-certification/src/hash/response_hash.rs +++ b/packages/ic-http-certification/src/hash/response_hash.rs @@ -17,8 +17,8 @@ pub struct ResponseHeaders { pub certificate_expression: Option, } -/// Filters headers of [crate::types::Response] according to [crate::types::ResponseCertification] -/// returned from [crate::cel::cel_to_certification]. +/// Filters the headers of an [HttpResponse] according to a CEL expression defined by +/// [DefaultResponseCertification]. pub fn filter_response_headers<'a>( response: &HttpResponse, response_certification: &DefaultResponseCertification<'a>, @@ -109,18 +109,24 @@ pub fn response_headers_hash(status_code: &u64, response_headers: &ResponseHeade representation_independent_hash(&headers_to_verify) } + /// Calculates the /// [Representation Independent Hash](https://internetcomputer.org/docs/current/references/ic-interface-spec/#hash-of-map) -/// of a [crate::types::Response] according to [crate::types::ResponseCertification] returned from -/// [crate::cel::cel_to_certification]. +/// of an [HttpResponse] according to a CEL expression defined by [DefaultResponseCertification]. +/// +/// An optional response body hash may be provided if this is known beforehand. If this override is not +/// provided then the response body hash will be calculated by this function. pub fn response_hash( response: &HttpResponse, response_certification: &DefaultResponseCertification, + response_body_hash: Option, ) -> Hash { + let response_body_hash = response_body_hash.unwrap_or(hash(&response.body)); + let filtered_headers = filter_response_headers(response, response_certification); let concatenated_hashes = [ response_headers_hash(&response.status_code.into(), &filtered_headers), - hash(&response.body), + response_body_hash, ] .concat(); @@ -205,7 +211,7 @@ mod tests { hex::decode("3393250e3cedc30408dcb7e8963898c3d7549b8a0b76496b82fdfeae99c2ac78") .unwrap(); - let result = response_hash(&response, &response_certification); + let result = response_hash(&response, &response_certification, None); assert_eq!(result, expected_hash.as_slice()); } @@ -227,9 +233,12 @@ mod tests { body: HELLO_WORLD_BODY.into(), }; - let result = response_hash(&response, &response_certification); - let result_without_excluded_headers = - response_hash(&response_without_excluded_headers, &response_certification); + let result = response_hash(&response, &response_certification, None); + let result_without_excluded_headers = response_hash( + &response_without_excluded_headers, + &response_certification, + None, + ); assert_eq!(result, result_without_excluded_headers); } @@ -245,7 +254,7 @@ mod tests { hex::decode("a2ffb50ef8971650c2fb46c0a2788b7d5ac5a027d635175e8e06b419ce6c4cda") .unwrap(); - let result = response_hash(&response, &response_certification); + let result = response_hash(&response, &response_certification, None); assert_eq!(result, expected_hash.as_slice()); } @@ -269,9 +278,12 @@ mod tests { body: HELLO_WORLD_BODY.into(), }; - let result = response_hash(&response, &response_certification); - let result_without_excluded_headers = - response_hash(&response_without_excluded_headers, &response_certification); + let result = response_hash(&response, &response_certification, None); + let result_without_excluded_headers = response_hash( + &response_without_excluded_headers, + &response_certification, + None, + ); assert_eq!(result, result_without_excluded_headers); } @@ -373,10 +385,27 @@ mod tests { assert_eq!(result, result_without_excluded_headers); } - /// We remove white space from CEL expressions to ease the calculation - /// of the expected hashes. Generating the hash for a string with so much whitespace manually - /// may be prone to error in copy/pasting the string into a website and missing a leading/trailing - /// newline or a tab character somewhere. + #[test] + fn response_hash_with_body_hash_override() { + let response_certification = DefaultResponseCertification::certified_response_headers(&[ + "Accept-Encoding", + "Cache-Control", + ]); + let response = create_response(CERTIFIED_HEADERS_CEL_EXPRESSION); + let response_body_hash: Hash = + hex::decode("5462fc394013080effc31d578ec3fff8b44cdf24738b38a77ce4afacbc93a7f5") + .unwrap() + .try_into() + .unwrap(); + let expected_hash = + hex::decode("1afc744a377cb8785d1078f53f9bbc9160d86b7a05f490e42c89366326eaef20") + .unwrap(); + + let result = response_hash(&response, &response_certification, Some(response_body_hash)); + + assert_eq!(result, expected_hash.as_slice()); + } + fn create_response(cel_expression: &str) -> HttpResponse { HttpResponse { status_code: 200, @@ -398,6 +427,10 @@ mod tests { } } + /// Remove white space from CEL expressions to ease the calculation + /// of the expected hashes. Generating the hash for a string with so much whitespace manually + /// may be prone to error in copy/pasting the string into a website and missing a leading/trailing + /// newline or a tab character somewhere. fn remove_whitespace<'a>(s: &'a str) -> String { s.chars().filter(|c| !c.is_whitespace()).collect() } diff --git a/packages/ic-http-certification/src/lib.rs b/packages/ic-http-certification/src/lib.rs index 0a356e95..93c9beb9 100644 --- a/packages/ic-http-certification/src/lib.rs +++ b/packages/ic-http-certification/src/lib.rs @@ -1,6 +1,14 @@ /*! # Internet Computer HTTP Certification +HTTP Certification is a sub-protocol of the [Internet Computer](https://internetcomputer.org/) [HTTP Gateway Protocol](https://internetcomputer.org/docs/current/references/http-gateway-protocol-spec). It is used to verify HTTP responses received by an HTTP Gateway from a [canister](https://internetcomputer.org/how-it-works/canister-lifecycle/), with respect to the corresponding HTTP request sent by the HTTP Gateway to the canister. This allows HTTP Gateways to verify that the responses they receive from canisters are authentic and have not been tampered with by a malicious replica. + +This crate provides a foundation for implementing the HTTP Certification protocol in Rust canisters. Certification is implemented in a number of steps: + +1. [Defining CEL expressions](#defining-cel-expressions) +2. [Creating certifications](#creating-certifications) +3. ...coming soon!!! + ## Defining CEL Expressions [CEL](https://github.com/google/cel-spec) (Common Expression Language) is a portable expression language that can be used to enable different applications to more easily interoperate. It can be seen as the computation or expression counterpart to [Protocol Buffers](https://github.com/protocolbuffers/protobuf). @@ -14,27 +22,27 @@ CEL expressions can be created in two ways, by using the [CEL builder](#using-th Note that the [CelExpression](cel::CelExpression) enum is not a CEL expression itself, but rather a Rust representation of a CEL expression. To convert a [CelExpression](cel::CelExpression) into its [String] representation, use [CelExpression.to_string](cel::CelExpression::to_string()) or [create_cel_expr](cel::create_cel_expr()). This applies to CEL expressions created both by the [CEL builder](#using-the-cel-builder) and [directly](#directly-creating-a-cel-expression). ```rust -use ic_http_certification::cel::CelExpression; +use ic_http_certification::cel::{CelExpression, DefaultCelExpression}; -let cel_expr = CelExpression::DefaultCertification(None).to_string(); +let cel_expr = CelExpression::Default(DefaultCelExpression::Skip).to_string(); ``` Alternatively: ```rust -use ic_http_certification::cel::{CelExpression, create_cel_expr}; +use ic_http_certification::cel::{CelExpression, DefaultCelExpression, create_cel_expr}; -let certification = CelExpression::DefaultCertification(None); +let certification = CelExpression::Default(DefaultCelExpression::Skip); let cel_expr = create_cel_expr(&certification); ``` ### Using the CEL builder -The CEL builder interface is provided to ease the creation of CEL expressions through an ergonmic interface. If this interface does not meet your needs, you can also [create CEL expressions directly](#directly-creating-a-cel-expression). To define a CEL expression, start with [DefaultCelBuilder]. This struct provides a set of methods that can be used to define how your request and response pair should be certified. +The CEL builder interface is provided to ease the creation of CEL expressions through an ergonmic interface. If this interface does not meet your needs, you can also [create CEL expressions directly](#directly-creating-a-cel-expression). To define a CEL expression, start with [DefaultCelBuilder]. This struct provides a set of associated functions that can be used to define how your request and response pair should be certified. -When certifying requests, the request body and method are always certified. To additionally certify request headers and query parameters, use [with_request_headers](cel::DefaultFullCelExpressionBuilder::with_request_headers()) and [with_request_query_parameters](cel::DefaultFullCelExpressionBuilder::with_request_query_parameters()) respectively. Both methods take a [str] slice as an argument. +When certifying requests, the request body and method are always certified. To additionally certify request headers and query parameters, use [with_request_headers](cel::DefaultFullCelExpressionBuilder::with_request_headers()) and [with_request_query_parameters](cel::DefaultFullCelExpressionBuilder::with_request_query_parameters()) respectively. Both associated functions take a [str] slice as an argument. -When certifying a response, the response body and status code are always certified. To additionally certify response headers, use [with_response_certification](cel::DefaultFullCelExpressionBuilder::with_response_certification()). This method takes the [DefaultResponseCertification](DefaultResponseCertification) enum as an argument. To specify header inclusions, use the [certified_response_headers](DefaultResponseCertification::certified_response_headers) function of the [DefaultResponseCertification](DefaultResponseCertification) enum. Or to certify all response headers, with some exclusions, use the [response_header_exclusions](DefaultResponseCertification::response_header_exclusions) function of the [DefaultResponseCertification](DefaultResponseCertification) enum. Both functions take a [str] slice as an argument. +When certifying a response, the response body and status code are always certified. To additionally certify response headers, use [with_response_certification](cel::DefaultFullCelExpressionBuilder::with_response_certification()). This associated function takes the [DefaultResponseCertification](DefaultResponseCertification) enum as an argument. To specify header inclusions, use the [certified_response_headers](DefaultResponseCertification::certified_response_headers) associated function of the [DefaultResponseCertification](DefaultResponseCertification) enum. Or to certify all response headers, with some exclusions, use the [response_header_exclusions](DefaultResponseCertification::response_header_exclusions) associated function of the [DefaultResponseCertification](DefaultResponseCertification) enum. Both associated functions take a [str] slice as an argument. #### Fully certified request / response pair @@ -44,7 +52,7 @@ To define a fully certified request and response pair, including request headers use ic_http_certification::{DefaultCelBuilder, DefaultResponseCertification}; let cel_expr = DefaultCelBuilder::full_certification() - .with_request_headers(&["Accept", "Accept-Encoding", "If-Match"]) + .with_request_headers(&["Accept", "Accept-Encoding", "If-None-Match"]) .with_request_query_parameters(&["foo", "bar", "baz"]) .with_response_certification(DefaultResponseCertification::certified_response_headers(&[ "Cache-Control", @@ -87,12 +95,12 @@ let cel_expr = DefaultCelBuilder::full_certification() #### Skipping request certification -Request certification can be skipped entirely by using [DefaultCelBuilder::response_certification](DefaultCelBuilder::response_certification()) instead of [DefaultCelBuilder::full_certification](DefaultCelBuilder::full_certification()). For example: +Request certification can be skipped entirely by using [DefaultCelBuilder::response_only_certification](DefaultCelBuilder::response_only_certification()) instead of [DefaultCelBuilder::full_certification](DefaultCelBuilder::full_certification()). For example: ```rust use ic_http_certification::{DefaultCelBuilder, DefaultResponseCertification}; -let cel_expr = DefaultCelBuilder::response_certification() +let cel_expr = DefaultCelBuilder::response_only_certification() .with_response_certification(DefaultResponseCertification::response_header_exclusions(&[ "Date", "Cookie", @@ -103,14 +111,14 @@ let cel_expr = DefaultCelBuilder::response_certification() #### Partially certified response -Similiarly to request certification, any number of response headers can be provided via the [certified_response_headers](DefaultResponseCertification::certified_response_headers) function of the [DefaultResponseCertification](DefaultResponseCertification) enum when calling [with_response_certification](cel::DefaultFullCelExpressionBuilder::with_response_certification()). The provided array can also be an empty. If the array is empty, or the method is not called, then no response headers will be certified. +Similiarly to request certification, any number of response headers can be provided via the [certified_response_headers](DefaultResponseCertification::certified_response_headers) associated function of the [DefaultResponseCertification](DefaultResponseCertification) enum when calling [with_response_certification](cel::DefaultFullCelExpressionBuilder::with_response_certification()). The provided array can also be an empty. If the array is empty, or the associated function is not called, then no response headers will be certified. For example, to certify only the response body and status code: ```rust use ic_http_certification::DefaultCelBuilder; -let cel_expr = DefaultCelBuilder::response_certification().build(); +let cel_expr = DefaultCelBuilder::response_only_certification().build(); ``` @@ -119,18 +127,18 @@ This can also be done more explicitly: ```rust use ic_http_certification::{DefaultCelBuilder, DefaultResponseCertification}; -let cel_expr = DefaultCelBuilder::response_certification() +let cel_expr = DefaultCelBuilder::response_only_certification() .with_response_certification(DefaultResponseCertification::certified_response_headers(&[])) .build(); ``` -The same applies when both when using [DefaultCelBuilder::response_certification](DefaultCelBuilder::response_certification()) and [DefaultCelBuilder::full_certification](DefaultCelBuilder::full_certification()). +The same applies both when using [DefaultCelBuilder::response_only_certification](DefaultCelBuilder::response_only_certification()) and [DefaultCelBuilder::full_certification](DefaultCelBuilder::full_certification()). ```rust use ic_http_certification::DefaultCelBuilder; let cel_expr = DefaultCelBuilder::full_certification() - .with_request_headers(&["Accept", "Accept-Encoding", "If-Match"]) + .with_request_headers(&["Accept", "Accept-Encoding", "If-None-Match"]) .with_request_query_parameters(&["foo", "bar", "baz"]) .build(); ``` @@ -151,34 +159,118 @@ Skipping certification may seem counter-intuitive at first, but it is not always Typically these requests have been routed through `raw` Internet Computer URLs in the past, but this is dangerous because `raw` URLs allow any responding replica to decide whether or not certification is required. In contrast, by skipping certification using the above method with a non-`raw` URL, a replica will no longer be able to decide whether or not certification is required and instead this decision will be made by the canister itself and the result will go through consensus. -### Directly creating a CEL expression +## Creating certifications + +Once a CEL expression has been defined, it can be used in conjunction with an [HTTP request](HttpRequest) and [HTTP response](HttpResponse) to create a [Certification]. The [Certification] enum has three variants, each with a corresponding associated function used to create that particular variant: + +- The [Full](Certification::Full) variant is used to include both the [HTTP request](HttpRequest) and the corresponding [HTTP response](HttpResponse) in certification. +- The [ResponseOnly](Certification::ResponseOnly) variant is used to include only the [HTTP response](HttpResponse) in certification and exclude the corresponding [HTTP request](HttpRequest) from certification. +- The [Skip](Certification::Skip) variant is used to skip certification entirely. + +### Full certification + +To perform a full certification, a CEL expression created from [DefaultCelBuilder::full_certification] is required, along with an [HttpRequest] and [HttpResponse] and optionally, a pre-calculated response body hash. For example: + +```rust +use ic_http_certification::{Certification, HttpRequest, HttpResponse, DefaultCelBuilder, DefaultResponseCertification}; + +let cel_expr = DefaultCelBuilder::full_certification() + .with_request_headers(&["Accept", "Accept-Encoding", "If-None-Match"]) + .with_request_query_parameters(&["foo", "bar", "baz"]) + .with_response_certification(DefaultResponseCertification::certified_response_headers(&[ + "Cache-Control", + "ETag", + ])) + .build(); + +let request = HttpRequest { + method: "GET".to_string(), + url: "/index.html?foo=a&bar=b&baz=c".to_string(), + headers: vec![ + ("Accept".to_string(), "application/json".to_string()), + ("Accept-Encoding".to_string(), "gzip".to_string()), + ("If-None-Match".to_string(), "987654321".to_string()), + ], + body: vec![], +}; + +let response = HttpResponse { + status_code: 200, + headers: vec![ + ("Cache-Control".to_string(), "no-cache".to_string()), + ("ETag".to_string(), "123456789".to_string()), + ], + body: vec![1, 2, 3, 4, 5, 6], +}; + +let certification = Certification::full(&cel_expr, &request, &response, None); +``` + +### Response-only certification + +To perform a response-only certification, a CEL expression created from [DefaultCelBuilder::response_only_certification] is required, along with an [HttpResponse] and optionally, a pre-calculated response body hash. For example: + +```rust +use ic_http_certification::{Certification, HttpResponse, DefaultCelBuilder, DefaultResponseCertification}; + +let cel_expr = DefaultCelBuilder::response_only_certification() + .with_response_certification(DefaultResponseCertification::certified_response_headers(&[ + "Cache-Control", + "ETag", + ])) + .build(); + +let response = HttpResponse { + status_code: 200, + headers: vec![ + ("Cache-Control".to_string(), "no-cache".to_string()), + ("ETag".to_string(), "123456789".to_string()), + ], + body: vec![1, 2, 3, 4, 5, 6], +}; + +let certification = Certification::response_only(&cel_expr, &response, None); +``` + +### Skipping certification + +Skipping certification does not need an explicit CEL expression to be defined since it's always the same. For example: -To define a CEL expression, start with the [CelExpression](cel::CelExpression) enum. This enum provides a set of variants that can be used to define different types of CEL expressions supported by Internet Computer HTTP Gateways. Currently only one variant is supported, known as the "default" certification expression, but more may be added in the future as HTTP certification evolves over time. +```rust +use ic_http_certification::Certification; + +let certification = Certification::skip(); +``` + +## Directly creating a CEL expression -When certifying requests, the request body and method are always certified. To additionally certify request headers and query parameters, use the [headers](cel::DefaultRequestCertification::headers) and [query_parameters](cel::DefaultRequestCertification::query_parameters) of [DefaultRequestCertification](cel::DefaultRequestCertification) struct. Both properties take a [str] slice as an argument. +To define a CEL expression, start with the [CelExpression](cel::CelExpression) enum. This enum provides a set of variants that can be used to define different types of CEL expressions supported by Internet Computer HTTP Gateways. Currently only one variant is supported, known as the "default" certification expression, but more may be added in the future as the HTTP certification protocol evolves over time. -When certifying a response, the response body and status code are always certified. To additionally certify response headers, use the [certified_response_headers](DefaultResponseCertification::certified_response_headers) function of the [DefaultResponseCertification](DefaultResponseCertification) enum. Or to certify all response headers, with some exclusions, use the [response_header_exclusions](DefaultResponseCertification::response_header_exclusions) function of the [DefaultResponseCertification](DefaultResponseCertification) enum. Both functions take a [str] slice as an argument. +When certifying requests, the request body and method are always certified. To additionally certify request headers and query parameters, use the [headers](cel::DefaultRequestCertification::headers) and [query_parameters](cel::DefaultRequestCertification::query_parameters) fields of the [DefaultRequestCertification](cel::DefaultRequestCertification) struct. Both fields take a [str] slice as an argument. + +When certifying a response, the response body and status code are always certified. To additionally certify response headers, use the [certified_response_headers](DefaultResponseCertification::certified_response_headers) associated function of the [DefaultResponseCertification](DefaultResponseCertification) enum. Or to certify all response headers, with some exclusions, use the [response_header_exclusions](DefaultResponseCertification::response_header_exclusions) associated function of the [DefaultResponseCertification](DefaultResponseCertification) enum. Both associated functions take a [str] slice as an argument. Note that the example CEL expressions provided below are formatted for readability. The actual CEL expressions produced by [CelExpression::to_string](cel::CelExpression::to_string()) and [create_cel_expr](cel::create_cel_expr()) are minified. The minified CEL expression is preferred because it is more compact, resulting in a smaller payload and a faster evaluation time for the HTTP Gateway that is verifying the certification, but the formatted versions are also accepted. -#### Fully certified request / response pair +### Fully certified request / response pair To define a fully certified request and response pair, including request headers, query parameters, and response headers: ```rust use std::borrow::Cow; -use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultRequestCertification, DefaultResponseCertification}; - -let cel_expr = CelExpression::DefaultCertification(Some(DefaultCertification { - request_certification: Some(DefaultRequestCertification { - headers: Cow::Borrowed(&["Accept", "Accept-Encoding", "If-Match"]), - query_parameters: Cow::Borrowed(&["foo", "bar", "baz"]), - }), - response_certification: DefaultResponseCertification::certified_response_headers(&[ - "ETag", - "Cache-Control", - ]), -})); +use ic_http_certification::cel::{CelExpression, DefaultCelExpression, DefaultFullCelExpression, DefaultRequestCertification, DefaultResponseCertification}; + +let cel_expr = CelExpression::Default(DefaultCelExpression::Full( + DefaultFullCelExpression { + request: DefaultRequestCertification { + headers: Cow::Borrowed(&["Accept", "Accept-Encoding", "If-None-Match"]), + query_parameters: Cow::Borrowed(&["foo", "bar", "baz"]), + }, + response: DefaultResponseCertification::certified_response_headers(&[ + "ETag", + "Cache-Control", + ]), + })); ``` This will produce the following CEL expression: @@ -187,7 +279,7 @@ This will produce the following CEL expression: default_certification ( ValidationArgs { request_certification: RequestCertification { - certified_request_headers: ["Accept", "Accept-Encoding", "If-Match"], + certified_request_headers: ["Accept", "Accept-Encoding", "If-None-Match"], certified_query_parameters: ["foo", "bar", "baz"] }, response_certification: ResponseCertification { @@ -202,26 +294,27 @@ default_certification ( ) ``` -#### Partially certified request +### Partially certified request -Any number of request headers or query parameters can be provided via the [headers](cel::DefaultRequestCertification::headers) and [query_parameters](cel::DefaultRequestCertification::query_parameters) properties of the [DefaultRequestCertification](cel::DefaultRequestCertification) struct, and both can be an empty array. If the [headers](cel::DefaultRequestCertification::headers) property is empty, no request headers will be certified. Likewise for the [query_parameters](cel::DefaultRequestCertification::query_parameters) property, if it is empty then no query parameters will be certified. If both are empty, only the request body and method will be certified. +Any number of request headers or query parameters can be provided via the [headers](cel::DefaultRequestCertification::headers) and [query_parameters](cel::DefaultRequestCertification::query_parameters) fields of the [DefaultRequestCertification](cel::DefaultRequestCertification) struct, and both can be an empty array. If the [headers](cel::DefaultRequestCertification::headers) field is empty, no request headers will be certified. Likewise for the [query_parameters](cel::DefaultRequestCertification::query_parameters) field, if it is empty then no query parameters will be certified. If both are empty, only the request body and method will be certified. For example, to certify only the request body and method: ```rust use std::borrow::Cow; -use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultRequestCertification, DefaultResponseCertification}; - -let cel_expr = CelExpression::DefaultCertification(Some(DefaultCertification { - request_certification: Some(DefaultRequestCertification { - headers: Cow::Borrowed(&["Accept", "Accept-Encoding", "If-Match"]), - query_parameters: Cow::Borrowed(&["foo", "bar", "baz"]), - }), - response_certification: DefaultResponseCertification::certified_response_headers(&[ - "ETag", - "Cache-Control", - ]), -})); +use ic_http_certification::cel::{CelExpression, DefaultCelExpression, DefaultFullCelExpression, DefaultRequestCertification, DefaultResponseCertification}; + +let cel_expr = CelExpression::Default(DefaultCelExpression::Full( + DefaultFullCelExpression { + request: DefaultRequestCertification { + headers: Cow::Borrowed(&[]), + query_parameters: Cow::Borrowed(&[]), + }, + response: DefaultResponseCertification::certified_response_headers(&[ + "ETag", + "Cache-Control", + ]), + })); ``` This will produce the following CEL expression: @@ -245,20 +338,21 @@ default_certification ( ) ``` -#### Skipping request certification +### Skipping request certification -Request certification can be skipped entirely by setting the [request_certification](cel::DefaultCertification::request_certification) property of the [DefaultCertification](cel::DefaultCertification) struct to [None]. For example: +Request certification can be skipped entirely by using the [ResponseOnly](DefaultCelExpression::ResponseOnly) variant of the [DefaultCelExpression](DefaultCelExpression). For example: ```rust -use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultResponseCertification}; - -let cel_expr = CelExpression::DefaultCertification(Some(DefaultCertification { - request_certification: None, - response_certification: DefaultResponseCertification::certified_response_headers(&[ - "ETag", - "Cache-Control", - ]), -})); +use std::borrow::Cow; +use ic_http_certification::cel::{CelExpression, DefaultCelExpression, DefaultResponseOnlyCelExpression, DefaultResponseCertification}; + +let cel_expr = CelExpression::Default(DefaultCelExpression::ResponseOnly( + DefaultResponseOnlyCelExpression { + response: DefaultResponseCertification::certified_response_headers(&[ + "ETag", + "Cache-Control", + ]), + })); ``` This will produce the following CEL expression: @@ -279,21 +373,23 @@ default_certification ( ) ``` -#### Partially certified response +### Partially certified response -Similiarly to request certification, any number of response headers can be provided via the [certified_response_headers](DefaultResponseCertification::certified_response_headers) function of the [DefaultResponseCertification](DefaultResponseCertification) enum, and it can also be an empty array. If the array is empty, no response headers will be certified. For example: +Similiarly to request certification, any number of response headers can be provided via the [certified_response_headers](DefaultResponseCertification::certified_response_headers) associated function of the [DefaultResponseCertification](DefaultResponseCertification) enum, and it can also be an empty array. If the array is empty, no response headers will be certified. For example: ```rust use std::borrow::Cow; -use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultRequestCertification, DefaultResponseCertification}; - -let cel_expr = CelExpression::DefaultCertification(Some(DefaultCertification { - request_certification: Some(DefaultRequestCertification { - headers: Cow::Borrowed(&["Accept", "Accept-Encoding", "If-Match"]), - query_parameters: Cow::Borrowed(&["foo", "bar", "baz"]), - }), - response_certification: DefaultResponseCertification::certified_response_headers(&[]), -})); +use ic_http_certification::cel::{CelExpression, DefaultCelExpression, DefaultFullCelExpression, DefaultRequestCertification, DefaultResponseCertification}; + + +let cel_expr = CelExpression::Default(DefaultCelExpression::Full( + DefaultFullCelExpression { + request: DefaultRequestCertification { + headers: Cow::Borrowed(&["Accept", "Accept-Encoding", "If-None-Match"]), + query_parameters: Cow::Borrowed(&["foo", "bar", "baz"]), + }, + response: DefaultResponseCertification::certified_response_headers(&[]), + })); ``` This will produce the following CEL expression: @@ -302,7 +398,7 @@ This will produce the following CEL expression: default_certification ( ValidationArgs { request_certification: RequestCertification { - certified_request_headers: ["Accept", "Accept-Encoding", "If-Match"], + certified_request_headers: ["Accept", "Accept-Encoding", "If-None-Match"], certified_query_parameters: ["foo", "bar", "baz"] }, response_certification: ResponseCertification { @@ -314,19 +410,20 @@ default_certification ( ) ``` -If the [response_header_exclusions](DefaultResponseCertification::response_header_exclusions) funciton is used, an empty array will certify _all_ response headers. For example: +If the [response_header_exclusions](DefaultResponseCertification::response_header_exclusions) associated function is used, an empty array will certify _all_ response headers. For example: ```rust use std::borrow::Cow; -use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultRequestCertification, DefaultResponseCertification}; - -let cel_expr = CelExpression::DefaultCertification(Some(DefaultCertification { - request_certification: Some(DefaultRequestCertification { - headers: Cow::Borrowed(&["Accept", "Accept-Encoding", "If-Match"]), - query_parameters: Cow::Borrowed(&["foo", "bar", "baz"]), - }), - response_certification: DefaultResponseCertification::response_header_exclusions(&[]), -})); +use ic_http_certification::cel::{CelExpression, DefaultCelExpression, DefaultFullCelExpression, DefaultRequestCertification, DefaultResponseCertification}; + +let cel_expr = CelExpression::Default(DefaultCelExpression::Full( + DefaultFullCelExpression { + request: DefaultRequestCertification { + headers: Cow::Borrowed(&["Accept", "Accept-Encoding", "If-None-Match"]), + query_parameters: Cow::Borrowed(&["foo", "bar", "baz"]), + }, + response: DefaultResponseCertification::response_header_exclusions(&[]), + })); ``` This will produce the following CEL expression: @@ -335,7 +432,7 @@ This will produce the following CEL expression: default_certification ( ValidationArgs { request_certification: RequestCertification { - certified_request_headers: ["Accept", "Accept-Encoding", "If-Match"], + certified_request_headers: ["Accept", "Accept-Encoding", "If-None-Match"], certified_query_parameters: ["foo", "bar", "baz"] }, response_certification: ResponseCertification { @@ -349,14 +446,14 @@ default_certification ( To skip response certification completely, then certification overall must be skipped completely. It wouldn't be useful to certify a request without certifying a response. So if anything is certified, then it must at least include the response. See the next section for more details on skipping certification entirely. -#### Skipping certification +### Skipping certification To skip certification entirely: ```rust -use ic_http_certification::cel::{CelExpression, DefaultCertification}; +use ic_http_certification::cel::{CelExpression, DefaultCelExpression}; -let cel_expr = CelExpression::DefaultCertification(None); +let cel_expr = CelExpression::Default(DefaultCelExpression::Skip); ``` This will produce the following CEL expression: @@ -383,10 +480,15 @@ Typically these requests have been routed through `raw` Internet Computer URLs i )] pub mod cel; -pub use cel::{CelExpression, DefaultCelBuilder, DefaultResponseCertification}; +pub use cel::{ + CelExpression, DefaultCelBuilder, DefaultCelExpression, DefaultFullCelExpression, + DefaultResponseCertification, DefaultResponseOnlyCelExpression, +}; pub mod hash; pub use hash::*; pub mod error; pub use error::*; pub mod http; pub use crate::http::*; +pub mod tree; +pub use tree::*; diff --git a/packages/ic-http-certification/src/tree/certification.rs b/packages/ic-http-certification/src/tree/certification.rs new file mode 100644 index 00000000..6dfc7225 --- /dev/null +++ b/packages/ic-http-certification/src/tree/certification.rs @@ -0,0 +1,265 @@ +use crate::{ + request_hash, response_hash, DefaultCelBuilder, DefaultFullCelExpression, + DefaultResponseOnlyCelExpression, HttpCertificationResult, HttpRequest, HttpResponse, +}; +use ic_certification::Hash; +use ic_representation_independent_hash::hash; + +/// A certified [request](crate::HttpResponse) and [response](crate::HttpResponse) pair. +/// +/// It contains three variants: +/// +/// - The [Skip](Certification::Skip) variant excludes both an [HTTP request](crate::HttpRequest) and the +/// corresponding [HTTP response](crate::HttpResponse) from certification. Create this variant using +/// the associated [skip()](Certification::skip()) function. +/// +/// - The [ResponseOnly](Certification::ResponseOnly) variant includes an +/// [HTTP response](crate::HttpResponse) but excludes the corresponding [HTTP request](crate::HttpRequest) +/// from certification. Create this variant using the associated +/// [response_only()](Certification::response_only()) function. +/// +/// - The [Full](Certification::Full) variant includes both an [HTTP response](crate::HttpResponse) and +/// the corresponding [HTTP request](crate::HttpRequest) in certification. Create this variant using +/// the [full()](Certification::full()) function. +#[derive(Debug)] +pub enum Certification { + /// A certification that excludes both the [HTTP request](crate::HttpRequest) and + /// the corresponding [HTTP response](crate::HttpResponse). + /// + /// The [cel_expr_hash](Certification::Skip::cel_expr_hash) property is the hash + /// of a [CEL expression](crate::DefaultCelExpression::Skip) used to exclude both the + /// [HTTP request](crate::HttpRequest) and the corresponding [HTTP response](crate::HttpResponse) + /// from certification. + Skip { + /// The hash of a [CEL expression](crate::DefaultCelExpression::Skip) used to exclude both + /// the [HTTP request](crate::HttpRequest) and [HTTP response](crate::HttpResponse) from + /// certification. + cel_expr_hash: Hash, + }, + + /// A certification that includes an [HTTP response](crate::HttpResponse), but excludes the + /// corresponding [HTTP request](crate::HttpRequest). + /// + /// The [cel_expr_hash](Certification::ResponseOnly::cel_expr_hash) property is the hash + /// of a [CEL expression](crate::DefaultCelExpression::ResponseOnly) used to include an + /// [HTTP request](crate::HttpRequest) but exclude the corresponding + /// [HTTP response](crate::HttpResponse) from certification. + /// + /// The [response_hash](Certification::ResponseOnly::response_hash) property is the + /// hash of the [HTTP response](crate::HttpResponse) calculated according to a + /// [CEL expression](crate::DefaultCelExpression::ResponseOnly). + /// + /// The [CEL expression](crate::DefaultCelExpression::ResponseOnly) used to produce + /// [response_hash](Certification::ResponseOnly::response_hash) + /// is also used to produce the + /// [cel_expr_hash](Certification::ResponseOnly::cel_expr_hash). + ResponseOnly { + /// The hash of a [CEL expression](crate::DefaultCelExpression::ResponseOnly) used to include an + /// [HTTP request](crate::HttpRequest) but exclude the corresponding + /// [HTTP response](crate::HttpResponse) from certification. + /// + /// The [CEL expression](crate::DefaultCelExpression::ResponseOnly) that produces this hash + /// is also used to produce the + /// [HTTP response hash](Certification::ResponseOnly::response_hash). + cel_expr_hash: Hash, + + /// The + /// [Representation Independent Hash](https://internetcomputer.org/docs/current/references/ic-interface-spec/#hash-of-map) + /// of an [HTTP response](crate::HttpResponse), calculated according to a + /// [CEL expression](crate::DefaultCelExpression::ResponseOnly). + /// + /// The [CEL expression](crate::DefaultCelExpression::ResponseOnly) used to calculate the hash of + /// this [response](crate::HttpResponse), is also used to produce the + /// [cel_expr_hash](Certification::ResponseOnly::cel_expr_hash) property. + response_hash: Hash, + }, + + /// A certification that includes both an [HTTP response](crate::HttpResponse) and the corresponding + /// [HTTP request](crate::HttpRequest). + /// + /// The [cel_expr_hash](Certification::Full::cel_expr_hash) property is the hash + /// of a [CEL expression](crate::DefaultCelExpression::Full) used to include both the + /// [HTTP request](crate::HttpRequest) and the corresponding [HTTP response](crate::HttpResponse) + /// in certification. + /// + /// The [response_hash](Certification::Full::response_hash) property is the + /// hash of the [HTTP response](crate::HttpResponse) calculated according to a + /// [CEL expression](crate::DefaultCelExpression::Full). + /// + /// The [request_hash](Certification::Full::request_hash) property is the hash of a + /// [HTTP response](crate::HttpResponse) calculated according to a + /// [CEL expression](crate::DefaultCelExpression::Full). + /// + /// The [CEL expression](crate::DefaultCelExpression::Full) used to produce both + /// [response_hash](Certification::Full::response_hash) and + /// [request_hash](Certification::Full::request_hash) is also used to produce the + /// [cel_expr_hash](Certification::Full::cel_expr_hash). + Full { + /// The hash of a [CEL expression](crate::DefaultCelExpression::Full) used to include an + /// [HTTP request](crate::HttpRequest) but exclude the corresponding + /// [HTTP response](crate::HttpResponse) from certification. + /// + /// The [CEL expression](crate::DefaultCelExpression::Full) that produces this hash + /// is also used to produce the + /// [HTTP response hash](Certification::Full::response_hash) and the + /// [HTTP request hash](Certification::Full::request_hash). + cel_expr_hash: Hash, + + /// The + /// [Representation Independent Hash](https://internetcomputer.org/docs/current/references/ic-interface-spec/#hash-of-map) + /// of an [HTTP response](crate::HttpResponse), calculated according to a + /// [CEL expression](crate::DefaultCelExpression::Full). + /// + /// The [CEL expression](crate::DefaultCelExpression::Full) used to calculate the hash of + /// this [request](crate::HttpRequest), is also used to produce the + /// [cel_expr_hash](Certification::Full::cel_expr_hash) property. + request_hash: Hash, + + /// The + /// [Representation Independent Hash](https://internetcomputer.org/docs/current/references/ic-interface-spec/#hash-of-map) + /// of an [HTTP response](crate::HttpResponse), calculated according to a + /// [CEL expression](crate::DefaultCelExpression::Full). + /// + /// The [CEL expression](crate::DefaultCelExpression::Full) used to calculate the hash of + /// this [response](crate::HttpResponse), is also used to produce the + /// [cel_expr_hash](Certification::Full::cel_expr_hash) property. + response_hash: Hash, + }, +} + +impl Certification { + /// Creates the [Skip](Certification::Skip) variant of the [Certification] enum, excluding both an + /// [HTTP request](crate::HttpRequest) and the corresponding [HTTP response](crate::HttpResponse) + /// from certification. + pub fn skip() -> HttpCertificationResult { + let cel_expr = DefaultCelBuilder::skip_certification().to_string(); + let cel_expr_hash = hash(&cel_expr.as_bytes()); + + Ok(Certification::Skip { cel_expr_hash }) + } + + /// Creates the [ResponseOnly](Certification::ResponseOnly) variant of the [Certification] enum, + /// including an [HTTP response](crate::HttpResponse) but excluding the corresponding + /// [HTTP request](crate::HttpRequest) from certification. + pub fn response_only( + cel_expr: &DefaultResponseOnlyCelExpression, + response: &HttpResponse, + response_body_hash: Option, + ) -> HttpCertificationResult { + let cel_expr_hash = hash(cel_expr.to_string().as_bytes()); + let response_hash = response_hash(response, &cel_expr.response, response_body_hash); + + Ok(Certification::ResponseOnly { + cel_expr_hash, + response_hash, + }) + } + + /// Creates the [Full](Certification::Full) variant of the [Certification] enum, including both an + /// [HTTP request](crate::HttpRequest) and the corresponding [HTTP request](crate::HttpRequest) + /// in certification. + pub fn full( + cel_expr: &DefaultFullCelExpression, + request: &HttpRequest, + response: &HttpResponse, + response_body_hash: Option, + ) -> HttpCertificationResult { + let cel_expr_hash = hash(cel_expr.to_string().as_bytes()); + let request_hash = request_hash(request, &cel_expr.request)?; + let response_hash = response_hash(response, &cel_expr.response, response_body_hash); + + Ok(Certification::Full { + cel_expr_hash, + request_hash, + response_hash, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::DefaultResponseCertification; + use rstest::*; + + #[rstest] + fn no_certification() { + let cel_expr = DefaultCelBuilder::skip_certification().to_string(); + let expected_cel_expr_hash = hash(cel_expr.as_bytes()); + + let result = Certification::skip().unwrap(); + + assert!(matches!( + result, + Certification::Skip { cel_expr_hash } if cel_expr_hash == expected_cel_expr_hash + )); + } + + #[rstest] + fn response_only_certification() { + let cel_expr = DefaultCelBuilder::response_only_certification() + .with_response_certification(DefaultResponseCertification::certified_response_headers( + &["ETag", "Cache-Control"], + )) + .build(); + let expected_cel_expr_hash = hash(cel_expr.to_string().as_bytes()); + + let response = &HttpResponse { + status_code: 200, + body: vec![], + headers: vec![], + }; + let expected_response_hash = response_hash(response, &cel_expr.response, None); + + let result = Certification::response_only(&cel_expr, response, None).unwrap(); + + assert!(matches!( + result, + Certification::ResponseOnly { + cel_expr_hash, + response_hash + } if cel_expr_hash == expected_cel_expr_hash && + response_hash == expected_response_hash + )) + } + + #[rstest] + fn full_certification() { + let cel_expr = DefaultCelBuilder::full_certification() + .with_request_headers(&["If-Match"]) + .with_request_query_parameters(&["foo", "bar", "baz"]) + .with_response_certification(DefaultResponseCertification::certified_response_headers( + &["ETag", "Cache-Control"], + )) + .build(); + let expected_cel_expr_hash = hash(cel_expr.to_string().as_bytes()); + + let request = &HttpRequest { + body: vec![], + headers: vec![], + method: "GET".to_string(), + url: "/index.html".to_string(), + }; + let expected_request_hash = request_hash(request, &cel_expr.request).unwrap(); + + let response = &HttpResponse { + status_code: 200, + body: vec![], + headers: vec![], + }; + let expected_response_hash = response_hash(response, &cel_expr.response, None); + + let result = Certification::full(&cel_expr, request, response, None).unwrap(); + + assert!(matches!( + result, + Certification::Full { + cel_expr_hash, + request_hash, + response_hash + } if cel_expr_hash == expected_cel_expr_hash && + request_hash == expected_request_hash && + response_hash == expected_response_hash + )) + } +} diff --git a/packages/ic-http-certification/src/tree/mod.rs b/packages/ic-http-certification/src/tree/mod.rs new file mode 100644 index 00000000..92f51fa6 --- /dev/null +++ b/packages/ic-http-certification/src/tree/mod.rs @@ -0,0 +1,8 @@ +//! The Tree module contains functions and builders for managing certified +//! [request](crate::HttpRequest) and [response](crate::HttpResponse) pairs in a +//! purpose-build HTTP certification data structure. +//! +//! Certifications are prepared using the [Certification] enum. + +mod certification; +pub use certification::*; diff --git a/packages/ic-response-verification/src/cel/ast_mapping.rs b/packages/ic-response-verification/src/cel/ast_mapping.rs index c4a62843..b491186c 100644 --- a/packages/ic-response-verification/src/cel/ast_mapping.rs +++ b/packages/ic-response-verification/src/cel/ast_mapping.rs @@ -1,9 +1,12 @@ use crate::cel::error::{CelParserError, CelParserResult}; use crate::cel::parser::CelValue; -use ic_http_certification::cel::{ - CelExpression, DefaultCertification, DefaultRequestCertification, +use ic_http_certification::{ + cel::{ + CelExpression, DefaultCelExpression, DefaultFullCelExpression, DefaultRequestCertification, + DefaultResponseOnlyCelExpression, + }, + DefaultResponseCertification, }; -use ic_http_certification::DefaultResponseCertification; use std::borrow::Cow; use std::collections::HashMap; @@ -185,7 +188,7 @@ pub(crate) fn map_cel_ast<'a>(cel: &'a CelValue<'a>) -> CelParserResult Err(CelParserError::ExtraneousValidationArgsProperty), (None, None) => Err(CelParserError::MissingValidationArgsProperty), - (Some(_), None) => Ok(CelExpression::DefaultCertification(None)), + (Some(_), None) => Ok(CelExpression::Default(DefaultCelExpression::Skip)), (None, Some(certification)) => { let certification = validate_object(certification, "Certification")?; @@ -193,10 +196,16 @@ pub(crate) fn map_cel_ast<'a>(cel: &'a CelValue<'a>) -> CelParserResult(parts: &[T]) -> Vec