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
67 changes: 61 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,72 @@ 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
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
<http://localhost:8080>:

```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
Expand All @@ -97,10 +151,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
Expand Down
19 changes: 7 additions & 12 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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=<the commit the release names> \
./scripts/release-build.sh

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 '<the release public key>'
```

Expand Down
6 changes: 3 additions & 3 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 7 additions & 18 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 |
|---|---|
Expand Down Expand Up @@ -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`
Expand Down
15 changes: 5 additions & 10 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
```
Expand All @@ -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 }}'
```

Expand All @@ -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 }}'
```

Expand Down
6 changes: 3 additions & 3 deletions docs/design.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
Loading