Skip to content

Commit 50fc733

Browse files
committed
Add APK signing
1 parent 3189a9b commit 50fc733

File tree

4 files changed

+258
-5
lines changed

4 files changed

+258
-5
lines changed

.github/ANDROID_SIGNING.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Android APK Signing Guide
2+
3+
## Overview
4+
5+
Android requires all APKs to be signed before they can be installed on devices. This repository supports both **debug signing** (for development/testing) and **release signing** (for production releases).
6+
7+
## CI/PR Builds (Debug Signing)
8+
9+
PR builds in the `ci.yml` workflow automatically sign APKs with a **debug keystore**. These are suitable for:
10+
- Testing on development devices
11+
- Internal QA
12+
- Feature validation
13+
14+
The debug-signed APKs can be downloaded from the PR's workflow artifacts and installed on any Android device with developer mode enabled.
15+
16+
## Release Builds (Production Signing)
17+
18+
For production releases via the `release.yml` workflow, you should configure a **release keystore** using GitHub Secrets.
19+
20+
### Creating a Release Keystore
21+
22+
If you don't already have a release keystore, create one:
23+
24+
```bash
25+
keytool -genkeypair -v \
26+
-keystore betaflight-release.keystore \
27+
-storepass <YOUR_STORE_PASSWORD> \
28+
-alias betaflight \
29+
-keypass <YOUR_KEY_PASSWORD> \
30+
-keyalg RSA \
31+
-keysize 2048 \
32+
-validity 10000 \
33+
-dname "CN=Betaflight,O=Betaflight,C=US"
34+
```
35+
36+
**⚠️ IMPORTANT**: Keep this keystore file and passwords secure. If you lose them, you cannot update your app on the Play Store!
37+
38+
### Configure GitHub Secrets
39+
40+
Add the following secrets to your GitHub repository (Settings → Secrets and variables → Actions):
41+
42+
1. **ANDROID_KEYSTORE_BASE64**
43+
```bash
44+
base64 -w 0 betaflight-release.keystore
45+
```
46+
Copy the output and paste it as the secret value.
47+
48+
2. **ANDROID_KEYSTORE_PASSWORD**
49+
The password you used for `-storepass` when creating the keystore.
50+
51+
3. **ANDROID_KEY_ALIAS**
52+
The alias you used (e.g., `betaflight`).
53+
54+
4. **ANDROID_KEY_PASSWORD**
55+
The password you used for `-keypass` when creating the keystore.
56+
57+
### How Release Signing Works
58+
59+
When you trigger a release build:
60+
61+
1. If **all four secrets are configured**, the workflow will:
62+
- Decode the base64 keystore
63+
- Sign both the APK and AAB with your release key
64+
- Upload signed artifacts with `-release-signed` suffix
65+
66+
2. If **secrets are NOT configured**, the workflow will:
67+
- Fall back to debug signing
68+
- Upload APKs with `-debug-signed` suffix
69+
- ⚠️ These cannot be uploaded to the Play Store!
70+
71+
## Verifying Signed APKs
72+
73+
To verify an APK signature:
74+
75+
```bash
76+
# Check signature
77+
jarsigner -verify -verbose -certs your-app.apk
78+
79+
# View certificate details
80+
keytool -printcert -jarfile your-app.apk
81+
```
82+
83+
For release APKs, the certificate should match your release keystore.
84+
For debug APKs, the certificate will show "CN=Android Debug".
85+
86+
## Installing Signed APKs
87+
88+
### Debug-signed APKs
89+
- Enable "Install from unknown sources" or "Install unknown apps" on your Android device
90+
- Download the APK from GitHub Actions artifacts
91+
- Install via file manager or `adb install path/to/app.apk`
92+
93+
### Release-signed APKs
94+
- Can be installed the same way as debug APKs
95+
- Can be uploaded to Google Play Store for distribution
96+
- Must be signed with the same keystore for app updates
97+
98+
## Security Best Practices
99+
100+
1. **Never commit keystores to the repository**
101+
2. **Keep keystore passwords in GitHub Secrets only**
102+
3. **Back up your release keystore securely** (preferably in multiple secure locations)
103+
4. **Use strong passwords** for both keystore and key alias
104+
5. **Limit access** to GitHub Secrets to trusted maintainers only
105+
106+
## Troubleshooting
107+
108+
### "APK not signed" error during installation
109+
- The APK must be signed (either debug or release)
110+
- Check workflow logs to ensure signing step completed successfully
111+
112+
### "App not installed" or "Package conflicts" error
113+
- You may have an existing version signed with a different key
114+
- Uninstall the old version first, then install the new APK
115+
116+
### Release workflow falls back to debug signing
117+
- Check that all four GitHub Secrets are configured correctly
118+
- Verify the base64-encoded keystore is valid: `echo "$SECRET" | base64 -d > test.keystore`
119+
- Check workflow logs for any error messages in the "Setup release keystore" step

