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
52 changes: 52 additions & 0 deletions .github/workflows/verify_release_assets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,55 @@ jobs:
echo "Fix the assets on ${TAG}."
exit 1
fi

- name: check signing certificate
if: ${{ !cancelled() }}
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.event.release.tag_name || inputs.tag }}
CERT_SHA: 88b0a8a6fb94ee73a454a0f92732bd408e92e51ec1e9556744e5f00556100977
PLAY_CERT_SHA: c20da21dda9d35452675341911b001e53280b5c891ab909b13bf27637740a6af
run: |
set -u
ver="${TAG#v}"
colon_fp() { echo "$1" | tr 'a-f' 'A-F' | sed 's/../&:/g; s/:$//'; }
cert_colon=$(colon_fp "${CERT_SHA}")
play_colon=$(colon_fp "${PLAY_CERT_SHA}")
fail=0
gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" --jq '.body' > body.md
if grep -qF "${cert_colon}" < <(tr 'a-f' 'A-F' < body.md); then
echo " OK release notes carry the signing fingerprint"
else
echo " MISS release notes do not carry ${cert_colon}, append scripts/apk_verification.md"
fail=1
fi
for fp in $(grep -oiE '([0-9a-f]{2}:){31}[0-9a-f]{2}' body.md | tr 'a-f' 'A-F' | sort -u || true); do
if [ "${fp}" != "${cert_colon}" ] && [ "${fp}" != "${play_colon}" ]; then
echo " BAD release notes carry an unknown fingerprint ${fp}"
fail=1
fi
done
bt=$(ls "${ANDROID_HOME}/build-tools" | sort -V | tail -1)
apksigner="${ANDROID_HOME}/build-tools/${bt}/apksigner"
mkdir -p apks
for apk in "Aster-Mail.apk" "Aster-Mail-${ver}-full.apk" "Aster-Mail-fdroid-${ver}.apk"; do
if ! gh release download "${TAG}" --repo "${GITHUB_REPOSITORY}" --pattern "${apk}" --dir apks --clobber; then
echo " MISS ${apk} could not be downloaded"
fail=1
continue
fi
digests=$("${apksigner}" verify --print-certs "apks/${apk}" | tr -d '\r' | grep -E '^Signer #[0-9]+ certificate SHA-256 digest:' | awk '{print $NF}' | sort -u || true)
if [ "${digests}" = "${CERT_SHA}" ]; then
echo " OK ${apk} is signed with the release certificate"
else
echo " BAD ${apk} signers: ${digests:-none}, expected ${CERT_SHA}"
fail=1
fi
done
if [ "${fail}" != "0" ]; then
echo
echo "Every APK on a release must be signed with the release certificate, and the"
echo "release notes must publish its fingerprint so people can check their download."
echo "Fix ${TAG}."
exit 1
fi
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ _ui*.xml
!/scripts/release.sh
!/scripts/build_fdroid_in_container.sh
!/scripts/README_release.md
!/scripts/apk_verification.md
!/scripts/check_fdroid_assets.sh
!/scripts/sync_translations.py
/.test_artifacts/
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,23 @@ Aster runs on a zero-access architecture located in Germany. This means we store

