mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 11:31:08 +00:00
Signing with DroidDeck's own key: hand-over from the testkey, test builds and a four-package release
This commit is contained in:
1 parent
498a37db32
commit
cd28b189a2
8 files changed
+453
No files matched your search
@@ -66,6 +66,18 @@ jobs:
|
||||
# The pull request as it would be once merged onto main is what gets built. When it cannot
|
||||
# be merged, the conflicting files and the conflicting hunks go into the run summary and the
|
||||
# pull-request comment, and the job stops here: there is nothing coherent to build.
|
||||
# DroidDeck's signing key lives only in GitHub environment secrets (see sign.yml). A key file
|
||||
# committed by mistake would be in the history for good: stop at the first one. The AOSP
|
||||
# testkey is public and meant to be here.
|
||||
- name: No private key in the tree
|
||||
run: |
|
||||
set -euo pipefail
|
||||
keys=$(git ls-files | grep -iE '\.(jks|keystore|p12|pfx|pk8|pem|key)$' | grep -vx 'keystore/testkey.p12' || true)
|
||||
if [ -n "$keys" ]; then
|
||||
echo "::error::a private key is committed - remove it from the history and make a new one: $keys"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Merge onto main, or say what conflicts
|
||||
if: github.event_name == 'pull_request_target'
|
||||
run: |
|
||||
@@ -480,3 +492,15 @@ jobs:
|
||||
retention-days: 30
|
||||
# An apk is a zip already; compressing it again buys nothing and costs seconds.
|
||||
compression-level: 0
|
||||
|
||||
# The apk signed with DroidDeck's own key (tools/release), so a test build installs over a release
|
||||
# and keeps its data. Main's builds sign straight away (signing-main); any other branch's wait for
|
||||
# the owner's approval (signing-branch). A pull request's never: its code is not ours to sign, and
|
||||
# the job that would hold the key never runs for it. The testkey apk above is unaffected.
|
||||
sign:
|
||||
needs: build
|
||||
if: github.event_name != 'pull_request_target'
|
||||
uses: ./.github/workflows/sign.yml
|
||||
with:
|
||||
environment: ${{ github.ref == 'refs/heads/main' && 'signing-main' || 'signing-branch' }}
|
||||
variants: '["standard"]'
|
||||
@@ -0,0 +1,66 @@
|
||||
name: Release APKs
|
||||
run-name: release ${{ inputs.version }} from main ${{ github.sha }}
|
||||
|
||||
# Every package of a release, signed with DroidDeck's own key: the standard app and the named ones
|
||||
# in tools/release/variants.txt, one matrix job each. Nothing is rebuilt - the apk is the one the
|
||||
# Build APK run of this exact main commit made (with its caches) and that was tested, so the
|
||||
# release is those bytes. Waits for the owner's approval (the release environment); publishing the
|
||||
# GitHub release stays a separate, manual step.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Version for the file names, e.g. 0.1.7
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
concurrency:
|
||||
group: release
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
find:
|
||||
name: find the main build
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
run_id: ${{ steps.find.outputs.run_id }}
|
||||
variants: ${{ steps.find.outputs.variants }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- id: find
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ $VERSION =~ ^[0-9A-Za-z._-]+$ ]] || { echo "::error::version '$VERSION': letters, digits, . _ - only"; exit 1; }
|
||||
# This commit's Build APK run - a push or a dispatch on main, never a pull request's.
|
||||
runs=$(gh run list -R "$GITHUB_REPOSITORY" --workflow build.yml --branch main --commit "$GITHUB_SHA" \
|
||||
--status success --json databaseId,event,headSha -L 20)
|
||||
id=$(jq -r --arg sha "$GITHUB_SHA" \
|
||||
'[.[] | select(.headSha == $sha and (.event == "push" or .event == "workflow_dispatch"))][0].databaseId // empty' <<<"$runs")
|
||||
if [ -z "$id" ]; then
|
||||
echo "::error::main ${GITHUB_SHA::7} has no successful Build APK run yet: wait for it, then release"
|
||||
exit 1
|
||||
fi
|
||||
variants=$(awk '!/^#/ && NF { print $1 }' tools/release/variants.txt | jq -R . | jq -sc .)
|
||||
echo "run_id=$id" >> "$GITHUB_OUTPUT"
|
||||
echo "variants=$variants" >> "$GITHUB_OUTPUT"
|
||||
echo "Signing Build APK run $id (main ${GITHUB_SHA::7}) as $VERSION: $variants" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
sign:
|
||||
needs: find
|
||||
uses: ./.github/workflows/sign.yml
|
||||
with:
|
||||
environment: release
|
||||
variants: ${{ needs.find.outputs.variants }}
|
||||
run_id: ${{ needs.find.outputs.run_id }}
|
||||
label: ${{ inputs.version }}
|
||||
artifact: droiddeck-release
|
||||
@@ -0,0 +1,104 @@
|
||||
name: Sign APKs
|
||||
|
||||
# Called, never run by itself: turns a "Build APK" apk into packages signed with DroidDeck's own key,
|
||||
# one matrix job per package in tools/release/variants.txt (tools/release/sign-apk.sh does the work
|
||||
# and checks it). The key lives only in the GitHub environment named by the caller:
|
||||
# signing-main builds of main
|
||||
# signing-branch builds of any other branch - waits for the owner's approval
|
||||
# release release.yml - main only, waits for the owner's approval
|
||||
# Pull requests never get here (build.yml skips this for pull_request_target), so code from a pull
|
||||
# request can never be signed with the key or come near it.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
environment:
|
||||
type: string
|
||||
required: true
|
||||
variants:
|
||||
description: JSON array of names from tools/release/variants.txt
|
||||
type: string
|
||||
required: true
|
||||
run_id:
|
||||
description: The Build APK run whose apk to sign; empty for the calling run's own
|
||||
type: string
|
||||
default: ''
|
||||
label:
|
||||
description: Goes in the file names (a version); empty for <branch>-<short sha>
|
||||
type: string
|
||||
default: ''
|
||||
artifact:
|
||||
type: string
|
||||
default: droiddeck-signed
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
sign:
|
||||
name: sign ${{ matrix.variant }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
environment: ${{ inputs.environment }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
variant: ${{ fromJSON(inputs.variants) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: The apk (this run's build)
|
||||
if: inputs.run_id == ''
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: droiddeck-apk
|
||||
path: in
|
||||
|
||||
- name: The apk (a main build)
|
||||
if: inputs.run_id != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RUN: ${{ inputs.run_id }}
|
||||
run: gh run download "$RUN" -R "$GITHUB_REPOSITORY" -n droiddeck-apk -D in
|
||||
|
||||
- name: Sign
|
||||
env:
|
||||
RELEASE_KEYSTORE_B64: ${{ secrets.RELEASE_KEYSTORE_B64 }}
|
||||
RELEASE_STORE_PASSWORD: ${{ secrets.RELEASE_STORE_PASSWORD }}
|
||||
RELEASE_KEY_ALIAS: ${{ secrets.RELEASE_KEY_ALIAS }}
|
||||
ENVIRONMENT: ${{ inputs.environment }}
|
||||
VARIANT: ${{ matrix.variant }}
|
||||
LABEL: ${{ inputs.label }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "$RELEASE_KEYSTORE_B64" ]; then
|
||||
echo "::error::environment '$ENVIRONMENT' has no signing key: run tools/release/make-release-key.sh once"
|
||||
exit 1
|
||||
fi
|
||||
export RELEASE_KEYSTORE="$RUNNER_TEMP/release.p12"
|
||||
( umask 077; base64 -d <<<"$RELEASE_KEYSTORE_B64" > "$RELEASE_KEYSTORE" )
|
||||
BUILD_TOOLS=$(ls -d "$ANDROID_HOME"/build-tools/* | sort -V | tail -n1)
|
||||
export BUILD_TOOLS
|
||||
[ -n "$LABEL" ] || LABEL="${GITHUB_REF_NAME//\//-}-${GITHUB_SHA::7}"
|
||||
suffix=$(awk -v v="$VARIANT" '!/^#/ && $1 == v { print $3 }' tools/release/variants.txt)
|
||||
[ "$suffix" = - ] && suffix=
|
||||
apks=(in/*.apk)
|
||||
[ ${#apks[@]} = 1 ] && [ -f "${apks[0]}" ] || { echo "::error::expected one apk in the build"; exit 1; }
|
||||
mkdir -p out
|
||||
out="out/DroidDeck-$LABEL$suffix.apk"
|
||||
tools/release/sign-apk.sh "${apks[0]}" "$VARIANT" "$out"
|
||||
( cd out && sha256sum -- *.apk > "SHA256SUMS-$VARIANT.txt" && cat "SHA256SUMS-$VARIANT.txt" )
|
||||
echo "- \`$(basename "$out")\` - $(cut -c1-16 "out/SHA256SUMS-$VARIANT.txt")…" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Forget the key
|
||||
if: always()
|
||||
run: rm -f "$RUNNER_TEMP/release.p12"
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ inputs.artifact }}-${{ matrix.variant }}
|
||||
path: out/*
|
||||
if-no-files-found: error
|
||||
+12
@@ -14,3 +14,15 @@ app/src/main/assets/linuxfs/
|
||||
|
||||
# Kotlin compiler session files (build-tool state, never source).
|
||||
.kotlin/
|
||||
|
||||
# Signing keys never enter the repo (DroidDeck's lives in GitHub environment secrets; see
|
||||
# tools/release/make-release-key.sh). keystore/testkey.p12 is the public AOSP testkey.
|
||||
*.jks
|
||||
*.keystore
|
||||
*.p12
|
||||
*.pfx
|
||||
*.pk8
|
||||
*.pem
|
||||
!keystore/testkey.p12
|
||||
droiddeck-release-key/
|
||||
password.txt
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
# axml_rename.py in.apk out.apk old=new ... : rewrite whole strings in AndroidManifest.xml's string
|
||||
# pool (binary XML), leaving every other entry of the apk byte-identical. Unsigned output.
|
||||
import struct, sys, zipfile
|
||||
|
||||
def dec_len8(b, o):
|
||||
n = b[o]; o += 1
|
||||
if n & 0x80: n = ((n & 0x7f) << 8) | b[o]; o += 1
|
||||
return n, o
|
||||
|
||||
def dec_len16(b, o):
|
||||
n = struct.unpack_from('<H', b, o)[0]; o += 2
|
||||
if n & 0x8000: n = ((n & 0x7fff) << 16) | struct.unpack_from('<H', b, o)[0]; o += 2
|
||||
return n, o
|
||||
|
||||
def enc_len8(n):
|
||||
return bytes([n]) if n < 0x80 else bytes([0x80 | (n >> 8), n & 0xff])
|
||||
|
||||
def enc_len16(n):
|
||||
return struct.pack('<H', n) if n < 0x8000 else struct.pack('<HH', 0x8000 | (n >> 16), n & 0xffff)
|
||||
|
||||
def rewrite(axml, repl):
|
||||
ftype, fhs, fsize = struct.unpack_from('<HHI', axml, 0)
|
||||
assert ftype == 0x0003, 'not binary XML'
|
||||
sp = fhs
|
||||
ctype, chs, csize, count, scount, flags, sstart, stystart = struct.unpack_from('<HHIIIIII', axml, sp)
|
||||
assert ctype == 0x0001
|
||||
utf8 = bool(flags & 0x100)
|
||||
offs = struct.unpack_from('<%dI' % count, axml, sp + chs)
|
||||
sty_offs = axml[sp + chs + 4 * count: sp + chs + 4 * count + 4 * scount]
|
||||
base = sp + sstart
|
||||
strings = []
|
||||
for off in offs:
|
||||
o = base + off
|
||||
if utf8:
|
||||
_, o = dec_len8(axml, o); n, o = dec_len8(axml, o)
|
||||
strings.append(axml[o:o + n].decode('utf-8'))
|
||||
else:
|
||||
n, o = dec_len16(axml, o)
|
||||
strings.append(axml[o:o + 2 * n].decode('utf-16-le'))
|
||||
hits = 0
|
||||
for i, s in enumerate(strings):
|
||||
if s in repl: strings[i] = repl[s]; hits += 1
|
||||
data = bytearray(); new_offs = []
|
||||
for s in strings:
|
||||
new_offs.append(len(data))
|
||||
if utf8:
|
||||
e = s.encode('utf-8'); data += enc_len8(len(s.encode('utf-16-le')) // 2) + enc_len8(len(e)) + e + b'\0'
|
||||
else:
|
||||
e = s.encode('utf-16-le'); data += enc_len16(len(e) // 2) + e + b'\0\0'
|
||||
while len(data) % 4: data += b'\0'
|
||||
styles = axml[sp + stystart: sp + csize] if scount else b''
|
||||
new_sstart = chs + 4 * count + 4 * scount
|
||||
new_stystart = new_sstart + len(data) if scount else 0
|
||||
body = struct.pack('<%dI' % count, *new_offs) + sty_offs + bytes(data) + styles
|
||||
new_csize = chs + len(body)
|
||||
header = struct.pack('<HHIIIIII', ctype, chs, new_csize, count, scount, flags, new_sstart, new_stystart)
|
||||
header += axml[sp + 28: sp + chs]
|
||||
rest = axml[sp + csize:]
|
||||
out = axml[:sp] + header + body + rest
|
||||
out = out[:4] + struct.pack('<I', len(out)) + out[8:]
|
||||
return out, hits
|
||||
|
||||
src, dst = sys.argv[1], sys.argv[2]
|
||||
repl = dict(a.split('=', 1) for a in sys.argv[3:])
|
||||
with zipfile.ZipFile(src) as zi, zipfile.ZipFile(dst, 'w') as zo:
|
||||
for info in zi.infolist():
|
||||
if info.filename.startswith('META-INF/') and info.filename.split('/')[-1].split('.')[-1] in ('SF', 'RSA', 'DSA', 'EC', 'MF'):
|
||||
continue
|
||||
b = zi.read(info)
|
||||
if info.filename == 'AndroidManifest.xml':
|
||||
b, hits = rewrite(b, repl)
|
||||
print(f'manifest: {hits} strings replaced')
|
||||
zo.writestr(info, b, compress_type=info.compress_type)
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
# make-release-key.sh - run ONCE, by the owner. Makes DroidDeck's signing key and gives it to GitHub.
|
||||
#
|
||||
# - The key is made on this device, in ~/droiddeck-release-key (outside the repo), with a long
|
||||
# random password written beside it. Nothing secret is printed.
|
||||
# - The key and password go to GitHub as secrets of the three signing environments
|
||||
# (signing-main, signing-branch, release). GitHub never shows a secret again.
|
||||
# - The key's certificate digest - public, not a secret - goes to keystore/release-signer.sha256,
|
||||
# which CI checks every signed apk against. That file is the only thing to commit.
|
||||
#
|
||||
# BACK UP ~/droiddeck-release-key (both files) somewhere safe - a password manager, a USB stick.
|
||||
# Without it no update can ever be signed for the people who installed a signed build.
|
||||
set -euo pipefail
|
||||
|
||||
REPO=The412Banner/DroidDeck
|
||||
ENVIRONMENTS=(signing-main signing-branch release)
|
||||
ALIAS=droiddeck
|
||||
OUT=${DROIDDECK_KEY_DIR:-$HOME/droiddeck-release-key}
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
repo=$(cd "$here/../.." && pwd)
|
||||
|
||||
die() { echo "error: $*" >&2; exit 1; }
|
||||
command -v keytool >/dev/null || die "keytool not found (Termux: pkg install openjdk-17)"
|
||||
command -v gh >/dev/null || die "gh not found"
|
||||
gh auth status >/dev/null 2>&1 || die "gh is not logged in (gh auth login)"
|
||||
[ -e "$OUT/droiddeck-release.p12" ] && die "a key already exists in $OUT - there must only ever be one"
|
||||
case "$OUT" in "$repo"|"$repo"/*) die "the key must not be made inside the repo" ;; esac
|
||||
for env in "${ENVIRONMENTS[@]}"; do
|
||||
gh api "repos/$REPO/environments/$env" >/dev/null 2>&1 || die "GitHub environment '$env' does not exist yet"
|
||||
done
|
||||
|
||||
umask 077
|
||||
mkdir -p "$OUT"
|
||||
KS="$OUT/droiddeck-release.p12"
|
||||
# 48 random characters; PKCS12 uses one password for the store and the key.
|
||||
KS_PW=$(head -c 60 /dev/urandom | base64 -w0 | tr -d '/+=')
|
||||
KS_PW=${KS_PW:0:48}
|
||||
[ ${#KS_PW} -ge 40 ] || die "could not make a password"
|
||||
export KS_PW
|
||||
printf '%s\n' "$KS_PW" > "$OUT/password.txt"
|
||||
|
||||
keytool -genkeypair -keystore "$KS" -storetype PKCS12 -alias "$ALIAS" \
|
||||
-keyalg RSA -keysize 4096 -validity 10950 -dname "CN=DroidDeck, O=The412Banner" \
|
||||
-storepass:env KS_PW -keypass:env KS_PW >/dev/null 2>&1 || die "keytool could not make the key"
|
||||
listing=$(keytool -list -v -keystore "$KS" -storepass:env KS_PW -alias "$ALIAS")
|
||||
SHA=$(awk '/SHA256:/ { print $2 }' <<<"$listing" | head -n1 | tr -d ':' | tr 'A-F' 'a-f')
|
||||
[[ $SHA =~ ^[0-9a-f]{64}$ ]] || die "could not read the key's certificate digest"
|
||||
|
||||
for env in "${ENVIRONMENTS[@]}"; do
|
||||
base64 -w0 < "$KS" | gh secret set RELEASE_KEYSTORE_B64 --env "$env" -R "$REPO" >/dev/null
|
||||
printf '%s' "$KS_PW" | gh secret set RELEASE_STORE_PASSWORD --env "$env" -R "$REPO" >/dev/null
|
||||
printf '%s' "$ALIAS" | gh secret set RELEASE_KEY_ALIAS --env "$env" -R "$REPO" >/dev/null
|
||||
echo "secrets set for environment: $env"
|
||||
done
|
||||
unset KS_PW
|
||||
|
||||
printf '%s\n' "$SHA" > "$repo/keystore/release-signer.sha256"
|
||||
cat <<EOF
|
||||
|
||||
DroidDeck's signing key is made and in GitHub.
|
||||
|
||||
Key: $KS
|
||||
Password: $OUT/password.txt
|
||||
Digest: $SHA (public - written to keystore/release-signer.sha256)
|
||||
|
||||
Now:
|
||||
1. Back up the folder $OUT (both files) to at least two safe places.
|
||||
2. Commit keystore/release-signer.sha256 (only that file).
|
||||
EOF
|
||||
Executable
+95
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env bash
|
||||
# sign-apk.sh <in.apk> <variant> <out.apk>
|
||||
#
|
||||
# Turns a CI build (signed with the public AOSP testkey) into one package of tools/release/variants.txt
|
||||
# signed with DroidDeck's own key, carrying the hand-over from the testkey:
|
||||
# - v1 + v2 are signed by the testkey, v3 by our key, with a signed lineage testkey -> our key;
|
||||
# - an install from before (testkey) updates to it and keeps its data;
|
||||
# - the testkey loses its rollback right, so an apk signed with the testkey alone - which anyone
|
||||
# can make - is refused as an update to it (device-proven on the Pocket FIT, 2026-09-24);
|
||||
# - --rotation-min-sdk-version 28: the lineage applies from Android 9, not only from 13.
|
||||
#
|
||||
# The key comes from the environment and is never written anywhere but the path given:
|
||||
# RELEASE_KEYSTORE path to the PKCS12 keystore
|
||||
# RELEASE_STORE_PASSWORD its password (the key has the same one: PKCS12)
|
||||
# RELEASE_KEY_ALIAS the key's alias
|
||||
# BUILD_TOOLS Android build-tools dir (zipalign, apksigner, aapt)
|
||||
# RELEASE_SIGNER_SHA256 optional: the expected certificate digest, else keystore/release-signer.sha256
|
||||
set -euo pipefail
|
||||
|
||||
die() { echo "::error::$*" >&2; exit 1; }
|
||||
[ $# -eq 3 ] || die "usage: sign-apk.sh <in.apk> <variant> <out.apk>"
|
||||
in=$1 variant=$2 out=$3
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
repo=$(cd "$here/../.." && pwd)
|
||||
|
||||
pkg=$(awk -v v="$variant" '!/^#/ && $1 == v { print $2 }' "$here/variants.txt")
|
||||
[ -n "$pkg" ] || die "no variant '$variant' in tools/release/variants.txt"
|
||||
expected=${RELEASE_SIGNER_SHA256:-$(tr -d ' \n' < "$repo/keystore/release-signer.sha256" 2>/dev/null || true)}
|
||||
[[ $expected =~ ^[0-9a-f]{64}$ ]] \
|
||||
|| die "keystore/release-signer.sha256 is missing: make the key first (tools/release/make-release-key.sh)"
|
||||
: "${RELEASE_KEYSTORE:?}" "${RELEASE_STORE_PASSWORD:?}" "${RELEASE_KEY_ALIAS:?}" "${BUILD_TOOLS:?}"
|
||||
[ -s "$RELEASE_KEYSTORE" ] || die "the release keystore is empty - is the secret set for this environment?"
|
||||
|
||||
TESTKEY="$repo/keystore/testkey.p12"
|
||||
TESTKEY_SHA256=a40da80a59d170caa950cf15c18c454d47a39b26989d8b640ecd745ba71bf5dc
|
||||
export TESTKEY_PASSWORD=android
|
||||
work=$(mktemp -d)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
# The package name and everything the manifest derives from it; component class names stay.
|
||||
P=com.droiddeck.launcher
|
||||
if [ "$pkg" = "$P" ]; then
|
||||
python3 "$here/axml_rename.py" "$in" "$work/renamed.apk"
|
||||
else
|
||||
python3 "$here/axml_rename.py" "$in" "$work/renamed.apk" \
|
||||
"$P=$pkg" "$P.androidx-startup=$pkg.androidx-startup" "$P.logs=$pkg.logs" \
|
||||
"$P.documents=$pkg.documents" "$P.home=$pkg.home" \
|
||||
"$P.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION=$pkg.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
|
||||
fi
|
||||
got=$("$BUILD_TOOLS/aapt" dump badging "$work/renamed.apk" | sed -n "s/^package: name='\([^']*\)'.*/\1/p")
|
||||
[ "$got" = "$pkg" ] || die "$variant: the apk says package '$got', not '$pkg'"
|
||||
if [ "$pkg" != "$P" ]; then
|
||||
# A provider left on the old authority would clash with the standard app installed beside it.
|
||||
tree=$("$BUILD_TOOLS/aapt" dump xmltree "$work/renamed.apk" AndroidManifest.xml)
|
||||
grep -E 'android:(authorities|taskAffinity)' <<<"$tree" | grep -F "\"$P." \
|
||||
&& die "$variant: the manifest still names $P in an authority or task"
|
||||
fi
|
||||
|
||||
"$BUILD_TOOLS/zipalign" -p -f 4 "$work/renamed.apk" "$work/aligned.apk"
|
||||
|
||||
# The lineage: the testkey vouches for our key. Made fresh each time (it is two certificates and
|
||||
# a signature, nothing secret beyond the key that signs it).
|
||||
"$BUILD_TOOLS/apksigner" rotate --out "$work/lineage" \
|
||||
--old-signer --ks "$TESTKEY" --ks-type PKCS12 --ks-pass env:TESTKEY_PASSWORD \
|
||||
--ks-key-alias testkey --key-pass env:TESTKEY_PASSWORD --set-rollback false \
|
||||
--new-signer --ks "$RELEASE_KEYSTORE" --ks-type PKCS12 --ks-pass env:RELEASE_STORE_PASSWORD \
|
||||
--ks-key-alias "$RELEASE_KEY_ALIAS" --key-pass env:RELEASE_STORE_PASSWORD
|
||||
|
||||
"$BUILD_TOOLS/apksigner" sign \
|
||||
--ks "$TESTKEY" --ks-type PKCS12 --ks-pass env:TESTKEY_PASSWORD \
|
||||
--ks-key-alias testkey --key-pass env:TESTKEY_PASSWORD \
|
||||
--next-signer --ks "$RELEASE_KEYSTORE" --ks-type PKCS12 --ks-pass env:RELEASE_STORE_PASSWORD \
|
||||
--ks-key-alias "$RELEASE_KEY_ALIAS" --key-pass env:RELEASE_STORE_PASSWORD \
|
||||
--lineage "$work/lineage" --rotation-min-sdk-version 28 \
|
||||
--v1-signing-enabled true --v2-signing-enabled true --v3-signing-enabled true --v4-signing-enabled false \
|
||||
--out "$out" "$work/aligned.apk"
|
||||
rm -f "$out.idsig"
|
||||
|
||||
# What the apk must carry, checked rather than assumed.
|
||||
"$BUILD_TOOLS/apksigner" verify --min-sdk-version 26 "$out" >/dev/null || die "$variant: does not verify"
|
||||
# (Listed first, then searched: grep -q stops reading early, which fails a pipe under pipefail.)
|
||||
listing=$(unzip -l "$out")
|
||||
grep -qE 'META-INF/.*\.(SF|RSA)$' <<<"$listing" || die "$variant: no JAR signature (old installers)"
|
||||
certs=$("$BUILD_TOOLS/apksigner" verify --min-sdk-version 28 --print-certs "$out")
|
||||
v3=$(sed -n 's/^Signer #1 certificate SHA-256 digest: //p' <<<"$certs")
|
||||
[ "$v3" = "$expected" ] || die "$variant: Android 9+ sees signer $v3, not DroidDeck's key $expected"
|
||||
lineage=$("$BUILD_TOOLS/apksigner" lineage --in "$out" --print-certs)
|
||||
grep -q "Signer #1 in lineage certificate SHA-256 digest: $TESTKEY_SHA256" <<<"$lineage" \
|
||||
|| die "$variant: the lineage does not start at the testkey - old installs could not update"
|
||||
grep -q "Signer #2 in lineage certificate SHA-256 digest: $expected" <<<"$lineage" \
|
||||
|| die "$variant: the lineage does not hand over to DroidDeck's key"
|
||||
rollback=$(awk '/Signer #1 in lineage/{s=1} /Signer #2 in lineage/{s=0} s && /rollback capability/' <<<"$lineage")
|
||||
grep -q false <<<"$rollback" \
|
||||
|| die "$variant: the testkey kept its rollback right - a testkey-signed apk could replace this one"
|
||||
echo "$variant ($pkg): signed, hand-over from the testkey, v3 signer $expected"
|
||||
@@ -0,0 +1,9 @@
|
||||
# The packages a signed build ships as. The same apk, with only the manifest's package name (and
|
||||
# the names derived from it) changed: some phones give their performance modes only to apps with
|
||||
# certain names. Each is its own app to Android - installs beside the others, with its own data.
|
||||
#
|
||||
# name package file suffix
|
||||
standard com.droiddeck.launcher -
|
||||
pubg com.tencent.ig -pubg
|
||||
antutu com.antutu.benchmark.full -antutu
|
||||
ludashi com.ludashi.benchmark -ludashi
|
||||
Reference in new issue
Block a user