Skip to content
Open
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
69 changes: 51 additions & 18 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -202,18 +202,32 @@ jobs:
# release-android job + docs/contributing.rst "Android release signing"
# there); signing itself happens in scripts/sign_apk.sh via the Makefile.
# Keep the two implementations consistent when changing either.
# gptme delivers the keystore as a base64 secret instead of age-encrypted
# in-repo, pins the signer cert SHA-256, and fails closed on missing
# secrets — planned to be adopted here too (see issue tracker).
- uses: adnsio/setup-age-action@v1.2.0
- name: Load Android secrets
if: env.KEY_ANDROID_JKS != null
# Keystore is delivered as a base64 secret (KEY_ANDROID_JKS_B64) and decoded
# here; the signer cert SHA-256 is pinned in ANDROID_CERT_SHA256 (repo variable)
# and verified after each APK is signed.
- name: Require signing configuration on release tags
if: startsWith(github.ref, 'refs/tags/v')
env:
KEY_ANDROID_JKS: ${{ secrets.KEY_ANDROID_JKS }}
HAS_SIGNING: >-
${{ secrets.KEY_ANDROID_JKS_B64 != '' &&
secrets.KEY_ANDROID_JKS_STOREPASS != '' &&
secrets.KEY_ANDROID_JKS_KEYPASS != '' &&
vars.ANDROID_CERT_SHA256 != '' }}
run: |
printf "$KEY_ANDROID_JKS" > android.jks.key
cat android.jks.age | age -d -i android.jks.key -o android.jks
rm android.jks.key
if [ "$HAS_SIGNING" != "true" ]; then
echo "::error::Android release signing is not fully configured."
echo "::error::Provision KEY_ANDROID_JKS_B64, KEY_ANDROID_JKS_STOREPASS, KEY_ANDROID_JKS_KEYPASS"
echo "::error::secrets and ANDROID_CERT_SHA256 repo variable before cutting a release tag."
exit 1
fi

- name: Load Android keystore
if: ${{ secrets.KEY_ANDROID_JKS_B64 != '' }}
env:
KEY_ANDROID_JKS_B64: ${{ secrets.KEY_ANDROID_JKS_B64 }}
run: |
printf '%s' "$KEY_ANDROID_JKS_B64" | base64 --decode > android.jks
chmod 600 android.jks

- name: Assemble
env:
Expand All @@ -222,6 +236,28 @@ jobs:
run: |
make dist/aw-android.${{ matrix.type }}

- name: Verify APK signer certificate
if: matrix.type == 'apk' && vars.ANDROID_CERT_SHA256 != ''
env:
ANDROID_CERT_SHA256: ${{ vars.ANDROID_CERT_SHA256 }}
run: |
apksigner=$(find "$ANDROID_HOME/build-tools" -name "apksigner" -print | sort -V | tail -1)
apk=dist/aw-android.apk
actual=$("$apksigner" verify --print-certs "$apk" \
| grep "Signer #1 certificate SHA-256 digest:" \
| awk '{print $NF}')
if [ -z "$actual" ]; then
echo "::error::Could not extract signer certificate from $apk"
exit 1
fi
if [ "$actual" != "$ANDROID_CERT_SHA256" ]; then
echo "::error::Signer certificate SHA-256 mismatch — possible key rotation or wrong keystore."
echo "::error:: expected: $ANDROID_CERT_SHA256"
echo "::error:: actual: $actual"
exit 1
fi
echo "Signer certificate verified: $actual"

- name: Upload
uses: actions/upload-artifact@v4
with:
Expand Down Expand Up @@ -421,16 +457,13 @@ jobs:
fi
echo "SUPPLY_TRACK=${SUPPLY_TRACK}" >> $GITHUB_ENV

- uses: adnsio/setup-age-action@v1.2.0
- name: Load Android secrets
- name: Load Fastlane API credentials
env:
KEY_FASTLANE_API: ${{ secrets.KEY_FASTLANE_API }}
KEY_FASTLANE_API_B64: ${{ secrets.KEY_FASTLANE_API_B64 }}
run: |
printf "$KEY_FASTLANE_API" > fastlane/api-8546008605074111507-287154-450dc77b365f.json.key
cat fastlane/api-8546008605074111507-287154-450dc77b365f.json.age \
| age -d -i fastlane/api-8546008605074111507-287154-450dc77b365f.json.key \
-o fastlane/api-8546008605074111507-287154-450dc77b365f.json
rm fastlane/api-8546008605074111507-287154-450dc77b365f.json.key
printf '%s' "$KEY_FASTLANE_API_B64" | base64 --decode \
> fastlane/api-8546008605074111507-287154-450dc77b365f.json
chmod 600 fastlane/api-8546008605074111507-287154-450dc77b365f.json