Go to [astermail.org](https://astermail.org) to create a free account. If you would like to contribute code to Aster, see [CONTRIBUTING.md](https://github.com/Aster-Privacy/.github/blob/main/CONTRIBUTING.md) for instructions.

## Verify your download

Every APK we publish on [GitHub Releases](https://github.com/Aster-Privacy/Aster-Android/releases) and through F-Droid is signed with the Aster release certificate. Before you install an APK you downloaded, check that its signing certificate matches this one.

- **Package name:** `org.astermail.android`
- **SHA-256 certificate fingerprint:** `88:B0:A8:A6:FB:94:EE:73:A4:54:A0:F9:27:32:BD:40:8E:92:E5:1E:C1:E9:55:67:44:E5:F0:05:56:10:09:77`

To check a downloaded APK on a computer, run this command from the Android SDK build tools:

```
apksigner verify --print-certs Aster-Mail.apk
```

The `certificate SHA-256 digest` line must show the fingerprint above, in lowercase and without colons. To check the app on your phone, use a verification app such as AppVerifier with the package name and fingerprint above. After the first install, Android accepts an update only if it's signed with the same certificate.

If you install Aster Mail from Google Play, Google Play signs it with its own key, so that copy has a different fingerprint: `C2:0D:A2:1D:DA:9D:35:45:26:75:34:19:11:B0:01:E5:32:80:B5:C8:91:AB:90:9B:13:BF:27:63:77:40:A6:AF`. Because the certificates differ, you can't install a GitHub APK over a Google Play install, or the reverse, without uninstalling first.

## Community

Join our [Discord](https://discord.gg/R4XqRUfgWZ) to share feedback, ask questions, and contribute to the privacy community. You can also find us on [X](https://x.com/AsterPrivacy) and [Reddit](https://www.reddit.com/r/AsterPrivacy).
Expand Down
7 changes: 4 additions & 3 deletions scripts/README_release.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ Add `--dry-run` to build, sign, and verify without pushing or publishing anythin
1. Write the Play changelog to `fastlane/metadata/android/en-US/changelogs/<versionCode>.txt` and
commit it on `main`. The hard cap is 500 bytes, enforced by `validate-fastlane-supply-metadata`.
2. Write the GitHub release notes to `../.release_work/notes-<version>.md`. Start with the standard
install alert, then group under `## What's new` and `## Fixed`.
install alert, then group under `## What's new` and `## Fixed`. Don't add the signing
fingerprint: the script appends `scripts/apk_verification.md` for you.

## What the script does

Expand All @@ -24,8 +25,8 @@ Add `--dry-run` to build, sign, and verify without pushing or publishing anythin
| Bump | Sets `versionName` and `versionCode` in `app/build.gradle.kts`, commits, tags. |
| Build | `assembleFullRelease bundleFullRelease` on this machine, then the `fdroid` flavor from the tag inside the F-Droid buildserver container (`scripts/build_fdroid_in_container.sh`, Docker Desktop must be running). The two builds are separate on purpose, see below. |
| Sign | Signs the unsigned fdroid APK with `apksigner` and `--alignment-preserved`, never `zipalign`. |
| Verify | Both APKs must carry cert SHA-256 `88b0a8a6…`, the fdroid APK must have no `0xd935` alignment padding, no CRLF in text assets or `META-INF/services`, and its dex must reference no Google Play Services, Firebase, or Play classes. |
| Publish | Creates the GitHub release with `Aster-Mail.apk`, `Aster-Mail-<version>-full.apk`, and `Aster-Mail-fdroid-<version>.apk`, then re-uploads `Aster-Mail.apk` to the current Aster-Mail **Latest** release, which is what astermail.org serves. |
| Verify | Both APKs must carry cert SHA-256 `88b0a8a6…`, the fdroid APK must have no `0xd935` alignment padding, no CRLF in text assets or `META-INF/services`, and its dex must reference no Google Play Services, Firebase, or Play classes. The notes must not be empty, and the final notes must carry the release fingerprint and no other fingerprint. |
| Publish | Creates the GitHub release with `Aster-Mail.apk`, `Aster-Mail-<version>-full.apk`, and `Aster-Mail-fdroid-<version>.apk`, then re-uploads `Aster-Mail.apk` to the current Aster-Mail **Latest** release, which is what astermail.org serves. The **Verify the APK** section from `scripts/apk_verification.md` is appended to both release bodies when it's missing. |
| Play | Uploads the AAB with `fastlane supply` if a service account is configured, otherwise copies the AAB to `~/Downloads` and says so. |
| Audit | Runs the script named by `ASTER_CHANNEL_AUDIT`, if set, and prints the per-channel result. |

Expand Down
10 changes: 10 additions & 0 deletions scripts/apk_verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## Verify the APK

Every Aster Mail APK in this release is signed with the Aster release certificate. Before you install one, check that its signing certificate matches this fingerprint.

- **Package name:** `org.astermail.android`
- **SHA-256 certificate fingerprint:** `88:B0:A8:A6:FB:94:EE:73:A4:54:A0:F9:27:32:BD:40:8E:92:E5:1E:C1:E9:55:67:44:E5:F0:05:56:10:09:77`

To check a downloaded APK on a computer, run `apksigner verify --print-certs Aster-Mail.apk` from the Android SDK build tools. The `certificate SHA-256 digest` line shows the same value in lowercase without colons. To check the app on your phone, use a verification app such as AppVerifier.

If you install Aster Mail from Google Play, Google Play signs it with its own key, so that copy has a different fingerprint: `C2:0D:A2:1D:DA:9D:35:45:26:75:34:19:11:B0:01:E5:32:80:B5:C8:91:AB:90:9B:13:BF:27:63:77:40:A6:AF`
38 changes: 37 additions & 1 deletion scripts/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,14 @@ clone="$work/Aster-Android-$ver"
out_dir="$work/out-$ver"
notes="$work/notes-$ver.md"
cert_sha="88b0a8a6fb94ee73a454a0f92732bd408e92e51ec1e9556744e5f00556100977"
play_cert_sha="c20da21dda9d35452675341911b001e53280b5c891ab909b13bf27637740a6af"

say() { echo; echo "== $* =="; }
die() { echo "FAIL: $*" >&2; exit 1; }
colon_fp() { echo "$1" | tr 'a-f' 'A-F' | sed 's/../&:/g; s/:$//'; }
cert_colon="$(colon_fp "$cert_sha")"
play_colon="$(colon_fp "$play_cert_sha")"
has_fp() { grep -qF "$1" < <(tr 'a-f' 'A-F' < "$2"); }

say "preflight"
sdk="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-${LOCALAPPDATA:-}/Android/Sdk}}"
Expand All @@ -66,6 +71,7 @@ gh auth status >/dev/null 2>&1 || die "gh is not authenticated"
[ -f "$repo_root/app/google-services.json" ] || die "app/google-services.json missing (gitignored build input)"
[ -f "$repo_root/keystore/aster-mail-upload-v3.jks" ] || die "signing keystore missing"
[ -f "$notes" ] || die "no release notes at $notes"
grep -q '[^[:space:]]' "$notes" || die "release notes at $notes are empty"
echo "apksigner: $apksigner (build-tools $bt_ver)"
echo "notes: $notes"

Expand All @@ -80,6 +86,14 @@ git clone -q --branch main https://github.com/Aster-Privacy/Aster-Android.git "$
cd "$clone"
echo "HEAD $(git rev-parse --short HEAD)"

verification="$clone/scripts/apk_verification.md"
[ -f "$verification" ] || die "scripts/apk_verification.md missing on main"
has_fp "$cert_colon" "$verification" || die "scripts/apk_verification.md does not carry the release fingerprint $cert_colon"
has_fp "$play_colon" "$verification" || die "scripts/apk_verification.md does not carry the Play fingerprint $play_colon"
for fp in $(grep -oiE '([0-9a-f]{2}:){31}[0-9a-f]{2}' "$verification" | tr 'a-f' 'A-F'); do
[ "$fp" = "$cert_colon" ] || [ "$fp" = "$play_colon" ] || die "scripts/apk_verification.md carries an unknown fingerprint $fp"
done

cur_vc=$(grep -oE 'versionCode = [0-9]+' app/build.gradle.kts | grep -oE '[0-9]+' | head -1)
[ -n "$cur_vc" ] || die "could not read versionCode from main"
for tag in $(git tag -l 'v*' --sort=-v:refname | head -20); do
Expand Down Expand Up @@ -239,6 +253,19 @@ PYEOL
echo " OK fdroid APK text assets use LF line endings"
fi

say "release notes"
final_notes="$out_dir/notes-$ver.md"
if has_fp "$cert_colon" "$notes"; then
cp "$notes" "$final_notes"
else
{ cat "$notes"; printf '\n\n'; cat "$verification"; } > "$final_notes"
fi
has_fp "$cert_colon" "$final_notes" || die "release notes do not carry the release fingerprint"
for fp in $(grep -oiE '([0-9a-f]{2}:){31}[0-9a-f]{2}' "$final_notes" | tr 'a-f' 'A-F'); do
[ "$fp" = "$cert_colon" ] || [ "$fp" = "$play_colon" ] || die "release notes carry an unknown fingerprint $fp"
done
echo " OK $final_notes carries the signing fingerprint"

if [ "$dry_run" = 1 ]; then
say "dry run complete"
echo "artifacts in $out_dir"
Expand All @@ -257,12 +284,21 @@ say "GitHub release"
assets=("$out_dir/Aster-Mail.apk" "$out_dir/Aster-Mail-$ver-full.apk")
if [ "$skip_fdroid" != "1" ]; then assets+=("$out_dir/Aster-Mail-fdroid-$ver.apk"); fi
gh release create "v$ver" --repo Aster-Privacy/Aster-Android --title "v$ver" \
--notes-file "$notes" "${assets[@]}"
--notes-file "$final_notes" "${assets[@]}"

say "carry APK to the site download target"
mail_tag=$(gh api repos/Aster-Privacy/Aster-Mail/releases/latest -q .tag_name)
gh release upload "$mail_tag" --repo Aster-Privacy/Aster-Mail --clobber "$out_dir/Aster-Mail.apk"
echo "uploaded Aster-Mail.apk to Aster-Mail $mail_tag"
mail_body="$out_dir/mail-body-$mail_tag.md"
gh release view "$mail_tag" --repo Aster-Privacy/Aster-Mail --json body --jq .body > "$mail_body"
if has_fp "$cert_colon" "$mail_body"; then
echo "Aster-Mail $mail_tag notes already carry the signing fingerprint"
else
{ cat "$mail_body"; printf '\n\n'; cat "$verification"; } > "$mail_body.new"
gh release edit "$mail_tag" --repo Aster-Privacy/Aster-Mail --notes-file "$mail_body.new"
echo "added the signing fingerprint to the Aster-Mail $mail_tag notes"
fi

say "Google Play"
play_key="${ASTER_PLAY_SERVICE_ACCOUNT_JSON:-$repo_root/../.ops/play_service_account.json}"
Expand Down
Loading