Last active
April 17, 2024 00:03
-
-
Save obfusk/cfab950649631c3ece723b9eb277304b to your computer and use it in GitHub Desktop.
verify APK and get SHA-256 of first cert
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.io.File; | |
import java.io.IOException; | |
import java.security.NoSuchAlgorithmException; | |
import java.security.cert.CertificateEncodingException; | |
import java.security.cert.X509Certificate; | |
import java.util.List; | |
import com.android.apksig.ApkVerificationIssue; | |
import com.android.apksig.ApkVerifier; | |
import com.android.apksig.apk.ApkFormatException; | |
public class Cert { | |
public static void main(String[] args) { | |
try { | |
ApkVerifier.Builder builder = new ApkVerifier.Builder(new File(args[0])); | |
ApkVerifier.Result result = builder.build().verify(); | |
if (result.isVerified()) { | |
List<X509Certificate> signerCerts = result.getSignerCertificates(); | |
String versions = String.join(",", | |
"v1=" + (result.isVerifiedUsingV1Scheme() ? "true" : "false"), | |
"v2=" + (result.isVerifiedUsingV2Scheme() ? "true" : "false"), | |
"v3=" + (result.isVerifiedUsingV3Scheme() ? "true" : "false")); | |
String header = "verified\n" + versions + "\n" + signerCerts.size() + "\n"; | |
System.out.write(header.getBytes("UTF-8")); | |
for (X509Certificate signerCert : signerCerts) { | |
byte[] cert = signerCert.getEncoded(); | |
System.out.write((cert.length + ":").getBytes("UTF-8")); | |
System.out.write(cert); | |
} | |
} else { | |
for (ApkVerificationIssue error : result.getErrors()) { | |
System.err.println("Error: " + error); | |
} | |
System.exit(1); | |
} | |
} catch (IOException | NoSuchAlgorithmException | CertificateEncodingException | ApkFormatException e) { | |
System.exit(1); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
# encoding: utf-8 | |
# SPDX-FileCopyrightText: 2024 FC (Fay) Stegerman <[email protected]> | |
# SPDX-License-Identifier: AGPL-3.0-or-later | |
import hashlib | |
import os | |
import shutil | |
import subprocess | |
from pathlib import Path | |
from typing import Dict, List, Optional, Tuple | |
CACHE_DIR = Path.home() / ".cache" / "python-apksig" | |
SDK_ENV = ("ANDROID_HOME", "ANDROID_SDK", "ANDROID_SDK_ROOT") | |
SDK_JAR = "lib/apksigner.jar" | |
APKSIGNER_JARS = ("/usr/share/java/apksigner.jar", "/usr/lib/android-sdk/build-tools/debian/apksigner.jar") | |
CERT_JAVA_CODE = r""" | |
import java.io.File; | |
import java.io.IOException; | |
import java.security.NoSuchAlgorithmException; | |
import java.security.cert.CertificateEncodingException; | |
import java.security.cert.X509Certificate; | |
import java.util.List; | |
import com.android.apksig.ApkVerificationIssue; | |
import com.android.apksig.ApkVerifier; | |
import com.android.apksig.apk.ApkFormatException; | |
public class Cert { | |
public static void main(String[] args) { | |
try { | |
ApkVerifier.Builder builder = new ApkVerifier.Builder(new File(args[0])); | |
ApkVerifier.Result result = builder.build().verify(); | |
if (result.isVerified()) { | |
List<X509Certificate> signerCerts = result.getSignerCertificates(); | |
String versions = String.join(",", | |
"v1=" + (result.isVerifiedUsingV1Scheme() ? "true" : "false"), | |
"v2=" + (result.isVerifiedUsingV2Scheme() ? "true" : "false"), | |
"v3=" + (result.isVerifiedUsingV3Scheme() ? "true" : "false")); | |
String header = "verified\n" + versions + "\n" + signerCerts.size() + "\n"; | |
System.out.write(header.getBytes("UTF-8")); | |
for (X509Certificate signerCert : signerCerts) { | |
byte[] cert = signerCert.getEncoded(); | |
System.out.write((cert.length + ":").getBytes("UTF-8")); | |
System.out.write(cert); | |
} | |
} else { | |
for (ApkVerificationIssue error : result.getErrors()) { | |
System.err.println("Error: " + error); | |
} | |
System.exit(1); | |
} | |
} catch (IOException | NoSuchAlgorithmException | CertificateEncodingException | ApkFormatException e) { | |
System.exit(1); | |
} | |
} | |
} | |
"""[1:] | |
CERT_JAVA_SHA256 = hashlib.sha256(CERT_JAVA_CODE.encode()).hexdigest() | |
class Error(Exception): | |
pass | |
def get_signing_certs(apkfile: Path, *, java: str, apksigner_jar: str, | |
cert_java: Path) -> Tuple[List[bytes], Dict[str, bool]]: | |
""" | |
Get APK signing key certificates using apksigner JAR. | |
NB: this validates the signature(s)! | |
""" | |
if cert_java.suffix == ".java": | |
cert_arg = str(cert_java) | |
classpath = apksigner_jar | |
else: | |
cert_arg = cert_java.stem | |
classpath = f"{cert_java.parent}:{apksigner_jar}" | |
args = (java, "-classpath", classpath, cert_arg, str(apkfile)) | |
try: | |
out = subprocess.run(args, check=True, stdout=subprocess.PIPE).stdout | |
except subprocess.CalledProcessError as e: | |
raise Error(f"Verification with apksigner failed: {e}") from e | |
except FileNotFoundError as e: | |
raise Error(f"Could not run apksigner: {e}") from e | |
try: | |
verified, versions, num_certs_str, certs_data = out.split(b"\n", 3) | |
num_certs = int(num_certs_str) | |
if verified != b"verified" or num_certs < 1: | |
raise Error("Verification output mismatch") | |
vsns = {k: v == "true" for kv in versions.decode().split(",") for k, v in [kv.split("=")]} | |
if sorted(vsns.keys()) != ["v1", "v2", "v3"] or not any(vsns.values()): | |
raise Error("Verification output mismatch") | |
certs = [] | |
for i in range(num_certs): | |
cert_size_str, certs_data = certs_data.split(b":", 1) | |
cert_size = int(cert_size_str) | |
cert, certs_data = certs_data[:cert_size], certs_data[cert_size:] | |
if len(cert) != cert_size: | |
raise Error("Verification output mismatch") | |
certs.append(cert) | |
if certs_data: | |
raise Error("Verification output mismatch") | |
except ValueError: | |
raise Error("Verification output mismatch") # pylint: disable=W0707 | |
return certs, vsns | |
def get_cert_java(apksigner_jar: str, javac: Optional[str], *, | |
cache_dir: Optional[Path] = None) -> Path: | |
""" | |
Get path to Cert.java or Cert.class. | |
Cert.java is saved in CACHE_DIR and compiled to Cert.class with javac if | |
available. | |
""" | |
if cache_dir is None: | |
cache_dir = CACHE_DIR | |
cert_java = cache_dir / "Cert.java" | |
cert_class = cert_java.with_suffix(".class") | |
if not (cert_java.exists() and get_sha256(cert_java) == CERT_JAVA_SHA256): | |
cache_dir.mkdir(mode=0o700, exist_ok=True) | |
cert_java.write_text(CERT_JAVA_CODE, encoding="utf-8") | |
if cert_class.exists(): | |
cert_class.unlink() | |
if javac: | |
args = (javac, "-classpath", f"{cert_java.parent}:{apksigner_jar}", str(cert_java)) | |
subprocess.run(args, check=False) | |
return cert_class if cert_class.exists() else cert_java | |
def get_apksigner_jar(*, jars: Optional[List[str]] = None, | |
env: Optional[Dict[str, str]] = None) -> str: | |
""" | |
Find apksigner JAR using $ANDROID_HOME etc. | |
""" | |
env_get = os.environ.get if env is None else env.get | |
if jars is None: | |
jars = [env_get("APKSIGNER_JAR") or "", *APKSIGNER_JARS] | |
for jar in jars: | |
if jar and os.path.exists(jar): | |
return jar | |
for k in SDK_ENV: | |
if home := env_get(k): | |
tools = os.path.join(home, "build-tools") | |
if os.path.exists(tools): | |
for vsn in sorted(os.listdir(tools), key=_vsn, reverse=True): | |
jar = os.path.join(tools, vsn, *SDK_JAR.split("/")) | |
if os.path.exists(jar): | |
return jar | |
raise Error("Could not locate apksigner JAR") | |
def get_java(*, java_home: Optional[str] = None) -> Tuple[str, Optional[str]]: | |
""" | |
Find java (and possibly javac) using $JAVA_HOME/$PATH. | |
""" | |
java = javac = None | |
if not java_home: | |
java_home = os.environ.get("JAVA_HOME") | |
if java_home: | |
java = os.path.join(java_home, "bin/java") | |
javac = os.path.join(java_home, "bin/javac") | |
if not (java and os.path.exists(java)): | |
java = shutil.which("java") | |
javac = shutil.which("javac") | |
if not (java and os.path.exists(java)): | |
raise Error("Could not locate java") | |
return java, (javac if javac and os.path.exists(javac) else None) | |
def _vsn(v: str) -> Tuple[int, ...]: | |
if "-rc" in v: | |
v = v.replace("-rc", ".0.", 1) | |
else: | |
v = v + ".1.0" | |
return tuple(int(x) if x.isdigit() else -1 for x in v.split(".")) | |
def get_sha256(file: Path) -> str: | |
""" | |
Get SHA-256 digest of file. | |
""" | |
sha = hashlib.sha256() | |
with file.open("rb") as fh: | |
while data := fh.read(4096): | |
sha.update(data) | |
return sha.hexdigest() | |
if __name__ == "__main__": | |
import sys | |
java, javac = get_java() | |
apksigner_jar = get_apksigner_jar() | |
cert_java = get_cert_java(apksigner_jar, javac) | |
apkfile = Path(sys.argv[1]) | |
certs, versions = get_signing_certs(apkfile, java=java, apksigner_jar=apksigner_jar, cert_java=cert_java) | |
print("versions:", versions) | |
for cert in certs: | |
print("fingerprint:", hashlib.sha256(cert).hexdigest()) | |
# vim: set tw=80 sw=4 sts=4 et fdm=marker : |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment