docs: describe how to verify the relay revision, APK and native libraries

Also state plainly what is not reproducible today and what we plan to do
about it.
This commit is contained in:
Julius Joska
2026-10-23 09:15:15 +02:00
parent ae127decd9
commit d2eb0d4f8c
+149
View File
@@ -0,0 +1,149 @@
# Verifying Šepot
This document lists what you can check yourself today, how to do it, and what
cannot be checked yet. If something here does not match what you observe,
please report it to security@sepot.cz.
## 1. Which relay code is running
The relay announces its source and revision:
```sh
curl -s https://relay.sepot.cz/v1/server-info
# {"source":"https://github.com/juliusjoska/sepot","revision":"<commit>", ...}
```
The revision is baked into the container image at build time
(`deploy/relay/Dockerfile`, build argument `GIT_SHA`, also stored as the OCI
label `org.opencontainers.image.revision`). To read the code behind it:
```sh
git clone https://github.com/juliusjoska/sepot && cd sepot
git show --stat <commit>
git log --oneline <commit> -- rust/ proto/ deploy/relay/
```
**Current state (October 2026).** The relay deployed on 9 October 2026 reports
`4b3f4bb`. That is a commit of our private development repository, so GitHub
does not know it. The public commit with the same relay code is
[`b824f62`](https://github.com/juliusjoska/sepot/commit/b824f628788afa3e1a3b64fa90de1fc7bf50feaf).
The two trees differ only in source comments, test fixture IP addresses in
`push.rs`, the `repository` URL in `rust/Cargo.toml`, and migration
`0001_init.sql`, which in the private tree already contained the
`attempt_count` column that `0003_queue_attempt_count.sql` adds with
`ADD COLUMN IF NOT EXISTS`. The resulting schema and behaviour are the same.
**From the next relay deployment on**, images are built from this public
repository only. The build script refuses a commit that is not on `main` here
and not yet mirrored to GitHub, and refuses to build when the relay sources in
the public and the development repository differ. The reported revision is
then a full commit hash you can open on GitHub.
What this does *not* prove: the revision is reported by the server itself.
There is no remote attestation, so you are trusting us that the binary was
built from that commit. The relay is designed so that this trust matters as
little as possible: message content is end-to-end encrypted on your device and
the relay never holds the keys.
## 2. Android APK
Direct downloads are attached to the
[GitHub releases](https://github.com/juliusjoska/sepot/releases), together
with a `SHA256SUMS` file.
Since 1.2.1 the APK on GitHub is the universal APK that Google Play generates
from the same app bundle it serves to Play users, signed with the same key.
That means:
- it installs over the Google Play version and vice versa, and
- its signing certificate must be:
```
SHA-256: 9B:4C:DA:44:A1:80:81:26:3D:79:86:01:2B:9D:11:CA:72:AB:D0:24:9C:D9:DC:AB:82:5F:BE:AE:95:40:2D:02
apksigner prints it as 9b4cda44a18081263d7986012b9d11ca72abd0249cd9dcab825fbeae95402d02
```
Check a download:
```sh
sha256sum -c sepot-1.2.1-SHA256SUMS.txt
apksigner verify --print-certs sepot-1.2.1-universal.apk | grep 'SHA-256'
```
Check the app already installed on your phone (USB debugging enabled):
```sh
adb pull "$(adb shell pm path cz.sepot.sepot | head -1 | cut -d: -f2)" installed.apk
apksigner verify --print-certs installed.apk | grep 'SHA-256'
```
**Older direct APKs (1.1.1 and earlier)** were signed with our upload key
(`E2:13:D6:60:63:B9:0E:A1:73:90:1C:37:9E:4B:43:9C:76:8A:F1:AF:C2:D6:55:68:CC:46:CF:31:36:72:3F:31`).
Android will not update such an install with the 1.2.1 APK. Write down your
24-word recovery phrase, uninstall, install the new APK and restore the
account from the phrase. Message history is not carried over.
Source of each release is the tag of the same name (`v1.2.1`, …). The public
tree is an export of our development tree: comments are rewritten, deployment
configuration is left out, and so is a store-screenshot demo mode. That mode
is behind a compile-time flag (`SEPOT_DEMO`) that is not set for release
builds, so the compiler removes it.
## 3. Native cryptography library
End-to-end encryption uses [libsignal](https://github.com/signalapp/libsignal)
through the Dart package [`libsignal`](https://pub.dev/packages/libsignal)
(5.0.9 at the moment). The Dart package itself is pinned by its SHA-256 in
`app/pubspec.lock`. Its build hook downloads a prebuilt native library,
`libsignal_frb`, from the GitHub releases of
[djx-y-z/libsignal_dart](https://github.com/djx-y-z/libsignal_dart) and only
compares it with a checksum file from the same release.
We pin those binaries ourselves in
[`app/tool/native_libs.sha256`](../app/tool/native_libs.sha256): the upstream
archives and the library exactly as it ends up in the APK (the Android Gradle
plugin strips it while packaging). Our release build runs
`app/tool/verify_native_libs.sh` on the finished bundle and does not ship it on
any mismatch.
You can repeat both checks:
```sh
# the library inside an APK or AAB matches the pins
app/tool/verify_native_libs.sh sepot-1.2.1-universal.apk
# the pins follow from the upstream release: download, check archive hashes,
# strip with llvm-strip from the Android NDK and compare
LLVM_STRIP=/path/to/ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip \
app/tool/verify_native_libs.sh --from-upstream
```
## 4. What is not reproducible (yet)
- **Android builds are not bit-for-bit reproducible.** A build from the tag
gives you a working app, but not the same bytes: the Flutter AOT snapshot
depends on the build environment, and Google Play re-signs and splits our
bundle. Today you can compare the signing certificate, the APK hash between
download channels, and the native crypto library. You cannot compare the
compiled Dart code against your own build.
- **`libsignal_frb` is a third-party prebuilt binary.** The pins guarantee
that every build ships the same file and that the file is the published
upstream release. They do not prove that the release was built from the
`libsignal_frb` source; the upstream release has no build provenance
attestation.
- **iOS** binaries from the App Store are encrypted by Apple and cannot be
compared with anything.
- **The relay image** is not reproducible either: the base images are
referenced by tag and the build applies current Debian security updates.
## 5. Plan
1. Relay revision equal to a public commit: build tooling is in place, the next
relay deployment will report it.
2. Build `libsignal_frb` from source on our own build server together with the
upgrade to the current libsignal release, and publish the resulting hashes
next to the pins above.
3. Relay: pin base images by digest and publish the image digest with each
relay deployment.
4. Work towards a reproducible Android build that independent rebuilders
(for example F-Droid) can confirm.