.github/workflows/ci.yml

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,18 +169,62 @@ jobs:
169169
tauriScript: yarn tauri android
170170
includeUpdaterJson: false
171171

172+
- name: Generate debug keystore
173+
shell: bash
174+
run: |
175+
set -euo pipefail
176+
KEYSTORE_PATH="${HOME}/.android/debug.keystore"
177+
mkdir -p "${HOME}/.android"
178+
if [ ! -f "$KEYSTORE_PATH" ]; then
179+
echo "Generating debug keystore at $KEYSTORE_PATH"
180+
keytool -genkeypair -v \
181+
-keystore "$KEYSTORE_PATH" \
182+
-storepass android \
183+
-alias androiddebugkey \
184+
-keypass android \
185+
-keyalg RSA \
186+
-keysize 2048 \
187+
-validity 10000 \
188+
-dname "CN=Android Debug,O=Android,C=US"
189+
else
190+
echo "Debug keystore already exists at $KEYSTORE_PATH"
191+
fi
192+
193+
- name: Sign APK with debug key
194+
shell: bash
195+
run: |
196+
set -euo pipefail
197+
UNSIGNED_APK=$(find src-tauri/gen/android/app/build/outputs/apk -name "*-unsigned.apk" | head -1)
198+
if [ -z "$UNSIGNED_APK" ]; then
199+
echo "Error: No unsigned APK found"
200+
exit 1
201+
fi
202+
echo "Found unsigned APK: $UNSIGNED_APK"
203+
SIGNED_APK="${UNSIGNED_APK%-unsigned.apk}-debug-signed.apk"
204+
echo "Signing APK to: $SIGNED_APK"
205+
jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \
206+
-keystore "${HOME}/.android/debug.keystore" \
207+
-storepass android \
208+
-keypass android \
209+
"$UNSIGNED_APK" \
210+
androiddebugkey
211+
# Zipalign the signed APK
212+
${ANDROID_HOME}/build-tools/$(ls ${ANDROID_HOME}/build-tools | tail -1)/zipalign -v 4 "$UNSIGNED_APK" "$SIGNED_APK"
213+
echo "Signed APK created: $SIGNED_APK"
214+
ls -lh "$SIGNED_APK"
215+
172216
- name: Inspect Android bundle output
173217
if: always()
174218
run: |
175219
echo "Android APK artifacts:"
176220
find src-tauri/gen/android -type f -name "*.apk" -print || true
177221
178-
- name: Upload Android APK
222+
- name: Upload signed Android APK
179223
uses: actions/upload-artifact@v4
180224
with:
181-
name: android-apk
225+
name: android-apk-debug-signed
182226
path: |
183-
src-tauri/gen/android/app/build/outputs/apk/**/*.apk
227+
src-tauri/gen/android/app/build/outputs/apk/**/*-debug-signed.apk
184228
if-no-files-found: warn
185229
retention-days: 14
186230

