From d4296c122e924735a8b002ead30e1358799ff9a0 Mon Sep 17 00:00:00 2001 From: Serraniel Date: Sun, 25 Oct 2026 21:21:32 +0100 Subject: [PATCH 1/2] docs: correct claims that the project was never released Six files stated that Sendan has no published release. Seven releases exist, v0.3.3 is current, and the claims had been false since v0.1.0 was tagged on 31 August. They were not stale in a harmless way. The worst of them told a reader that the project's headline guarantee does not work: SECURITY.md: "Nothing published carries these signatures yet. v0.1.0 is tagged, but the workflow that builds and signs its artefacts was never started, and the release keys do not exist." Checked before removing it, rather than assumed. The v0.3.3 manifest verifies under both documented keys: minisign -Vm manifest.json -P RWQt826yhqM+... Signature and comment signature verified Trusted comment: sendan v0.3.3 client asset manifest go run ./tools/pq-sign verify release.pqpub manifest.json signature verified Both keys match the constants compiled into cmd/sendan-cli/verify.go, so the note was wrong about the keys existing too. docs/design.md opened by telling the reader that none of it is built. It is the first entry in the README's documentation table, so that was the first thing a reviewer of the design read. The v0.5.0 placeholders are replaced by a $VERSION variable rather than another literal, so the snippets stay copy-pasteable from one edit. Two bugs came out of this rather than only stale prose: - The image tags carry no `v`. The registry holds 0.1.0 through 0.3.3 and no v-prefixed tag, so `cosign verify ghcr.io/...:v0.5.0` was wrong in form as well as version and would have failed for anyone who substituted a real release. compose.yaml now records the convention where somebody pinning a deployment will read it. - docs/cli.md asked for Go 1.25. go.mod requires 1.26.0, so the documented build could not work on the version it named. Fixes the three "build this yourself because there is no image" passages in docs/deployment.md and compose.yaml, which told people to build from source as the only option while a signed multi-arch image was published. Signed-off-by: Serraniel Co-Authored-By: Claude Opus 5 Signed-off-by: Serraniel --- README.md | 7 ++++--- SECURITY.md | 19 +++++++------------ compose.yaml | 6 +++--- docs/cli.md | 25 +++++++------------------ docs/deployment.md | 15 +++++---------- docs/design.md | 6 +++--- 6 files changed, 29 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 59e21d2..d54b862 100644 --- a/README.md +++ b/README.md @@ -97,10 +97,11 @@ from, including this one. must be enabled explicitly. - **Single-container deployment.** One static binary serving an embedded web client, on an image with no distribution, no package manager and no shell. - *(Builds and runs; not yet published — see the status above.)* + *(Published at `ghcr.io/serraniel/sendan` for `linux/amd64` and + `linux/arm64`, signed, with provenance and an SBOM.)* - **Cross-platform command line client**, sharing one cryptographic - implementation with the server. *(Sends and receives; no binary published - yet — see the status above.)* + implementation with the server. *(Signed static binaries for six + platforms; [`docs/cli.md`](docs/cli.md) has the install steps.)* - **Light and dark themes**, following the system by default with a control to override it, on one layout that holds from a small phone to a desktop display. No web font is loaded and no request leaves the instance: the whole diff --git a/SECURITY.md b/SECURITY.md index a4fac29..1adc8a6 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -177,13 +177,6 @@ assume. Each release publishes a binary for every supported platform and a [github.com/Serraniel/sendan/releases](https://github.com/Serraniel/sendan/releases). [`docs/cli.md`](docs/cli.md) has the per-platform download and install steps. -> [!NOTE] -> **Nothing published carries these signatures yet.** `v0.1.0` is tagged, but -> the workflow that builds and signs its artefacts was never started, and the -> release keys do not exist. This describes what the pipeline publishes once -> both are in place. Until then, build from source — which is the stronger -> check anyway, and is the second procedure below. - **Check what you downloaded:** ```sh @@ -196,9 +189,11 @@ the release was built from the source in this repository — for that, build it yourself and compare: ```sh -git checkout v0.5.0 # the tag the release names +VERSION=v0.3.3 # the tag the release names + +git checkout "$VERSION" -SENDAN_VERSION=v0.5.0 \ +SENDAN_VERSION="$VERSION" \ SENDAN_COMMIT= \ ./scripts/release-build.sh @@ -255,8 +250,8 @@ sendan verify https://files.example.org ``` instance https://files.example.org - claims v0.5.0, commit 1a2b3c4, unmodified - manifest v0.5.0 + claims v0.3.3, commit 1a2b3c4, unmodified + manifest v0.3.3 signed by this build's release key ✓ 41 of 41 assets match the published client @@ -315,7 +310,7 @@ The first is [minisign](https://jedisct1.github.io/minisign/) format, so the command line client is not the only thing that can check it: ```sh -gh release download v0.5.0 -p 'manifest.json*' +gh release download v0.3.3 -p 'manifest.json*' minisign -Vm manifest.json -P '' ``` diff --git a/compose.yaml b/compose.yaml index f62ca09..aa37f37 100644 --- a/compose.yaml +++ b/compose.yaml @@ -14,9 +14,9 @@ name: sendan services: sendan: # Both keys on purpose: `docker compose up` uses the published image, and - # `docker compose up --build` builds this checkout instead. Until a release - # is tagged there is no published image, so the second is the one that - # works. + # `docker compose up --build` builds this checkout instead. The tag follows + # the releases and carries no `v`, so pin `:0.3.3` rather than `:v0.3.3` + # for a deployment that should not move on its own. image: ghcr.io/serraniel/sendan:latest build: . restart: unless-stopped diff --git a/docs/cli.md b/docs/cli.md index fd3f32e..aa3d5a0 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -8,16 +8,9 @@ asked to obtain and trust — see [`docs/design.md`](design.md) §7.1 — so it none of the server's dependencies: no database drivers, no object storage client, no embedded web client. -> [!IMPORTANT] -> **No release has been published yet.** The release pipeline is in place and -> produces binaries and checksums for every platform below, but no tag has been -> cut, so today the only way to get `sendan` is to build it. The download -> instructions describe what a release will provide; the build instructions -> describe what works now. - ## Building it -Needs [Go](https://go.dev/dl/) 1.25 or later, and nothing else. +Needs [Go](https://go.dev/dl/) 1.26 or later, and nothing else. ```sh git clone https://github.com/Serraniel/sendan @@ -37,8 +30,8 @@ They land in `dist/`, with a `SHA256SUMS` beside them. ## Installing a release -Once a release exists, each one carries a static binary per platform. There is -nothing to install: the binary has no runtime dependency. +Each release carries a static binary per platform. There is nothing to install: +the binary has no runtime dependency. | Platform | File | |---|---| @@ -218,20 +211,16 @@ Ed25519 RWQt826yhqM+nKsvcrv1lu/eePUVXmE2haeCmGUBpgzwu7CWVZxyRUVk SLH-DSA U/vAc3e81vHgYeqfxDWYGHjGAtTZm4bz5Tz4lM42TJs= ``` -> [!IMPORTANT] -> **Nothing is signed with them yet.** `v0.1.0` is tagged but carries no -> artefacts, so `sendan verify` against that release finds nothing to check. -> Until it is rebuilt, verify with `--manifest` pointing at a manifest you -> produced yourself: +> [!TIP] +> **`--manifest` against a manifest you built yourself is the stronger check**, +> because it compares the instance to a client you produced rather than to a +> statement somebody else signed: > > ```sh > (cd web && npm ci && npm run build) > ./scripts/asset-manifest.sh /tmp/manifest.json > sendan verify https://files.example.org --manifest /tmp/manifest.json > ``` -> -> That is the stronger check anyway: it compares the instance against a client -> you built, rather than against a statement somebody else signed. The first signature is in [minisign](https://jedisct1.github.io/minisign/) format, so `minisign -Vm manifest.json -P RWQt826yhqM+nKsvcrv1lu/eePUVXmE2haeCmGUBpgzwu7CWVZxyRUVk` diff --git a/docs/deployment.md b/docs/deployment.md index f386dea..058be6e 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -4,11 +4,6 @@ Sendan is one static binary. The container image is that binary on an empty base — no distribution, no package manager, no shell. Around 17 MB, of which about 16 is the program. -> [!NOTE] -> **No image has been published yet**, because no release has been tagged. The -> pipeline that builds and signs it is in place. Until a tag exists, build the -> image yourself from a checkout — the instructions below work either way. - ## The short way [`compose.yaml`](../compose.yaml) in the repository root runs Sendan behind a @@ -22,8 +17,8 @@ That is the whole deployment. The domain must already resolve to the host, and ports 80 and 443 must reach it, because the proxy proves control of the name to get a certificate. -Until a release is tagged there is no published image, so build this checkout -instead: +That pulls the published image. To build this checkout instead — which is what +you want when running a modified tree, or to reproduce an image yourself: ```sh SENDAN_DOMAIN=files.example.org docker compose up -d --build @@ -335,7 +330,7 @@ binary of the same version are the same program. Each image is signed at release time: ```sh -cosign verify ghcr.io/serraniel/sendan:v0.5.0 \ +cosign verify ghcr.io/serraniel/sendan:0.3.3 \ --certificate-identity-regexp '^https://github\.com/Serraniel/sendan/\.github/workflows/release\.yml@' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com ``` @@ -351,7 +346,7 @@ architecture. The provenance is what ties an image to this repository and to the workflow run that produced it: ```sh -docker buildx imagetools inspect ghcr.io/serraniel/sendan:v0.5.0 \ +docker buildx imagetools inspect ghcr.io/serraniel/sendan:0.3.3 \ --format '{{ json .Provenance }}' ``` @@ -363,7 +358,7 @@ base images the build used. The bill of materials, for asking what is in the image without running it: ```sh -docker buildx imagetools inspect ghcr.io/serraniel/sendan:v0.5.0 \ +docker buildx imagetools inspect ghcr.io/serraniel/sendan:0.3.3 \ --format '{{ json .SBOM }}' ``` diff --git a/docs/design.md b/docs/design.md index 24e2461..312fd53 100644 --- a/docs/design.md +++ b/docs/design.md @@ -1,9 +1,9 @@ # Sendan — Design > [!NOTE] -> This document describes software that has not yet been implemented. It records -> decisions and their reasoning so that they need not be re-argued, and so that -> the cryptographic scheme can be reviewed before it is built. +> This document records design decisions and their reasoning, so that they need +> not be re-argued. It was written ahead of the implementation, which is why it +> reads as a plan in places; the scheme it describes is the one that is built. ## 1. Stack From 88d1cedacb87cb8befb54df852e6f30a4fadeaa7 Mon Sep 17 00:00:00 2001 From: Serraniel Date: Sun, 25 Oct 2026 21:23:48 +0100 Subject: [PATCH 2/2] docs: give the README an install path The README had none. Its headings ran Try it, Features, Cryptographic design, Browser requirements, Documentation, Roadmap, License, Security - somebody who wanted to run Sendan had to reach docs/deployment.md through a table two thirds of the way down, and until the previous commit that document opened by saying no image had been published. Adds a "Run it" section between "Try it" and "Features": - a single `docker run` for a local trial - `compose.yaml` for a real deployment, with the reason the first is not sufficient for one: links are built from SENDAN_BASE_URL, and streaming an upload needs HTTP/2, which browsers only offer over TLS - the `:0.3.3` rather than `:latest` pin, and the absent `v` - the command line client, with the checksum step not left as an exercise Both commands were run rather than written from the documentation. The published image starts with no configuration at all and serves the client: $ docker run -d -p 18080:8080 -v ... ghcr.io/serraniel/sendan:0.3.3 {"msg":"metadata store ready","location":"sqlite:/var/lib/sendan/sendan.db"} {"msg":"blob store ready","location":"file:/var/lib/sendan/blobs"} {"msg":"reaper started","interval":"5m0s","batch":500} {"msg":"listening","addr":":8080"} GET / 200, 2617 bytes GET /api/source 200, v0.3.3 and both documented download URLs return 200. Also corrects the `sendan verify` sample output in "Try it", which still showed v0.3.0 and 31 assets. The demo instance reports v0.3.3 at commit a5fbc01 and the v0.3.3 manifest carries 33 assets, so the numbers are now the ones a reader reproduces by running the command against the demo. Signed-off-by: Serraniel Co-Authored-By: Claude Opus 5 Signed-off-by: Serraniel --- README.md | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d54b862..8e6f97e 100644 --- a/README.md +++ b/README.md @@ -65,11 +65,11 @@ that was published: ```console $ sendan verify https://demo.sendan.app instance https://demo.sendan.app - claims v0.3.0, commit 7d7904e, unmodified - manifest v0.3.0 + claims v0.3.3, commit a5fbc01, unmodified + manifest v0.3.3 signed by this build's release keys, classical and post-quantum - ✓ 31 of 31 assets match the published client + ✓ 33 of 33 assets match the published client ``` That compares what the instance serves against a manifest taken from the @@ -77,6 +77,60 @@ release rather than from the instance, so the answer does not depend on the instance being honest. Run it against any instance somebody sends you a link from, including this one. +## Run it + +One container, no runtime dependency. The image defaults to SQLite and +filesystem storage, so this is a working instance on +: + +```sh +docker run -d --name sendan -p 8080:8080 \ + -v sendan:/var/lib/sendan \ + ghcr.io/serraniel/sendan:0.3.3 +``` + +Image tags follow the releases and carry **no `v`**, so pin `:0.3.3` rather +than `:latest` for a deployment that should not move on its own. + +That is enough to try it locally. It is deliberately not enough to deploy: +download links are built from `SENDAN_BASE_URL`, which defaults to +`http://localhost:8080`, and the browser client needs TLS for more than +confidentiality — streaming an upload requires HTTP/2, which browsers only +offer over a secure origin. + +So for anything reachable by other people, [`compose.yaml`](compose.yaml) runs +Sendan behind a reverse proxy that obtains its own certificate: + +```sh +SENDAN_DOMAIN=files.example.org docker compose up -d +``` + +The domain must already resolve to the host and ports 80 and 443 must reach it, +because the proxy proves control of the name to get a certificate. That is the +whole deployment. + +| Going further | | +|---|---| +| PostgreSQL, S3-compatible storage, backups, the at-rest master key | [`docs/deployment.md`](docs/deployment.md) | +| Every variable and its default | [`docs/configuration.md`](docs/configuration.md) | + +### The command line client + +A single static binary, for Linux, macOS and Windows on both architectures. +Using it means never executing an instance's code, which is the one thing a +browser cannot avoid: + +```sh +curl -LO https://github.com/Serraniel/sendan/releases/latest/download/sendan-linux-amd64 +curl -LO https://github.com/Serraniel/sendan/releases/latest/download/SHA256SUMS + +sha256sum -c SHA256SUMS --ignore-missing +chmod +x sendan-linux-amd64 && sudo mv sendan-linux-amd64 /usr/local/bin/sendan +``` + +[`docs/cli.md`](docs/cli.md) has the other five platforms, every command and +option, and how to check the release signatures by hand. + ## Features - **End-to-end encryption in every code path.** There is no configuration in