- name: Release with fastlane
run: |
Expand Down
14 changes: 5 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,13 @@ jobs:
with:
bundler-cache: true

- uses: adnsio/setup-age-action@v1.2.0

- name: Load Fastlane secrets
- name: Load Fastlane API credentials
env:
KEY_FASTLANE_API: ${{ secrets.KEY_FASTLANE_API }}
KEY_FASTLANE_API_B64: ${{ secrets.KEY_FASTLANE_API_B64 }}
run: |
printf "$KEY_FASTLANE_API" > fastlane/api-8546008605074111507-287154-450dc77b365f.json.key
cat fastlane/api-8546008605074111507-287154-450dc77b365f.json.age \
| age -d -i fastlane/api-8546008605074111507-287154-450dc77b365f.json.key \
-o fastlane/api-8546008605074111507-287154-450dc77b365f.json
rm fastlane/api-8546008605074111507-287154-450dc77b365f.json.key
printf '%s' "$KEY_FASTLANE_API_B64" | base64 --decode \
> fastlane/api-8546008605074111507-287154-450dc77b365f.json
chmod 600 fastlane/api-8546008605074111507-287154-450dc77b365f.json

- name: Update versionCode
uses: Wandalen/wretry.action@v3.8.0_js_action
Expand Down
Binary file removed android.jks.age
Binary file not shown.
Binary file not shown.
33 changes: 29 additions & 4 deletions scripts/sign_apk.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
# release signing (gptme/gptme .github/workflows/tauri.yml release-android job,
# documented in docs/contributing.rst "Android release signing" there).
# Keep the two implementations consistent when changing either.
# gptme additionally pins the signer cert SHA-256 and fails closed when
# signing secrets are missing — planned to be adopted here too.
#
# Optional: set ANDROID_CERT_SHA256 to the expected signer cert SHA-256 digest
# (from `apksigner verify --print-certs`). When set, the script verifies the
# APK's actual signer cert matches — fails loudly on mismatch.

set -e

Expand Down Expand Up @@ -43,19 +45,42 @@ fi
# Using apksigner for APKs instead of jarsigner since API 30+: https://stackoverflow.com/a/69473649
# Using jarsigner for AABs since apksigner doesn't support them
if [[ $input == *.apk ]]; then
apksigner=$(find $ANDROID_HOME/build-tools -name "apksigner" -print | head -n 1)
apksigner=$(find $ANDROID_HOME/build-tools -name "apksigner" -print | sort -V | tail -n 1)
$apksigner sign --ks android.jks --ks-key-alias activitywatch \
--ks-pass env:JKS_STOREPASS --key-pass env:JKS_KEYPASS \
$input

# Verify
# Verify signature integrity
$apksigner verify $input

# Verify signer cert SHA-256 if pinned (set ANDROID_CERT_SHA256 to pin)
if [ -n "${ANDROID_CERT_SHA256:-}" ]; then
actual=$($apksigner verify --print-certs "$input" \
| grep "Signer #1 certificate SHA-256 digest:" \
| awk '{print $NF}')
if [ -z "$actual" ]; then
echo "ERROR: Could not extract signer certificate SHA-256 from $input"
exit 1
fi
if [ "$actual" != "$ANDROID_CERT_SHA256" ]; then
echo "ERROR: Signer certificate SHA-256 mismatch — possible key rotation or wrong keystore."
echo " expected: $ANDROID_CERT_SHA256"
echo " actual: $actual"
exit 1
fi
echo "Signer certificate verified: $actual"
fi
fi
if [[ $input == *.aab ]]; then
jarsigner -verbose \
-keystore android.jks \
-storepass $JKS_STOREPASS -keypass $JKS_KEYPASS \
$input activitywatch

# Verify the bundle before it can be uploaded. `-strict` turns signer and
# certificate problems that jarsigner otherwise reports as warnings into a
# non-zero exit status.
jarsigner -verify -strict "$input"
fi

# Move to output destination
Expand Down