.github/workflows/release.yml

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,103 @@ jobs:
124124
run: |
125125
yarn tauri:build:android
126126
127+
- name: Setup release keystore (if available)
128+
if: ${{ env.ANDROID_KEYSTORE_BASE64 != '' }}
129+
env:
130+
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
131+
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
132+
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
133+
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
134+
shell: bash
135+
run: |
136+
set -euo pipefail
137+
echo "Setting up release keystore from secrets"
138+
echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > release.keystore
139+
echo "KEYSTORE_PATH=$(pwd)/release.keystore" >> $GITHUB_ENV
140+
echo "KEYSTORE_PASSWORD=$ANDROID_KEYSTORE_PASSWORD" >> $GITHUB_ENV
141+
echo "KEY_ALIAS=$ANDROID_KEY_ALIAS" >> $GITHUB_ENV
142+
echo "KEY_PASSWORD=$ANDROID_KEY_PASSWORD" >> $GITHUB_ENV
143+
144+
- name: Sign APK/AAB (release or debug)
145+
shell: bash
146+
run: |
147+
set -euo pipefail
148+
149+
# Determine signing configuration
150+
if [ -f "release.keystore" ]; then
151+
echo "Using release keystore for signing"
152+
KEYSTORE="release.keystore"
153+
STORE_PASS="${KEYSTORE_PASSWORD}"
154+
KEY_ALIAS="${KEY_ALIAS}"
155+
KEY_PASS="${KEY_PASSWORD}"
156+
SUFFIX="release-signed"
157+
else
158+
echo "No release keystore found - using debug keystore"
159+
mkdir -p "${HOME}/.android"
160+
KEYSTORE="${HOME}/.android/debug.keystore"
161+
if [ ! -f "$KEYSTORE" ]; then
162+
echo "Generating debug keystore"
163+
keytool -genkeypair -v \
164+
-keystore "$KEYSTORE" \
165+
-storepass android \
166+
-alias androiddebugkey \
167+
-keypass android \
168+
-keyalg RSA \
169+
-keysize 2048 \
170+
-validity 10000 \
171+
-dname "CN=Android Debug,O=Android,C=US"
172+
fi
173+
STORE_PASS="android"
174+
KEY_ALIAS="androiddebugkey"
175+
KEY_PASS="android"
176+
SUFFIX="debug-signed"
177+
fi
178+
179+
# Sign APK
180+
UNSIGNED_APK=$(find src-tauri/gen/android/app/build/outputs/apk -name "*-unsigned.apk" | head -1)
181+
if [ -n "$UNSIGNED_APK" ]; then
182+
echo "Signing APK: $UNSIGNED_APK"
183+
SIGNED_APK="${UNSIGNED_APK%-unsigned.apk}-${SUFFIX}.apk"
184+
jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \
185+
-keystore "$KEYSTORE" \
186+
-storepass "$STORE_PASS" \
187+
-keypass "$KEY_PASS" \
188+
"$UNSIGNED_APK" \
189+
"$KEY_ALIAS"
190+
# Zipalign
191+
BUILD_TOOLS_VERSION=$(ls ${ANDROID_HOME}/build-tools | tail -1)
192+
${ANDROID_HOME}/build-tools/${BUILD_TOOLS_VERSION}/zipalign -f -v 4 "$UNSIGNED_APK" "$SIGNED_APK"
193+
echo "Signed APK created: $SIGNED_APK"
194+
ls -lh "$SIGNED_APK"
195+
else
196+
echo "Warning: No unsigned APK found"
197+
fi
198+
199+
# Sign AAB (if release keystore available)
200+
if [ -f "release.keystore" ]; then
201+
UNSIGNED_AAB=$(find src-tauri/gen/android/app/build/outputs/bundle -name "*.aab" | head -1)
202+
if [ -n "$UNSIGNED_AAB" ]; then
203+
echo "Signing AAB: $UNSIGNED_AAB"
204+
SIGNED_AAB="${UNSIGNED_AAB%.aab}-signed.aab"
205+
jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \
206+
-keystore "$KEYSTORE" \
207+
-storepass "$STORE_PASS" \
208+
-keypass "$KEY_PASS" \
209+
"$UNSIGNED_AAB" \
210+
"$KEY_ALIAS"
211+
mv "$UNSIGNED_AAB" "$SIGNED_AAB"
212+
echo "Signed AAB created: $SIGNED_AAB"
213+
ls -lh "$SIGNED_AAB"
214+
fi
215+
fi
216+
127217
- name: Upload APK/AAB artifacts
128218
uses: actions/upload-artifact@v4
129219
with:
130220
name: betaflight-android
131221
path: |
132-
src-tauri/gen/android/app/build/outputs/apk/universal/release/*.apk
133-
src-tauri/gen/android/app/build/outputs/bundle/universalRelease/*.aab
222+
src-tauri/gen/android/app/build/outputs/apk/universal/release/*-signed.apk
223+
src-tauri/gen/android/app/build/outputs/bundle/universalRelease/*-signed.aab
134224
if-no-files-found: warn
135225
retention-days: 30
136226

debug.keystore

2.4 KB
Binary file not shown.

0 commit comments

Comments
 (0)