diff --git a/.sources/upstream.json b/.sources/upstream.json index c5390506..bf40a958 100644 --- a/.sources/upstream.json +++ b/.sources/upstream.json @@ -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", diff --git a/docs/guides/frontends/static-site/access-protection.md b/docs/guides/frontends/static-site/access-protection.md index 6c72b786..f4548570 100644 --- a/docs/guides/frontends/static-site/access-protection.md +++ b/docs/guides/frontends/static-site/access-protection.md @@ -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 @@ -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. - + diff --git a/docs/guides/frontends/static-site/headers.md b/docs/guides/frontends/static-site/headers.md index f58b4020..7a915dfd 100644 --- a/docs/guides/frontends/static-site/headers.md +++ b/docs/guides/frontends/static-site/headers.md @@ -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 @@ -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: @@ -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 @@ -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. - + diff --git a/docs/guides/frontends/static-site/how-it-works.md b/docs/guides/frontends/static-site/how-it-works.md index da891f83..30e2f269 100644 --- a/docs/guides/frontends/static-site/how-it-works.md +++ b/docs/guides/frontends/static-site/how-it-works.md @@ -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 @@ -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 @@ -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. - + diff --git a/docs/guides/frontends/static-site/overview.md b/docs/guides/frontends/static-site/overview.md index 6d6968a3..eed9e2de 100644 --- a/docs/guides/frontends/static-site/overview.md +++ b/docs/guides/frontends/static-site/overview.md @@ -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 @@ -47,7 +47,7 @@ canisters: dir: dist # the directory of files to serve ``` -Replace `` with a released version (e.g. `v0.3.3`); see the +Replace `` 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. @@ -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). - + diff --git a/docs/guides/frontends/static-site/redirects.md b/docs/guides/frontends/static-site/redirects.md index bf37e9cc..dc9a86cf 100644 --- a/docs/guides/frontends/static-site/redirects.md +++ b/docs/guides/frontends/static-site/redirects.md @@ -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 @@ -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. - + diff --git a/docs/guides/frontends/static-site/routing.md b/docs/guides/frontends/static-site/routing.md index e692fd97..f4794d7f 100644 --- a/docs/guides/frontends/static-site/routing.md +++ b/docs/guides/frontends/static-site/routing.md @@ -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 @@ -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. - + diff --git a/docs/guides/frontends/static-site/site-files.md b/docs/guides/frontends/static-site/site-files.md index 1d4f68a3..112ad253 100644 --- a/docs/guides/frontends/static-site/site-files.md +++ b/docs/guides/frontends/static-site/site-files.md @@ -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 @@ -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). - + diff --git a/docs/guides/frontends/static-site/verifying-contents.md b/docs/guides/frontends/static-site/verifying-contents.md index 28e6f4c9..cef1ef17 100644 --- a/docs/guides/frontends/static-site/verifying-contents.md +++ b/docs/guides/frontends/static-site/verifying-contents.md @@ -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 @@ -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 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 @@ -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 state_hash '()' -e ic + icp canister call 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 `; `-e ` 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 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 @@ -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 @@ -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. - +