diff --git a/examples/http-certification/assets/README.md b/examples/http-certification/assets/README.md index 2d2c3c5..69c221e 100644 --- a/examples/http-certification/assets/README.md +++ b/examples/http-certification/assets/README.md @@ -1,7 +1,5 @@ # Serving static assets over HTTP -## Overview - This guide walks through an example project that demonstrates how to create a canister that can serve certified static assets (HTML, CSS, JS) over HTTP. The example project presents a very simple single-page JavaScript application. Assets are embedded into the canister when it is compiled. This is not a beginner's canister development guide. Many fundamental concepts that a relatively experienced canister developer should already know will be omitted. Concepts specific to asset certification will be called out here and can help to understand the [full code example](https://github.com/dfinity/response-verification/tree/main/examples/http-certification/assets). @@ -269,7 +267,7 @@ fn certify_all_assets() { ## Serving assets -The `serve_asset` function is responsible for serving assets. It uses the `serve_asset` function from the `AssetRouter` to serve the assets. This function returns an `HttpResponse` that can be returned to the caller. +The `serve_asset` function from the `AssetRouter` is responsible for serving assets. This function returns an `HttpResponse` that can be returned to the caller. ```rust fn serve_asset(req: &HttpRequest) -> HttpResponse<'static> { @@ -342,7 +340,9 @@ fn serve_metrics() -> HttpResponse<'static> { ## Testing the canister -To test the canister, you can use the `dfx` command-line tool. First, run DFX: +This example uses a canister called `http_certification_assets_backend`. + +To test the canister, you can use [`dfx`](https://internetcomputer.org/docs/current/developer-docs/getting-started/install) to start a local instance of the replica: ```shell dfx start --background --clean diff --git a/examples/http-certification/custom-assets/README.md b/examples/http-certification/custom-assets/README.md index 850b5cb..770d98b 100644 --- a/examples/http-certification/custom-assets/README.md +++ b/examples/http-certification/custom-assets/README.md @@ -1,7 +1,5 @@ # Serving static assets over HTTP (custom) -## Overview - This guide walks through an example project that demonstrates how to create a canister that can serve certified static assets (HTML, CSS, JS) over HTTP. The example project presents a very simple single-page JavaScript application. Assets are embedded into the canister when it is compiled. This is not a beginner's canister development guide. Many fundamental concepts that a relatively experienced canister developer should already know will be omitted. Concepts specific to HTTP Certification will be called out here and can help to understand the [full code example](https://github.com/dfinity/response-verification/tree/main/examples/http-certification/custom-assets). @@ -76,9 +74,9 @@ fn post_upgrade() { } ``` -## CEL Expressions +## CEL expressions -The CEL expression definition is simpler in the case of assets compared to the [JSON API example](https://internetcomputer.org/docs/current/developer-docs/http-compatible-canisters/serving-json-over-http) as the same CEL expression is used for every asset, including the fallback response. +The CEL expression definition is simpler in the case of assets compared to the [JSON API example](https://internetcomputer.org/docs/current/developer-docs/http-compatible-canisters/serving-json-over-http) as the same CEL expression is used for every asset including the fallback response. ```rust lazy_static! { @@ -199,7 +197,7 @@ fn certify_asset_response( } ``` -The next function to look at is another reusable function to certify an asset with a specific encoding. This function will check for a file with an additional file extension matching the requested encoding in the statically included asset directory. +Next is a reusable function to certify an asset with a specific encoding. This function will check for a file with an additional file extension matching the requested encoding in the statically included asset directory. For example, when certifying `index.html` with `gzip` encoding, this function will check for `index.html.gzip`. If the encoded asset exists, then it is certified using a procedure similar to the previously defined `certify_asset_response` function. The primary difference in this function is where the encoded asset response is stored. @@ -550,7 +548,9 @@ fn http_request(req: HttpRequest) -> HttpResponse { ## Testing the canister -To test the canister, you can use the `dfx` command-line tool. First, run DFX: +This example uses a canister called `http_certification_custom_assets_backend`. + +To test the canister, you can use [`dfx`](https://internetcomputer.org/docs/current/developer-docs/getting-started/install) to start a local instance of the replica: ```shell dfx start --background --clean diff --git a/examples/http-certification/json-api/README.md b/examples/http-certification/json-api/README.md index 12f7486..c94f34b 100644 --- a/examples/http-certification/json-api/README.md +++ b/examples/http-certification/json-api/README.md @@ -1,7 +1,5 @@ # Serving JSON over HTTP -## Overview - This guide walks through an example project that demonstrates how to create a canister that can serve certified JSON over HTTP. The example project presents a very simple REST API for creating and listing to-do items. There is no authentication or persistent storage. This is not a beginner's canister development guide. Many fundamental concepts that a relatively experienced canister developer should already know will be omitted. Concepts specific to HTTP certification will be called out here and can help to understand the [full code example](https://github.com/dfinity/response-verification/tree/main/examples/http-certification/json-api). @@ -42,7 +40,7 @@ fn post_upgrade() { CEL expressions only need to be set up once and can then be reused until the next canister upgrade. Responses can also be set up once and reused. If the response is static and will not change throughout the canister's lifetime, then it only needs to be certified once. If the response can change, however, then it will need to be re-certified every time it changes. -`DefaultResponseOnlyCelExpression` is used when only the response is to be certified. If the request is also to be certified, then `DefaultFullCelExpression` should be used. Alternatively, the higher-level `DefaultCelExpression` can hold any type of CEL expression using the "Default" scheme. In the future, there may be more schemes, and the higher-level `CelExpression` will be able to hold CEL expressions from those different schemes. It is up to the developers to decide how they want to store and organize their CEL expressions. +`DefaultResponseOnlyCelExpression` is used when only the response is to be certified. If the request is also to be certified, then `DefaultFullCelExpression` should be used. Alternatively, the higher-level `DefaultCelExpression` can hold any type of CEL expression using the "Default" scheme. In the future, there may be more schemes and the higher-level `CelExpression` will be able to hold CEL expressions from those different schemes. It is up to the developers to decide how they want to store and organize their CEL expressions. In this example, there are two different CEL expressions used, a "full" CEL expression and a "response-only" CEL expression. The "full" CEL expression is used for the certified "todos" and the "response-only" CEL expression for the "Not found" response. For more information on defining CEL expressions, see the relevant section in the [`ic-http-certification` docs](https://docs.rs/ic-http-certification/latest/ic_http_certification/#defining-cel-expressions). @@ -83,7 +81,7 @@ lazy_static! { } ``` -## Response Headers +## Response headers The security headers added to responses are based on the [OWASP Secure Headers project](https://owasp.org/www-project-secure-headers/index.html). @@ -380,7 +378,7 @@ fn upgrade_to_update_call_handler( Upgrading to an `update` call will instruct the HTTP gateway to remake the request as an [`update` call](https://internetcomputer.org/docs/current/references/ic-interface-spec/#http-call). As an update call, the response to this request does not need to be certified. Since the canister's state has changed, however, the static `query` call responses will need to be re-certified. The same functions that certified these responses in the first place can be reused to achieve this. -For creating todo items: +For creating to-do items: ```rust fn create_todo_item_handler(req: &HttpRequest, _params: &Params) -> HttpResponse<'static> { @@ -411,7 +409,7 @@ fn create_todo_item_handler(req: &HttpRequest, _params: &Params) -> HttpResponse } ``` -For updating todo items: +For updating to-do items: ```rust fn update_todo_item_handler(req: &HttpRequest, params: &Params) -> HttpResponse<'static> { @@ -437,7 +435,7 @@ fn update_todo_item_handler(req: &HttpRequest, params: &Params) -> HttpResponse< } ``` -And, finally, for deleting todo items: +And, finally, for deleting to-do items: ```rust fn delete_todo_item_handler(_req: &HttpRequest, params: &Params) -> HttpResponse<'static> { @@ -524,7 +522,9 @@ fn insert_update_route(method: &str, path: &str, route_handler: RouteHandler) { ## Testing the canister -To test the canister, you can use the `dfx` command-line tool. First, run DFX: +This example uses a canister called `http_certification_json_api_backend`. + +To test the canister, you can use [`dfx`](https://internetcomputer.org/docs/current/developer-docs/getting-started/install) to start a local instance of the replica: ```shell dfx start --background --clean @@ -533,10 +533,10 @@ dfx start --background --clean Then, deploy the canister: ```shell -dfx deploy +dfx deploy http_certification_json_api_backend ``` -To fetch TODO items: +To fetch to-do items: ```shell curl -s \ @@ -544,7 +544,7 @@ curl -s \ --resolve "$(dfx canister id http_certification_json_api_backend).localhost:$(dfx info webserver-port):127.0.0.1" | jq ``` -To add a TODO item: +To add a to-do item: ```shell curl -s -X POST \ @@ -554,7 +554,7 @@ curl -s -X POST \ -d '{ "title": "Learn Motoko" }' | jq ``` -To update a TODO item: +To update a to-do item: ```shell curl -s -X PATCH \ @@ -564,7 +564,7 @@ curl -s -X PATCH \ -d '{ "completed": true }' | jq ``` -To delete a TODO item: +To delete a to-do item: ```shell curl -s -X DELETE \ diff --git a/examples/http-certification/skip-certification/README.md b/examples/http-certification/skip-certification/README.md index 21c807d..0a91030 100644 --- a/examples/http-certification/skip-certification/README.md +++ b/examples/http-certification/skip-certification/README.md @@ -1,16 +1,14 @@ # Skipping certification for HTTP responses -## Overview - This guide walks through an example project that demonstrates how to skip HTTP certification for all possible responses from a canister. -WARNING!!! This means that a malicious replica can return whatever data it wants in response to requests directed towards the canister. Think carefully about whether or not this is the right fit for the canister. If certification should only be skipped for certain paths, then check out the ["Serving static assets over HTTP"](https://internetcomputer.org/docs/current/developer-docs/web-apps/http-compatible-canisters/serving-static-assets-over-http) guide where this approach is covered in more detail. +**WARNING** This means that a malicious replica can return whatever data it wants in response to requests directed towards the canister. Think carefully about whether or not this is the right fit for the canister. If certification should only be skipped for certain paths, then check out the ["Serving static assets over HTTP"](https://internetcomputer.org/docs/current/developer-docs/web-apps/http-compatible-canisters/serving-static-assets-over-http) guide where this approach is covered in more detail. -This is not a beginner's canister development guide. Many fundamental concepts that a relatively experienced canister developer should already know will be omitted. Concepts specific to HTTP Certification will be called out here and can help to understand the [full code example](https://github.com/dfinity/response-verification/tree/main/examples/http-certification/skip-certification). +This is not a beginner's canister development guide. Many fundamental concepts that a relatively experienced canister developer should already know will be omitted. Concepts specific to HTTP certification will be called out here and can help to understand the [full code example](https://github.com/dfinity/response-verification/tree/main/examples/http-certification/skip-certification). ## Prerequisites -This is a relatively simple guide so there's no prerequisites as such, but it's recommended to check out the full certification guides to make sure that certification is not a good fit for your project. +This is a relatively simple guide, so there are no prerequisites as such, but it's recommended to check out the full certification guides to make sure that certification is not a good fit for your project. - [x] Complete the ["Serving static assets over HTTP"](https://internetcomputer.org/docs/current/developer-docs/web-apps/http-compatible-canisters/serving-static-assets-over-http) guide. - [x] Complete the ["Custom HTTP Canisters"](https://internetcomputer.org/docs/current/developer-docs/http-compatible-canisters/custom-http-canisters) guide. @@ -28,7 +26,7 @@ use ic_http_certification::utils::skip_certification_certified_data; #[init] fn init() { - set_certified_data(&skip_certification_certified_data()); +    set_certified_data(&skip_certification_certified_data()); } ``` @@ -42,11 +40,11 @@ use ic_http_certification::utils::add_skip_certification_header; #[query] fn http_request() -> HttpResponse<'static> { - let mut response = create_response(); +    let mut response = create_response(); - add_skip_certification_header(data_certificate().unwrap(), &mut response); +    add_skip_certification_header(data_certificate().unwrap(), &mut response); - response +    response } ``` @@ -54,19 +52,21 @@ The call to `data_certificate` returns a certificate that proves the canister's ## Testing the canister -Start DFX: +This example uses a canister called `http_certification_skip_certification_backend`. + +To test the canister, you can use [`dfx`](https://internetcomputer.org/docs/current/developer-docs/getting-started/install) to start a local instance of the replica: ```shell dfx start --background --clean ``` -Deploy the canister: +Then, deploy the canister: ```shell dfx deploy http_certification_skip_certification_backend ``` -Make a request to the canister using cURL: +Make a request to the canister using curl: ```shell curl -s http://localhost:$(dfx info webserver-port)?canisterId=$(dfx canister id http_certification_skip_certification_backend) | jq @@ -80,7 +80,7 @@ You should see output similar to the following: } ``` -Alternatively, print the URL in the terminal and then open in a browser: +Alternatively, print the URL in the terminal and then open it in a browser: ```shell echo http://localhost:$(dfx info webserver-port)?canisterId=$(dfx canister id http_certification_skip_certification_backend) diff --git a/examples/http-certification/upgrade-to-update-call/README.md b/examples/http-certification/upgrade-to-update-call/README.md index fb4302e..aec1eb2 100644 --- a/examples/http-certification/upgrade-to-update-call/README.md +++ b/examples/http-certification/upgrade-to-update-call/README.md @@ -1,26 +1,22 @@ # Upgrading HTTP calls to update calls -## Overview +This guide walks through an example project that demonstrates how to use the ["Upgrade to Update call"](https://internetcomputer.org/docs/current/references/http-gateway-protocol-spec#upgrade-to-update-calls) feature of the HTTP gateway. -This guide walks through an example project that demonstrates how to use the ["Upgrade to Update call"](https://internetcomputer.org/docs/current/references/http-gateway-protocol-spec#upgrade-to-update-calls) feature of the HTTP Gateway. - -Since browsers are unable to directly interact with the IC network, the HTTP Gateway acts as a bridge between the two. The HTTP Gateway forwards requests from clients to canisters and forwards responses from canisters back to clients. Before returning responses from canister back to clients, the HTTP Gateway verifies the certification of the response to ensure that they have not been tampered with. +Since browsers are unable to directly interact with the ICP network, the HTTP gateway acts as a bridge between the two. The HTTP gateway forwards requests from clients to canisters and forwards responses from canisters back to clients. Before returning responses from the canister back to clients, the HTTP gateway verifies the certification of the response to ensure that they have not been tampered with. Upgrading query calls to upgrade calls allows for the certification of any kind of dynamic response by leveraging ICP's consensus protocol without having to statically certify the response ahead of time. This is the simplest way to add _secure_ HTTP support to a canister. -A similairly simple yet more performant, but _insecure_ approach is to skip certification entirely. This is not recommended unless you are absolutely sure that certification really does not make sense for your canister. Check the ["Skipping certification for HTTP responses"](https://internetcomputer.org/docs/current/developer-docs/web-apps/http-compatible-canisters/skipping-certification-for-http-responses) guide for more details on how to do that. - -This is not a beginner's canister development guide. Many fundamental concepts that a relatively experienced canister developer should already know will be omitted. Concepts specific to upgrading to an update call will be called out here and can help to understand the [full code example](https://github.com/dfinity/response-verification/tree/main/examples/http-certification/upgrade-to-update-call). +A similarly simple yet more performant, but _insecure_ approach is to skip certification entirely. This is not recommended unless you are absolutely sure that certification really does not make sense for your canister. Check the ["Skipping certification for HTTP responses"](https://internetcomputer.org/docs/current/developer-docs/web-apps/http-compatible-canisters/skipping-certification-for-http-responses) guide for more details on how to do that. ## How it works -When the HTTP Gateway receives a request from a client, it will forward the request to the target canister's `http_request` method as a query call. To upgrade this query call to an update call, the canister returns a response that sets the optional `upgrade` field to `opt true`. Ommiting this field, or setting it to `opt false` will result in the HTTP Gateway treating the query call response as-is, without upgrading. +When the HTTP gateway receives a request from a client, it will forward the request to the target canister's `http_request` method as a query call. To upgrade this query call to an update call, the canister returns a response that sets the optional `upgrade` field to `opt true`. Omitting this field, or setting it to `opt false` will result in the HTTP Gateway treating the query call response as-is, without upgrading. -Upon receiving a response from the canister with the `upgrade` field set to `opt true`, the HTTP Gateway will repeat the original request as an update call to the `http_request_update` method of the canister. The canister can then respond to the update call with any dynamic response and leverage the ICP consensus protocol for security. The certification resulting from putting this response through consensus will be verified by the HTTP Gateway to ensure it has not been tampered with. +Upon receiving a response from the canister with the `upgrade` field set to `opt true`, the HTTP gateway will repeat the original request as an update call to the `http_request_update` method of the canister. The canister can then respond to the update call with any dynamic response and leverage the ICP consensus protocol for security. The certification resulting from putting this response through consensus will be verified by the HTTP Gateway to ensure it has not been tampered with. ## Rust -This example project features both Rust and Motoko code. If you rather follow the Motoko version, you can skip this section and go straight to the [section covering Motoko](#motoko). +This example project features both Rust and Motoko code. If you would rather follow the Motoko version, you can skip this section and go straight to the [section covering Motoko](#motoko). The Rust code is split into two functions: `http_request` and `http_request_update`. The `http_request` function is the entry point for the query call from the HTTP Gateway. It returns an `HttpResponse` with the `upgrade` field set to `Some(true)` (via the `build_update` method on the `HttpResponse::builder` struct). The `http_request_update` function is the entry point for the update call from the HTTP Gateway. It returns an `HttpUpdateResponse` with a custom status code and body. @@ -30,15 +26,15 @@ use ic_http_certification::{HttpResponse, HttpUpdateResponse}; #[query] fn http_request() -> HttpResponse<'static> { - HttpResponse::builder().with_upgrade(true).build() +    HttpResponse::builder().with_upgrade(true).build() } #[update] fn http_request_update() -> HttpUpdateResponse<'static> { - HttpResponse::builder() - .with_status_code(StatusCode::IM_A_TEAPOT) - .with_body(b"I'm a teapot") - .build_update() +    HttpResponse::builder() +        .with_status_code(StatusCode::IM_A_TEAPOT) +        .with_body(b"I'm a teapot") +        .build_update() } ``` @@ -51,82 +47,84 @@ The Motoko code is split into two functions: `http_request` and `http_request_up import Text "mo:base/Text"; actor Http { - type HeaderField = (Text, Text); - - type HttpRequest = { - method : Text; - url : Text; - headers : [HeaderField]; - body : Blob; - certificate_version : ?Nat16; - }; - - type HttpUpdateRequest = { - method : Text; - url : Text; - headers : [HeaderField]; - body : Blob; - }; - - type HttpResponse = { - status_code : Nat16; - headers : [HeaderField]; - body : Blob; - upgrade : ?Bool; - }; - - type HttpUpdateResponse = { - status_code : Nat16; - headers : [HeaderField]; - body : Blob; - }; - - public query func http_request(_req: HttpRequest) : async HttpResponse { - return { - status_code = 200; - headers = []; - body = ""; - upgrade = ?true; - }; - }; - - public func http_request_update(_req: HttpUpdateRequest) : async HttpUpdateResponse { - return { - status_code = 418; - headers = []; - body = Text.encodeUtf8("I'm a teapot"); - }; - }; +  type HeaderField = (Text, Text); + +  type HttpRequest = { +    method : Text; +    url : Text; +    headers : [HeaderField]; +    body : Blob; +    certificate_version : ?Nat16; +  }; + +  type HttpUpdateRequest = { +    method : Text; +    url : Text; +    headers : [HeaderField]; +    body : Blob; +  }; + +  type HttpResponse = { +    status_code : Nat16; +    headers : [HeaderField]; +    body : Blob; +    upgrade : ?Bool; +  }; + +  type HttpUpdateResponse = { +    status_code : Nat16; +    headers : [HeaderField]; +    body : Blob; +  }; + +  public query func http_request(_req: HttpRequest) : async HttpResponse { +    return { +      status_code = 200; +      headers = []; +      body = ""; +      upgrade = ?true; +    }; +  }; + +  public func http_request_update(_req: HttpUpdateRequest) : async HttpUpdateResponse { +    return { +      status_code = 418; +      headers = []; +      body = Text.encodeUtf8("I'm a teapot"); +    }; +  }; }; ``` ## Testing the canister -Start DFX: +This example uses a Rust canister called `http_certification_upgrade_to_update_call_rust_backend` or a Motoko canister called `http_certification_upgrade_to_update_call_motoko_backend`. + +To test the canister, you can use [`dfx`](https://internetcomputer.org/docs/current/developer-docs/getting-started/install) to start a local instance of the replica: ```shell dfx start --background --clean ``` -Deploy the Rust canister: +#### Testing the Rust canister ```shell dfx deploy http_certification_upgrade_to_update_call_rust_backend ``` -Or deploy the Motoko canister: +Make a request to the canister using curl: ```shell -dfx deploy http_certification_upgrade_to_update_call_motoko_backend +curl -v http://localhost:$(dfx info webserver-port)?canisterId=$(dfx canister id http_certification_upgrade_to_update_call_rust_backend) ``` -Make a request to the Rust canister using cURL: +#### Testing the Motoko canister ```shell -curl -v http://localhost:$(dfx info webserver-port)?canisterId=$(dfx canister id http_certification_upgrade_to_update_call_rust_backend) +dfx deploy http_certification_upgrade_to_update_call_motoko_backend ``` -Or the Motoko canister: +Make a request to the canister using curl: ```shell curl -v http://localhost:$(dfx info webserver-port)?canisterId=$(dfx canister id http_certification_upgrade_to_update_call_motoko_backend) diff --git a/packages/ic-asset-certification/README.md b/packages/ic-asset-certification/README.md index 57c42e3..e7e6887 100644 --- a/packages/ic-asset-certification/README.md +++ b/packages/ic-asset-certification/README.md @@ -1,35 +1,32 @@ -# Asset Certification - -## Overview +# Asset certification Asset certification is a specialized form of -[HTTP Certification](https://internetcomputer.org/docs/current/developer-docs/http-compatible-canisters/custom-http-canisters) -purpose-built for certifying static assets in [ICP](https://internetcomputer.org/) canisters. +[HTTP certification](https://internetcomputer.org/docs/current/developer-docs/http-compatible-canisters/custom-http-canisters) +built for certifying static assets in [ICP](https://internetcomputer.org/) canisters. The `ic-asset-certification` crate provides the necessary functionality to certify and serve static assets from Rust canisters. This is implemented in the following steps: -1. [Preparing assets](#preparing-assets) -2. [Configuring asset certification](#configuring-asset-certification) -3. [Inserting assets into the asset router](#inserting-assets-into-the-asset-router) -4. [Serving assets](#serving-assets) -5. [Deleting assets](#deleting-assets) -6. [Querying assets](#querying-assets) +1. [Preparing assets](#preparing-assets). +2. [Configuring asset certification](#configuring-asset-certification). +3. [Inserting assets into the asset router](#inserting-assets-into-the-asset-router). +4. [Serving assets](#serving-assets). +5. [Deleting assets](#deleting-assets). +6. [Querying assets](#querying-assets). For canisters that need it, it's also possible to [delete assets](#deleting-assets). ## Preparing assets -This library is unopinionated about where assets come from, so that is not -covered in detail here, but there are three main options: +This library is unopinionated about where assets come from. However, there are three main options: - Embedding assets in the canister at compile time: - - [include_bytes!](https://doc.rust-lang.org/std/macro.include_bytes.html) - - [include_dir!](https://docs.rs/include_dir/latest/include_dir/index.html) -- Uploading assets via canister endpoints at runtime. - - The [DFX asset canister](https://github.com/dfinity/sdk/blob/master/docs/design/asset-canister-interface.md) is a good example of this approach. +   - [include_bytes!](https://doc.rust-lang.org/std/macro.include_bytes.html) +   - [include_dir!](https://docs.rs/include_dir/latest/include_dir/index.html) +- Uploading assets via canister endpoints at runtime: +   - The [`dfx` asset canister](https://github.com/dfinity/sdk/blob/master/docs/design/asset-canister-interface.md) is a good example of this approach. - Generating assets dynamically in code, at runtime. With the assets in memory, they can be converted into the `Asset` type: @@ -38,13 +35,13 @@ With the assets in memory, they can be converted into the `Asset` type: use ic_asset_certification::Asset; let asset = Asset::new( - "index.html", - b"

