fix(android): enforce consistent APK signing
This commit is contained in:
1 parent
01be5293e2
commit
16daa4155f
8 files changed
+250
-46
No files matched your search
@@ -17,7 +17,9 @@ on:
|
||||
- "packages/client/**"
|
||||
- "packages/shared/**"
|
||||
- "bun.lock"
|
||||
- ".github/workflows/mobile-build.yml"
|
||||
- "scripts/build-android-local.sh"
|
||||
- "scripts/verify-android-apk-signature.mjs"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -117,6 +119,21 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Verify existing APK uses the pinned signer
|
||||
if: steps.plan.outputs.asset_ready == 'true'
|
||||
env:
|
||||
CURRENT_TAG: ${{ inputs.release_tag || github.event.release.tag_name }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/native-assets"
|
||||
gh release download "$CURRENT_TAG" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--pattern 'edgeever-android-v*-arm64-v8a.apk' \
|
||||
--dir "$RUNNER_TEMP/native-assets"
|
||||
apk_path="$(find "$RUNNER_TEMP/native-assets" -type f -name 'edgeever-android-v*-arm64-v8a.apk' -print -quit)"
|
||||
test -n "$apk_path"
|
||||
node scripts/verify-android-apk-signature.mjs "$apk_path"
|
||||
|
||||
reuse-release-apk:
|
||||
needs: release-plan
|
||||
if: >-
|
||||
@@ -130,6 +147,9 @@ jobs:
|
||||
name: Reuse Android release APK
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Copy latest compatible APK without renaming
|
||||
env:
|
||||
CURRENT_TAG: ${{ inputs.release_tag || github.event.release.tag_name }}
|
||||
@@ -142,6 +162,8 @@ jobs:
|
||||
--pattern 'edgeever-android-v*-arm64-v8a.apk' \
|
||||
--dir "$RUNNER_TEMP/native-assets"
|
||||
test "$(find "$RUNNER_TEMP/native-assets" -type f -name 'edgeever-android-v*-arm64-v8a.apk' | wc -l | tr -d ' ')" = "1"
|
||||
apk_path="$(find "$RUNNER_TEMP/native-assets" -type f -name 'edgeever-android-v*-arm64-v8a.apk' -print -quit)"
|
||||
node scripts/verify-android-apk-signature.mjs "$apk_path"
|
||||
gh release upload "$CURRENT_TAG" "$RUNNER_TEMP/native-assets/"* \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--clobber
|
||||
@@ -190,12 +212,7 @@ jobs:
|
||||
- name: Reset Metro cache for this checkout
|
||||
run: bun --cwd apps/mobile expo export --platform android --clear --max-workers 1 --output-dir "$RUNNER_TEMP/edgeever-metro-warmup"
|
||||
|
||||
- name: Build fast test APK
|
||||
if: github.event_name == 'push'
|
||||
run: bun run build:android:fast
|
||||
|
||||
- name: Restore Android upload keystore
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
- name: Restore Android signing keystore
|
||||
env:
|
||||
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||
run: |
|
||||
@@ -203,6 +220,16 @@ jobs:
|
||||
printf '%s' "$ANDROID_KEYSTORE_BASE64" | openssl base64 -d -A > "$RUNNER_TEMP/edgeever-upload.p12"
|
||||
chmod 600 "$RUNNER_TEMP/edgeever-upload.p12"
|
||||
|
||||
- name: Build fast main-branch APK
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
ANDROID_KEYSTORE_FILE: ${{ runner.temp }}/edgeever-upload.p12
|
||||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
EDGE_EVER_ANDROID_APK_ARCHS: arm64-v8a
|
||||
run: bun run build:android:fast
|
||||
|
||||
- name: Build signed release APK for GitHub Release
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
@@ -215,15 +242,13 @@ jobs:
|
||||
|
||||
- name: Verify release APK signature
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
"$ANDROID_SDK_ROOT/build-tools/36.0.0/apksigner" verify --verbose app/build/outputs/apk/release/app-release.apk
|
||||
working-directory: apps/mobile/android
|
||||
run: node scripts/verify-android-apk-signature.mjs apps/mobile/android/app/build/outputs/apk/release/app-release.apk
|
||||
|
||||
- name: Upload APK
|
||||
if: github.event_name == 'push'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: edgeever-android-test-release-apk
|
||||
name: edgeever-android-main-apk
|
||||
path: apps/mobile/android/app/build/outputs/apk/release/*.apk
|
||||
if-no-files-found: error
|
||||
compression-level: 0
|
||||
@@ -239,5 +264,5 @@ jobs:
|
||||
gh release upload "$RELEASE_TAG" "$release_asset" --repo "$GITHUB_REPOSITORY" --clobber
|
||||
|
||||
- name: Remove temporary upload keystore
|
||||
if: always() && github.event_name == 'workflow_dispatch'
|
||||
if: always()
|
||||
run: rm -f "$RUNNER_TEMP/edgeever-upload.p12"
|
||||
+36
-14
@@ -1,6 +1,6 @@
|
||||
# EdgeEver Mobile Builds
|
||||
|
||||
EdgeEver Mobile is built with Expo and React Native. Daily Android test packages are built directly on GitHub Actions, without using EAS Build quota.
|
||||
EdgeEver Mobile is built with Expo and React Native. Main-branch Android packages are built directly on GitHub Actions, without using EAS Build quota.
|
||||
|
||||
## App Updates
|
||||
|
||||
@@ -15,22 +15,32 @@ bunx eas-cli update --channel production --environment production --message "Des
|
||||
|
||||
The update runtime uses the app-version policy. Any native dependency, Expo SDK, native module, or native configuration change requires incrementing `expo.version` and shipping a new store build before publishing code that depends on that native change. EAS Update does not replace Google Play or App Store updates for native binaries.
|
||||
|
||||
## Android Debug APK
|
||||
## Main-Branch Android APK
|
||||
|
||||
The `Build EdgeEver Mobile` workflow runs on GitHub Actions and produces a debug APK artifact.
|
||||
The `Build EdgeEver Mobile` workflow runs on GitHub Actions and produces a
|
||||
fast arm64 APK after mobile changes land on `main`. This APK uses the same
|
||||
pinned production signing certificate as formal GitHub Release APKs. The fast
|
||||
build only disables R8, resource shrinking, and PNG crunching; it must never
|
||||
fall back to `debug.keystore`.
|
||||
|
||||
It runs:
|
||||
|
||||
```sh
|
||||
bun install --frozen-lockfile
|
||||
bun run typecheck:mobile
|
||||
cd apps/mobile
|
||||
bunx expo prebuild --platform android --non-interactive --clean
|
||||
cd android
|
||||
./gradlew assembleDebug
|
||||
bun run build:android:fast
|
||||
```
|
||||
|
||||
The APK is uploaded as a GitHub Actions artifact named `edgeever-android-debug-apk`.
|
||||
The APK is uploaded as a GitHub Actions artifact named
|
||||
`edgeever-android-main-apk`. If the stable signing credentials are unavailable,
|
||||
the workflow fails and does not upload an APK with a different signer.
|
||||
|
||||
For a local fast build, load the same signing environment used for release
|
||||
builds:
|
||||
|
||||
```sh
|
||||
bun run build:android:fast:local
|
||||
```
|
||||
|
||||
## Release Builds
|
||||
|
||||
@@ -42,15 +52,27 @@ Every formal GitHub Release must include a directly installable APK. Build the p
|
||||
bun run build:android:apk:local
|
||||
```
|
||||
|
||||
This produces `apps/mobile/android/app/build/outputs/apk/release/app-release.apk`. Verify its application version, signer, and SHA-256 before uploading it to the matching GitHub Release. Additional ABIs should only be published for an explicit compatibility need; the Play AAB continues to include all supported architectures. A release whose audited change range affects mobile runtime code, shared code used by mobile, mobile dependencies, native configuration, or APK build tooling must rebuild the APK from that release commit.
|
||||
This produces `apps/mobile/android/app/build/outputs/apk/release/app-release.apk`.
|
||||
Before upload, automation requires exactly one signer and pins its certificate
|
||||
SHA-256 to
|
||||
`22bf52a9501c89020f5acc966960152c826bfa64f31e578e858d088f8cd75d87`.
|
||||
Any other signer fails the build. Verify the application version and APK
|
||||
SHA-256 as well. Additional ABIs should only be published for an explicit
|
||||
compatibility need; the Play AAB continues to include all supported
|
||||
architectures. A release whose audited change range affects mobile runtime
|
||||
code, shared code used by mobile, mobile dependencies, native configuration,
|
||||
APK build tooling, or signer verification must rebuild the APK from that
|
||||
release commit.
|
||||
|
||||
If the audited change range does not affect the mobile binary, the most recent compatible, verified APK may be attached again without rebuilding. Keep its original versioned filename and checksum, and state the source release explicitly; never rename an older binary to the current release version.
|
||||
|
||||
The release workflow performs this audit automatically. Web-only releases use
|
||||
an Ubuntu planning job and copy the single verified `arm64-v8a` APK from the
|
||||
previous formal Release. The self-hosted Android runner is scheduled only when
|
||||
the audited range includes mobile runtime code, shared mobile packages,
|
||||
dependencies, native configuration, or Android build tooling.
|
||||
The release workflow performs this audit automatically and verifies the pinned
|
||||
signer for both newly built and reused APKs. Web-only releases use an Ubuntu
|
||||
planning job and copy the single verified `arm64-v8a` APK from the previous
|
||||
formal Release. The self-hosted Android runner is scheduled only when the
|
||||
audited range includes mobile runtime code, shared mobile packages,
|
||||
dependencies, native configuration, Android build tooling, or signer
|
||||
verification.
|
||||
|
||||
### Recommended local Play build
|
||||
|
||||
|
||||
+2
-1
@@ -21,6 +21,7 @@
|
||||
"dev:mobile": "bun --cwd apps/mobile expo start",
|
||||
"prepare:mobile:icons": "bun scripts/prepare-mobile-icons.mjs",
|
||||
"build:android:fast": "bash scripts/build-android-local.sh fast",
|
||||
"build:android:fast:local": "bun --env-file=${EDGE_EVER_ANDROID_ENV_FILE:-$HOME/.config/edgeever/android/signing.env} run build:android:fast",
|
||||
"build:android:apk": "bash scripts/build-android-local.sh apk",
|
||||
"build:android:apk:local": "bun --env-file=${EDGE_EVER_ANDROID_ENV_FILE:-$HOME/.config/edgeever/android/signing.env} run build:android:apk",
|
||||
"build:android:play": "bash scripts/build-android-local.sh play",
|
||||
@@ -38,7 +39,7 @@
|
||||
"build:desktop": "EDGE_EVER_DESKTOP_BUILD=1 bun run build:web && bun run build:desktop:sidecar && bun run --cwd apps/desktop dist",
|
||||
"benchmark:desktop:sidecar": "bun scripts/benchmark-desktop-sidecar.mjs",
|
||||
"verify:web-performance": "bun scripts/verify-web-performance.mjs",
|
||||
"test:release-planning": "bun test scripts/plan-native-release.test.mjs scripts/check-native-release-assets.test.mjs scripts/create-mac-update-metadata.test.mjs scripts/release.test.mjs scripts/validate-store-delivery.test.mjs scripts/store-delivery.test.mjs apps/web/src/lib/version-check.test.mjs apps/mobile/src/lib/mobile-release.test.ts",
|
||||
"test:release-planning": "bun test scripts/plan-native-release.test.mjs scripts/check-native-release-assets.test.mjs scripts/create-mac-update-metadata.test.mjs scripts/release.test.mjs scripts/validate-store-delivery.test.mjs scripts/store-delivery.test.mjs scripts/verify-android-apk-signature.test.mjs apps/web/src/lib/version-check.test.mjs apps/mobile/src/lib/mobile-release.test.ts",
|
||||
"release": "bun scripts/release.mjs",
|
||||
"publish:stores": "bun scripts/store-delivery.mjs",
|
||||
"test:desktop:sidecar": "bun scripts/test-desktop-sidecar.mjs",
|
||||
|
||||
@@ -77,22 +77,6 @@ COMMON_ARGS=(
|
||||
-Dorg.gradle.jvmargs=-Xmx6g\ -XX:MaxMetaspaceSize=1g\ -Dfile.encoding=UTF-8
|
||||
)
|
||||
|
||||
if [[ "$MODE" == "fast" ]]; then
|
||||
echo "构建 arm64 真机测试 Release APK(关闭 R8、资源压缩和 PNG crunch)..."
|
||||
run_timed "Gradle 快速 APK 构建" ./gradlew assembleRelease \
|
||||
"${COMMON_ARGS[@]}" \
|
||||
-PreactNativeArchitectures=arm64-v8a \
|
||||
-Pandroid.enableMinifyInReleaseBuilds=false \
|
||||
-Pandroid.enableShrinkResourcesInReleaseBuilds=false \
|
||||
-Pandroid.enablePngCrunchInReleaseBuilds=false \
|
||||
-Pandroid.injected.signing.store.file="$ANDROID_DIR/app/debug.keystore" \
|
||||
-Pandroid.injected.signing.store.password=android \
|
||||
-Pandroid.injected.signing.key.alias=androiddebugkey \
|
||||
-Pandroid.injected.signing.key.password=android
|
||||
echo "完成: $ANDROID_DIR/app/build/outputs/apk/release/app-release.apk"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
: "${ANDROID_KEYSTORE_FILE:?请设置 ANDROID_KEYSTORE_FILE(本地上传密钥路径)}"
|
||||
: "${ANDROID_KEYSTORE_PASSWORD:?请设置 ANDROID_KEYSTORE_PASSWORD}"
|
||||
: "${ANDROID_KEY_ALIAS:?请设置 ANDROID_KEY_ALIAS}"
|
||||
@@ -101,6 +85,30 @@ fi
|
||||
PLAY_ARCHS="${EDGE_EVER_ANDROID_ARCHS:-armeabi-v7a,arm64-v8a,x86,x86_64}"
|
||||
APK_ARCHS="${EDGE_EVER_ANDROID_APK_ARCHS:-arm64-v8a}"
|
||||
KEYSTORE_FILE="$(cd "$(dirname "$ANDROID_KEYSTORE_FILE")" && pwd)/$(basename "$ANDROID_KEYSTORE_FILE")"
|
||||
APK_PATH="$ANDROID_DIR/app/build/outputs/apk/release/app-release.apk"
|
||||
APKSIGNER_PATH="$ANDROID_SDK_ROOT/build-tools/36.0.0/apksigner"
|
||||
|
||||
if [[ "$MODE" == "fast" ]]; then
|
||||
echo "构建生产签名 arm64 快速 APK(关闭 R8、资源压缩和 PNG crunch)..."
|
||||
run_timed "Gradle 快速 APK 构建" ./gradlew assembleRelease \
|
||||
"${COMMON_ARGS[@]}" \
|
||||
-PreactNativeArchitectures=arm64-v8a \
|
||||
-Pandroid.enableMinifyInReleaseBuilds=false \
|
||||
-Pandroid.enableShrinkResourcesInReleaseBuilds=false \
|
||||
-Pandroid.enablePngCrunchInReleaseBuilds=false \
|
||||
-Pandroid.injected.signing.store.file="$KEYSTORE_FILE" \
|
||||
-Pandroid.injected.signing.store.password="$ANDROID_KEYSTORE_PASSWORD" \
|
||||
-Pandroid.injected.signing.key.alias="$ANDROID_KEY_ALIAS" \
|
||||
-Pandroid.injected.signing.key.password="$ANDROID_KEY_PASSWORD" \
|
||||
-Pandroid.injected.signing.store.type=PKCS12
|
||||
test -s "$APK_PATH"
|
||||
test -x "$APKSIGNER_PATH"
|
||||
run_timed "校验 APK 固定签名" \
|
||||
node "$PROJECT_ROOT/scripts/verify-android-apk-signature.mjs" \
|
||||
"$APK_PATH" "$APKSIGNER_PATH"
|
||||
echo "完成: $APK_PATH"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$MODE" == "apk" ]]; then
|
||||
echo "构建生产签名 APK(${APK_ARCHS})..."
|
||||
@@ -112,13 +120,13 @@ if [[ "$MODE" == "apk" ]]; then
|
||||
-Pandroid.injected.signing.key.alias="$ANDROID_KEY_ALIAS" \
|
||||
-Pandroid.injected.signing.key.password="$ANDROID_KEY_PASSWORD" \
|
||||
-Pandroid.injected.signing.store.type=PKCS12
|
||||
APK_PATH="$ANDROID_DIR/app/build/outputs/apk/release/app-release.apk"
|
||||
APKSIGNER_PATH="$ANDROID_SDK_ROOT/build-tools/36.0.0/apksigner"
|
||||
AAPT2_PATH="$ANDROID_SDK_ROOT/build-tools/36.0.0/aapt2"
|
||||
test -s "$APK_PATH"
|
||||
test -x "$APKSIGNER_PATH"
|
||||
test -x "$AAPT2_PATH"
|
||||
run_timed "校验 APK 签名" "$APKSIGNER_PATH" verify --verbose "$APK_PATH"
|
||||
run_timed "校验 APK 固定签名" \
|
||||
node "$PROJECT_ROOT/scripts/verify-android-apk-signature.mjs" \
|
||||
"$APK_PATH" "$APKSIGNER_PATH"
|
||||
run_timed "读取 APK 信息" bash -c '"$1" dump badging "$2" | sed -n "1p"' _ "$AAPT2_PATH" "$APK_PATH"
|
||||
run_timed "计算 APK SHA-256" shasum -a 256 "$APK_PATH"
|
||||
echo "完成: $APK_PATH"
|
||||
|
||||
@@ -24,7 +24,12 @@ export const planNativeRelease = (platform, changedFiles) => {
|
||||
|
||||
const relevantFiles =
|
||||
platform === "mobile"
|
||||
? new Set(["bun.lock", "scripts/build-android-local.sh"])
|
||||
? new Set([
|
||||
".github/workflows/mobile-build.yml",
|
||||
"bun.lock",
|
||||
"scripts/build-android-local.sh",
|
||||
"scripts/verify-android-apk-signature.mjs",
|
||||
])
|
||||
: new Set([
|
||||
".github/workflows/desktop-build.yml",
|
||||
"bun.lock",
|
||||
|
||||
@@ -19,6 +19,8 @@ describe("native release planning", () => {
|
||||
"packages/shared/src/index.ts",
|
||||
"bun.lock",
|
||||
"scripts/build-android-local.sh",
|
||||
"scripts/verify-android-apk-signature.mjs",
|
||||
".github/workflows/mobile-build.yml",
|
||||
];
|
||||
expect(planNativeRelease("mobile", changedFiles)).toEqual({
|
||||
rebuild: true,
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, readdirSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export const EDGE_EVER_ANDROID_SIGNER_SHA256 =
|
||||
"22bf52a9501c89020f5acc966960152c826bfa64f31e578e858d088f8cd75d87";
|
||||
|
||||
const normalizeFingerprint = (value) =>
|
||||
value.toLowerCase().replaceAll(":", "").trim();
|
||||
|
||||
export const verifyAndroidSignerOutput = (output) => {
|
||||
const signerDigests = [
|
||||
...output.matchAll(
|
||||
/Signer #\d+ certificate SHA-256 digest:\s*([0-9a-f:]+)/gi,
|
||||
),
|
||||
].map((match) => normalizeFingerprint(match[1]));
|
||||
|
||||
if (signerDigests.length !== 1) {
|
||||
throw new Error(
|
||||
`Expected exactly one Android signer, found ${signerDigests.length}.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (signerDigests[0] !== EDGE_EVER_ANDROID_SIGNER_SHA256) {
|
||||
throw new Error(
|
||||
`Android signer mismatch: expected ${EDGE_EVER_ANDROID_SIGNER_SHA256}, received ${signerDigests[0]}.`,
|
||||
);
|
||||
}
|
||||
|
||||
return signerDigests[0];
|
||||
};
|
||||
|
||||
const findApkSigner = (explicitPath) => {
|
||||
if (explicitPath) return explicitPath;
|
||||
|
||||
const sdkRoot = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME;
|
||||
if (!sdkRoot) {
|
||||
throw new Error(
|
||||
"ANDROID_SDK_ROOT or ANDROID_HOME is required to locate apksigner.",
|
||||
);
|
||||
}
|
||||
|
||||
const buildToolsRoot = join(sdkRoot, "build-tools");
|
||||
const versions = readdirSync(buildToolsRoot, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name)
|
||||
.sort((left, right) =>
|
||||
right.localeCompare(left, undefined, { numeric: true }),
|
||||
);
|
||||
|
||||
for (const version of versions) {
|
||||
const candidate = join(buildToolsRoot, version, "apksigner");
|
||||
if (existsSync(candidate)) return candidate;
|
||||
}
|
||||
|
||||
throw new Error(`Could not find apksigner under ${buildToolsRoot}.`);
|
||||
};
|
||||
|
||||
const run = () => {
|
||||
const [apkPath, explicitApkSigner] = process.argv.slice(2);
|
||||
if (!apkPath) {
|
||||
console.error(
|
||||
"Usage: node scripts/verify-android-apk-signature.mjs <apk-path> [apksigner-path]",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const apkSigner = findApkSigner(explicitApkSigner);
|
||||
const result = spawnSync(
|
||||
apkSigner,
|
||||
["verify", "--verbose", "--print-certs", apkPath],
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
const output = `${result.stdout || ""}\n${result.stderr || ""}`;
|
||||
|
||||
if (result.status !== 0) {
|
||||
process.stderr.write(output);
|
||||
throw new Error(`apksigner rejected ${apkPath}.`);
|
||||
}
|
||||
|
||||
const signerSha256 = verifyAndroidSignerOutput(output);
|
||||
process.stdout.write(`Android signer SHA-256: ${signerSha256}\n`);
|
||||
};
|
||||
|
||||
if (
|
||||
process.argv[1] &&
|
||||
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
||||
) {
|
||||
run();
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
EDGE_EVER_ANDROID_SIGNER_SHA256,
|
||||
verifyAndroidSignerOutput,
|
||||
} from "./verify-android-apk-signature.mjs";
|
||||
|
||||
describe("Android APK signer verification", () => {
|
||||
test("accepts the single pinned EdgeEver signer", () => {
|
||||
expect(
|
||||
verifyAndroidSignerOutput(
|
||||
`Signer #1 certificate SHA-256 digest: ${EDGE_EVER_ANDROID_SIGNER_SHA256}`,
|
||||
),
|
||||
).toBe(EDGE_EVER_ANDROID_SIGNER_SHA256);
|
||||
});
|
||||
|
||||
test("normalizes colon-separated uppercase fingerprints", () => {
|
||||
const fingerprint = EDGE_EVER_ANDROID_SIGNER_SHA256.toUpperCase()
|
||||
.match(/.{2}/g)
|
||||
.join(":");
|
||||
expect(
|
||||
verifyAndroidSignerOutput(
|
||||
`Signer #1 certificate SHA-256 digest: ${fingerprint}`,
|
||||
),
|
||||
).toBe(EDGE_EVER_ANDROID_SIGNER_SHA256);
|
||||
});
|
||||
|
||||
test("rejects an APK signed by a different certificate", () => {
|
||||
expect(() =>
|
||||
verifyAndroidSignerOutput(
|
||||
`Signer #1 certificate SHA-256 digest: ${"0".repeat(64)}`,
|
||||
),
|
||||
).toThrow("Android signer mismatch");
|
||||
});
|
||||
|
||||
test("rejects missing or multiple signers", () => {
|
||||
expect(() => verifyAndroidSignerOutput("Verified")).toThrow(
|
||||
"Expected exactly one Android signer",
|
||||
);
|
||||
expect(() =>
|
||||
verifyAndroidSignerOutput(
|
||||
[
|
||||
`Signer #1 certificate SHA-256 digest: ${EDGE_EVER_ANDROID_SIGNER_SHA256}`,
|
||||
`Signer #2 certificate SHA-256 digest: ${EDGE_EVER_ANDROID_SIGNER_SHA256}`,
|
||||
].join("\n"),
|
||||
),
|
||||
).toThrow("Expected exactly one Android signer");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user