diff --git a/.github/workflows/build-next.yml b/.github/workflows/build-next.yml index c9588d60..220f1b7d 100644 --- a/.github/workflows/build-next.yml +++ b/.github/workflows/build-next.yml @@ -20,6 +20,8 @@ jobs: - uses: ./.github/actions/build with: gradle-args: ":app:assembleDebug :app:assembleNext --no-daemon" + - name: Checksums + run: sha256sum app/build/outputs/apk/debug/app-debug.apk app/build/outputs/apk/next/app-next.apk - uses: actions/upload-artifact@v7 with: name: subtracks-next diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ae3a139e..e8942447 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,6 +29,17 @@ jobs: keyproperties-base64: ${{ secrets.SUBTRACKS_KEYPROPERTIES_BASE64 }} keystore-password: ${{ secrets.SUBTRACKS_UPLOAD_STORE_PASSWORD }} key-password: ${{ secrets.SUBTRACKS_UPLOAD_KEY_PASSWORD }} + - name: Export release certificate + env: + KEYSTORE_BASE64: ${{ secrets.SUBTRACKS_KEYSTORE_BASE64 }} + KEYPROPERTIES_BASE64: ${{ secrets.SUBTRACKS_KEYPROPERTIES_BASE64 }} + KEYSTORE_PASSWORD: ${{ secrets.SUBTRACKS_UPLOAD_STORE_PASSWORD }} + run: | + umask 077 + echo "$KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/keystore" + alias=$(printf '%s' "$KEYPROPERTIES_BASE64" | base64 -d | sed -n 's/^keyAlias=//p' | tr -d '\r') + nix develop --command keytool -exportcert -rfc -keystore "$RUNNER_TEMP/keystore" -alias "$alias" -storepass "$KEYSTORE_PASSWORD" -file app/build/outputs/release-certificate.pem + rm -f "$RUNNER_TEMP/keystore" - uses: actions/upload-artifact@v7 with: name: release @@ -75,14 +86,37 @@ jobs: - name: Remove Play service account if: always() run: rm -f "$RUNNER_TEMP/play.json" - - name: Collect release APK - run: cp dist/apk/release/app-release.apk subtracks.apk + - name: Checksums and signature + id: checksums + run: | + cp dist/apk/release/app-release.apk subtracks.apk + sha256sum subtracks.apk > SHA256SUMS + echo "Artifact SHA-256:" + sha256sum dist/apk/release/app-release.apk dist/bundle/release/app-release.aab + echo "APK signature:" + certs=$(nix develop --command bash -c '"$ANDROID_HOME/build-tools/37.0.0/apksigner" verify --print-certs subtracks.apk') + printf '%s\n' "$certs" + fingerprint=$(printf '%s\n' "$certs" | sed -n 's/.*certificate SHA-256 digest: //p' | head -1) + echo "fingerprint=$fingerprint" >> "$GITHUB_OUTPUT" + - name: Compose release notes + run: | + { + cat "metadata/en-US/changelogs/${{ needs.build.outputs.version-code }}.txt" + echo + echo '---' + echo + echo "APK SHA-256: \`$(cut -d ' ' -f1 SHA256SUMS)\`" + echo + echo "Signing certificate SHA-256: \`${{ steps.checksums.outputs.fingerprint }}\`" + echo + echo 'The signing certificate is attached as `release-certificate.pem`.' + } > release-notes.md - name: Create GitHub release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - args=(--verify-tag --title "$GITHUB_REF_NAME" --notes-file "metadata/en-US/changelogs/${{ needs.build.outputs.version-code }}.txt") + args=(--verify-tag --title "$GITHUB_REF_NAME" --notes-file release-notes.md) if [ "${{ steps.stable.outputs.value }}" != "true" ]; then args+=(--prerelease) fi - gh release create "$GITHUB_REF_NAME" subtracks.apk "${args[@]}" + gh release create "$GITHUB_REF_NAME" subtracks.apk SHA256SUMS dist/release-certificate.pem "${args[@]}" diff --git a/README.md b/README.md index d8debc18..ae32d9f6 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,10 @@ subtracks is an open source Android client for [Subsonic-compatible](http://www. +## Verifying releases + +APKs are signed. Every GitHub release attaches `SHA256SUMS` and the signing certificate (`release-certificate.pem`); the `next` build key is committed. See [signing/README.md](signing/README.md) for the fingerprints and the commands to verify a download. + ## Features - **Large Libraries:** Built for very large libraries, from syncing the whole collection to playing playlists with thousands of tracks. diff --git a/signing/README.md b/signing/README.md new file mode 100644 index 00000000..0f7f8bb5 --- /dev/null +++ b/signing/README.md @@ -0,0 +1,40 @@ +# Verifying a subtracks download + +subtracks ships through three channels, each with its own signer: + +- **GitHub releases and Google Play** — signed with the release key. Every GitHub + release attaches `subtracks.apk`, a `SHA256SUMS` file and the public certificate + `release-certificate.pem`. +- **`next` builds** (the `subtracks-next` artifact from CI) — signed with the + public debug key committed here as [`next-certificate.pem`](next-certificate.pem). +- **F-Droid** — re-signed by F-Droid with its own key; see F-Droid's published + signing key. + +## `next` builds + +Certificate SHA-256: + +``` +DF:31:3F:18:58:E9:AD:F4:7F:44:65:B2:80:8F:DC:E5:B9:F8:8A:48:35:DC:D0:79:95:19:61:C3:05:05:08:37 +``` + +To check a `next` APK: + +```sh +apksigner verify --print-certs subtracks-next.apk +``` + +The `SHA-256 digest` line must equal the fingerprint above (the certificate is +committed at [`next-certificate.pem`](next-certificate.pem)). + +## GitHub releases + +Each release attaches the APK, the checksums and the certificate: + +```sh +sha256sum -c SHA256SUMS +apksigner verify --print-certs subtracks.apk # compare with the .pem +keytool -printcert -file release-certificate.pem # prints its SHA-256 +``` + +The signing certificate SHA-256 is also listed in the release notes. diff --git a/signing/next-certificate.pem b/signing/next-certificate.pem new file mode 100644 index 00000000..85d1c660 --- /dev/null +++ b/signing/next-certificate.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDEzCCAfugAwIBAgIIWpLReZ2f+20wDQYJKoZIhvcNAQEMBQAwNzELMAkGA1UE +BhMCVVMxEDAOBgNVBAoTB0FuZHJvaWQxFjAUBgNVBAMTDUFuZHJvaWQgRGVidWcw +IBcNMjYxMDA0MDMyNTU4WhgPMjA1NDAyMTkwMzI1NThaMDcxCzAJBgNVBAYTAlVT +MRAwDgYDVQQKEwdBbmRyb2lkMRYwFAYDVQQDEw1BbmRyb2lkIERlYnVnMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn4/a05V9p2CHVx2YWwPFHtecUocy +WmGQGPhN0VepKU2DC6LDCM7X8YBqS+RyIiDDVqmlUrnYu1CgbaglAySz8vMQBjUo +4XUiC7iyNX9JP5exfnGmFsWMM6zHIVyOViX1ZryZlMUAA+CXXj72Mkv1YAijtEu0 +JFHvGgUDDhPsJmToHQtEDHhvN+nUWR7zl7mVcrE3Al6YT+rfSgGOl5Sg6umjyx3c +VwtiQCR5eDW9YiGCDufjBJYox2oNFdvMViPcn9bJu1maAS+SMHL9gpcysriSDLp/ +rDs6an1zIP76zRPOVMJ/Aqi1JvMtcCBREY5qk3qwtNWQ6YnBcHKJ+7oHzwIDAQAB +oyEwHzAdBgNVHQ4EFgQUM7dLj1ZLZvtvLOlI9cap5RldteAwDQYJKoZIhvcNAQEM +BQADggEBACDujVeOnL95ybrTm9mwlVsG0DZ5ttRxt6JHEnVjHWOxv/W0F5FanEz1 +DonQIpRa4eOJ06eBvPcBVpNuLKzczpO1Ue3AaX6kwjWD4n6XYMVWZP6euOj2wvwK +93Kf4iG6ObMJ6WMA91VIvtTMvDGhzoL/5Kk5+esBbzLrBWLU/VJrySABFU5cwnqF +cP7kGR5JvHuJKiuJ/99yXZeg0piI3n/rdEYsjWkpIIIfaFY2j/n1r+evQ9J/aln/ +i0dif06Fl11KdFF4456nGH59S0SeckG9jKo04nj4fKeMqwE/7MDRpDa+a0e9Xu7Z +0jGgQKg1SFbI8xMrJn2ybdWquVZtKHo= +-----END CERTIFICATE-----