Hello World!

".as_slice(), +    "index.html", +    b"

Hello World!

".as_slice(), ); ``` -It is recommended to use references when including assets directly into the -canister, to avoid duplicating the content. This is particularly important for +It is recommended to use references when including assets directly in the +canister to avoid duplicating the content. This is particularly important for larger assets. ```rust @@ -52,8 +49,8 @@ use ic_asset_certification::Asset; let pretty_big_asset = include_bytes!("lib.rs"); let asset = Asset::new( - "assets/pretty-big-asset.gz", - pretty_big_asset.as_slice(), +    "assets/pretty-big-asset.gz", +    pretty_big_asset.as_slice(), ); ``` @@ -65,35 +62,35 @@ use ic_asset_certification::Asset; let name = "World"; let asset = Asset::new( - "index.html", - format!("

Hello {name}!

").into_bytes(), +    "index.html", +    format!("

Hello {name}!

").into_bytes(), ); ``` ## Configuring asset certification `AssetConfig` defines the configuration for any files that will be certified. -The configuration can either be matched to an individual file by path, or to +The configuration can either be matched to an individual file by path or to many files by a glob. In both cases, the following options can be configured for each asset: - `content_type` - - Providing this option will certify and serve a `Content-Type` header with - the provided value. - - If this value is not provided, the `Content-Type` header will not be - inserted. - - If the `Content-Type` header is not sent to the browser, the browser will - try to guess the content type based on the file extension, unless an - `X-Content-Type-Options: nosniff` header is sent. - - Not certifying the `Content-Type` header will also allow a malicious replica - to insert its own `Content-Type` header, which could lead to a security - vulnerability. +   - Providing this option will certify and serve a `Content-Type` header with +     the provided value. +   - If this value is not provided, the `Content-Type` header will not be +     inserted. +   - If the `Content-Type` header is not sent to the browser, the browser will +     try to guess the content type based on the file extension, unless an +     `X-Content-Type-Options: nosniff` header is sent. +   - Not certifying the `Content-Type` header will also allow a malicious replica +     to insert its own `Content-Type` header, which could lead to a security +     vulnerability. - `headers` - - Any additional headers provided will be certified and served with the - asset. - - It's important to include any headers that can affect browser behavior, - particularly [security headers](https://owasp.org/www-project-secure-headers/index.html). +   - Any additional headers provided will be certified and served with the +     asset. +   - It's important to include any headers that can affect browser behavior, +     particularly [security headers](https://owasp.org/www-project-secure-headers/index.html). - `encodings` - A list of alternative encodings that can be used to serve the asset. - Each entry is a tuple of the encoding name and the file @@ -135,7 +132,7 @@ the option of registering the asset as a fallback response for a particular scope. This can be used to configure 404 pages or single-page application entry points, for example. -When serving assets, if a requested path does not exactly match any assets then +When serving assets, if a requested path does not exactly match any assets, then a search is conducted for an asset configured with the fallback scope that most closely matches the requested asset's path. @@ -173,25 +170,25 @@ use ic_http_certification::StatusCode; use ic_asset_certification::{AssetConfig, AssetFallbackConfig}; let config = AssetConfig::File { - path: "index.html".to_string(), - content_type: Some("text/html".to_string()), - headers: vec![ - ("Cache-Control".to_string(), "public, no-cache, no-store".to_string()), - ], - fallback_for: vec![AssetFallbackConfig { - scope: "/".to_string(), - status_code: Some(StatusCode::OK), - }], - aliased_by: vec!["/".to_string()], - encodings: vec![ - AssetEncoding::Brotli.default(), - AssetEncoding::Gzip.default() - ], +    path: "index.html".to_string(), +    content_type: Some("text/html".to_string()), +    headers: vec![ +        ("Cache-Control".to_string(), "public, no-cache, no-store".to_string()), +    ], +    fallback_for: vec![AssetFallbackConfig { +        scope: "/".to_string(), +        status_code: Some(StatusCode::OK), +    }], +    aliased_by: vec!["/".to_string()], +    encodings: vec![ +        AssetEncoding::Brotli.default(), +        AssetEncoding::Gzip.default() +    ], }; ``` It's also possible to configure multiple fallbacks for a single asset. The -following example configures an individual HTML file to be served by the on the +following example configures an individual HTML file to be served on the `/404.html` path, in addition to serving as the fallback for the `/js` and `/css` scopes. @@ -214,33 +211,33 @@ use ic_http_certification::StatusCode; use ic_asset_certification::{AssetConfig, AssetFallbackConfig}; let config = AssetConfig::File { - path: "404.html".to_string(), - content_type: Some("text/html".to_string()), - headers: vec![ - ("Cache-Control".to_string(), "public, no-cache, no-store".to_string()), - ], - fallback_for: vec![ - AssetFallbackConfig { - scope: "/css".to_string(), - status_code: Some(StatusCode::NOT_FOUND), - }, - AssetFallbackConfig { - scope: "/js".to_string(), - status_code: Some(StatusCode::NOT_FOUND), - }, - ], - aliased_by: vec![ - "/404".to_string(), - "/404/".to_string(), - "/404.html".to_string(), - "/not-found".to_string(), - "/not-found/".to_string(), - "/not-found/index.html".to_string(), - ], - encodings: vec![ - AssetEncoding::Brotli.default(), - AssetEncoding::Gzip.default(), - ], +    path: "404.html".to_string(), +    content_type: Some("text/html".to_string()), +    headers: vec![ +        ("Cache-Control".to_string(), "public, no-cache, no-store".to_string()), +    ], +    fallback_for: vec![ +        AssetFallbackConfig { +            scope: "/css".to_string(), +            status_code: Some(StatusCode::NOT_FOUND), +        }, +        AssetFallbackConfig { +            scope: "/js".to_string(), +            status_code: Some(StatusCode::NOT_FOUND), +        }, +    ], +    aliased_by: vec![ +        "/404".to_string(), +        "/404/".to_string(), +        "/404.html".to_string(), +        "/not-found".to_string(), +        "/not-found/".to_string(), +        "/not-found/index.html".to_string(), +    ], +    encodings: vec![ +        AssetEncoding::Brotli.default(), +        AssetEncoding::Gzip.default(), +    ], }; ``` @@ -254,23 +251,23 @@ Standard Unix-style glob syntax is supported: - `?` matches any single character. - `*` matches zero or more characters. - `**` recursively matches directories but is only legal in three - situations. - - If the glob starts with `**/`, then it matches all directories. - For example, `**/foo` matches `foo` and `bar/foo` but not - `foo/bar`. - - If the glob ends with `/**`, then it matches all sub-entries. - For example, `foo/**` matches `foo/a` and `foo/a/b`, but not - `foo`. - - If the glob contains `/**/` anywhere within the pattern, then it - matches zero or more directories. - - Using `**` anywhere else is illegal. - - The glob `**` is allowed and means "match everything". +   situations. +   - If the glob starts with `**/`, then it matches all directories. +     For example, `**/foo` matches `foo` and `bar/foo` but not +     `foo/bar`. +   - If the glob ends with `/**`, then it matches all sub-entries. +     For example, `foo/**` matches `foo/a` and `foo/a/b`, but not +     `foo`. +   - If the glob contains `/**/` anywhere within the pattern, then it +     matches zero or more directories. +   - Using `**` anywhere else is illegal. +   - The glob `**` is allowed and means "match everything." - `{a,b}` matches `a` or `b` where `a` and `b` are arbitrary glob - patterns. (N.B. Nesting `{...}` is not currently allowed.) +   patterns. (N.B. Nesting `{...}` is not currently allowed.) - `[ab]` matches `a` or `b` where `a` and `b` are characters. - `[!ab]` to match any character except for `a` and `b`. - Metacharacters such as `*` and `?` can be escaped with character - class notation. e.g., `[*]` matches `*`. +   class notation, e.g., `[*]` matches `*`. For example, the following pattern will match all `.js` files in the `js` directory: @@ -280,22 +277,22 @@ use ic_http_certification::StatusCode; use ic_asset_certification::AssetConfig; let config = AssetConfig::Pattern { - pattern: "js/*.js".to_string(), - content_type: Some("application/javascript".to_string()), - headers: vec![ - ("Cache-Control".to_string(), "public, max-age=31536000, immutable".to_string()), - ], - encodings: vec![ - AssetEncoding::Brotli.default(), - AssetEncoding::Gzip.default(), - ], +    pattern: "js/*.js".to_string(), +    content_type: Some("application/javascript".to_string()), +    headers: vec![ +        ("Cache-Control".to_string(), "public, max-age=31536000, immutable".to_string()), +    ], +    encodings: vec![ +        AssetEncoding::Brotli.default(), +        AssetEncoding::Gzip.default(), +    ], }; ``` ### Configuring redirects Redirects can be configured using the `AssetConfig::Redirect` variant. This -variant takes a `from` and `to` paths, and a redirect `kind`. +variant takes `from` and `to` paths, and a redirect `kind`. When a request is made to the `from` path, the client will be redirected to the `to` path. The `AssetConfig::Redirect` config is not matched against any `Asset`s. @@ -325,13 +322,13 @@ The following example configures a permanent redirect from `/old` to `/new`: use ic_asset_certification::{AssetConfig, AssetRedirectKind}; let config = AssetConfig::Redirect { - from: "/old".to_string(), - to: "/new".to_string(), - kind: AssetRedirectKind::Permanent, - headers: vec![( - "content-type".to_string(), - "text/plain; charset=utf-8".to_string(), - )], +  from: "/old".to_string(), +  to: "/new".to_string(), +  kind: AssetRedirectKind::Permanent, +  headers: vec![( +    "content-type".to_string(), +    "text/plain; charset=utf-8".to_string(), +  )], }; ``` @@ -349,95 +346,95 @@ use ic_asset_certification::{Asset, AssetConfig, AssetFallbackConfig, AssetRoute let mut asset_router = AssetRouter::default(); let assets = vec![ - Asset::new( - "index.html", - b"

Hello World!

".as_slice(), - ), - Asset::new( - "index.html.gz", - [0, 1, 2, 3, 4, 5] - ), - Asset::new( - "index.html.br", - [6, 7, 8, 9, 10, 11] - ), - Asset::new( - "app.js", - b"console.log('Hello World!');".as_slice(), - ), - Asset::new( - "app.js.gz", - [12, 13, 14, 15, 16, 17], - ), - Asset::new( - "app.js.br", - [18, 19, 20, 21, 22, 23], - ), - Asset::new( - "css/app-ba74b708.css", - b"html,body{min-height:100vh;}".as_slice(), - ), - Asset::new( - "css/app-ba74b708.css.gz", - [24, 25, 26, 27, 28, 29], - ), - Asset::new( - "css/app-ba74b708.css.br", - [30, 31, 32, 33, 34, 35], - ), +    Asset::new( +        "index.html", +        b"

Hello World!

".as_slice(), +    ), +    Asset::new( +        "index.html.gz", +        [0, 1, 2, 3, 4, 5] +    ), +    Asset::new( +        "index.html.br", +        [6, 7, 8, 9, 10, 11] +    ), +    Asset::new( +        "app.js", +        b"console.log('Hello World!');".as_slice(), +    ), +    Asset::new( +        "app.js.gz", +        [12, 13, 14, 15, 16, 17], +    ), +    Asset::new( +        "app.js.br", +        [18, 19, 20, 21, 22, 23], +    ), +    Asset::new( +      "css/app-ba74b708.css", +      b"html,body{min-height:100vh;}".as_slice(), +    ), +    Asset::new( +        "css/app-ba74b708.css.gz", +        [24, 25, 26, 27, 28, 29], +    ), +    Asset::new( +        "css/app-ba74b708.css.br", +        [30, 31, 32, 33, 34, 35], +    ), ]; let asset_configs = vec![ - AssetConfig::File { - path: "index.html".to_string(), - content_type: Some("text/html".to_string()), - headers: vec![( - "cache-control".to_string(), - "public, no-cache, no-store".to_string(), - )], - fallback_for: vec![AssetFallbackConfig { - scope: "/".to_string(), - status_code: Some(StatusCode::OK), - }], - aliased_by: vec!["/".to_string()], - encodings: vec![ - AssetEncoding::Brotli.default(), - AssetEncoding::Gzip.default(), - ], - }, - AssetConfig::Pattern { - pattern: "**/*.js".to_string(), - content_type: Some("text/javascript".to_string()), - headers: vec![( - "cache-control".to_string(), - "public, max-age=31536000, immutable".to_string(), - )], - encodings: vec![ - AssetEncoding::Brotli.default(), - AssetEncoding::Gzip.default(), - ], - }, - AssetConfig::Pattern { - pattern: "**/*.css".to_string(), - content_type: Some("text/css".to_string()), - headers: vec![( - "cache-control".to_string(), - "public, max-age=31536000, immutable".to_string(), - )], - encodings: vec![ - AssetEncoding::Brotli.default(), - AssetEncoding::Gzip.default(), - ], - }, - AssetConfig::Redirect { - from: "/old".to_string(), - to: "/new".to_string(), - kind: AssetRedirectKind::Permanent, - headers: vec![( - "content-type".to_string(), - "text/plain; charset=utf-8".to_string(), - )], - }, +    AssetConfig::File { +        path: "index.html".to_string(), +        content_type: Some("text/html".to_string()), +        headers: vec![( +            "cache-control".to_string(), +            "public, no-cache, no-store".to_string(), +        )], +        fallback_for: vec![AssetFallbackConfig { +            scope: "/".to_string(), +            status_code: Some(StatusCode::OK), +        }], +        aliased_by: vec!["/".to_string()], +        encodings: vec![ +            AssetEncoding::Brotli.default(), +            AssetEncoding::Gzip.default(), +        ], +    }, +    AssetConfig::Pattern { +        pattern: "**/*.js".to_string(), +        content_type: Some("text/javascript".to_string()), +        headers: vec![( +            "cache-control".to_string(), +            "public, max-age=31536000, immutable".to_string(), +        )], +        encodings: vec![ +            AssetEncoding::Brotli.default(), +            AssetEncoding::Gzip.default(), +        ], +    }, +    AssetConfig::Pattern { +        pattern: "**/*.css".to_string(), +        content_type: Some("text/css".to_string()), +        headers: vec![( +            "cache-control".to_string(), +            "public, max-age=31536000, immutable".to_string(), +        )], +        encodings: vec![ +            AssetEncoding::Brotli.default(), +            AssetEncoding::Gzip.default(), +        ], +    }, +    AssetConfig::Redirect { +        from: "/old".to_string(), +        to: "/new".to_string(), +        kind: AssetRedirectKind::Permanent, +        headers: vec![( +            "content-type".to_string(), +            "text/plain; charset=utf-8".to_string(), +        )], +    }, ]; asset_router.certify_assets(assets, asset_configs).unwrap(); @@ -474,7 +471,7 @@ asset_router.init_with_tree(http_certification_tree.clone()); ## Serving assets Assets can be served by calling the `serve_asset` method on the `AssetRouter`. -This method will return a response, a witness and an expression path, which can be used +This method will return a response, a witness, and an expression path, which can be used alongside the canister's data certificate to add the required certificate header to the response. ```rust @@ -484,22 +481,22 @@ use ic_asset_certification::{Asset, AssetConfig, AssetFallbackConfig, AssetRoute let mut asset_router = AssetRouter::default(); let asset = Asset::new( - "index.html", - b"

Hello World!

".as_slice(), +    "index.html", +    b"

Hello World!

".as_slice(), ); let asset_config = AssetConfig::File { - path: "index.html".to_string(), - content_type: Some("text/html".to_string()), - headers: vec![ - ("Cache-Control".to_string(), "public, no-cache, no-store".to_string()), - ], - fallback_for: vec![AssetFallbackConfig { - scope: "/".to_string(), - status_code: Some(StatusCode::OK), - }], - aliased_by: vec!["/".to_string()], - encodings: vec![], +    path: "index.html".to_string(), +    content_type: Some("text/html".to_string()), +    headers: vec![ +        ("Cache-Control".to_string(), "public, no-cache, no-store".to_string()), +    ], +    fallback_for: vec![AssetFallbackConfig { +        scope: "/".to_string(), +        status_code: Some(StatusCode::OK), +    }], +    aliased_by: vec!["/".to_string()], +    encodings: vec![], }; let http_request = HttpRequest::get("/").build(); @@ -508,14 +505,14 @@ asset_router.certify_assets(vec![asset], vec![asset_config]).unwrap(); let (mut response, witness, expr_path) = asset_router.serve_asset(&http_request).unwrap(); -// this should normally be retrieved using `ic_cdk::api::data_certificate()`. +// This should normally be retrieved using `ic_cdk::api::data_certificate()`. let data_certificate = vec![1, 2, 3]; add_v2_certificate_header( - data_certificate, - &mut response, - &witness, - &expr_path, +    data_certificate, +    &mut response, +    &witness, +    &expr_path, ); ``` @@ -529,22 +526,22 @@ There are three ways to delete assets from the asset router: ### Deleting assets by configuration -Deleting assets by configuration is similar to (certifying them)[#inserting-assets-into-the-asset-router]. +Deleting assets by configuration is similar to [certifying them](#inserting-assets-into-the-asset-router). Depending on the configuration provided to the `certify_assets` function, multiple responses may be generated for the same asset. To ensure that all generated responses are deleted, the `delete_assets` function accepts the same configuration. -If a configuration different to the one used to certify assets in the first place is provided, -one of two things can happen. +If a configuration different from the one used to certify assets in the first place is provided, +one of two things can happen: -If the configuration inclues a file that was not certified in the first place, it will be silently ignored. -For example, if the configuration provided to `certify_assets` includes the Brotli and Gzip encodings, but the -configuration provided to `delete_assets` includes Brotli, Gzip and Deflate, the Brotli and Gzip encoded files will be deleted, while the Deflate file is ignored, since it doesn't exist. +1. If the configuration includes a file that was not certified in the first place, it will be silently ignored. + For example, if the configuration provided to `certify_assets` includes the Brotli and Gzip encodings, but the + configuration provided to `delete_assets` includes Brotli, Gzip, and Deflate. The Brotli and Gzip encoded files will be deleted, while the Deflate file is ignored, since it doesn't exist. -If the configuration excludes a file that was certified, it will not be deleted. For example, if the configuration, -provided to `certify_assets` includes the Brotli and Gzip encodings, but the configuration provided to `delete_assets` -only includes Brotli, then the Gzip file will not be deleted. +2. If the configuration excludes a file that was certified, it will not be deleted. For example, if the configuration, + provided to `certify_assets` includes the Brotli and Gzip encodings, but the configuration provided to `delete_assets` + only includes Brotli, then the Gzip file will not be deleted. Assuming the same base example used above to demonstrate certifying assets: @@ -555,95 +552,95 @@ use ic_asset_certification::{Asset, AssetConfig, AssetFallbackConfig, AssetRoute let mut asset_router = AssetRouter::default(); let assets = vec![ - Asset::new( - "index.html", - b"

Hello World!

".as_slice(), - ), - Asset::new( - "index.html.gz", - &[0, 1, 2, 3, 4, 5] - ), - Asset::new( - "index.html.br", - &[6, 7, 8, 9, 10, 11] - ), - Asset::new( - "app.js", - b"console.log('Hello World!');".as_slice(), - ), - Asset::new( - "app.js.gz", - &[12, 13, 14, 15, 16, 17], - ), - Asset::new( - "app.js.br", - &[18, 19, 20, 21, 22, 23], - ), - Asset::new( - "css/app-ba74b708.css", - b"html,body{min-height:100vh;}".as_slice(), - ), - Asset::new( - "css/app-ba74b708.css.gz", - &[24, 25, 26, 27, 28, 29], - ), - Asset::new( - "css/app-ba74b708.css.br", - &[30, 31, 32, 33, 34, 35], - ), +    Asset::new( +        "index.html", +        b"

Hello World!

".as_slice(), +    ), +    Asset::new( +        "index.html.gz", +        &[0, 1, 2, 3, 4, 5] +    ), +    Asset::new( +        "index.html.br", +        &[6, 7, 8, 9, 10, 11] +    ), +    Asset::new( +        "app.js", +        b"console.log('Hello World!');".as_slice(), +    ), +    Asset::new( +        "app.js.gz", +        &[12, 13, 14, 15, 16, 17], +    ), +    Asset::new( +        "app.js.br", +        &[18, 19, 20, 21, 22, 23], +    ), +    Asset::new( +        "css/app-ba74b708.css", +        b"html,body{min-height:100vh;}".as_slice(), +    ), +    Asset::new( +        "css/app-ba74b708.css.gz", +        &[24, 25, 26, 27, 28, 29], +    ), +    Asset::new( +        "css/app-ba74b708.css.br", +        &[30, 31, 32, 33, 34, 35], +    ), ]; let asset_configs = vec![ - AssetConfig::File { - path: "index.html".to_string(), - content_type: Some("text/html".to_string()), - headers: vec![( - "cache-control".to_string(), - "public, no-cache, no-store".to_string(), - )], - fallback_for: vec![AssetFallbackConfig { - scope: "/".to_string(), - status_code: Some(StatusCode::OK), - }], - aliased_by: vec!["/".to_string()], - encodings: vec![ - AssetEncoding::Brotli.default_config(), - AssetEncoding::Gzip.default_config(), - ], - }, - AssetConfig::Pattern { - pattern: "**/*.js".to_string(), - content_type: Some("text/javascript".to_string()), - headers: vec![( - "cache-control".to_string(), - "public, max-age=31536000, immutable".to_string(), - )], - encodings: vec![ - AssetEncoding::Brotli.default_config(), - AssetEncoding::Gzip.default_config(), - ], - }, - AssetConfig::Pattern { - pattern: "**/*.css".to_string(), - content_type: Some("text/css".to_string()), - headers: vec![( - "cache-control".to_string(), - "public, max-age=31536000, immutable".to_string(), - )], - encodings: vec![ - AssetEncoding::Brotli.default_config(), - AssetEncoding::Gzip.default_config(), - ], - }, - AssetConfig::Redirect { - from: "/old".to_string(), - to: "/new".to_string(), - kind: AssetRedirectKind::Permanent, - headers: vec![( - "content-type".to_string(), - "text/plain; charset=utf-8".to_string(), - )], - }, +    AssetConfig::File { +        path: "index.html".to_string(), +        content_type: Some("text/html".to_string()), +        headers: vec![( +            "cache-control".to_string(), +            "public, no-cache, no-store".to_string(), +        )], +        fallback_for: vec![AssetFallbackConfig { +            scope: "/".to_string(), +            status_code: Some(StatusCode::OK), +        }], +        aliased_by: vec!["/".to_string()], +        encodings: vec![ +            AssetEncoding::Brotli.default_config(), +            AssetEncoding::Gzip.default_config(), +        ], +    }, +    AssetConfig::Pattern { +        pattern: "**/*.js".to_string(), +        content_type: Some("text/javascript".to_string()), +        headers: vec![( +            "cache-control".to_string(), +            "public, max-age=31536000, immutable".to_string(), +        )], +        encodings: vec![ +            AssetEncoding::Brotli.default_config(), +            AssetEncoding::Gzip.default_config(), +        ], +    }, +    AssetConfig::Pattern { +        pattern: "**/*.css".to_string(), +        content_type: Some("text/css".to_string()), +        headers: vec![( +            "cache-control".to_string(), +            "public, max-age=31536000, immutable".to_string(), +        )], +        encodings: vec![ +            AssetEncoding::Brotli.default_config(), +            AssetEncoding::Gzip.default_config(), +        ], +    }, +    AssetConfig::Redirect { +        from: "/old".to_string(), +        to: "/new".to_string(), +        kind: AssetRedirectKind::Permanent, +        headers: vec![( +            "content-type".to_string(), +            "text/plain; charset=utf-8".to_string(), +        )], +    }, ]; asset_router.certify_assets(assets, asset_configs).unwrap(); @@ -653,94 +650,94 @@ To delete the `index.html` asset, along with the fallback configuration for the ```rust asset_router - .delete_assets( - vec![ - Asset::new( - "index.html", - b"

Hello World!

".as_slice(), - ), - Asset::new("index.html.gz", &[0, 1, 2, 3, 4, 5]), - Asset::new("index.html.br", &[6, 7, 8, 9, 10, 11]), - ], - vec![AssetConfig::File { - path: "index.html".to_string(), - content_type: Some("text/html".to_string()), - headers: vec![( - "cache-control".to_string(), - "public, no-cache, no-store".to_string(), - )], - fallback_for: vec![AssetFallbackConfig { - scope: "/".to_string(), - status_code: Some(StatusCode::OK), - }], - aliased_by: vec!["/".to_string()], - encodings: vec![ - AssetEncoding::Brotli.default_config(), - AssetEncoding::Gzip.default_config(), - ], - }], - ) - .unwrap(); +    .delete_assets( +        vec![ +            Asset::new( +                "index.html", +                b"

Hello World!

".as_slice(), +            ), +            Asset::new("index.html.gz", &[0, 1, 2, 3, 4, 5]), +            Asset::new("index.html.br", &[6, 7, 8, 9, 10, 11]), +        ], +        vec![AssetConfig::File { +            path: "index.html".to_string(), +            content_type: Some("text/html".to_string()), +            headers: vec![( +                "cache-control".to_string(), +                "public, no-cache, no-store".to_string(), +            )], +            fallback_for: vec![AssetFallbackConfig { +                scope: "/".to_string(), +                status_code: Some(StatusCode::OK), +            }], +            aliased_by: vec!["/".to_string()], +            encodings: vec![ +                AssetEncoding::Brotli.default_config(), +                AssetEncoding::Gzip.default_config(), +            ], +        }], +    ) +    .unwrap(); ``` To delete the `app.js` asset, along with the alternative encodings: ```rust asset_router - .delete_assets( - vec![ - Asset::new("app.js", b"console.log('Hello World!');".as_slice()), - Asset::new("app.js.gz", &[12, 13, 14, 15, 16, 17]), - Asset::new("app.js.br", &[18, 19, 20, 21, 22, 23]), - ], - vec![AssetConfig::Pattern { - pattern: "**/*.js".to_string(), - content_type: Some("text/javascript".to_string()), - headers: vec![( - "cache-control".to_string(), - "public, max-age=31536000, immutable".to_string(), - )], - encodings: vec![ - AssetEncoding::Brotli.default_config(), - AssetEncoding::Gzip.default_config(), - ], - }], - ) - .unwrap(); +    .delete_assets( +        vec![ +            Asset::new("app.js", b"console.log('Hello World!');".as_slice()), +            Asset::new("app.js.gz", &[12, 13, 14, 15, 16, 17]), +            Asset::new("app.js.br", &[18, 19, 20, 21, 22, 23]), +        ], +        vec![AssetConfig::Pattern { +            pattern: "**/*.js".to_string(), +            content_type: Some("text/javascript".to_string()), +            headers: vec![( +                "cache-control".to_string(), +                "public, max-age=31536000, immutable".to_string(), +            )], +            encodings: vec![ +                AssetEncoding::Brotli.default_config(), +                AssetEncoding::Gzip.default_config(), +            ], +        }], +    ) +    .unwrap(); ``` To delete the `css/app-ba74b708.css` asset, along with the alternative encodings: ```rust asset_router.delete_assets( - vec![ - Asset::new( - "css/app-ba74b708.css", - b"html,body{min-height:100vh;}".as_slice(), - ), - Asset::new( - "css/app-ba74b708.css.gz", - &[24, 25, 26, 27, 28, 29], - ), - Asset::new( - "css/app-ba74b708.css.br", - &[30, 31, 32, 33, 34, 35], - ), - ], - vec![ - AssetConfig::Pattern { - pattern: "**/*.css".to_string(), - content_type: Some("text/css".to_string()), - headers: vec![( - "cache-control".to_string(), - "public, max-age=31536000, immutable".to_string(), - )], - encodings: vec![ - AssetEncoding::Brotli.default_config(), - AssetEncoding::Gzip.default_config(), - ], - }, - ] +    vec![ +        Asset::new( +            "css/app-ba74b708.css", +            b"html,body{min-height:100vh;}".as_slice(), +        ), +        Asset::new( +            "css/app-ba74b708.css.gz", +            &[24, 25, 26, 27, 28, 29], +        ), +        Asset::new( +            "css/app-ba74b708.css.br", +            &[30, 31, 32, 33, 34, 35], +        ), +    ], +    vec![ +        AssetConfig::Pattern { +            pattern: "**/*.css".to_string(), +            content_type: Some("text/css".to_string()), +            headers: vec![( +                "cache-control".to_string(), +                "public, max-age=31536000, immutable".to_string(), +            )], +            encodings: vec![ +                AssetEncoding::Brotli.default_config(), +                AssetEncoding::Gzip.default_config(), +            ], +        }, +    ] ).unwrap(); ``` @@ -748,19 +745,19 @@ And finally, to delete the `/old` redirect: ```rust asset_router - .delete_assets( - vec![], - vec![AssetConfig::Redirect { - from: "/old".to_string(), - to: "/new".to_string(), - kind: AssetRedirectKind::Permanent, - headers: vec![( - "content-type".to_string(), - "text/plain; charset=utf-8".to_string(), - )], - }], - ) - .unwrap(); +    .delete_assets( +        vec![], +        vec![AssetConfig::Redirect { +            from: "/old".to_string(), +            to: "/new".to_string(), +            kind: AssetRedirectKind::Permanent, +            headers: vec![( +                "content-type".to_string(), +                "text/plain; charset=utf-8".to_string(), +            )], +        }], +    ) +    .unwrap(); ``` After deleting any assets, make sure to set the canister's @@ -774,18 +771,16 @@ set_certified_data(&asset_router.root_hash()); ### Deleting assets by path -To delete assets by path, use the -[delete_assets_by_path](AssetRouter::delete_assets_by_path) function. +To delete assets by path, use the `delete_assets_by_path` function. -Depending on the configuration provided to the [certify_assets](AssetRouter::certify_assets) function, +Depending on the configuration provided to the `certify_assets` function, multiple responses may be generated for the same asset. These assets may exist on different paths, -for example if the `alias` configuration is used. If `alias` paths are not passed to this function, +for example, if the `alias` configuration is used. If `alias` paths are not passed to this function, they will not be deleted. If multiple encodings exist for a path, all encodings will be deleted. -Fallbacks are also not deleted, to delete them, use the -[delete_fallback_assets_by_path](AssetRouter::delete_fallback_assets_by_path) function. +Fallbacks are also not deleted; to delete them, use the `delete_fallback_assets_by_path` function. Assuming the same base example used above to demonstrate certifying assets: @@ -796,92 +791,92 @@ use ic_asset_certification::{Asset, AssetConfig, AssetFallbackConfig, AssetRoute let mut asset_router = AssetRouter::default(); let assets = vec![ - Asset::new( - "index.html", - b"

Hello World!

".as_slice(), - ), - Asset::new( - "index.html.gz", - &[0, 1, 2, 3, 4, 5] - ), - Asset::new( - "index.html.br", - &[6, 7, 8, 9, 10, 11] - ), - Asset::new( - "app.js", - b"console.log('Hello World!');".as_slice(), - ), - Asset::new( - "app.js.gz", - &[12, 13, 14, 15, 16, 17], - ), - Asset::new( - "app.js.br", - &[18, 19, 20, 21, 22, 23], - ), - Asset::new( - "css/app-ba74b708.css", - b"html,body{min-height:100vh;}".as_slice(), - ), - Asset::new( - "css/app-ba74b708.css.gz", - &[24, 25, 26, 27, 28, 29], - ), - Asset::new( - "css/app-ba74b708.css.br", - &[30, 31, 32, 33, 34, 35], - ), +    Asset::new( +        "index.html", +        b"

Hello World!

".as_slice(), +    ), +    Asset::new( +        "index.html.gz", +        &[0, 1, 2, 3, 4, 5] +    ), +    Asset::new( +        "index.html.br", +        &[6, 7, 8, 9, 10, 11] +    ), +    Asset::new( +        "app.js", +        b"console.log('Hello World!');".as_slice(), +    ), +    Asset::new( +        "app.js.gz", +        &[12, 13, 14, 15, 16, 17], +    ), +    Asset::new( +        "app.js.br", +        &[18, 19, 20, 21, 22, 23], +    ), +    Asset::new( +        "css/app-ba74b708.css", +        b"html,body{min-height:100vh;}".as_slice(), +    ), +    Asset::new( +        "css/app-ba74b708.css.gz", +        &[24, 25, 26, 27, 28, 29], +    ), +    Asset::new( +        "css/app-ba74b708.css.br", +        &[30, 31, 32, 33, 34, 35], +    ), ]; let asset_configs = vec![ - AssetConfig::File { - path: "index.html".to_string(), - content_type: Some("text/html".to_string()), - headers: vec![( - "cache-control".to_string(), - "public, no-cache, no-store".to_string(), - )], - fallback_for: vec![AssetFallbackConfig { - scope: "/".to_string(), - status_code: Some(StatusCode::OK), - }], - aliased_by: vec!["/".to_string()], - encodings: vec![ - AssetEncoding::Brotli.default_config(), - AssetEncoding::Gzip.default_config(), - ], - }, - AssetConfig::Pattern { - pattern: "**/*.js".to_string(), - content_type: Some("text/javascript".to_string()), - headers: vec![( - "cache-control".to_string(), - "public, max-age=31536000, immutable".to_string(), - )], - encodings: vec![ - AssetEncoding::Brotli.default_config(), - AssetEncoding::Gzip.default_config(), - ], - }, - AssetConfig::Pattern { - pattern: "**/*.css".to_string(), - content_type: Some("text/css".to_string()), - headers: vec![( - "cache-control".to_string(), - "public, max-age=31536000, immutable".to_string(), - )], - encodings: vec![ - AssetEncoding::Brotli.default_config(), - AssetEncoding::Gzip.default_config(), - ], - }, - AssetConfig::Redirect { - from: "/old".to_string(), - to: "/new".to_string(), - kind: AssetRedirectKind::Permanent, - headers: vec![("content-type".to_string(), "text/plain".to_string())], - }, +    AssetConfig::File { +        path: "index.html".to_string(), +        content_type: Some("text/html".to_string()), +        headers: vec![( +            "cache-control".to_string(), +            "public, no-cache, no-store".to_string(), +        )], +        fallback_for: vec![AssetFallbackConfig { +            scope: "/".to_string(), +            status_code: Some(StatusCode::OK), +        }], +        aliased_by: vec!["/".to_string()], +        encodings: vec![ +            AssetEncoding::Brotli.default_config(), +            AssetEncoding::Gzip.default_config(), +        ], +    }, +    AssetConfig::Pattern { +        pattern: "**/*.js".to_string(), +        content_type: Some("text/javascript".to_string()), +        headers: vec![( +            "cache-control".to_string(), +            "public, max-age=31536000, immutable".to_string(), +        )], +        encodings: vec![ +            AssetEncoding::Brotli.default_config(), +            AssetEncoding::Gzip.default_config(), +        ], +    }, +    AssetConfig::Pattern { +        pattern: "**/*.css".to_string(), +        content_type: Some("text/css".to_string()), +        headers: vec![( +            "cache-control".to_string(), +            "public, max-age=31536000, immutable".to_string(), +        )], +        encodings: vec![ +            AssetEncoding::Brotli.default_config(), +            AssetEncoding::Gzip.default_config(), +        ], +    }, +    AssetConfig::Redirect { +        from: "/old".to_string(), +        to: "/new".to_string(), +        kind: AssetRedirectKind::Permanent, +        headers: vec![("content-type".to_string(), "text/plain".to_string())], +    }, ]; asset_router.certify_assets(assets, asset_configs).unwrap(); @@ -891,21 +886,21 @@ To delete the `index.html` asset, along with the fallback configuration for the ```rust asset_router - .delete_assets_by_path( - vec![ - "/index.html", // deletes the index.html asset, along with all encodings - "/" // deletes the `/` alias for index.html, along with all encodings - ], - ) - .unwrap(); +    .delete_assets_by_path( +        vec![ +            "/index.html", // deletes the index.html asset, along with all encodings +            "/" // deletes the `/` alias for index.html, along with all encodings +        ], +    ) +    .unwrap(); asset_router - .delete_fallback_assets_by_path( - vec![ - "/" // deletes the fallback configuration for the `/` scope, along with all encodings - ] - ) - .unwrap(); +    .delete_fallback_assets_by_path( +       vec![ +          "/" // deletes the fallback configuration for the `/` scope, along with all encodings +      ] +   ) +  .unwrap(); ``` To delete the `app.js`asset, along with the alternative encodings: @@ -960,13 +955,13 @@ The `get_assets()` function returns all standard assets, while the `get_fallback The `AssetMap` can be used to query assets by `path`, `encoding`, and `starting_range`. -For standard assets, the path refers to the asset's path, e.g. `/index.html`. +For standard assets, the path refers to the asset's path, e.g., `/index.html`. -For fallback assets, the path refers to the scope that the fallback is valid for, e.g. `/`. See the `fallback_for` config option for more information on fallback scopes. +For fallback assets, the path refers to the scope that the fallback is valid for, e.g., `/`. See the `fallback_for` config option for more information on fallback scopes. -For all types of assets, the encoding refers to the encoding of the asset, see `AssetEncoding`. +For all types of assets, the encoding refers to the encoding of the asset; see `AssetEncoding`. -Assets greater than 2mb are split into multiple ranges, the starting range allows retrieval of +Assets greater than 2 MiB are split into multiple ranges; the starting range allows retrieval of individual chunks of these large assets. The first range is `Some(0)`, the second range is `Some(ASSET_CHUNK_SIZE)`, the third range is `Some(ASSET_CHUNK_SIZE * 2)`, and so on. The entire asset can also be retrieved by passing `None` as the `starting_range`. Note that `ASSET_CHUNK_SIZE` is a constant defined in the `ic_asset_certification` crate. diff --git a/packages/ic-asset-certification/src/lib.rs b/packages/ic-asset-certification/src/lib.rs index 0e14d9d..da68a92 100644 --- a/packages/ic-asset-certification/src/lib.rs +++ b/packages/ic-asset-certification/src/lib.rs @@ -1,36 +1,33 @@ -//! -//! # Asset Certification -//! -//! ## Overview +//! # Asset certification //! //! Asset certification is a specialized form of -//! [HTTP Certification](https://internetcomputer.org/docs/current/developer-docs/http-compatible-canisters/custom-http-canisters) -//! purpose-built for certifying static assets in ICP canisters. +//! [HTTP certification](https://internetcomputer.org/docs/current/developer-docs/http-compatible-canisters/custom-http-canisters) +//! purpose-built for certifying static assets in [ICP](https://internetcomputer.org/) canisters. //! //! The `ic-asset-certification` crate provides the necessary functionality to //! certify and serve static assets from Rust canisters. //! //! This is implemented in the following steps: //! -//! 1. [Preparing assets](#preparing-assets) -//! 2. [Configuring asset certification](#configuring-asset-certification) -//! 3. [Inserting assets into the asset router](#inserting-assets-into-the-asset-router) -//! 4. [Serving assets](#serving-assets) -//! 5. [Deleting assets](#deleting-assets) -//! 6. [Querying assets](#querying-assets) +//! 1. [Preparing assets](#preparing-assets). +//! 2. [Configuring asset certification](#configuring-asset-certification). +//! 3. [Inserting assets into the asset router](#inserting-assets-into-the-asset-router). +//! 4. [Serving assets](#serving-assets). +//! 5. [Deleting assets](#deleting-assets). +//! 6. [Querying assets](#querying-assets). //! //! For canisters that need it, it's also possible to [delete assets](#deleting-assets). //! //! ## Preparing assets //! //! This library is unopinionated about where assets come from, so that is not -//! covered in detail here, but there are three main options: +//! covered in detail here. However, there are three main options: //! //! - Embedding assets in the canister at compile time: //! - [include_bytes!](https://doc.rust-lang.org/std/macro.include_bytes.html) //! - [include_dir!](https://docs.rs/include_dir/latest/include_dir/index.html) -//! - Uploading assets via canister endpoints at runtime. -//! - The [DFX asset canister](https://github.com/dfinity/sdk/blob/master/docs/design/asset-canister-interface.md) is a good example of this approach. +//! - Uploading assets via canister endpoints at runtime: +//! - The [`dfx` asset canister](https://github.com/dfinity/sdk/blob/master/docs/design/asset-canister-interface.md) is a good example of this approach. //! - Generating assets dynamically in code, at runtime. //! //! With the assets in memory, they can be converted into the [Asset] type: @@ -45,7 +42,7 @@ //! ``` //! //! It is recommended to use references when including assets directly into the -//! canister, to avoid duplicating the content. This is particularly important for +//! canister to avoid duplicating the content. This is particularly important for //! larger assets. //! //! ```rust @@ -74,7 +71,7 @@ //! ## Configuring asset certification //! //! [AssetConfig] defines the configuration for any files that will be certified. -//! The configuration can either be matched to an individual file by [path](AssetConfig::File), or to +//! The configuration can either be matched to an individual file by [path](AssetConfig::File) or to //! many files by a [glob](AssetConfig::Pattern). //! //! In both cases, the following options can be configured for each asset: @@ -136,7 +133,7 @@ //! scope. This can be used to configure 404 pages or single-page application //! entry points, for example. //! -//! When serving assets, if a requested path does not exactly match any assets then +//! When serving assets, if a requested path does not exactly match any assets, then //! a search is conducted for an asset configured with the fallback scope that most //! closely matches the requested asset's path. //! @@ -192,7 +189,7 @@ //! ``` //! //! It's also possible to configure multiple fallbacks for a single asset. The -//! following example configures an individual HTML file to be served by the on the +//! following example configures an individual HTML file to be served on the //! `/404.html` path, in addition to serving as the fallback for the `/js` and `/css` //! scopes. //! @@ -270,7 +267,7 @@ //! - `[ab]` matches `a` or `b` where `a` and `b` are characters. //! - `[!ab]` to match any character except for `a` and `b`. //! - Metacharacters such as `*` and `?` can be escaped with character -//! class notation. e.g., `[*]` matches `*`. +//! class notation, e.g., `[*]` matches `*`. //! //! For example, the following pattern will match all `.js` files in the `js` //! directory: @@ -295,7 +292,7 @@ //! ### Configuring redirects //! //! Redirects can be configured using the [AssetConfig::Redirect] variant. This -//! variant takes a `from` and `to` paths, and a redirect [kind](AssetRedirectKind). +//! variant takes `from` and `to` paths, and a redirect [kind](AssetRedirectKind). //! When a request is made to the `from` path, the client will be redirected to the //! `to` path. The [AssetConfig::Redirect] config is not matched against any [Asset]s. //! @@ -472,7 +469,7 @@ //! ## Serving assets //! //! Assets can be served by calling the `serve_asset` method on the `AssetRouter`. -//! This method will return a response, a witness and an expression path, which can be used +//! This method will return a response, a witness, and an expression path, which can be used //! alongside the canister's data certificate to add the required certificate header to the response. //! //! ```rust @@ -504,7 +501,7 @@ //! //! asset_router.certify_assets(vec![asset], vec![asset_config]).unwrap(); //! -//! // this should normally be retrieved using `ic_cdk::api::data_certificate()`. +//! // This should normally be retrieved using `ic_cdk::api::data_certificate()`. //! let data_certificate = vec![1, 2, 3]; //! let response = asset_router.serve_asset(&data_certificate, &http_request).unwrap(); //!``` @@ -518,20 +515,20 @@ //! //! ### Deleting assets by configuration //! -//! Deleting assets by configuration is similar to (certifying them)[#inserting-assets-into-the-asset-router]. +//! Deleting assets by configuration is similar to [certifying them](#inserting-assets-into-the-asset-router). //! //! Depending on the configuration provided to the [certify_assets](AssetRouter::certify_assets) function, //! multiple responses may be generated for the same asset. To ensure that all generated responses are deleted, //! the [delete_assets](AssetRouter::delete_assets) function accepts the same configuration. //! -//! If a configuration different to the one used to certify assets in the first place is provided, -//! one of two things can happen. +//! If a configuration different from the one used to certify assets in the first place is provided, +//! one of two things can happen: //! -//! If the configuration inclues a file that was not certified in the first place, it will be silently ignored. +//! 1. If the configuration includes a file that was not certified in the first place, it will be silently ignored. //! For example, if the configuration provided to `certify_assets` includes the Brotli and Gzip encodings, but the -//! configuration provided to `delete_assets` includes Brotli, Gzip and Deflate, the Brotli and Gzip encoded files will be deleted, while the Deflate file is ignored, since it doesn't exist. +//! configuration provided to `delete_assets` includes Brotli, Gzip, and Deflate. the Brotli and Gzip encoded files will be deleted, while the Deflate file is ignored, since it doesn't exist. //! -//! If the configuration excludes a file that was certified, it will not be deleted. For example, if the configuration, +//! 2. If the configuration excludes a file that was certified, it will not be deleted. For example, if the configuration, //! provided to `certify_assets` includes the Brotli and Gzip encodings, but the configuration provided to `delete_assets` //! only includes Brotli, then the Gzip file will not be deleted. //! @@ -787,12 +784,12 @@ //! //! Depending on the configuration provided to the [certify_assets](AssetRouter::certify_assets) function, //! multiple responses may be generated for the same asset. These assets may exist on different paths, -//! for example if the `alias` configuration is used. If `alias` paths are not passed to this function, +//! for example, if the `alias` configuration is used. If `alias` paths are not passed to this function, //! they will not be deleted. //! //! If multiple encodings exist for a path, all encodings will be deleted. //! -//! Fallbacks are also not deleted, to delete them, use the +//! Fallbacks are also not deleted; to delete them, use the //! [delete_fallback_assets_by_path](AssetRouter::delete_fallback_assets_by_path) function. //! //! Assuming the same base example used above to demonstrate certifying assets: @@ -989,15 +986,15 @@ //! [get_fallback_assets()](AssetRouter::get_fallback_assets) function returns all fallback assets. //! //! The [AssetMap] can be used to query assets by `path`, `encoding`, and `starting_range`. -//! For standard assets, the path refers to the asset's path, e.g. `/index.html`. +//! For standard assets, the path refers to the asset's path, e.g., `/index.html`. //! -//! For fallback assets, the path refers to the scope that the fallback is valid for, e.g. `/`. +//! For fallback assets, the path refers to the scope that the fallback is valid for, e.g., `/`. //! See the [fallback_for](crate::AssetConfig::File::fallback_for) config option for more information //! on fallback scopes. //! -//! For all types of assets, the encoding refers to the encoding of the asset, see [AssetEncoding]. +//! For all types of assets, the encoding refers to the encoding of the asset; see [AssetEncoding]. //! -//! Assets greater than 2mb are split into multiple ranges, the starting range allows retrieval of +//! Assets greater than 2 MiB are split into multiple ranges; the starting range allows retrieval of //! individual chunks of these large assets. The first range is `Some(0)`, the second range is //! `Some(ASSET_CHUNK_SIZE)`, the third range is `Some(ASSET_CHUNK_SIZE * 2)`, and so on. The entire asset can //! also be retrieved by passing `None` as the `starting_range`. diff --git a/packages/ic-http-certification/README.md b/packages/ic-http-certification/README.md index 2fbec3c..080722b 100644 --- a/packages/ic-http-certification/README.md +++ b/packages/ic-http-certification/README.md @@ -1,14 +1,12 @@ # HTTP certification -## Overview - HTTP certification is a sub-protocol of the [ICP](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. This allows HTTP gateways to verify that the responses they receive from canisters are authentic and have not been tampered with. The `ic-http-certification` crate provides the 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. [Creating an HTTP certification tree](#creating-an-http-certification-tree) +1. [Defining CEL expressions](#defining-cel-expressions). +2. [Creating certifications](#creating-certifications). +3. [Creating an HTTP certification tree](#creating-an-http-certification-tree). ## Defining CEL expressions @@ -57,20 +55,20 @@ Note that if the request is certified, the response must also be certified. It i When a request is certified: - The request body and method are always certified. -- The request headers and query parameters are optionally certified using the `with_request_headers` and `with_request_query_parameters` associated functions respectively. Both associated functions take a `str` slice as an argument. +- The request headers and query parameters are optionally certified using the `with_request_headers` and `with_request_query_parameters` associated functions, respectively. Both associated functions take a `str` slice as an argument. When a response is certified: - The response body and status code are always certified. - The response headers are optionally certified using the `with_response_certification` associated function. This function takes the `DefaultResponseCertification` enum as an argument. - - To specify header inclusions, use the `certified_response_headers` associated function of the `DefaultResponseCertification` enum. - - To certify all response headers (with some optional exclusions) use the `response_header_exclusions` associated function of the `DefaultResponseCertification` enum. Both functions take a `str` slice as an argument. +   - To specify header inclusions, use the `certified_response_headers` associated function of the `DefaultResponseCertification` enum. +   - To certify all response headers (with some optional exclusions), use the `response_header_exclusions` associated function of the `DefaultResponseCertification` enum. Both functions take a `str` slice as an argument. -Regardless of what is included in certification, the request path is always used to determine if that certification should be used. It's also possible to set a certification for a "scope" or "directory" of paths, see [Defining tree paths](#defining-tree-paths) for more information on this. +Regardless of what is included in certification, the request path is always used to determine if that certification should be used. It's also possible to set a certification for a "scope" or "directory" of paths; see [Defining tree paths](#defining-tree-paths) for more information on this. -When defining CEL expressions it's important to determine what should be certified and what can be safely excluded from certification. For example, if a response header is not certified, it will not be included in the certification and will not be verified by the HTTP gateway, meaning that the value of this header cannot be trusted by clients. As a general rule of thumb, starting with a fully certified request and response pair is a good idea, and then removing parts of the certification as needed. +When defining CEL expressions, it's important to determine what should be certified and what can be safely excluded from certification. For example, if a response header is not certified, it will not be included in the certification and will not be verified by the HTTP gateway, meaning that the value of this header cannot be trusted by clients. As a general rule of thumb, starting with a fully certified request and response pair is a good idea and then removing parts of the certification as needed. -It should be considered unsafe to exclude anything from request certification that can change the expected response. The request method for example can drastically affect what action is taken by the canister, and so excluding it from certification would allow a malicious replica to respond with the expected responses for `'GET'` request, even though a `'POST'` request was made. +It should be considered unsafe to exclude anything from request certification that can change the expected response. The request method, for example, can drastically affect what action is taken by the canister, and so excluding it from certification would allow a malicious replica to respond with the expected responses for a `'GET'` request, even though a `'POST'` request was made. For responses, it should be considered unsafe to exclude anything from response certification that will be used by clients in a meaningful way. For example, excluding the `Content-Type` header from certification would allow a malicious replica to respond with a different content type than expected, which could cause clients to misinterpret the response. @@ -84,18 +82,18 @@ For example: use ic_http_certification::{DefaultCelBuilder, DefaultResponseCertification}; let cel_expr = DefaultCelBuilder::full_certification() - .with_request_headers(vec!["Accept", "Accept-Encoding", "If-None-Match"]) - .with_request_query_parameters(vec!["foo", "bar", "baz"]) - .with_response_certification(DefaultResponseCertification::certified_response_headers(vec![ - "Cache-Control", - "ETag", - ])) - .build(); +    .with_request_headers(vec!["Accept", "Accept-Encoding", "If-None-Match"]) +    .with_request_query_parameters(vec!["foo", "bar", "baz"]) +    .with_response_certification(DefaultResponseCertification::certified_response_headers(vec![ +        "Cache-Control", +        "ETag", +    ])) +    .build(); ``` #### Partially certified request -Any number of request headers or request query parameters can be certified via `with_request_headers` and `with_request_query_parameters` respectively. Both methods will accept empty arrays, which is the same as not calling them at all. Likewise for `with_request_query_parameters`, if it is called with an empty array, or not called at all, then no request query parameters will be certified. If both are called with an empty array, or neither are called, then only the request body and method will be certified, in addition to the response. As a reminder here, the response is always at least partially certified if the request is certified. +Any number of request headers or request query parameters can be certified via `with_request_headers` and `with_request_query_parameters` respectively. Both methods will accept empty arrays, which is the same as not calling them at all. Likewise for `with_request_query_parameters`, if it is called with an empty array or not called at all, then no request query parameters will be certified. If both are called with an empty array, or neither is called, then only the request body and method will be certified, in addition to the response. As a reminder here, the response is always at least partially certified if the request is certified. For example, to certify only the request body and method, in addition to the response: @@ -103,11 +101,11 @@ For example, to certify only the request body and method, in addition to the res use ic_http_certification::{DefaultCelBuilder, DefaultResponseCertification}; let cel_expr = DefaultCelBuilder::full_certification() - .with_response_certification(DefaultResponseCertification::certified_response_headers(vec![ - "Cache-Control", - "ETag", - ])) - .build(); +    .with_response_certification(DefaultResponseCertification::certified_response_headers(vec![ +        "Cache-Control", +        "ETag", +    ])) +    .build(); ``` Alternatively, this can be done more explicitly: @@ -116,13 +114,13 @@ Alternatively, this can be done more explicitly: use ic_http_certification::{DefaultCelBuilder, DefaultResponseCertification}; let cel_expr = DefaultCelBuilder::full_certification() - .with_request_headers(vec![]) - .with_request_query_parameters(vec![]) - .with_response_certification(DefaultResponseCertification::certified_response_headers(vec![ - "Cache-Control", - "ETag", - ])) - .build(); +    .with_request_headers(vec![]) +    .with_request_query_parameters(vec![]) +    .with_response_certification(DefaultResponseCertification::certified_response_headers(vec![ +        "Cache-Control", +        "ETag", +    ])) +    .build(); ``` #### Skipping request certification @@ -135,12 +133,12 @@ For example: use ic_http_certification::{DefaultCelBuilder, DefaultResponseCertification}; let cel_expr = DefaultCelBuilder::response_only_certification() - .with_response_certification(DefaultResponseCertification::response_header_exclusions(vec![ - "Date", - "Cookie", - "Set-Cookie", - ])) - .build(); +    .with_response_certification(DefaultResponseCertification::response_header_exclusions(vec![ +        "Date", +        "Cookie", +        "Set-Cookie", +    ])) +    .build(); ``` #### Partially certified response @@ -161,19 +159,19 @@ This can also be done more explicitly: use ic_http_certification::{DefaultCelBuilder, DefaultResponseCertification}; let cel_expr = DefaultCelBuilder::response_only_certification() - .with_response_certification(DefaultResponseCertification::certified_response_headers(vec![])) - .build(); +    .with_response_certification(DefaultResponseCertification::certified_response_headers(vec![])) +    .build(); ``` -The same applies when both when using `DefaultCelBuilder::response_only_certification` and `DefaultCelBuilder::full_certification`: +The same applies 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(vec!["Accept", "Accept-Encoding", "If-None-Match"]) - .with_request_query_parameters(vec!["foo", "bar", "baz"]) - .build(); +    .with_request_headers(vec!["Accept", "Accept-Encoding", "If-None-Match"]) +    .with_request_query_parameters(vec!["foo", "bar", "baz"]) +    .build(); ``` To skip response certification completely, certification overall must be skipped completely. It wouldn't be useful to certify a request without certifying a response. @@ -188,7 +186,7 @@ use ic_http_certification::DefaultCelBuilder; let cel_expr = DefaultCelBuilder::skip_certification(); ``` -Skipping certification may seem counter-intuitive at first, but it is not always possible to certify a request and response pair. For example, a canister method that will return different data for every user cannot be easily certified. +Skipping certification may seem counterintuitive at first, but it is not always possible to certify a request and response pair. For example, a canister method that will return different data for every user cannot be easily certified. Typically, these requests have been routed through `raw` ICP 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. @@ -204,7 +202,7 @@ Once a CEL expression has been defined, it can be used in conjunction with an `H ### 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. +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: @@ -212,34 +210,34 @@ For example: use ic_http_certification::{HttpCertification, HttpRequest, HttpResponse, DefaultCelBuilder, DefaultResponseCertification}; let cel_expr = DefaultCelBuilder::full_certification() - .with_request_headers(vec!["Accept", "Accept-Encoding", "If-None-Match"]) - .with_request_query_parameters(vec!["foo", "bar", "baz"]) - .with_response_certification(DefaultResponseCertification::certified_response_headers(vec![ - "Cache-Control", - "ETag", - ])) - .build(); +    .with_request_headers(vec!["Accept", "Accept-Encoding", "If-None-Match"]) +    .with_request_query_parameters(vec!["foo", "bar", "baz"]) +    .with_response_certification(DefaultResponseCertification::certified_response_headers(vec![ +        "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![], +    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()), - ("IC-CertificateExpression".to_string(), cel_expr.to_string()), - ], - body: vec![1, 2, 3, 4, 5, 6], - upgrade: None, +    status_code: 200, +    headers: vec![ +        ("Cache-Control".to_string(), "no-cache".to_string()), +        ("ETag".to_string(), "123456789".to_string()), +        ("IC-CertificateExpression".to_string(), cel_expr.to_string()), +    ], +    body: vec![1, 2, 3, 4, 5, 6], +    upgrade: None, }; let certification = HttpCertification::full(&cel_expr, &request, &response, None); @@ -247,7 +245,7 @@ let certification = HttpCertification::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. +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: @@ -255,21 +253,21 @@ For example: use ic_http_certification::{HttpCertification, HttpResponse, DefaultCelBuilder, DefaultResponseCertification}; let cel_expr = DefaultCelBuilder::response_only_certification() - .with_response_certification(DefaultResponseCertification::certified_response_headers(vec![ - "Cache-Control", - "ETag", - ])) - .build(); +    .with_response_certification(DefaultResponseCertification::certified_response_headers(vec![ +        "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()), - ("IC-CertificateExpression".to_string(), cel_expr.to_string()), - ], - body: vec![1, 2, 3, 4, 5, 6], - upgrade: None, +    status_code: 200, +    headers: vec![ +        ("Cache-Control".to_string(), "no-cache".to_string()), +        ("ETag".to_string(), "123456789".to_string()), +        ("IC-CertificateExpression".to_string(), cel_expr.to_string()), +    ], +    body: vec![1, 2, 3, 4, 5, 6], +    upgrade: None, }; let certification = HttpCertification::response_only(&cel_expr, &response, None).unwrap(); @@ -293,9 +291,9 @@ let certification = HttpCertification::skip(); Paths for the tree can be defined using the `HttpCertificationPath` struct and come in two types: `wildcard()` and `exact()`. Both types of paths may end with or without a trailing slash, but note that a path ending in a trailing slash is a distinct path from one that does not end with a trailing slash, and they will be treated as such by the tree. -Wildcard paths can be used to match a sub-path of a request URL. This can be useful for 404 responses, fallbacks or rewrites. They are defined using the `wildcard()` associated function. +Wildcard paths can be used to match a sub-path of a request URL. This can be useful for 404 responses, fallbacks, or rewrites. They are defined using the `wildcard()` associated function. -In this example, the certification entered into the tree with this path will be valid for any request URL that begins with `/js`, unless there is a more specific path in the tree (ex. `/js/example.js`). +In this example, the certification entered into the tree with this path will be valid for any request URL that begins with `/js`, unless there is a more specific path in the tree (e.g., `/js/example.js`). ```rust use ic_http_certification::HttpCertificationPath; @@ -303,7 +301,7 @@ use ic_http_certification::HttpCertificationPath; let path = HttpCertificationPath::wildcard("/js"); ``` -Exact paths are used to match an entire request URL. An exact path ending with a trailing slash refers to a file system directory, where as one without a trailing slash refers to an individual file. Both are separate paths within the certification tree and will be treated completely independently. +Exact paths are used to match an entire request URL. An exact path ending with a trailing slash refers to a file system directory, whereas one without a trailing slash refers to an individual file. Both are separate paths within the certification tree and will be treated completely independently. In this example, the certification entered into the tree with this path will only be valid for a request URL that is exactly `/js/example.js`. @@ -315,7 +313,7 @@ let path = HttpCertificationPath::exact("/js/example.js"); ### Using the HTTP certification tree -The `HttpCertificationTree` can be easily initialized with the `Default` trait and entries can be added to, removed from, or have witnesses generated by the tree using the `HttpCertificationTreeEntry` struct. The `HttpCertificationTreeEntry` requires a `HttpCertification` and an `HttpCertificationPath`. +The `HttpCertificationTree` can be easily initialized with the `Default` trait, and entries can be added to, removed from, or have witnesses generated by the tree using the `HttpCertificationTreeEntry` struct. The `HttpCertificationTreeEntry` requires a `HttpCertification` and an `HttpCertificationPath`. For example: @@ -323,34 +321,34 @@ For example: use ic_http_certification::{HttpCertification, HttpRequest, HttpResponse, DefaultCelBuilder, DefaultResponseCertification, HttpCertificationTree, HttpCertificationTreeEntry, HttpCertificationPath}; let cel_expr = DefaultCelBuilder::full_certification() - .with_request_headers(vec!["Accept", "Accept-Encoding", "If-None-Match"]) - .with_request_query_parameters(vec!["foo", "bar", "baz"]) - .with_response_certification(DefaultResponseCertification::certified_response_headers(vec![ - "Cache-Control", - "ETag", - ])) - .build(); +    .with_request_headers(vec!["Accept", "Accept-Encoding", "If-None-Match"]) +    .with_request_query_parameters(vec!["foo", "bar", "baz"]) +    .with_response_certification(DefaultResponseCertification::certified_response_headers(vec![ +        "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![], +    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()), - ("IC-CertificateExpression".to_string(), cel_expr.to_string()), - ], - body: vec![1, 2, 3, 4, 5, 6], - upgrade: None, +    status_code: 200, +    headers: vec![ +        ("Cache-Control".to_string(), "no-cache".to_string()), +        ("ETag".to_string(), "123456789".to_string()), +        ("IC-CertificateExpression".to_string(), cel_expr.to_string()), +    ], +    body: vec![1, 2, 3, 4, 5, 6], +    upgrade: None, }; let request_url = "/example.json"; @@ -373,11 +371,11 @@ http_certification_tree.delete(&entry); ### Handling upgrades -CEL expressions, certifications, the certification tree, and the corresponding requests and responses are not persisted across upgrades, by default. This means that if a canister is upgraded, all of this information will be lost. To handle upgrades effectively, all initialization logic run in the canister's `init` hook should also be run in the `post_upgrade` hook. This will ensure that the certification tree is correctly re-initialized after an upgrade. Most data structures, aside from the certification tree, can be persisted using stable memory, and the certification tree can be re-initialized using this persisted data. Care should be taken to not exceed the canister's instruction limit when re-initializing the certification tree, which can easily occur if the number of responses being certified grows very large. This case could potentially be addressed in the future by developing a stable memory compatible certification tree. +CEL expressions, certifications, the certification tree, and the corresponding requests and responses are not persisted across upgrades, by default. This means that if a canister is upgraded, all of this information will be lost. To handle upgrades effectively, all initialization logic run in the canister's `init` hook should also be run in the `post_upgrade` hook. This will ensure that the certification tree is correctly re-initialized after an upgrade. Most data structures, aside from the certification tree, can be persisted using stable memory, and the certification tree can be re-initialized using this persisted data. Care should be taken to not exceed the canister's instruction limit when re-initializing the certification tree, which can easily occur if the number of responses being certified grows very large. This case could potentially be addressed in the future by developing a stable memory-compatible certification tree. ### Changing data -In addition to initializing certifications in the `init` and `post_upgrade` hooks, if a response is changed during the canister's lifetime in response to an `update` call, the certification tree should be updated to reflect this change. This can be done by deleting the old certification from the tree and inserting the new certification. This should be done in the same `update` call as the response is changed to ensure that the certification tree is always up-to-date, otherwise, `query` calls returning that response will fail verification. +In addition to initializing certifications in the `init` and `post_upgrade` hooks, if a response is changed during the canister's lifetime in response to an `update` call, the certification tree should be updated to reflect this change. This can be done by deleting the old certification from the tree and inserting the new certification. This should be done in the same `update` call as the response is changed to ensure that the certification tree is always up-to-date; otherwise, `query` calls returning that response will fail verification. ## Directly creating a CEL expression @@ -406,42 +404,42 @@ use std::borrow::Cow; use ic_http_certification::cel::{CelExpression, DefaultCelExpression, DefaultFullCelExpression, DefaultRequestCertification, DefaultResponseCertification}; let cel_expr = CelExpression::Default(DefaultCelExpression::Full( - DefaultFullCelExpression { - request: DefaultRequestCertification::new( - vec!["Accept", "Accept-Encoding", "If-None-Match"], - vec!["foo", "bar", "baz"], - ), - response: DefaultResponseCertification::certified_response_headers(vec![ - "ETag", - "Cache-Control", - ]), - })); +  DefaultFullCelExpression { +    request: DefaultRequestCertification::new( +      vec!["Accept", "Accept-Encoding", "If-None-Match"], +      vec!["foo", "bar", "baz"], +    ), +    response: DefaultResponseCertification::certified_response_headers(vec![ +      "ETag", +      "Cache-Control", +    ]), +  })); ``` This will produce the following CEL expression: ```protobuf default_certification ( - ValidationArgs { - request_certification: RequestCertification { - certified_request_headers: ["Accept", "Accept-Encoding", "If-None-Match"], - certified_query_parameters: ["foo", "bar", "baz"] - }, - response_certification: ResponseCertification { - certified_response_headers: ResponseHeaderList { - headers: [ - "ETag", - "Cache-Control" - ] - } - } - } +  ValidationArgs { +    request_certification: RequestCertification { +      certified_request_headers: ["Accept", "Accept-Encoding", "If-None-Match"], +      certified_query_parameters: ["foo", "bar", "baz"] +    }, +    response_certification: ResponseCertification { +      certified_response_headers: ResponseHeaderList { +        headers: [ +          "ETag", +          "Cache-Control" +        ] +      } +    } +  } ) ``` ### Partially certified request -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. +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: @@ -450,36 +448,36 @@ use std::borrow::Cow; use ic_http_certification::cel::{CelExpression, DefaultCelExpression, DefaultFullCelExpression, DefaultRequestCertification, DefaultResponseCertification}; let cel_expr = CelExpression::Default(DefaultCelExpression::Full( - DefaultFullCelExpression { - request: DefaultRequestCertification::new( - vec![], - vec![], - ), - response: DefaultResponseCertification::certified_response_headers(vec![ - "ETag", - "Cache-Control", - ]), - })); +  DefaultFullCelExpression { +    request: DefaultRequestCertification::new( +      vec![], +      vec![], +    ), +    response: DefaultResponseCertification::certified_response_headers(vec![ +      "ETag", +      "Cache-Control", +    ]), +  })); ``` This will produce the following CEL expression: ```protobuf default_certification ( - ValidationArgs { - request_certification: RequestCertification { - certified_request_headers: [], - certified_query_parameters: [] - }, - response_certification: ResponseCertification { - certified_response_headers: ResponseHeaderList { - headers: [ - "ETag", - "Cache-Control" - ] - } - } - } +  ValidationArgs { +    request_certification: RequestCertification { +      certified_request_headers: [], +      certified_query_parameters: [] +    }, +    response_certification: ResponseCertification { +      certified_response_headers: ResponseHeaderList { +        headers: [ +          "ETag", +          "Cache-Control" +        ] +      } +    } +  } ) ``` @@ -494,35 +492,35 @@ 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(vec![ - "ETag", - "Cache-Control", - ]), - })); +  DefaultResponseOnlyCelExpression { +    response: DefaultResponseCertification::certified_response_headers(vec![ +      "ETag", +      "Cache-Control", +    ]), +  })); ``` This will produce the following CEL expression: ```protobuf default_certification ( - ValidationArgs { - no_request_certification: Empty {}, - response_certification: ResponseCertification { - certified_response_headers: ResponseHeaderList { - headers: [ - "ETag", - "Cache-Control" - ] - } - } - } +  ValidationArgs { +    no_request_certification: Empty {}, +    response_certification: ResponseCertification { +      certified_response_headers: ResponseHeaderList { +        headers: [ +          "ETag", +          "Cache-Control" +        ] +      } +    } +  } ) ``` ### Partially certified response -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. +Similarly 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: @@ -531,11 +529,11 @@ use std::borrow::Cow; use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultRequestCertification, DefaultResponseCertification}; let cel_expr = CelExpression::DefaultCertification(Some(DefaultCertification { - request: DefaultRequestCertification::new( - vec!["Accept", "Accept-Encoding", "If-None-Match"], - vec!["foo", "bar", "baz"], - ), - response_certification: DefaultResponseCertification::certified_response_headers(vec![]), +  request: DefaultRequestCertification::new( +    vec!["Accept", "Accept-Encoding", "If-None-Match"], +    vec!["foo", "bar", "baz"], +  ), +  response_certification: DefaultResponseCertification::certified_response_headers(vec![]), })); ``` @@ -543,17 +541,17 @@ This will produce the following CEL expression: ```protobuf default_certification ( - ValidationArgs { - request_certification: RequestCertification { - certified_request_headers: ["Accept", "Accept-Encoding", "If-None-Match"], - certified_query_parameters: ["foo", "bar", "baz"] - }, - response_certification: ResponseCertification { - certified_response_headers: ResponseHeaderList { - headers: [] - } - } - } +  ValidationArgs { +    request_certification: RequestCertification { +      certified_request_headers: ["Accept", "Accept-Encoding", "If-None-Match"], +      certified_query_parameters: ["foo", "bar", "baz"] +    }, +    response_certification: ResponseCertification { +      certified_response_headers: ResponseHeaderList { +        headers: [] +      } +    } +  } ) ``` @@ -564,30 +562,30 @@ use std::borrow::Cow; use ic_http_certification::cel::{CelExpression, DefaultCelExpression, DefaultFullCelExpression, DefaultRequestCertification, DefaultResponseCertification}; let cel_expr = CelExpression::Default(DefaultCelExpression::Full( - DefaultFullCelExpression { - request: DefaultRequestCertification::new( - vec!["Accept", "Accept-Encoding", "If-None-Match"], - vec!["foo", "bar", "baz"], - ), - response: DefaultResponseCertification::response_header_exclusions(vec![]), - })); +  DefaultFullCelExpression { +    request: DefaultRequestCertification::new( +      vec!["Accept", "Accept-Encoding", "If-None-Match"], +      vec!["foo", "bar", "baz"], +    ), +    response: DefaultResponseCertification::response_header_exclusions(vec![]), +  })); ``` This will produce the following CEL expression: ```protobuf default_certification ( - ValidationArgs { - request_certification: RequestCertification { - certified_request_headers: ["Accept", "Accept-Encoding", "If-None-Match"], - certified_query_parameters: ["foo", "bar", "baz"] - }, - response_certification: ResponseCertification { - response_header_exclusions: ResponseHeaderList { - headers: [] - } - } - } +  ValidationArgs { +    request_certification: RequestCertification { +      certified_request_headers: ["Accept", "Accept-Encoding", "If-None-Match"], +      certified_query_parameters: ["foo", "bar", "baz"] +    }, +    response_certification: ResponseCertification { +      response_header_exclusions: ResponseHeaderList { +        headers: [] +      } +    } +  } ) ``` @@ -607,8 +605,8 @@ This will produce the following CEL expression: ```protobuf default_certification ( - ValidationArgs { - no_certification: Empty {} - } +  ValidationArgs { +    no_certification: Empty {} +  } ) ``` diff --git a/packages/ic-http-certification/src/lib.rs b/packages/ic-http-certification/src/lib.rs index bd9c37b..0943541 100644 --- a/packages/ic-http-certification/src/lib.rs +++ b/packages/ic-http-certification/src/lib.rs @@ -1,15 +1,13 @@ /*! # HTTP Certification -## Overview - HTTP certification is a sub-protocol of the [ICP](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. This allows HTTP gateways to verify that the responses they receive from canisters are authentic and have not been tampered with. The `ic-http-certification` crate provides the 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. [Creating an HTTP certification tree](#creating-an-http-certification-tree) +1. [Defining CEL expressions](#defining-cel-expressions). +2. [Creating certifications](#creating-certifications). +3. [Creating an HTTP certification tree](#creating-an-http-certification-tree). ## Defining CEL expressions @@ -56,20 +54,20 @@ Note that if the request is certified, the response must also be certified. It i When a request is certified: - The request body and method are always certified. -- The request headers and query parameters are optionally certified using the `with_request_headers` and `with_request_query_parameters` associated functions respectively. Both associated functions take a `str` slice as an argument. +- The request headers and query parameters are optionally certified using the `with_request_headers` and `with_request_query_parameters` associated functions, respectively. Both associated functions take a `str` slice as an argument. When a response is certified: - The response body and status code are always certified. - The response headers are optionally certified using the `with_response_certification` associated function. This function takes the `DefaultResponseCertification` enum as an argument. - To specify header inclusions, use the `certified_response_headers` associated function of the `DefaultResponseCertification` enum. - - To certify all response headers (with some optional exclusions) use the `response_header_exclusions` associated function of the `DefaultResponseCertification` enum. Both functions take a `str` slice as an argument. + - To certify all response headers (with some optional exclusions), use the `response_header_exclusions` associated function of the `DefaultResponseCertification` enum. Both functions take a `str` slice as an argument. -Regardless of what is included in certification, the request path is always used to determine if that certification should be used. It's also possible to set a certification for a "scope" or "directory" of paths, see [Defining tree paths](#defining-tree-paths) for more information on this. +Regardless of what is included in certification, the request path is always used to determine if that certification should be used. It's also possible to set a certification for a "scope" or "directory" of paths; see [Defining tree paths](#defining-tree-paths) for more information on this. -When defining CEL expressions it's important to determine what should be certified and what can be safely excluded from certification. For example, if a response header is not certified, it will not be included in the certification and will not be verified by the HTTP gateway, meaning that the value of this header cannot be trusted by clients. As a general rule of thumb, starting with a fully certified request and response pair is a good idea, and then removing parts of the certification as needed. +When defining CEL expressions, it's important to determine what should be certified and what can be safely excluded from certification. For example, if a response header is not certified, it will not be included in the certification and will not be verified by the HTTP gateway, meaning that the value of this header cannot be trusted by clients. As a general rule of thumb, starting with a fully certified request and response pair is a good idea and then removing parts of the certification as needed. -It should be considered unsafe to exclude anything from request certification that can change the expected response. The request method for example can drastically affect what action is taken by the canister, and so excluding it from certification would allow a malicious replica to respond with the expected responses for `'GET'` request, even though a `'POST'` request was made. +It should be considered unsafe to exclude anything from request certification that can change the expected response. The request method, for example, can drastically affect what action is taken by the canister, and so excluding it from certification would allow a malicious replica to respond with the expected responses for a `'GET'` request, even though a `'POST'` request was made. For responses, it should be considered unsafe to exclude anything from response certification that will be used by clients in a meaningful way. For example, excluding the `Content-Type` header from certification would allow a malicious replica to respond with a different content type than expected, which could cause clients to misinterpret the response. @@ -94,7 +92,7 @@ let cel_expr = DefaultCelBuilder::full_certification() #### Partially certified request -Any number of request headers or request query parameters can be certified via `with_request_headers` and `with_request_query_parameters` respectively. Both methods will accept empty arrays, which is the same as not calling them at all. Likewise for `with_request_query_parameters`, if it is called with an empty array, or not called at all, then no request query parameters will be certified. If both are called with an empty array, or neither are called, then only the request body and method will be certified, in addition to the response. As a reminder here, the response is always at least partially certified if the request is certified. +Any number of request headers or request query parameters can be certified via `with_request_headers` and `with_request_query_parameters` respectively. Both methods will accept empty arrays, which is the same as not calling them at all. Likewise for `with_request_query_parameters`, if it is called with an empty array or not called at all, then no request query parameters will be certified. If both are called with an empty array, or neither is called, then only the request body and method will be certified, in addition to the response. As a reminder here, the response is always at least partially certified if the request is certified. For example, to certify only the request body and method, in addition to the response: @@ -165,7 +163,7 @@ let cel_expr = DefaultCelBuilder::response_only_certification() .build(); ``` -The same applies both when using [DefaultCelBuilder::response_only_certification](DefaultCelBuilder::response_only_certification()) and [DefaultCelBuilder::full_certification](DefaultCelBuilder::full_certification()). +The same applies when using [DefaultCelBuilder::response_only_certification](DefaultCelBuilder::response_only_certification()) and [DefaultCelBuilder::full_certification](DefaultCelBuilder::full_certification()). ```rust use ic_http_certification::DefaultCelBuilder; @@ -188,7 +186,7 @@ use ic_http_certification::DefaultCelBuilder; let cel_expr = DefaultCelBuilder::skip_certification(); ``` -Skipping certification may seem counter-intuitive at first, but it is not always possible to certify a request and response pair. For example, a canister method that will return different data for every user cannot be easily certified. +Skipping certification may seem counterintuitive at first, but it is not always possible to certify a request and response pair. For example, a canister method that will return different data for every user cannot be easily certified. Typically, these requests have been routed through `raw` ICP 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. @@ -204,7 +202,7 @@ Once a CEL expression has been defined, it can be used in conjunction with an [H ### 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. +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: @@ -243,7 +241,7 @@ let certification = HttpCertification::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. +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: @@ -288,9 +286,9 @@ let certification = HttpCertification::skip(); Paths for the tree can be defined using the [HttpCertificationPath] struct and come in two types - [Wildcard](HttpCertificationPath::wildcard()) and [Exact](HttpCertificationPath::exact()). Both types of paths may end with or without a trailing slash but note that a path ending in a trailing slash is a distinct path from one that does not end with a trailing slash and they will be treated as such by the tree. -Wildcard paths can be used to match a sub-path of a request URL. This can be useful for 404 responses, fallbacks or rewrites. They are defined using the [Wildcard](HttpCertificationPath::wildcard()) associated function. +Wildcard paths can be used to match a sub-path of a request URL. This can be useful for 404 responses, fallbacks, or rewrites. They are defined using the [Wildcard](HttpCertificationPath::wildcard()) associated function. -In this example, the certification entered into the tree with this path will be valid for any request URL that begins with `/js`, unless there is a more specific path in the tree (ex. `/js/example.js` or `/js/example`). +In this example, the certification entered into the tree with this path will be valid for any request URL that begins with `/js`, unless there is a more specific path in the tree (e.g., `/js/example.js` or `/js/example`). ```rust use ic_http_certification::HttpCertificationPath; @@ -298,7 +296,7 @@ use ic_http_certification::HttpCertificationPath; let path = HttpCertificationPath::wildcard("/js"); ``` -Exact paths are used to match an entire request URL. An exact path ending with a trailing slash refers to a file system directory, where as one without a trailing slash refers to an individual file. Both are separate paths within the certification tree and will be treated completely independently. +Exact paths are used to match an entire request URL. An exact path ending with a trailing slash refers to a file system directory, whereas one without a trailing slash refers to an individual file. Both are separate paths within the certification tree and will be treated completely independently. In this example, the certification entered into the tree with this path will only be valid for a request URL that is exactly `/js/example.js`. @@ -310,7 +308,7 @@ let path = HttpCertificationPath::exact("/js/example.js"); ### Using the HTTP certification tree -The [HttpCertificationTree] can be easily initialized with the [Default] trait and entries can be added to, removed from, or have witnesses generated by the tree using the [HttpCertificationTreeEntry] struct. The [HttpCertificationTreeEntry] requires an [HttpCertification] and an [HttpCertificationPath]. +The [HttpCertificationTree] can be easily initialized with the [Default] trait, and entries can be added to, removed from, or have witnesses generated by the tree using the [HttpCertificationTreeEntry] struct. The [HttpCertificationTreeEntry] requires an [HttpCertification] and an [HttpCertificationPath]. For example: @@ -364,11 +362,11 @@ http_certification_tree.delete(&entry); ### Handling upgrades -CEL expressions, certifications, the certification tree, and the corresponding requests and responses are not persisted across upgrades, by default. This means that if a canister is upgraded, all of this information will be lost. To handle upgrades effectively, all initialization logic run in the canister's `init` hook should also be run in the `post_upgrade` hook. This will ensure that the certification tree is correctly re-initialized after an upgrade. Most data structures, aside from the certification tree, can be persisted using stable memory, and the certification tree can be re-initialized using this persisted data. Care should be taken to not exceed the canister's instruction limit when re-initializing the certification tree, which can easily occur if the number of responses being certified grows very large. This case could potentially be addressed in the future by developing a stable memory compatible certification tree. +CEL expressions, certifications, the certification tree, and the corresponding requests and responses are not persisted across upgrades, by default. This means that if a canister is upgraded, all of this information will be lost. To handle upgrades effectively, all initialization logic run in the canister's `init` hook should also be run in the `post_upgrade` hook. This will ensure that the certification tree is correctly re-initialized after an upgrade. Most data structures, aside from the certification tree, can be persisted using stable memory, and the certification tree can be re-initialized using this persisted data. Care should be taken to not exceed the canister's instruction limit when re-initializing the certification tree, which can easily occur if the number of responses being certified grows very large. This case could potentially be addressed in the future by developing a stable memory-compatible certification tree. ### Changing data -In addition to initializing certifications in the `init` and `post_upgrade` hooks, if a response is changed during the canister's lifetime in response to an `update` call, the certification tree should be updated to reflect this change. This can be done by deleting the old certification from the tree and inserting the new certification. This should be done in the same `update` call as the response is changed to ensure that the certification tree is always up-to-date, otherwise, `query` calls returning that response will fail verification. +In addition to initializing certifications in the `init` and `post_upgrade` hooks, if a response is changed during the canister's lifetime in response to an `update` call, the certification tree should be updated to reflect this change. This can be done by deleting the old certification from the tree and inserting the new certification. This should be done in the same `update` call as the response is changed to ensure that the certification tree is always up-to-date; otherwise, `query` calls returning that response will fail verification. ## Directly creating a CEL expression @@ -430,7 +428,7 @@ default_certification ( ### 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) 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. +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: @@ -511,7 +509,7 @@ default_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) 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. +Similarly 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] enum, and it can also be an empty array. If the array is empty, no response headers will be certified. For example: