#!/bin/sh # Sourceable, secret-free helpers for the Kepr release gate. # # This file intentionally contains no signing or Gradle commands. It only # selects a stable SDK tool directory and validates public release metadata so # the publication script and its hermetic negative-path tests share one # implementation. # Return success only for a stable build-tools directory basename such as # "35.0.0". Preview/RC/beta/canary labels, "latest", leading-zero segments, # and arbitrary names are rejected before any version sorting occurs. is_stable_build_tools_basename() { build_tools_name=$1 old_ifs=$IFS IFS=. # shellcheck disable=SC2086 set -- $build_tools_name IFS=$old_ifs [ "$#" -eq 3 ] || return 1 for segment in "$@"; do case "$segment" in 0|[1-9]|[1-9][0-9]*) ;; *) return 1 ;; esac done } # Print the newest stable build-tools directory containing aapt2. Only # basenames that passed is_stable_build_tools_basename reach sort -V. find_stable_build_tools_dir() { sdk_root=$1 build_tools_root=$sdk_root/build-tools [ -d "$build_tools_root" ] || return 1 best_name= for candidate in "$build_tools_root"/*; do [ -d "$candidate" ] || continue candidate_name=${candidate##*/} is_stable_build_tools_basename "$candidate_name" || continue [ -x "$candidate/aapt2" ] || continue if [ -z "$best_name" ] || [ "$(printf '%s\n' "$best_name" "$candidate_name" | LC_ALL=C sort -V | tail -n 1)" = "$candidate_name" ]; then best_name=$candidate_name fi done [ -n "$best_name" ] || return 1 printf '%s\n' "$build_tools_root/$best_name" } # Read and validate the one-line semantic version authority. Keep this in the # shared helper so publication tests exercise the exact same root-file gate. read_root_semver() { version_file=$1 [ -r "$version_file" ] || return 1 root_version=$(sed -n '1p' "$version_file") || return 1 [ -n "$root_version" ] || return 1 is_strict_semver "$root_version" || return 1 # A second non-empty line would make VERSION ambiguous even if the first # line happened to be valid. extra_lines=$(sed -n '2,$p' "$version_file" | sed '/^[[:space:]]*$/d') [ -z "$extra_lines" ] || return 1 printf '%s\n' "$root_version" } is_strict_semver() { semantic_version=$1 case "$semantic_version" in ''|*[!0-9.]* ) return 1 ;; esac old_ifs=$IFS IFS=. # shellcheck disable=SC2086 set -- $semantic_version IFS=$old_ifs [ "$#" -eq 3 ] || return 1 for segment in "$@"; do case "$segment" in 0|[1-9]|[1-9][0-9]*) ;; *) return 1 ;; esac done } # Validate Gradle output-metadata.json, the APK's aapt2 badging, and VERSION as # one release identity. The Python standard library is used only for parsing # generated JSON; duplicate keys and ambiguous element lists are rejected. # Arguments: metadata-json badging-file root-version expected-app-id expected-output expected-abi validate_release_metadata() { metadata_file=$1 badging_file=$2 root_version=$3 expected_app_id=$4 expected_output=$5 expected_abi=$6 python3 - "$metadata_file" "$badging_file" "$root_version" "$expected_app_id" "$expected_output" "$expected_abi" <<'PY' import json import re import sys metadata_path, badging_path, root_version, expected_app_id, expected_output, expected_abi = sys.argv[1:] def reject(message): raise SystemExit("release metadata rejected: " + message) def strict_semver(value): if not isinstance(value, str) or not re.fullmatch(r"(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)", value): reject("version is not strict SemVer") for segment in value.split("."): if int(segment) > 2147483647: reject("version segment is out of range") class NoDuplicateKeys(dict): def __init__(self, pairs): seen = set() for key, value in pairs: if key in seen: reject("duplicate JSON key: " + key) seen.add(key) self[key] = value try: with open(metadata_path, encoding="utf-8") as stream: metadata = json.load(stream, object_pairs_hook=NoDuplicateKeys) except (OSError, json.JSONDecodeError) as error: reject("malformed Gradle metadata: " + str(error)) if not isinstance(metadata, dict): reject("Gradle metadata root is not an object") if metadata.get("applicationId") != expected_app_id: reject("Gradle applicationId does not match production package") elements = metadata.get("elements") if not isinstance(elements, list) or not all(isinstance(item, dict) for item in elements): reject("Gradle metadata elements are malformed") matches = [item for item in elements if item.get("outputFile") == expected_output] if len(matches) != 1: reject("Gradle metadata must contain exactly one matching APK element") element = matches[0] if element.get("versionName") != root_version: reject("Gradle versionName does not match VERSION") expected_code = element.get("versionCode") if isinstance(expected_code, bool) or not isinstance(expected_code, int) or expected_code < 1: reject("Gradle versionCode is missing or invalid") strict_semver(root_version) try: with open(badging_path, encoding="utf-8") as stream: badging = stream.read() except OSError as error: reject("APK badging could not be read: " + str(error)) package_lines = [line for line in badging.splitlines() if line.startswith("package:")] if len(package_lines) != 1: reject("APK badging must contain exactly one package line") match = re.fullmatch( r"package: name='([^']+)' versionCode='([^']+)' versionName='([^']+)'(?: .*)?", package_lines[0], ) if not match: reject("APK badging package line is malformed") apk_app_id, apk_code, apk_version = match.groups() if apk_app_id != expected_app_id: reject("APK applicationId does not match Gradle metadata") if apk_code != str(expected_code): reject("APK versionCode does not match Gradle metadata") if apk_version != root_version: reject("APK versionName does not match VERSION") strict_semver(apk_version) native_lines = [line for line in badging.splitlines() if line.startswith("native-code:")] if native_lines != [f"native-code: '{expected_abi}'"]: reject("APK native ABI does not match requested platform") PY } # PB-26-132: the publish path claims it validates the APK's signing identity, but # aapt2 badging reports no signature. Compare the APK's signer certificate digest # against the certificate in the configured release keystore — a build # environment that produced an artifact signed with a different key is exactly # the drift this refuses. Both helpers print a lowercase hex digest, or nothing # when the source could not be read, so a missing digest fails closed below. # # Arguments: keystore storepass alias release_certificate_sha256() { keystore=$1; storepass=$2; alias=$3 "$JAVA_HOME/bin/keytool" -list -v -keystore "$keystore" -storepass "$storepass" -alias "$alias" 2>/dev/null | sed -n 's/.*SHA256: //p' | sed -n '1p' | tr -d ':' | tr 'A-F' 'a-f' | tr -d '[:space:]' } # The release keystore is a trust anchor, not merely a local build input. # Keep the established certificate pinned so a temporary or replacement key # cannot pass a self-consistent local comparison and reach existing installs. EXPECTED_RELEASE_CERTIFICATE="e10b975178f0bee7fb5a4802519da70ec2988b6ef7d8c9401fbbcff5a1137668" assert_established_release_certificate() { actual=$1 if [ "$actual" != "$EXPECTED_RELEASE_CERTIFICATE" ]; then echo "refusing to publish: configured keystore is not the established Peach release certificate" >&2 return 1 fi } # Arguments: apksigner apk apk_signer_sha256() { apksigner=$1; apk=$2 "$apksigner" verify --print-certs "$apk" 2>/dev/null | sed -n 's/.*certificate SHA-256 digest: //p' | sed -n '1p' | tr -d ':' | tr 'A-F' 'a-f' | tr -d '[:space:]' } # Arguments: expected actual label assert_release_signer_matches() { expected=$1; actual=$2; label=$3 if [ -z "$expected" ] || [ -z "$actual" ]; then echo "refusing to publish: $label signing identity could not be established" >&2 return 1 fi if [ "$expected" != "$actual" ]; then echo "refusing to publish: $label is not signed by the release certificate" >&2 echo " release keystore certificate: $expected" >&2 echo " APK signer certificate: $actual" >&2 return 1 fi echo "release signing identity verified: $actual" }