Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .sources/upstream.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"synced": [
{
"repo": "dfinity/certified-assets",
"pinned": "65c0f32",
"pinned": "v0.4.0",
"source": "docs/",
"target": "docs/guides/frontends/static-site/",
"script": "scripts/sync-static-site.mjs",
Expand Down
4 changes: 2 additions & 2 deletions docs/guides/frontends/static-site/access-protection.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: "Put a login page in front of a private or preview site with revoca
sidebar:
order: 6
source_repo: "dfinity/certified-assets"
source_ref: "65c0f32"
source_ref: "v0.4.0"
---

By default every deployed app is public. **Access protection** puts a login screen
Expand Down Expand Up @@ -197,4 +197,4 @@ makes), unauthorized visitors can't pull your content. But:
Use it to keep a preview or in-progress app out of public view, not to protect
secrets from a determined adversary.

<!-- Generated by scripts/sync-static-site.mjs from dfinity/certified-assets docs/access-protection.md at 65c0f32. Do not edit directly: the next sync overwrites it. Content changes belong upstream. -->
<!-- Generated by scripts/sync-static-site.mjs from dfinity/certified-assets docs/access-protection.md at v0.4.0. Do not edit directly: the next sync overwrites it. Content changes belong upstream. -->
11 changes: 7 additions & 4 deletions docs/guides/frontends/static-site/headers.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: "The _headers file: cache-control, security headers, content types,
sidebar:
order: 4
source_repo: "dfinity/certified-assets"
source_ref: "65c0f32"
source_ref: "v0.4.0"
---

Add a file named `_headers` to the root of your asset directory to attach response
Expand Down Expand Up @@ -88,7 +88,7 @@ no CSP. If you want them, declare them in `_headers`. The only response headers
canister manages on its own are the ones tied to how it serves and certifies content:
`Content-Type`, `Content-Encoding`, `ETag`, `Content-Range` (on large-asset
[range responses](how-it-works.md#serving-large-assets)), the certification headers,
and (on HTML responses) its own `ic_env` cookie.
and (on HTML responses) its own `ic_env` cookie (two `Set-Cookie` headers, see below).

A useful baseline to copy and adapt:

Expand All @@ -109,7 +109,10 @@ A useful baseline to copy and adapt:
```

You may set `Set-Cookie` freely; note the canister adds its own `ic_env` cookie to
HTML responses, so don't reuse that name.
HTML responses, so don't reuse that name. It arrives as **two** `Set-Cookie` headers
with the same name and value but different `SameSite` attributes (`Lax`, and
`None; Partitioned` so the value survives in a cross-site iframe); a client stores
whichever it accepts and reads one snapshot either way.

## Reserved headers

Expand All @@ -134,4 +137,4 @@ immediately.
This list is intentionally conservative and may be relaxed in future releases; it's
easier to allow a header later than to start rejecting one that sites already rely on.

<!-- Generated by scripts/sync-static-site.mjs from dfinity/certified-assets docs/headers.md at 65c0f32. Do not edit directly: the next sync overwrites it. Content changes belong upstream. -->
<!-- Generated by scripts/sync-static-site.mjs from dfinity/certified-assets docs/headers.md at v0.4.0. Do not edit directly: the next sync overwrites it. Content changes belong upstream. -->
6 changes: 3 additions & 3 deletions docs/guides/frontends/static-site/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: "How the canister certifies responses, serves large assets, negotia
sidebar:
order: 8
source_repo: "dfinity/certified-assets"
source_ref: "65c0f32"
source_ref: "v0.4.0"
---

You don't need any of this to use certified-assets; the [overview](overview.md) is
Expand Down Expand Up @@ -99,7 +99,7 @@ certified **`206 Partial Content`** response carrying a `Content-Range`. For an
ordinary request the **[HTTP gateway](../../../references/http-gateway-protocol-spec.md)**
fetches the chunks and reassembles them into the full `200` the browser sees; a client
that sends a `Range` header gets back the chunk covering the bytes it asked for. Either
way every chunk is certified, so a large file is exactly as tamperproof as a small
way every chunk is certified, so a large file is exactly as tamper-proof as a small
one, and it's all transparent to the client.

## The sync plugin and its sandbox
Expand Down Expand Up @@ -170,4 +170,4 @@ in-place upgrade that keeps all state, while a **breaking** release reinstalls a
fresh sync re-uploads everything. See
[Releasing](https://github.com/dfinity/certified-assets/blob/main/README.md#releasing) for the details.

<!-- Generated by scripts/sync-static-site.mjs from dfinity/certified-assets docs/how-it-works.md at 65c0f32. Do not edit directly: the next sync overwrites it. Content changes belong upstream. -->
<!-- Generated by scripts/sync-static-site.mjs from dfinity/certified-assets docs/how-it-works.md at v0.4.0. Do not edit directly: the next sync overwrites it. Content changes belong upstream. -->
6 changes: 3 additions & 3 deletions docs/guides/frontends/static-site/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: "Deploy a built frontend, docs, or any folder of files to a caniste
sidebar:
order: 1
source_repo: "dfinity/certified-assets"
source_ref: "65c0f32"
source_ref: "v0.4.0"
---

Deploy a **static site** (a built frontend, docs, or any folder of files) to a
Expand Down Expand Up @@ -47,7 +47,7 @@ canisters:
dir: dist # the directory of files to serve
```

Replace `<version>` with a released version (e.g. `v0.3.3`); see the
Replace `<version>` with a released version (e.g. `v0.4.0`); see the
[available versions](https://github.com/dfinity/icp-cli-recipes/releases?q=static-site&expanded=true).
Pick the version here: because the recipe pins a matched canister + plugin pair,
there is no separate canister version to choose.
Expand Down Expand Up @@ -157,4 +157,4 @@ When you need finer control, each topic has its own page:

Curious how it works underneath? See [Under the hood](how-it-works.md).

<!-- Generated by scripts/sync-static-site.mjs from dfinity/certified-assets docs/overview.md at 65c0f32. Do not edit directly: the next sync overwrites it. Content changes belong upstream. -->
<!-- Generated by scripts/sync-static-site.mjs from dfinity/certified-assets docs/overview.md at v0.4.0. Do not edit directly: the next sync overwrites it. Content changes belong upstream. -->
4 changes: 2 additions & 2 deletions docs/guides/frontends/static-site/redirects.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: "The _redirects file: permanent and temporary redirects, rewrites,
sidebar:
order: 3
source_repo: "dfinity/certified-assets"
source_ref: "65c0f32"
source_ref: "v0.4.0"
---

Add a file named `_redirects` to the root of your asset directory to send one path
Expand Down Expand Up @@ -111,4 +111,4 @@ serve, and a verifying gateway would reject one anyway. (The same constraint is
Static rules (exact paths, `/*` subtrees, and fixed destinations) cover the common
cases and stay fully certifiable, so those are what `_redirects` supports.

<!-- Generated by scripts/sync-static-site.mjs from dfinity/certified-assets docs/redirects.md at 65c0f32. Do not edit directly: the next sync overwrites it. Content changes belong upstream. -->
<!-- Generated by scripts/sync-static-site.mjs from dfinity/certified-assets docs/redirects.md at v0.4.0. Do not edit directly: the next sync overwrites it. Content changes belong upstream. -->
4 changes: 2 additions & 2 deletions docs/guides/frontends/static-site/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: "How request paths resolve to files, clean URLs, trailing slashes,
sidebar:
order: 2
source_repo: "dfinity/certified-assets"
source_ref: "65c0f32"
source_ref: "v0.4.0"
---

This page explains how an incoming request path resolves to one of your files: the
Expand Down Expand Up @@ -131,4 +131,4 @@ for a complete, runnable project.
file's contents at a different URL.
- [Custom headers](headers.md): attach cache-control, CSP, and other headers to paths.

<!-- Generated by scripts/sync-static-site.mjs from dfinity/certified-assets docs/routing.md at 65c0f32. Do not edit directly: the next sync overwrites it. Content changes belong upstream. -->
<!-- Generated by scripts/sync-static-site.mjs from dfinity/certified-assets docs/routing.md at v0.4.0. Do not edit directly: the next sync overwrites it. Content changes belong upstream. -->
4 changes: 2 additions & 2 deletions docs/guides/frontends/static-site/site-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: "What gets uploaded, the special _redirects and _headers files, ski
sidebar:
order: 5
source_repo: "dfinity/certified-assets"
source_ref: "65c0f32"
source_ref: "v0.4.0"
---

This page covers what actually gets uploaded from your asset directory, the special
Expand Down Expand Up @@ -68,4 +68,4 @@ A file named `404.html` at the root of your directory becomes your site-wide
not-found page. If you don't provide one, a certified default is served instead. See
[not-found handling](routing.md#not-found-handling).

<!-- Generated by scripts/sync-static-site.mjs from dfinity/certified-assets docs/site-files.md at 65c0f32. Do not edit directly: the next sync overwrites it. Content changes belong upstream. -->
<!-- Generated by scripts/sync-static-site.mjs from dfinity/certified-assets docs/site-files.md at v0.4.0. Do not edit directly: the next sync overwrites it. Content changes belong upstream. -->
90 changes: 77 additions & 13 deletions docs/guides/frontends/static-site/verifying-contents.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: "Prove a canister serves exactly a known build by reproducing its s
sidebar:
order: 7
source_repo: "dfinity/certified-assets"
source_ref: "65c0f32"
source_ref: "v0.4.0"
---

Certification proves that what the canister **serves** matches what it has
Expand Down Expand Up @@ -35,18 +35,52 @@ the proof (see
[who verifies the certificate](how-it-works.md#who-verifies-the-certificate)).

**Not covered:** asset content bytes are folded in as their certified hashes, never
re-hashed; and permissions / authorization state are out of scope (they don't
affect *what* is served, only *who may sync*).
re-hashed. Two things a visitor receives are outside the model:

- **The `ic_env` cookie.** Every `text/html` response carries a certified
`set-cookie: ic_env` holding the canister's `PUBLIC_*` environment variables and
the IC root key. It is added when the response is certified rather than stored
with the asset, so it is not part of the hash, and `refresh_env` republishes it
without moving the hash. A frontend that reads a backend's canister id from it is
configured by state the hash does not cover: a controller can point it at a
different canister while the build stays byte-identical.
- **Access protection.** Who may *sync* (controllers, authorized principals) has no
bearing on what is served. The access-protection gate does: with it on, an
unauthenticated request for your content gets a certified `307` to the login
page (HTML) or a `401` (anything else) instead of the asset, and `cache-control`
is replaced with `no-store`. A matching hash says the canister holds your build,
not that a visitor can reach it.

## How to verify

You need the canister's id and its public source (the repo and the build steps
that produce the served directory).
You need the canister's id, its public source (the repo and the build steps that
produce the served directory), and a Rust toolchain to build the verifier.

1. **Reproduce the build.** Check out the source at the deployed version and run
the build to produce the site directory (`dist/`), exactly as the deploy does.
1. **Reproduce the build.** Check out the source at the version whose deployment
you are checking, and run the build to produce the site directory (`dist/`),
exactly as the deploy does.

2. **Compute the hash locally** with the `state-hash` tool, pointed at that
2. **Build the verifier at the canister's release.** Ask the canister which
release it runs, and build `state-hash` from that tag. It is not published as a
binary or to crates.io, which is the point: the verifier should come from the
same source you are trusting, not from someone's download.

```sh
icp canister call <canister-id> version '()' -n ic --query
# (record { major = 0 : nat32; minor = 3 : nat32; patch = 3 : nat32 })

cargo install --git https://github.com/dfinity/certified-assets \
--tag v0.3.3 --locked state-hash-cli
```

The release has to match the one that deployed the canister, for the reasons in
[the frozen contract](#the-frozen-contract). `--locked` is part of that match,
not a precaution: the committed `Cargo.lock` is what pins the compressor builds
whose output bytes the hash covers, and `cargo install` re-resolves dependencies
without it. A verifier on the right tag with a newer `brotli` patch computes a
different hash.

3. **Compute the hash locally** with the `state-hash` tool, pointed at that
directory (include any `_headers` / `_redirects` files, as deployed):

```sh
Expand All @@ -61,18 +95,39 @@ that produce the served directory).
match this value. Verifying those is between that platform and its users; the
tool deliberately doesn't guess at which settings someone else might have used.

3. **Read the canister's hash.** `state_hash` is a public, unguarded method, and
4. **Read the canister's hash.** `state_hash` is a public, unguarded method, and
an *update* call, so the reply is consensus-backed and trustworthy:

```sh
icp canister call <canister-id> state_hash '()' -e ic
icp canister call <canister-id> state_hash '()' -n ic
# (blob "\81\50\a6\5e…")
```

4. **Compare.** If the canister's hash equals the one you computed, it serves
Pass the argument explicitly: with none, `icp canister call` opens an
interactive prompt instead of sending an empty one. Target the canister by
**principal** with `-n <network>`; `-e <environment>` resolves a canister *name*
out of a local project, which a third-party verifier does not have.

To compare the two values directly, take the reply as raw bytes; the hash is
its last 32:

```sh
icp canister call <canister-id> state_hash '()' -n ic -o hex | tail -c 65
# 8150a65e854b9bbb…
```

32 zero bytes is not a hash: it means the canister has none to report, either
because it has never completed a sync or because one is in progress right
now. A sync drops the cached hash as soon as it starts, since from that point
the canister may serve content the old hash no longer describes, and only a
sync that runs to completion caches a new one. Read it again once the deploy
finishes. A canister that keeps reporting zeros was left mid-sync, and there
is nothing to verify it against.

5. **Compare.** If the canister's hash equals the one you computed, it serves
exactly the build you reproduced from source. If it doesn't, either the served
content, headers, or redirects do not match that source, or it was deployed
with compressors this tool doesn't know about (see step 2).
with compressors this tool doesn't know about (see step 3).

A match needs no further checking of *how* the canister was synced. The hash
covers every stored encoding by its own hash, so matching it means the canister
Expand Down Expand Up @@ -107,6 +162,15 @@ parameters baked into the hash:
- **Byte format.** A versioned, length-prefixed, domain-separated SHA-256
stream (see the `state-hash` crate). Independent of map/header iteration order,
but bound to this layout version.
- **Synthesized content.** The preparation adds what a deploy adds: the clean-URL
and trailing-slash rules derived from the asset keys, and a `/*` catch-all at
status `404`, pointing at your own root `404.html` if the directory has one and
otherwise at the built-in [`404` page](routing.md#not-found-handling), which it
then adds as well. A root `/*` rule of your own (a single-page app's, say)
replaces both. All of it comes from the tool rather than from your directory,
which is why a directory with no `404.html` still matches: the verifier adds, or
withholds, exactly what the deploy did. It is pinned to the tool's release like
everything above.

The contract can change between releases; when it does, the format version is
bumped and every previously-computed hash is expected to change. Within a release
Expand All @@ -128,4 +192,4 @@ visitor's browser. The last link in that chain is the visitor's gateway: over a
so the state hash still says what the canister committed to but no longer guarantees
that a visitor received it.

<!-- Generated by scripts/sync-static-site.mjs from dfinity/certified-assets docs/verifying-contents.md at 65c0f32. Do not edit directly: the next sync overwrites it. Content changes belong upstream. -->
<!-- Generated by scripts/sync-static-site.mjs from dfinity/certified-assets docs/verifying-contents.md at v0.4.0. Do not edit directly: the next sync overwrites it. Content changes belong upstream. -->
Loading