diff --git a/.github/workflows/create-release-pr.yml b/.github/workflows/create-release-pr.yml index 4ab1707a..2a5ce5ef 100644 --- a/.github/workflows/create-release-pr.yml +++ b/.github/workflows/create-release-pr.yml @@ -34,6 +34,10 @@ jobs: push: false github_token: ${{ secrets.GITHUB_TOKEN }} + # commitizen is formatting the .cz.yaml file in a way that Prettier does not like + - name: Format + run: pnpm format + - name: Print Version run: echo "Bumping to version ${{ steps.cz.outputs.version }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4d605139..e368cfb8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,6 +67,11 @@ jobs: # env: # CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} + - name: Release ic-http-certification Cargo crate + run: cargo publish -p ic-http-certification --token ${CRATES_TOKEN} + env: + CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} + - name: Pack @dfinity/certification-testing NPM package working-directory: packages/ic-certification-testing-wasm run: npm pack --pack-destination ../../ @@ -117,6 +122,7 @@ jobs: with: artifacts: > target/package/ic-certification-${{ github.ref_name }}.crate, + target/package/ic-http-certification-${{ github.ref_name }}.crate, target/package/ic-representation-independent-hash-${{ github.ref_name }}.crate, target/package/ic-cbor-${{ github.ref_name }}.crate, target/package/ic-certificate-verification-${{ github.ref_name }}.crate, diff --git a/Cargo.lock b/Cargo.lock index 79cc96bb..a259282b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1487,6 +1487,13 @@ dependencies = [ "strum_macros 0.23.1", ] +[[package]] +name = "ic-http-certification" +version = "1.3.0" +dependencies = [ + "rstest", +] + [[package]] name = "ic-ic00-types" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 4249b276..b6981119 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "packages/ic-certification", "packages/ic-certificate-verification", "packages/ic-certification-testing", + "packages/ic-http-certification", "packages/ic-representation-independent-hash", "packages/ic-response-verification", "packages/ic-response-verification-test-utils", @@ -22,6 +23,7 @@ default-members = [ "packages/ic-certification", "packages/ic-certificate-verification", "packages/ic-certification-testing", + "packages/ic-http-certification", "packages/ic-representation-independent-hash", "packages/ic-response-verification", "packages/ic-response-verification-test-utils", diff --git a/README.md b/README.md index 4ee935d4..ee7faf1a 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,16 @@ This package encapsulates the protocol for such verification. It is used by [the | `pnpm test` | Test all NPM packages | | `pnpm format` | Format all NPM packages | +### HTTP Certification + +- [Cargo crate](./packages/ic-http-certification/README.md) + +| Command | Description | +| ----------------------------------------------------- | ---------------------- | +| `cargo build -p ic-http-certification` | Build Cargo crate | +| `cargo test -p ic-http-certification` | Test Cargo crate | +| `cargo doc -p ic-http-certification --no-deps --open` | Build Cargo crate docs | + ### Response Verification - [Cargo crate](./packages/ic-response-verification/README.md) @@ -147,7 +157,7 @@ See [Conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) for m - Follow the [Package naming conventions](#package-naming-conventions) when naming the package. - Add the package's package manager file to the `version_files` field in `.cz.yaml`. - `package.json` for NPM packages - - `Cargo.toml` for Cargo crates + - Nothing for for Cargo crates, it is already covered by the root `Cargo.toml` - Set the initial version of the package in its package manager file to match the current version in the `version` field in `.cz.yaml` - For `package.json`, set the version manually - For `Cargo.toml`, use `version.workspace = true` @@ -158,7 +168,7 @@ See [Conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) for m - Add the package to the `members` section in `Cargo.toml` and the `default-members` section - If the package must be compiled to WASM then do not add it to the `default-members` section - Add a `Release ic- Cargo crate` job to the `Release` workflow in `.github/workflows/release.yml` - - Add `target/package/ic--${{ github.ref_name }}.crate` to the `artifacts` property in the `Create Github release` job of the `Create Release PR` workflow in `.github/workflows/create-release-pr.yml` + - Add `target/package/ic--${{ github.ref_name }}.crate` to the `artifacts` property in the `Create Github release` job of the `Release` workflow in `.github/workflows/release.yml` - Make sure every entry except the last is comma delimited - If the crate has dependenencies in this repository, make sure it is published _after_ the dependencies - If the crate has a dependent in this repository, make sure it is published _before_ the dependents diff --git a/packages/ic-http-certification/Cargo.toml b/packages/ic-http-certification/Cargo.toml new file mode 100644 index 00000000..cca19fa9 --- /dev/null +++ b/packages/ic-http-certification/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "ic-http-certification" +description = "Certification for HTTP responses for the Internet Computer" +readme = "README.md" +documentation = "https://docs.rs/ic-http-certification" +categories = ["api-bindings", "data-structures", "no-std"] +keywords = ["internet-computer", "agent", "utility", "icp", "dfinity"] +include = ["src", "Cargo.toml", "LICENSE", "README.md"] + +version.workspace = true +authors.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +homepage.workspace = true + +[dev-dependencies] +rstest.workspace = true diff --git a/packages/ic-http-certification/LICENSE b/packages/ic-http-certification/LICENSE new file mode 100644 index 00000000..274d16b7 --- /dev/null +++ b/packages/ic-http-certification/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 DFINITY Foundation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/ic-http-certification/README.md b/packages/ic-http-certification/README.md new file mode 100644 index 00000000..23b38cd7 --- /dev/null +++ b/packages/ic-http-certification/README.md @@ -0,0 +1,234 @@ +# Internet Computer HTTP Certification + +## Defining CEL Expressions + +[CEL](https://github.com/google/cel-spec) (Common Expression Language) is a portable expression language that can be used to enable different applications to more easily interoperate. It can be seen as the computation or expression counterpart to [Protocol Buffers](https://github.com/protocolbuffers/protobuf). + +CEL expressions lie at the heart of the Internet Computer's HTTP certification system. They are used to define the conditions under which a request and response pair should be certified and what should be included from the corresponding request and response objects in the certification. + +To define a CEL expression, start with the `CelExpression` enum. This enum provides a set of variants that can be used to define different types of CEL expressions supported by Internet Computer HTTP Gateways. Currently only one variant is supported, known as the "default" certification expression, but more may be added in the future as HTTP certification evolves over time. + +When certifying requests, the request body and method are always certified. To additionally certify request headers and query parameters, use the `headers` and `query_paramters` of `DefaultRequestCertification` struct. Both properties take a `str` slice as an argument. + +When certifying a response, the response body and status code are always certified. To additionally certify response headers, use the `CertifiedResponseHeaders` variant of the `DefaultResponseCertification` enum. Or to certify all response headers, with some exclusions, use the `ResponseHeaderExclusions` variant of the `DefaultResponseCertification` enum. Both variants take a `str` slice as an argument. + +Note that the example CEL expressions provided below are formatted for readability. The actual CEL expressions produced by the `create_cel_expr` are minified. The minified CEL expression is preferred because it is more compact, resulting in a smaller payload and a faster evaluation time for the HTTP Gateway that is verifying the certification, but the formatted versions are also accepted. + +### Fully certified request / response pair + +To define a fully certified request and response pair, including request headers, query parameters, and response headers: + +```rust +use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultRequestCertification, DefaultResponseCertification}; + +let certification = CelExpression::DefaultCertification(Some(DefaultCertification { + request_certification: Some(DefaultRequestCertification { + headers: &["Accept", "Accept-Encoding", "If-Match"], + query_parameters: &["foo", "bar", "baz"], + }), + response_certification: DefaultResponseCertification::CertifiedResponseHeaders(&[ + "ETag", + "Cache-Control", + ]), +})); +``` + +This will produce the following CEL expression: + +```protobuf +default_certification ( + ValidationArgs { + request_certification: RequestCertification { + certified_request_headers: ["Accept", "Accept-Encoding", "If-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` properties of the `DefaultRequestCertification` struct, and both can be an empty array. If the `headers` property is empty, no request headers will be certified. Likewise for the `query_paramters` property, if it is empty then no query parameters will be certified. If both are empty, only the request body and method will be certified. + +For example, to certify only the request body and method: + +```rust +use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultRequestCertification, DefaultResponseCertification}; + +let certification = CelExpression::DefaultCertification(Some(DefaultCertification { + request_certification: Some(DefaultRequestCertification { + headers: &[], + query_parameters: &[], + }), + response_certification: DefaultResponseCertification::CertifiedResponseHeaders(&[ + "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" + ] + } + } + } +) +``` + +### Skipping request certification + +Request certification can be skipped entirely by setting the `request_certification` property of the `DefaultCertification` struct to `None`. For example: + +```rust +use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultResponseCertification}; + +let certification = CelExpression::DefaultCertification(Some(DefaultCertification { + request_certification: None, + response_certification: DefaultResponseCertification::CertifiedResponseHeaders(&[ + "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" + ] + } + } + } +) +``` + +### Partially certified response + +Similiarly to request certification, any number of response headers can be provided via the `CertifiedResponseHeaders` variant of the `DefaultResponseCertification` enum, and it can also be an empty array. If the array is empty, no response headers will be certified. For example: + +```rust +use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultRequestCertification, DefaultResponseCertification}; + +let certification = CelExpression::DefaultCertification(Some(DefaultCertification { + request_certification: Some(DefaultRequestCertification { + headers: &["Accept", "Accept-Encoding", "If-Match"], + query_parameters: &["foo", "bar", "baz"], + }), + response_certification: DefaultResponseCertification::CertifiedResponseHeaders(&[]), +})); +``` + +This will produce the following CEL expression: + +```protobuf +default_certification ( + ValidationArgs { + request_certification: RequestCertification { + certified_request_headers: ["Accept", "Accept-Encoding", "If-Match"], + certified_query_parameters: ["foo", "bar", "baz"] + }, + response_certification: ResponseCertification { + certified_response_headers: ResponseHeaderList { + headers: [] + } + } + } +) +``` + +If the `ResponseHeaderExclusions` variant is used, an empty array will certify _all_ response headers. For example: + +```rust +use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultRequestCertification, DefaultResponseCertification}; + +let certification = CelExpression::DefaultCertification(Some(DefaultCertification { + request_certification: Some(DefaultRequestCertification { + headers: &["Accept", "Accept-Encoding", "If-Match"], + query_parameters: &["foo", "bar", "baz"], + }), + response_certification: DefaultResponseCertification::ResponseHeaderExclusions(&[]), +})); +``` + +This will produce the following CEL expression: + +```protobuf +default_certification ( + ValidationArgs { + request_certification: RequestCertification { + certified_request_headers: ["Accept", "Accept-Encoding", "If-Match"], + certified_query_parameters: ["foo", "bar", "baz"] + }, + response_certification: ResponseCertification { + response_header_exclusions: ResponseHeaderList { + headers: [] + } + } + } +) +``` + +To skip response certification, certification must be skipped completely. It wouldn't be useful to certify a request without certifying a response. So if anything is certified, then it must at least include the response. See the next section for more details on skipping certification entirely. + +### Skipping certification + +To skip certification entirely: + +```rust +use ic_http_certification::cel::{CelExpression, DefaultCertification}; + +let certification = CelExpression::DefaultCertification(None); +``` + +This will produce the following CEL expression: + +```protobuf +default_certification ( + ValidationArgs { + no_certification: Empty {} + } +) +``` + +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. + +Typically these requests have been routed through `raw` Internet Computer URLs in the past, but this is dangerous because `raw` URLs allow any responding replica to decide whether or not certification is required. In contrast, by skipping certification using the above method with a non-`raw` URL, a replica will no longer be able to decide whether or not certification is required and instead this decision will be made by the canister itself and the result will go through consensus. + +## Converting CEL expressions to their `String` representation + +Note that the `CelExpression` enum is not a CEL expression itself, but rather a Rust representation of a CEL expression. To convert a `CelExpression` into its `String` representation, use the `create_cel_expr` function. + +```rust +use ic_http_certification::cel::{CelExpression, create_cel_expr}; + +let certification = CelExpression::DefaultCertification(None); +let cel_expr = create_cel_expr(&certification); +``` diff --git a/packages/ic-http-certification/src/cel/cel_types.rs b/packages/ic-http-certification/src/cel/cel_types.rs new file mode 100644 index 00000000..03f9a61c --- /dev/null +++ b/packages/ic-http-certification/src/cel/cel_types.rs @@ -0,0 +1,75 @@ +/// A certification CEL expression defintion. +/// Contains an enum variant for each CEL function supported for certification. +/// Currently only one variant is supported: [CelExpression::DefaultCertification]. +#[derive(Debug, Clone)] +pub enum CelExpression<'a> { + /// A certification CEL expression definition that uses the `default_certification` function. + /// This is currently the only supported function. + /// + /// The enum's inner value is an [Option] to allow for opting in, or out of certification. + /// Providing [None] will opt out of certification, while providing [Some] will opt in to certification. + /// See [DefaultCertification] for more details on its available parameters. + DefaultCertification(Option>), +} + +/// A certification CEL expression definition that uses the `default_certification` function. +/// +/// [request_certification](DefaultCertification::request_certification) is used for configuring request certification, and +/// [response_certification](DefaultCertification::response_certification) is used for configuring response certification. +#[derive(Debug, Clone)] +pub struct DefaultCertification<'a> { + /// Options for configuring certification of a request. + /// + /// This is an [Option] to allow for opting in, or out of request certification. + /// See [DefaultRequestCertification] for more details on its available parameters. + pub request_certification: Option>, + + /// Options for configuring certification of a response. + /// + /// This is not an [Option] because response certification is the minimum required + /// when certifying a request and response pair. + /// See [DefaultResponseCertification] for more details on its available parameters. + pub response_certification: DefaultResponseCertification<'a>, +} + +/// Options for configuring certification of a request. +/// +/// The request method and body are always certified, but this struct allows configuring the +/// certification of request [headers](DefaultRequestCertification::headers) and +/// [query parameters](DefaultRequestCertification::query_parameters). +#[derive(Debug, Clone)] +pub struct DefaultRequestCertification<'a> { + /// A list of request headers to include in certification. + /// + /// As many or as little headers can be provided as desired. + /// Providing an empty list will result in no request headers being certified. + pub headers: &'a [&'a str], + + /// A list of request query parameters to include in certification. + /// + /// As many or as little query parameters can be provided as desired. + /// Providing an empty list will result in no request query parameters being certified. + pub query_parameters: &'a [&'a str], +} + +/// Options for configuring certification of a response. +/// +/// The response body and status code are always certified, but this struct allows configuring the +/// certification of response headers. Response headers may be included using the +/// [CertifiedResponseHeaders](DefaultResponseCertification::CertifiedResponseHeaders) variant, +/// and response headers may be excluded using the +/// [ResponseHeaderExclusions](DefaultResponseCertification::ResponseHeaderExclusions) variant. +#[derive(Debug, Clone)] +pub enum DefaultResponseCertification<'a> { + /// A list of response headers to include in certification. + /// + /// As many or as little headers can be provided as desired. + /// Providing an empty list will result in no response headers being certified. + CertifiedResponseHeaders(&'a [&'a str]), + + /// A list of response headers to exclude from certification. + /// + /// As many or as little headers can be provided as desired. + /// Providing an empty list will result in all response headers being certified. + ResponseHeaderExclusions(&'a [&'a str]), +} diff --git a/packages/ic-http-certification/src/cel/create_cel_expr.rs b/packages/ic-http-certification/src/cel/create_cel_expr.rs new file mode 100644 index 00000000..8a4ab297 --- /dev/null +++ b/packages/ic-http-certification/src/cel/create_cel_expr.rs @@ -0,0 +1,349 @@ +use super::{ + CelExpression, DefaultCertification, DefaultRequestCertification, DefaultResponseCertification, +}; + +/// Converts a CEL expression from a [CelExpression] object into it's [String] representation. +pub fn create_cel_expr(certification: &CelExpression) -> String { + match certification { + CelExpression::DefaultCertification(certification) => { + create_default_cel_expr(certification) + } + } +} + +fn create_default_cel_expr(certification: &Option) -> String { + let mut cel_expr = String::from("default_certification(ValidationArgs{"); + + match certification { + None => cel_expr.push_str("no_certification:Empty{}"), + Some(certification) => { + create_request_cel_expr(&mut cel_expr, certification.request_certification.as_ref()); + create_response_cel_expr(&mut cel_expr, &certification.response_certification); + } + } + + cel_expr.push_str("})"); + cel_expr +} + +fn create_request_cel_expr( + cel_expr: &mut String, + request_certification: Option<&DefaultRequestCertification>, +) { + match request_certification { + None => cel_expr.push_str("no_request_certification:Empty{},"), + Some(request_certification) => { + cel_expr + .push_str("request_certification:RequestCertification{certified_request_headers:["); + + if !request_certification.headers.is_empty() { + cel_expr.push('"'); + cel_expr.push_str(&request_certification.headers.join(r#"",""#)); + cel_expr.push('"'); + } + + cel_expr.push_str("],certified_query_parameters:["); + if !request_certification.query_parameters.is_empty() { + cel_expr.push('"'); + cel_expr.push_str(&request_certification.query_parameters.join(r#"",""#)); + cel_expr.push('"'); + } + + cel_expr.push_str("]},"); + } + } +} + +fn create_response_cel_expr( + cel_expr: &mut String, + response_certification: &DefaultResponseCertification, +) { + cel_expr.push_str("response_certification:ResponseCertification{"); + + let headers = match response_certification { + DefaultResponseCertification::CertifiedResponseHeaders(headers) => { + cel_expr.push_str("certified_response_headers"); + headers + } + DefaultResponseCertification::ResponseHeaderExclusions(headers) => { + cel_expr.push_str("response_header_exclusions"); + headers + } + }; + + cel_expr.push_str(":ResponseHeaderList{headers:["); + if !headers.is_empty() { + cel_expr.push('"'); + cel_expr.push_str(&headers.join(r#"",""#)); + cel_expr.push('"'); + } + cel_expr.push_str("]}}"); +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::*; + + #[rstest] + #[case::no_certification(no_certification(), no_certification_cel())] + #[case::no_request_header_inclusions( + no_request_response_inclusions(), + no_request_response_inclusions_cel() + )] + #[case::no_request_header_inclusions( + no_request_response_exclusions(), + no_request_response_exclusions_cel() + )] + #[case::include_request_response_header_exclusions( + include_request_response_header_exclusions(), + include_request_response_header_exclusions_cel() + )] + #[case::include_request_response_header_inclusions( + include_request_response_header_inclusions(), + include_request_response_header_inclusions_cel() + )] + #[case::empty_request_response_inclusions( + empty_request_response_inclusions(), + empty_request_response_inclusions_cel() + )] + #[case::empty_request_response_exclusions( + empty_request_response_exclusions(), + empty_request_response_exclusions_cel() + )] + fn create_cel_expr_test(#[case] certification: CelExpression, #[case] expected: String) { + let cel_expr = create_cel_expr(&certification); + + assert_eq!(cel_expr, expected); + } + + fn no_certification() -> CelExpression<'static> { + CelExpression::DefaultCertification(None) + } + + fn no_certification_cel() -> String { + remove_whitespace( + r#"default_certification( + ValidationArgs { + no_certification: Empty {} + } + )"#, + ) + } + + fn no_request_response_inclusions() -> CelExpression<'static> { + CelExpression::DefaultCertification(Some(DefaultCertification { + request_certification: None, + response_certification: DefaultResponseCertification::CertifiedResponseHeaders(&[ + "Cache-Control", + "ETag", + "Content-Length", + "Content-Type", + "Content-Encoding", + ]), + })) + } + + fn no_request_response_inclusions_cel() -> String { + remove_whitespace( + r#"default_certification( + ValidationArgs { + no_request_certification: Empty {}, + response_certification: ResponseCertification { + certified_response_headers: ResponseHeaderList { + headers: [ + "Cache-Control", + "ETag", + "Content-Length", + "Content-Type", + "Content-Encoding" + ] + } + } + } + )"#, + ) + } + + fn no_request_response_exclusions() -> CelExpression<'static> { + CelExpression::DefaultCertification(Some(DefaultCertification { + request_certification: None, + response_certification: DefaultResponseCertification::ResponseHeaderExclusions(&[ + "Date", + "Cookie", + "Set-Cookie", + ]), + })) + } + + fn no_request_response_exclusions_cel() -> String { + remove_whitespace( + r#"default_certification( + ValidationArgs { + no_request_certification: Empty {}, + response_certification: ResponseCertification { + response_header_exclusions: ResponseHeaderList { + headers: [ + "Date", + "Cookie", + "Set-Cookie" + ] + } + } + } + )"#, + ) + } + + fn include_request_response_header_exclusions() -> CelExpression<'static> { + CelExpression::DefaultCertification(Some(DefaultCertification { + request_certification: Some(DefaultRequestCertification { + headers: &["Accept", "Accept-Encoding", "If-Match"], + query_parameters: &["foo", "bar", "baz"], + }), + response_certification: DefaultResponseCertification::ResponseHeaderExclusions(&[ + "Date", + "Cookie", + "Set-Cookie", + ]), + })) + } + + fn include_request_response_header_exclusions_cel() -> String { + remove_whitespace( + r#"default_certification( + ValidationArgs { + request_certification: RequestCertification { + certified_request_headers: [ + "Accept", + "Accept-Encoding", + "If-Match" + ], + certified_query_parameters: [ + "foo", + "bar", + "baz" + ] + }, + response_certification: ResponseCertification { + response_header_exclusions: ResponseHeaderList { + headers: [ + "Date", + "Cookie", + "Set-Cookie" + ] + } + } + } + )"#, + ) + } + + fn include_request_response_header_inclusions() -> CelExpression<'static> { + CelExpression::DefaultCertification(Some(DefaultCertification { + request_certification: Some(DefaultRequestCertification { + headers: &["Accept", "Accept-Encoding", "If-Match"], + query_parameters: &["foo", "bar", "baz"], + }), + response_certification: DefaultResponseCertification::CertifiedResponseHeaders(&[ + "Cache-Control", + "ETag", + "Content-Length", + "Content-Type", + "Content-Encoding", + ]), + })) + } + + fn include_request_response_header_inclusions_cel() -> String { + remove_whitespace( + r#"default_certification( + ValidationArgs { + request_certification: RequestCertification { + certified_request_headers: [ + "Accept", + "Accept-Encoding", + "If-Match" + ], + certified_query_parameters: [ + "foo", + "bar", + "baz" + ] + }, + response_certification: ResponseCertification { + certified_response_headers: ResponseHeaderList { + headers: [ + "Cache-Control", + "ETag", + "Content-Length", + "Content-Type", + "Content-Encoding" + ] + } + } + } + )"#, + ) + } + + fn empty_request_response_inclusions() -> CelExpression<'static> { + CelExpression::DefaultCertification(Some(DefaultCertification { + request_certification: Some(DefaultRequestCertification { + headers: &[], + query_parameters: &[], + }), + response_certification: DefaultResponseCertification::CertifiedResponseHeaders(&[]), + })) + } + + fn empty_request_response_inclusions_cel() -> String { + remove_whitespace( + r#"default_certification( + ValidationArgs { + request_certification: RequestCertification { + certified_request_headers: [], + certified_query_parameters: [] + }, + response_certification: ResponseCertification { + certified_response_headers: ResponseHeaderList { + headers: [] + } + } + } + )"#, + ) + } + + fn empty_request_response_exclusions() -> CelExpression<'static> { + CelExpression::DefaultCertification(Some(DefaultCertification { + request_certification: Some(DefaultRequestCertification { + headers: &[], + query_parameters: &[], + }), + response_certification: DefaultResponseCertification::ResponseHeaderExclusions(&[]), + })) + } + + fn empty_request_response_exclusions_cel() -> String { + remove_whitespace( + r#"default_certification( + ValidationArgs { + request_certification: RequestCertification { + certified_request_headers: [], + certified_query_parameters: [] + }, + response_certification: ResponseCertification { + response_header_exclusions: ResponseHeaderList { + headers: [] + } + } + } + )"#, + ) + } + + fn remove_whitespace<'a>(s: &'a str) -> String { + s.chars().filter(|c| !c.is_whitespace()).collect() + } +} diff --git a/packages/ic-http-certification/src/cel/mod.rs b/packages/ic-http-certification/src/cel/mod.rs new file mode 100644 index 00000000..f31cd0bd --- /dev/null +++ b/packages/ic-http-certification/src/cel/mod.rs @@ -0,0 +1,8 @@ +//! The CEL modules contains functions and builders for creating CEL expression +//! definitions and conveting them into their `String` representation. + +mod cel_types; +pub use cel_types::*; + +mod create_cel_expr; +pub use create_cel_expr::*; diff --git a/packages/ic-http-certification/src/lib.rs b/packages/ic-http-certification/src/lib.rs new file mode 100644 index 00000000..0986c108 --- /dev/null +++ b/packages/ic-http-certification/src/lib.rs @@ -0,0 +1,243 @@ +//! # Internet Computer HTTP Certification +//! +//! ## Defining CEL Expressions +//! +//! [CEL](https://github.com/google/cel-spec) (Common Expression Language) is a portable expression language that can be used to enable different applications to more easily interoperate. It can be seen as the computation or expression counterpart to [Protocol Buffers](https://github.com/protocolbuffers/protobuf). +//! +//! CEL expressions lie at the heart of the Internet Computer's HTTP certification system. They are used to define the conditions under which a request and response pair should be certified and what should be included from the corresponding request and response objects in the certification. +//! +//! To define a CEL expression, start with the [CelExpression](cel::CelExpression) enum. This enum provides a set of variants that can be used to define different types of CEL expressions supported by Internet Computer HTTP Gateways. Currently only one variant is supported, known as the "default" certification expression, but more may be added in the future as HTTP certification evolves over time. +//! +//! When certifying requests, the request body and method are always certified. To additionally certify request headers and query parameters, use the [headers](cel::DefaultRequestCertification::headers) and [query_parameters](cel::DefaultRequestCertification::query_parameters) of [DefaultRequestCertification](cel::DefaultRequestCertification) struct. Both properties take a [str] slice as an argument. +//! +//! When certifying a response, the response body and status code are always certified. To additionally certify response headers, use the [CertifiedResponseHeaders](cel::DefaultResponseCertification::CertifiedResponseHeaders) variant of the [DefaultResponseCertification](cel::DefaultResponseCertification) enum. Or to certify all response headers, with some exclusions, use the [ResponseHeaderExclusions](cel::DefaultResponseCertification::ResponseHeaderExclusions) variant of the [DefaultResponseCertification](cel::DefaultResponseCertification) enum. Both variants take a [str] slice as an argument. +//! +//! Note that the example CEL expressions provided below are formatted for readability. The actual CEL expressions produced by the [create_cel_expr](cel::create_cel_expr) are minified. The minified CEL expression is preferred because it is more compact, resulting in a smaller payload and a faster evaluation time for the HTTP Gateway that is verifying the certification, but the formatted versions are also accepted. +//! +//! ### Fully certified request / response pair +//! +//! To define a fully certified request and response pair, including request headers, query parameters, and response headers: +//! +//! ```rust +//! use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultRequestCertification, DefaultResponseCertification}; +//! +//! let certification = CelExpression::DefaultCertification(Some(DefaultCertification { +//! request_certification: Some(DefaultRequestCertification { +//! headers: &["Accept", "Accept-Encoding", "If-Match"], +//! query_parameters: &["foo", "bar", "baz"], +//! }), +//! response_certification: DefaultResponseCertification::CertifiedResponseHeaders(&[ +//! "ETag", +//! "Cache-Control", +//! ]), +//! })); +//! ``` +//! +//! This will produce the following CEL expression: +//! +//! ```protobuf +//! default_certification ( +//! ValidationArgs { +//! request_certification: RequestCertification { +//! certified_request_headers: ["Accept", "Accept-Encoding", "If-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](cel::DefaultRequestCertification::headers) and [query_parameters](cel::DefaultRequestCertification::query_parameters) properties of the [DefaultRequestCertification](cel::DefaultRequestCertification) struct, and both can be an empty array. If the [headers](cel::DefaultRequestCertification::headers) property is empty, no request headers will be certified. Likewise for the [query_parameters](cel::DefaultRequestCertification::query_parameters) property, if it is empty then no query parameters will be certified. If both are empty, only the request body and method will be certified. +//! +//! For example, to certify only the request body and method: +//! +//! ```rust +//! use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultRequestCertification, DefaultResponseCertification}; +//! +//! let certification = CelExpression::DefaultCertification(Some(DefaultCertification { +//! request_certification: Some(DefaultRequestCertification { +//! headers: &[], +//! query_parameters: &[], +//! }), +//! response_certification: DefaultResponseCertification::CertifiedResponseHeaders(&[ +//! "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" +//! ] +//! } +//! } +//! } +//! ) +//! ``` +//! +//! ### Skipping request certification +//! +//! Request certification can be skipped entirely by setting the [request_certification](cel::DefaultCertification::request_certification) property of the [DefaultCertification](cel::DefaultCertification) struct to [None]. For example: +//! +//! ```rust +//! use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultResponseCertification}; +//! +//! let certification = CelExpression::DefaultCertification(Some(DefaultCertification { +//! request_certification: None, +//! response_certification: DefaultResponseCertification::CertifiedResponseHeaders(&[ +//! "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" +//! ] +//! } +//! } +//! } +//! ) +//! ``` +//! +//! ### Partially certified response +//! +//! Similiarly to request certification, any number of response headers can be provided via the [CertifiedResponseHeaders](cel::DefaultResponseCertification::CertifiedResponseHeaders) variant of the [DefaultResponseCertification](cel::DefaultResponseCertification) enum, and it can also be an empty array. If the array is empty, no response headers will be certified. For example: +//! +//! ```rust +//! use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultRequestCertification, DefaultResponseCertification}; +//! +//! let certification = CelExpression::DefaultCertification(Some(DefaultCertification { +//! request_certification: Some(DefaultRequestCertification { +//! headers: &["Accept", "Accept-Encoding", "If-Match"], +//! query_parameters: &["foo", "bar", "baz"], +//! }), +//! response_certification: DefaultResponseCertification::CertifiedResponseHeaders(&[]), +//! })); +//! ``` +//! +//! This will produce the following CEL expression: +//! +//! ```protobuf +//! default_certification ( +//! ValidationArgs { +//! request_certification: RequestCertification { +//! certified_request_headers: ["Accept", "Accept-Encoding", "If-Match"], +//! certified_query_parameters: ["foo", "bar", "baz"] +//! }, +//! response_certification: ResponseCertification { +//! certified_response_headers: ResponseHeaderList { +//! headers: [] +//! } +//! } +//! } +//! ) +//! ``` +//! +//! If the [ResponseHeaderExclusions](cel::DefaultResponseCertification::ResponseHeaderExclusions) variant is used, an empty array will certify _all_ response headers. For example: +//! +//! ```rust +//! use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultRequestCertification, DefaultResponseCertification}; +//! +//! let certification = CelExpression::DefaultCertification(Some(DefaultCertification { +//! request_certification: Some(DefaultRequestCertification { +//! headers: &["Accept", "Accept-Encoding", "If-Match"], +//! query_parameters: &["foo", "bar", "baz"], +//! }), +//! response_certification: DefaultResponseCertification::ResponseHeaderExclusions(&[]), +//! })); +//! ``` +//! +//! This will produce the following CEL expression: +//! +//! ```protobuf +//! default_certification ( +//! ValidationArgs { +//! request_certification: RequestCertification { +//! certified_request_headers: ["Accept", "Accept-Encoding", "If-Match"], +//! certified_query_parameters: ["foo", "bar", "baz"] +//! }, +//! response_certification: ResponseCertification { +//! response_header_exclusions: ResponseHeaderList { +//! headers: [] +//! } +//! } +//! } +//! ) +//! ``` +//! +//! To skip response certification, certification must be skipped completely. It wouldn't be useful to certify a request without certifying a response. So if anything is certified, then it must at least include the response. See the next section for more details on skipping certification entirely. +//! +//! ### Skipping certification +//! +//! To skip certification entirely: +//! +//! ```rust +//! use ic_http_certification::cel::{CelExpression, DefaultCertification}; +//! +//! let certification = CelExpression::DefaultCertification(None); +//! ``` +//! +//! This will produce the following CEL expression: +//! +//! ```protobuf +//! default_certification ( +//! ValidationArgs { +//! no_certification: Empty {} +//! } +//! ) +//! ``` +//! +//! 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. +//! +//! Typically these requests have been routed through `raw` Internet Computer URLs in the past, but this is dangerous because `raw` URLs allow any responding replica to decide whether or not certification is required. In contrast, by skipping certification using the above method with a non-`raw` URL, a replica will no longer be able to decide whether or not certification is required and instead this decision will be made by the canister itself and the result will go through consensus. +//! +//! ## Converting CEL expressions to their `String` representation +//! +//! Note that the [CelExpression](cel::CelExpression) enum is not a CEL expression itself, but rather a Rust representation of a CEL expression. To convert a [CelExpression](cel::CelExpression) into its [String] representation, use the [create_cel_expr](cel::create_cel_expr) function. +//! +//! ```rust +//! use ic_http_certification::cel::{CelExpression, create_cel_expr}; +//! +//! let certification = CelExpression::DefaultCertification(None); +//! let cel_expr = create_cel_expr(&certification); +//! ``` + +#![deny( + missing_docs, + missing_debug_implementations, + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links +)] + +pub mod cel;