Publish IKAR Android 1.0.4 and verify release artifacts
Tests / test (push) Canceled after 0s
Native crypto / native-crypto (push) Canceled after 0s
Release (main) / android (oneme) (push) Canceled after 0s
Release (main) / android (qlyra) (push) Canceled after 0s
Release (main) / windows (push) Canceled after 0s
Release (main) / linux (push) Canceled after 0s
Release (main) / macos (push) Canceled after 0s
Release (main) / ios (push) Canceled after 0s
Release (main) / release (push) Canceled after 0s
Tests / test (push) Canceled after 0s
Native crypto / native-crypto (push) Canceled after 0s
Release (main) / android (oneme) (push) Canceled after 0s
Release (main) / android (qlyra) (push) Canceled after 0s
Release (main) / windows (push) Canceled after 0s
Release (main) / linux (push) Canceled after 0s
Release (main) / macos (push) Canceled after 0s
Release (main) / ios (push) Canceled after 0s
Release (main) / release (push) Canceled after 0s
This commit is contained in:
+18
-10
@@ -1,45 +1,53 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import '../lib/core/transport/ikar_network.dart';
|
||||
import 'package:ikar/core/transport/ikar_network.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
Future<void> main(List<String> args) async {
|
||||
final version = args.isEmpty ? '1.0.0' : args[0];
|
||||
final code = args.length < 2 ? 1 : int.parse(args[1]);
|
||||
final reportSuffix = code == 1 ? '' : '-$version';
|
||||
final http = IkarNetwork.client();
|
||||
try {
|
||||
final base = Uri.parse('https://ikar.kusoft.xyz');
|
||||
final response = await (await http.getUrl(
|
||||
base.resolve('/api/apps/ikar/manifest'),
|
||||
)).close();
|
||||
if (response.statusCode != 200)
|
||||
if (response.statusCode != 200) {
|
||||
throw StateError('Release manifest unavailable');
|
||||
}
|
||||
final manifest = jsonDecode(await utf8.decodeStream(response));
|
||||
if (manifest['app']['packageName'] != 'ru.ikar.app' ||
|
||||
manifest['release']['version'] != '1.0.0+1')
|
||||
manifest['release']['version'] != '$version+$code') {
|
||||
throw StateError('Incorrect release identity');
|
||||
}
|
||||
final expected = (await File(
|
||||
'artifacts/ikar-1.0.0+1.apk.sha256',
|
||||
'artifacts/ikar-$version+$code.apk.sha256',
|
||||
).readAsString()).split(' ').first;
|
||||
if (manifest['release']['sha256'] != expected)
|
||||
if (manifest['release']['sha256'] != expected) {
|
||||
throw StateError('Manifest checksum mismatch');
|
||||
}
|
||||
final download = await (await http.getUrl(
|
||||
base.resolve(manifest['release']['downloadPath']),
|
||||
)).close();
|
||||
if (download.statusCode != 200)
|
||||
if (download.statusCode != 200) {
|
||||
throw StateError('APK download unavailable');
|
||||
}
|
||||
final digest = await sha256.bind(download).first;
|
||||
if (digest.toString() != expected)
|
||||
if (digest.toString() != expected) {
|
||||
throw StateError('Downloaded APK checksum mismatch');
|
||||
}
|
||||
final report = {
|
||||
'passed': true,
|
||||
'package': 'ru.ikar.app',
|
||||
'version': '1.0.0+1',
|
||||
'version': '$version+$code',
|
||||
'sha256': expected,
|
||||
'tlsValidation': true,
|
||||
'bytes': manifest['release']['packageSizeBytes'],
|
||||
'timestamp': DateTime.now().toUtc().toIso8601String(),
|
||||
};
|
||||
await File(
|
||||
'artifacts/release-verification.json',
|
||||
'artifacts/release-verification$reportSuffix.json',
|
||||
).writeAsString(const JsonEncoder.withIndent(' ').convert(report));
|
||||
stdout.writeln(jsonEncode(report));
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Verify APK identity, upgrade signature, fresh Flutter libraries and icon assets."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import zipfile
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def sha(data):
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def verify(args):
|
||||
apk = Path(args.apk)
|
||||
previous = Path(args.previous)
|
||||
sdk = Path(args.build_tools)
|
||||
metadata = subprocess.check_output([str(sdk / 'aapt.exe'), 'dump', 'badging', str(apk)], text=True, encoding='utf-8')
|
||||
identity = re.search(r"package: name='([^']+)' versionCode='([^']+)' versionName='([^']+)'", metadata)
|
||||
if identity is None or identity.groups() != ('ru.ikar.app', str(args.code), args.version):
|
||||
raise RuntimeError('Incorrect Android package or version')
|
||||
|
||||
def certificate(path):
|
||||
output = subprocess.check_output([str(sdk / 'apksigner.bat'), 'verify', '--verbose', '--print-certs', str(path)], text=True, encoding='utf-8')
|
||||
values = re.findall(r'Signer #\d+ certificate SHA-256 digest: ([0-9a-f]+)', output)
|
||||
if len(values) != 1 or 'Verified using v2 scheme (APK Signature Scheme v2): true' not in output:
|
||||
raise RuntimeError('APK signature verification failed')
|
||||
return values[0]
|
||||
|
||||
signer = certificate(apk)
|
||||
if signer != certificate(previous):
|
||||
raise RuntimeError('Signing certificate differs from the previously published APK')
|
||||
latest_source = max(path.stat().st_mtime for path in Path('lib').rglob('*.dart'))
|
||||
libraries = []
|
||||
assets = []
|
||||
with zipfile.ZipFile(apk) as package:
|
||||
icon_path = re.search(r"application-icon-160:'([^']+)'", metadata).group(1)
|
||||
icon_tree = subprocess.check_output([str(sdk / 'aapt.exe'), 'dump', 'xmltree', str(apk), icon_path], text=True, encoding='utf-8')
|
||||
inset = re.search(r'android:inset\([^)]*\)=\(type 0x6\)0x([0-9a-f]+)', icon_tree)
|
||||
if inset is None or int(inset[1], 16) >> 8 != 0:
|
||||
raise RuntimeError('Adaptive launcher icon still has the old inset')
|
||||
resources = subprocess.check_output([str(sdk / 'aapt.exe'), 'dump', '--values', 'resources', str(apk)], text=True, encoding='utf-8')
|
||||
paths = re.findall(r'[^\n]*ru\.ikar\.app:drawable/ic_launcher_foreground: t=0x03[^\n]*\n\s*\(string8\) "([^"]+)"', resources)
|
||||
expected_icons = {}
|
||||
for path in Path('android/app/src/main/res').glob('drawable-*/ic_launcher_foreground.png'):
|
||||
with Image.open(path) as image:
|
||||
expected_icons[image.size] = image.convert('RGBA').tobytes()
|
||||
if len(paths) != len(expected_icons):
|
||||
raise RuntimeError('Missing launcher foreground densities')
|
||||
for path in paths:
|
||||
with Image.open(io.BytesIO(package.read(path))) as image:
|
||||
if image.convert('RGBA').tobytes() != expected_icons.get(image.size):
|
||||
raise RuntimeError('Packaged launcher icon differs from the current drawing')
|
||||
for abi in ('armeabi-v7a', 'arm64-v8a', 'x86_64'):
|
||||
generated = Path(f'build/app/intermediates/flutter/ikarRelease/jniLibs/{abi}/libapp.so')
|
||||
stripped = Path(f'build/app/intermediates/stripped_native_libs/ikarRelease/stripIkarReleaseDebugSymbols/out/lib/{abi}/libapp.so')
|
||||
actual = sha(package.read(f'lib/{abi}/libapp.so'))
|
||||
if actual != sha(stripped.read_bytes()) or generated.stat().st_mtime < latest_source:
|
||||
raise RuntimeError(f'Stale Flutter library for {abi}')
|
||||
for name in ('libflutter.so', 'libfjs.so', 'libkomet_crypto.so', 'libkomet_tlottie.so'):
|
||||
if not package.read(f'lib/{abi}/{name}'):
|
||||
raise RuntimeError(f'Missing native dependency: {abi}/{name}')
|
||||
libraries.append({'abi': abi, 'apkSha256': actual, 'matchesStripped': True, 'generatedAfterLastSourceChange': True})
|
||||
for name in ('ikar.png', 'ikar_icon.png'):
|
||||
expected = Path('assets') / name
|
||||
if sha(package.read(f'assets/flutter_assets/assets/{name}')) != sha(expected.read_bytes()):
|
||||
raise RuntimeError(f'Stale icon asset: {name}')
|
||||
assets.append(name)
|
||||
report = {'passed': True, 'package': 'ru.ikar.app', 'version': args.version,
|
||||
'androidVersionCode': args.code, 'sha256': sha(apk.read_bytes()), 'bytes': apk.stat().st_size,
|
||||
'certificateSha256': signer, 'upgradeSignerMatches': True, 'libraries': libraries, 'currentIconAssets': assets,
|
||||
'adaptiveLauncherMatchesCurrentDrawing': True, 'adaptiveLauncherInsetZero': True}
|
||||
Path(f'artifacts/apk-verification-{args.version}.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
|
||||
print(json.dumps(report))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--apk', required=True)
|
||||
parser.add_argument('--previous', required=True)
|
||||
parser.add_argument('--version', required=True)
|
||||
parser.add_argument('--code', required=True, type=int)
|
||||
parser.add_argument('--build-tools', default='C:/Tools/AndroidSdk/build-tools/36.0.0')
|
||||
verify(parser.parse_args())
|
||||
Reference in New Issue
Block a user