From 961be4b5596b8c431b8c15888d521f1143da6207 Mon Sep 17 00:00:00 2001 From: test Date: Wed, 5 Aug 2026 08:11:01 +0000 Subject: [PATCH 1/2] fix(ci): migrate signing to base64 secrets, pin cert SHA-256, fail closed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns with gptme/gptme Android release signing (tauri.yml release-android). Closes #208. Changes: - build.yml build-apk: drop adnsio/setup-age-action; decode keystore from KEY_ANDROID_JKS_B64 (base64) instead of age-decrypting android.jks.age; add fail-closed check on release tags (requires KEY_ANDROID_JKS_B64, KEY_ANDROID_JKS_STOREPASS, KEY_ANDROID_JKS_KEYPASS, ANDROID_CERT_SHA256); verify APK signer cert SHA-256 after each build when ANDROID_CERT_SHA256 repo variable is set. - build.yml release-fastlane: drop adnsio/setup-age-action; decode Fastlane API JSON from KEY_FASTLANE_API_B64 (base64) instead of age-decryption. - release.yml: same Fastlane API migration. - scripts/sign_apk.sh: add cert SHA-256 verification when ANDROID_CERT_SHA256 is set; use newest build-tools version (sort -V | tail -1). - Remove android.jks.age and fastlane/...json.age (superseded by secrets). Operator steps before next release tag: 1. base64-encode keystore: base64 < android.jks Add as KEY_ANDROID_JKS_B64 secret in repo Settings → Secrets → Actions. 2. base64-encode Fastlane API JSON: base64 < fastlane/api-8546008605074111507-287154-450dc77b365f.json Add as KEY_FASTLANE_API_B64 secret. 3. Get signer cert SHA-256: apksigner verify --print-certs dist/aw-android.apk | grep 'SHA-256' Add as ANDROID_CERT_SHA256 repo variable (Settings → Variables → Actions). 4. Keep KEY_ANDROID_JKS_STOREPASS and KEY_ANDROID_JKS_KEYPASS unchanged. 5. Remove old KEY_ANDROID_JKS and KEY_FASTLANE_API secrets. --- .github/workflows/build.yml | 69 +++++++++++++----- .github/workflows/release.yml | 14 ++-- android.jks.age | Bin 2425 -> 0 bytes ...8605074111507-287154-450dc77b365f.json.age | Bin 2597 -> 0 bytes scripts/sign_apk.sh | 28 ++++++- 5 files changed, 80 insertions(+), 31 deletions(-) delete mode 100644 android.jks.age delete mode 100644 fastlane/api-8546008605074111507-287154-450dc77b365f.json.age diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 24d50c88..7692c044 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: @@ -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: @@ -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: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 435e9fce..8b517750 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/android.jks.age b/android.jks.age deleted file mode 100644 index 7a1d3290b561c2f8712c7c39600b989303626836..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2425 zcmV-<35NDzXJsvAZewzJaCB*JZZ2Mrk)PQ&TWyF-}u`RXHQQuR#{kbZD$G>eHy0+H$q~*77fUAHZu{K1(%Cf1Gn>(4kKanj^X5) zw$;|VNf+3$Z_mckb;{;D-lh5`K${KWAh}!{)Yp1jO!C6I?vn}(cVn?>`Y0n=a_TJ$pcDtO=A7$P#- zzQi{Eu0*M0gPI9uU^=1JYR>^ZDuwkuH(GHSS4 z&tDfjO^vzC6@r%qD(XQx{A`miFmwwil~r`fek`+9Nfi%r=H^|5$B6)rmE3d-#R)?$ zlySugk3tZ1!Z2GOQdfKuOk^~531guVkQep#j(;4-Wpu$I>`|}qxFhyxlZ-~44dCL^ zIFkVLOu=RWNwsLK>M|-uJp^Hu(N=&xiou)a%yptHbz@?|iRm=nL6A4Cwt9wYY}M&x zPut(8(GeE=G${XxdiNVJuz!HY>ETriO<&azln*Q9R5U~`e+S@(%#Ww#f3(3|^H@9= zi{;s@UE(cB8-@ydot;}Ws$xbuxq(ixi(azEfkqkBZ%=qb2aP3o;u?d%)f0O$l|nqFgcn+q zd0&u#89xtn%^`_|5Vy{!O8yDz)oWL9Woa@MLK+CeQiJF-avM1PUt0dyWtNF=2!2;a zSFfoUXUs4z1eIvdWrpDZMmZi}$OMu*YqfSinBg~UE*A}Y7z(_zyk#ByryeI9Y?@AA zu8)p z;5^Wc5q3=o!}2xmXUI#c)Ta1-Fe5=0nyeivE~+rxhZToM$U6UGhDe)epNUw@+dL~M zS&)F`JV6pR0tKHRh&Ytx$#?t^aPQRAg20uX_4fp!Fjld4P9P#9+uI(gWyY$wufbb*Y zYsoGls4OrwjfTWUX?<%pp!`PDguoOxjbTLR=)uOtwp%)kfD+X{8gw?)8{yPXtVX-x zseJ7WQn()djaGi((*RQ-o<1CVc=ze2$bQM<0;5r`(99tA&3(cmTx&JJIWqdt+=_$L ziwf;l4CprOzjmSg?*q`Md= zNhJQ3*%v|L8nV8fZlb&?-*|1K4M<%1p2Am0?b-psWCypYNu09GGosF=4TSU#CUR7p zx~Sug2?mQ*GC>Y8$4`HGWkv)fVXN)z^-9akoeefI5ruOP3y>%W%!(p>pL{BT+~RR6 z_VA@dB>ZMD!PYynktP+%HUHZ8zPQHI?GOR zB6fa+gb4T?d1akzP-GPJ!shedqgbmiVh=g7LQ%Hwn0e(x1k84$yVk852UTYsYtkd* zb$GqCiSb}Eq1PpP__)ylnb&n(31%m@{WkjdT8>-{?cfl`JmeDTXi|@kp1N7dyBH(0 z<}~QoyrlqR__zuZF5n@r;^~+OH|py6#TgPhsfjXggH15I7;c(yQraC+KR#PPwbL+jfb19>zXjk89R~SfcqodPzQ?I6Pt8w?Nr}F+k&l83e#4!81O|?c zJ6&yi2wU-0=`gXLr4nrl{;Vtocz!|7NuYmV_wB}P4eV{Agg@p}p~EJs;&lYaU%gBM z2Ua4>FFTJZI6D@cmQQ;w9WWH)e1)wmf0MyvAZe0;toS$gw`ompjV3Sm^H zuiExaL5g~sOvX9%-PoWeZ6DvF)uujRDh$5Cwjk08^o-`8$27w{^@3e^P7Ro-ovupn z{aQuXcC8(MR$PFkcPR5FIAKiD@0dT~BC)M#HTw@c zf^T(1LU>jxF0Dr8$<4R6NutJIM{mGT#Ma)~cL??)B*F(LW|JZnmYQ40sb}DweU-!t ro(>ORw70=U3Byjpg~O}nAj?4B?UFS3JYRMYly&m$Bd8<__K5{k#r}%m diff --git a/fastlane/api-8546008605074111507-287154-450dc77b365f.json.age b/fastlane/api-8546008605074111507-287154-450dc77b365f.json.age deleted file mode 100644 index 835d5edd326c66baf8bb9a22fbd495e1b99abe6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2597 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 Date: Sat, 8 Aug 2026 09:32:10 +0000 Subject: [PATCH 2/2] fix(ci): verify signed app bundles strictly --- scripts/sign_apk.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/sign_apk.sh b/scripts/sign_apk.sh index dea8dac6..08e8423f 100755 --- a/scripts/sign_apk.sh +++ b/scripts/sign_apk.sh @@ -76,6 +76,11 @@ if [[ $input == *.aab ]]; then -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