diff --git a/docs/VERIFY.md b/docs/VERIFY.md new file mode 100644 index 0000000..c84ce3b --- /dev/null +++ b/docs/VERIFY.md @@ -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":"", ...} +``` + +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 +git log --oneline -- 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.