Skip to content

Commit

Permalink
feat: add cel builder to ic-http-certification crate
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanosdev committed Nov 27, 2023
1 parent 287e508 commit 99c639d
Show file tree
Hide file tree
Showing 7 changed files with 1,189 additions and 419 deletions.
184 changes: 159 additions & 25 deletions packages/ic-http-certification/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,167 @@

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.

CEL expressions can be created in two ways, by using the [CEL builder](#using-the-cel-builder), or by directly creating a [CEL expression](#directly-creating-a-cel-expression).

### 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 `` representation, use `CelExpression.to_string` or `create_cel_expr`. This applies to CEL expressions created both by the [CEL builder](#using-the-cel-builder) and [directly](#directly-creating-a-cel-expression).

```rust
use ic_http_certification::cel::CelExpression;

let cel_expr = CelExpression::DefaultCertification(None).to_string();
```

Alternatively:

```rust
use ic_http_certification::cel::{CelExpression, create_cel_expr};

let certification = CelExpression::DefaultCertification(None);
let cel_expr = create_cel_expr(&certification);
```

### Using the CEL builder

The CEL builder interface is provided to ease the creation of CEL expressions through an ergonmic interface. If this interface does not meet your needs, you can also [create CEL expressions directly](#directly-creating-a-cel-expression). To define a CEL expression, start with [DefaultCelBuilder]. This struct provides a set of methods that can be used to define how your request and response pair should be certified.

When certifying requests, the request body and method are always certified. To additionally certify request headers and query parameters, use `with_request_headers` and `with_request_query_parameters` respectively. Both methods take a [str] slice as an argument.

When certifying a response, the response body and status code are always certified. To additionally certify response headers, use `with_response_certification`. This method takes the `DefaultResponseCertification` enum as an argument. To specify header inclusions, use the `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.

#### Fully certified request / response pair

To define a fully certified request and response pair, including request headers, query parameters, and response headers use `DefaultCelBuilder::full_certification`. For example:

```rust
use ic_http_certification::{DefaultCelBuilder, DefaultResponseCertification};

let cel_expr = DefaultCelBuilder::full_certification()
.with_request_headers(&["Accept", "Accept-Encoding", "If-Match"])
.with_request_query_parameters(&["foo", "bar", "baz"])
.with_response_certification(DefaultResponseCertification::CertifiedResponseHeaders(&[
"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. If `with_request_headers` is called with an empty array, or it is not called at all, then no request headers will be certified. 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.

For example, to certify only the request body and method:

```rust
use ic_http_certification::{DefaultCelBuilder, DefaultResponseCertification};

let cel_expr = DefaultCelBuilder::full_certification()
.with_response_certification(DefaultResponseCertification::CertifiedResponseHeaders(&[
"Cache-Control",
"ETag",
]))
.build();
```

Alternatively, this can be done more explicitly:

```rust
use ic_http_certification::{DefaultCelBuilder, DefaultResponseCertification};

let cel_expr = DefaultCelBuilder::full_certification()
.with_request_headers(&[])
.with_request_query_parameters(&[])
.with_response_certification(DefaultResponseCertification::CertifiedResponseHeaders(&[
"Cache-Control",
"ETag",
]))
.build();
```

#### Skipping request certification

Request certification can be skipped entirely by using `DefaultCelBuilder::response_certification` instead of `DefaultCelBuilder::full_certification`. For example:

```rust
use ic_http_certification::{DefaultCelBuilder, DefaultResponseCertification};

let cel_expr = DefaultCelBuilder::response_certification()
.with_response_certification(DefaultResponseCertification::ResponseHeaderExclusions(&[
"Date",
"Cookie",
"Set-Cookie",
]))
.build();
```

#### Partially certified response

Similiarly to request certification, any number of response headers can be provided via the `CertifiedResponseHeaders` variant of the `DefaultResponseCertification` enum when calling `with_response_certification`. The provided array can also be an empty. If the array is empty, or the method is not called, then no response headers will be certified.

For example, to certify only the response body and status code:

```rust
use ic_http_certification::DefaultCelBuilder;

let cel_expr = DefaultCelBuilder::response_certification().build();
```

This can also be done more explicitly:

```rust
use ic_http_certification::DefaultCelBuilder;

let cel_expr = DefaultCelBuilder::response_certification()
.with_response_certification(DefaultResponseCertification::CertifiedResponseHeaders(&[]))
.build();
```

The same applies when both when using `DefaultCelBuilder::response_certification` and `DefaultCelBuilder::full_certification`.

```rust
use ic_http_certification::DefaultCelBuilder;

let cel_expr = DefaultCelBuilder::full_certification()
.with_request_headers(&["Accept", "Accept-Encoding", "If-Match"])
.with_request_query_parameters(&["foo", "bar", "baz"])
.build();
```

To skip response certification completely, then certification overall must be skipped completely. It wouldn't be useful to certify a request without certifying a response. So if anything is certified, then it must at least include the response. See the next section for more details on skipping certification entirely.

#### Skipping certification

To skip certification entirely, use `skip_certification`, for example:

```rust
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.

Typically these requests have been routed through `raw` Internet Computer URLs in the past, but this is dangerous because `raw` URLs allow any responding replica to decide whether or not certification is required. In contrast, by skipping certification using the above method with a non-`raw` URL, a replica will no longer be able to decide whether or not certification is required and instead this decision will be made by the canister itself and the result will go through consensus.

### Directly creating a CEL expression

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.
Note that the example CEL expressions provided below are formatted for readability. The actual CEL expressions produced by `CelExpression::to_string` and `create_cel_expr` are minified. The minified CEL expression is preferred because it is more compact, resulting in a smaller payload and a faster evaluation time for the HTTP Gateway that is verifying the certification, but the formatted versions are also accepted.

### Fully certified request / response pair
#### Fully certified request / response pair

To define a fully certified request and response pair, including request headers, query parameters, and response headers:

```rust
use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultRequestCertification, DefaultResponseCertification};

let certification = CelExpression::DefaultCertification(Some(DefaultCertification {
let cel_expr = CelExpression::DefaultCertification(Some(DefaultCertification {
request_certification: Some(DefaultRequestCertification {
headers: &["Accept", "Accept-Encoding", "If-Match"],
query_parameters: &["foo", "bar", "baz"],
Expand Down Expand Up @@ -54,16 +199,16 @@ default_certification (
)
```

### Partially certified request
#### Partially certified request

Any number of request headers or query parameters can be provided via the `headers` and `query_parameters` properties of the `DefaultRequestCertification` struct, and both can be an empty array. If the `headers` property is empty, no request headers will be certified. Likewise for the `query_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.
Any number of request headers or query parameters can be provided via the `headers` and `query_parameters` properties of the `DefaultRequestCertification` struct, and both can be an empty array. If the `headers` property is empty, no request headers will be certified. Likewise for the `query_parameters` property, if it is empty then no query parameters will be certified. If both are empty, only the request body and method will be certified.

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 {
let cel_expr = CelExpression::DefaultCertification(Some(DefaultCertification {
request_certification: Some(DefaultRequestCertification {
headers: &[],
query_parameters: &[],
Expand Down Expand Up @@ -96,14 +241,14 @@ default_certification (
)
```

### Skipping request certification
#### Skipping request certification

Request certification can be skipped entirely by setting the `request_certification` property of the `DefaultCertification` struct to `None`. For example:

```rust
use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultResponseCertification};

let certification = CelExpression::DefaultCertification(Some(DefaultCertification {
let cel_expr = CelExpression::DefaultCertification(Some(DefaultCertification {
request_certification: None,
response_certification: DefaultResponseCertification::CertifiedResponseHeaders(&[
"ETag",
Expand All @@ -130,14 +275,14 @@ default_certification (
)
```

### Partially certified response
#### 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 {
let cel_expr = CelExpression::DefaultCertification(Some(DefaultCertification {
request_certification: Some(DefaultRequestCertification {
headers: &["Accept", "Accept-Encoding", "If-Match"],
query_parameters: &["foo", "bar", "baz"],
Expand Down Expand Up @@ -169,7 +314,7 @@ If the `ResponseHeaderExclusions` variant is used, an empty array will certify _
```rust
use ic_http_certification::cel::{CelExpression, DefaultCertification, DefaultRequestCertification, DefaultResponseCertification};

let certification = CelExpression::DefaultCertification(Some(DefaultCertification {
let cel_expr = CelExpression::DefaultCertification(Some(DefaultCertification {
request_certification: Some(DefaultRequestCertification {
headers: &["Accept", "Accept-Encoding", "If-Match"],
query_parameters: &["foo", "bar", "baz"],
Expand All @@ -196,16 +341,16 @@ default_certification (
)
```

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.
To skip response certification completely, then certification overall must be skipped completely. It wouldn't be useful to certify a request without certifying a response. So if anything is certified, then it must at least include the response. See the next section for more details on skipping certification entirely.

### Skipping certification
#### Skipping certification

To skip certification entirely:

```rust
use ic_http_certification::cel::{CelExpression, DefaultCertification};

let certification = CelExpression::DefaultCertification(None);
let cel_expr = CelExpression::DefaultCertification(None);
```

This will produce the following CEL expression:
Expand All @@ -221,14 +366,3 @@ default_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.

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);
```
Loading

0 comments on commit 99c639d

Please sign in to comment.