Skip to content

Instantly share code, notes, and snippets.

@obfusk
Last active April 17, 2024 00:03
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save obfusk/cfab950649631c3ece723b9eb277304b to your computer and use it in GitHub Desktop.
Save obfusk/cfab950649631c3ece723b9eb277304b to your computer and use it in GitHub Desktop.
verify APK and get SHA-256 of first cert
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);
}
}
}
#!/usr/bin/python3
# encoding: utf-8
# SPDX-FileCopyrightText: 2024 FC (Fay) Stegerman <flx@obfusk.net>
# 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