Apk签名后,apk文件目录下生成了一个名为META-INF目录,里面有三个文件,分别是MANIFEST.MF, CERT.SFCERT.RSA;

其中MANIFEST.MF中记录的是apk中所有文件的摘要值;CERT.SF中记录的是对MANIFEST.MF的摘要值,包括整个文件的摘要,还有文件中每一项的摘要;而CERT.RSA中记录的是对CERT.SF文件的签名,以及签名的公钥。这个目录下的签名文件是如何解析和校验的呢,我们回到pms的scanPackageLI方法。

[/frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java]

    /**  Scan a package and return the newly parsed package.*  Returns null in case of errors and the error code is stored in mLastScanError*/private PackageParser.Package scanPackageLI(File scanFile, int parseFlags, int scanFlags,long currentTime, UserHandle user) throws PackageManagerException {if (DEBUG_INSTALL) Slog.d(TAG, "Parsing: " + scanFile);…// Verify certificates against what was last scannedcollectCertificatesLI(pp, ps, pkg, scanFile, parseFlags);…
}

这里调用了collectCertificatesLI方法,传入pkg以及pkg目录,继续分析该方法:

[/frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java]

    private void collectCertificatesLI(PackageParser pp, PackageSetting ps, PackageParser.Package pkg,
File srcFile, int parseFlags) throws PackageManagerException {if (ps != null && ps.codePath.equals(srcFile) && ps.timeStamp == srcFile.lastModified()&& !isCompatSignatureUpdateNeeded(pkg) && !isRecoverSignatureUpdateNeeded(pkg)) {long mSigningKeySetId = ps.keySetData.getProperSigningKeySet();if (ps.signatures.mSignatures != null && ps.signatures.mSignatures.length != 0&& mSigningKeySetId != PackageKeySetData.KEYSET_UNASSIGNED) {pkg.mSignatures = ps.signatures.mSignatures;KeySetManagerService ksms = mSettings.mKeySetManagerService;synchronized (mPackages) {pkg.mSigningKeys = ksms.getPublicKeysFromKeySetLPr(mSigningKeySetId);}return;}} try {pp.collectCertificates(pkg, parseFlags);pp.collectManifestDigest(pkg);} catch (PackageParserException e) {throw PackageManagerException.from(e);}}

collectCertificatesLI方法中首先判断当前packages.xml是否存在,如果存在则把之前保存的apk签名信息取出并赋值给pkg。否则调用PackageParser的collertCertficates和collectManifestDigest方法分别解析手机apk的签名信息。

继续分析PackageParser里面的这两个方法:

[/frameworks/base/services/core/java/com/android/server/pm/PackageParser.java]

    public void collectCertificates(Package pkg, int flags) throws PackageParserException {pkg.mCertificates = null;pkg.mSignatures = null;pkg.mSigningKeys = null;collectCertificates(pkg, new File(pkg.baseCodePath), flags);…}

接着调用collectCertificates的一个重载版本:

    private static void collectCertificates(Package pkg, File apkFile, int flags)throws PackageParserException {final String apkPath = apkFile.getAbsolutePath();StrictJarFile jarFile = null;try {jarFile = new StrictJarFile(apkPath);…

方法的开头,首先创建了一个StrictJarFile(代码位于libcore\luni\src\main\java\java\util\jar\StrictJarFile.java,编译后存在于core.jar文件中)对象,先来看看其构造函数中的内容:

[libcore/luni/src/main/java/java/util/jar/StrictJarFile.java]

    public StrictJarFile(String fileName) throws IOException {…try {HashMap<String, byte[]> metaEntries = getMetaEntries();this.manifest = new Manifest(metaEntries.get(JarFile.MANIFEST_NAME), true);this.verifier = new JarVerifier(fileName, manifest, metaEntries);isSigned = verifier.readCertificates() && verifier.isSignedJar();…
}

这里构造了几个重要的对象。首先,获得了META-INF目录下所有文件名及其字节流。然后是构造了一个manifest对象,主要是用来处理对META-INF目录下MANIFEST.MF文件的操作。接着,构造了一个JarVeirifer(代码位于libcore\luni\src\main\java \java\util\jar\JarVerifier.java文件中,编译后存在于core.jar文件中)对象,这个对象主要实现了对Jar文件的验证工作,非常关键,后面的分析中会逐步提到。在构造函数的最后,调用了JarVeirifer.readCertificates函数:

[libcore/luni/src/main/java /java/util/jar/JarVerifier.java]

synchronized boolean readCertificates() {  if (metaEntries.isEmpty()) {  return false;  }  Iterator<String> it = metaEntries.keySet().iterator();  while (it.hasNext()) {  String key = it.next();  if (key.endsWith(".DSA") || key.endsWith(".RSA") || key.endsWith(".EC")) {  verifyCertificate(key);  it.remove();  }  }  return true;
}

代码遍历所有META-INF目录下的文件,找到以.DSA.RSA或者.EC结尾的文件,以这些名字结尾的文件都是所谓的签名证书文件。在本例中对应的是META-INF目录下的CERT.RSA签名文件。然后调用JarVeirifer.verifyCertificate函数:

[libcore/luni/src/main/java /java/util/jar/JarVerifier.java]

private void verifyCertificate(String certFile) {  String signatureFile = certFile.substring(0, certFile.lastIndexOf('.')) + ".SF";  byte[] sfBytes = metaEntries.get(signatureFile);  if (sfBytes == null) {  return;  }  ……  byte[] sBlockBytes = metaEntries.get(certFile);  try {  Certificate[] signerCertChain = JarUtils.verifySignature( new ByteArrayInputStream(sfBytes),  new ByteArrayInputStream(sBlockBytes));  if (signerCertChain != null) {  certificates.put(signatureFile, signerCertChain);  }  } ……

函数开头,首先找到与证书文件同名,但是以.SF结尾的签名文件,本例中即为META-INF目录下的CERT.SF文件。然后分别获得签名文件CERT.SF和证书文件CERT.RSA的字节流,调用JarUtils(代码位于libcore\luni\src\main\java\org\apache\harmony\security\utils\JarUtils.java文件中,编译后存在于core.jar文件中)的verifySignature函数,验证CERT.RSA文件中包含的对CERT.SF文件的签名是否正确。如果验证失败,则会抛出GeneralSecurityException异常;而如果验证成功,则会返回签名的证书链。回到JarVeirifer.verifyCertificate函数,如果JarUtils.verifySignature验证失败抛出异常,被捕获后会接着向上抛出SecurityException异常;

如果签名验证成功的话,会将证书链保存在certifcates属性变量中。而JarVerifier自己的isSignedJar函数,就是判断一下这个certificates属性变量是否为空。

boolean isSignedJar() {  return certificates.size() > 0;
}

如果不为空就代表这个Jar是签过名的,如果为空则代表其没有签过名。

继续分析JarVeirifer.verifyCertificate函数:

 ……
Attributes attributes = new Attributes();
HashMap<String, Attributes> entries = new HashMap<String, Attributes>();
try {  ManifestReader im = new ManifestReader(sfBytes, attributes);  im.readEntries(entries, null);
} catch (IOException e) {  return;
}
if (attributes.get(Attributes.Name.SIGNATURE_VERSION) == null) {  return;
}
boolean createdBySigntool = false;
String createdBy = attributes.getValue("Created-By");
if (createdBy != null) {  createdBySigntool = createdBy.indexOf("signtool") != -1;
}
……  

函数接下来读取了签名文件,也就是META-INF目录下CERT.SF文件中的内容。CERT.SF文件内容大致如下:

首先判断了是否有“Signature-Version”属性,如果没有的话,直接返回。再下来判断apk是否是由签名工具签的名,判断条件就是在“Created-By”属性值内有没有“signtool”字符串。本例中,签名版本是“1.0”,并且不是用其它签名工具签的名。如果不是用其它工具签名的话,接下来还会验证主属性中是否有“SHA1-Digest-Manifest-Main-Attributes”属性的值,这个属性值记录的是对META-INF目录下MANIFEST.MF文件内,头属性块的hash值。

……
byte[] manifestBytes = metaEntries.get(JarFile.MANIFEST_NAME);
if (manifestBytes == null) {  return;
}
……
if (mainAttributesEnd > 0 && !createdBySigntool) {  String digestAttribute = "-Digest-Manifest-Main-Attributes";  if (!verify(attributes, digestAttribute, manifestBytes, 0, mainAttributesEnd, false, true)) {  throw failedVerification(jarName, signatureFile);  }
}
……

接着调用了JarVerifier.verify对该摘要值进行验证:

private boolean verify(Attributes attributes, String entry, byte[] data,  int start, int end, boolean ignoreSecondEndline, boolean ignorable) {  for (int i = 0; i < DIGEST_ALGORITHMS.length; i++) {  String algorithm = DIGEST_ALGORITHMS[i];  String hash = attributes.getValue(algorithm + entry);  if (hash == null) {  continue;  }    MessageDigest md;  try {  md = MessageDigest.getInstance(algorithm);  } catch (NoSuchAlgorithmException e) {  continue;  }  if (ignoreSecondEndline && data[end - 1] == '\n' && data[end - 2] == '\n') {  md.update(data, start, end - 1 - start);  } else { md.update(data, start, end - start);  }  byte[] b = md.digest();  byte[] hashBytes = hash.getBytes(StandardCharsets.ISO_8859_1);  return MessageDigest.isEqual(b, Base64.decode(hashBytes));  }  return ignorable;
}  

JarVerifier.verify函数很简单,由于不知道到底是用什么算法算出的散列值,所以其会遍历所有的可能算法。这些算法都预先定义在DIGEST_ALGORITHMS这个JarVerifier内的静态字符串数组变量中:

private static final String[] DIGEST_ALGORITHMS = new String[] {  "SHA-512",  "SHA-384",  "SHA-256",  "SHA1",
}; 

可以看出,一共支持四种算法,本例中用到的是SHA1摘要算法。变量attributes表示的是一个属性块,而变量entry是要在attributes属性块中查找的属性名的一部分,它会与摘要算法的名称拼接成正真的属性名。接着会将在属性块中,对应属性名的属性值取出来,与data数据块中startend之间的数据,用同样算法算出的摘要值进行比较,如果一致就返回“true”,不一致则返回“false”。

    ……  String digestAttribute = createdBySigntool ? "-Digest" : "-Digest-Manifest";  if (!verify(attributes, digestAttribute, manifestBytes, 0, manifestBytes.length, false, false)) {  Iterator<Map.Entry<String, Attributes>> it = entries.entrySet().iterator();  while (it.hasNext()) {  Map.Entry<String, Attributes> entry = it.next();  Manifest.Chunk chunk = manifest.getChunk(entry.getKey());  if (chunk == null) {  return;  }  if (!verify(entry.getValue(), "-Digest", manifestBytes,  chunk.start, chunk.end, createdBySigntool, false)) {  throw invalidDigest(signatureFile, entry.getKey(), jarName);  }  }  }  metaEntries.put(signatureFile, null);  signatures.put(signatureFile, entries);
}  

JarVeirifer.verifyCertificate剩下的代码就很简单了,会比较MANIFEST.MF文件的整体摘要值和每一个属性块的摘要值,与CERT.SF文件中记录的是否一致。如果都验证通过的话,会将该签名文件的信息加到metaEntriessignatures属性变量中去。所以,在StrictJarFile构造的过程中就已经完成了两步验证:一是通过在CERT.RSA文件中记录的签名信息,验证了CERT.SF没有被篡改过;二是通过CERT.SF文件中记录的摘要值,验证了MANIFEST.MF没有被修改过。

回到PackageParser的collectCertificates方法中:

[/frameworks/base/services/core/java/com/android/server/pm/PackageParser.java]

……
final ZipEntry manifestEntry = jarFile.findEntry(ANDROID_MANIFEST_FILENAME);
if (manifestEntry == null) {  throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,"Package " + apkPath + " has no manifest");
}
final List<ZipEntry> toVerify = new ArrayList<>();
toVerify.add(manifestEntry);
if ((flags & PARSE_IS_SYSTEM) == 0) {  final Iterator<ZipEntry> i = jarFile.iterator();  while (i.hasNext()) {  final ZipEntry entry = i.next();    if (entry.isDirectory()) continue;  if (entry.getName().startsWith("META-INF/")) continue;  if (entry.getName().equals(ANDROID_MANIFEST_FILENAME)) continue;    toVerify.add(entry);  }
}
……

接下来的代码主要是用来确定,到底哪些文件需要进行验证。AndroidManifest.xml无论如何都要验证。如果不是系统,也就是普通的应用程序安装,必须要验证除去位于META-INF目录下所有文件之外的所有剩下的文件。

……
for (ZipEntry entry : toVerify) {  final Certificate[][] entryCerts = loadCertificates(jarFile, entry);  if (ArrayUtils.isEmpty(entryCerts)) {  throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,  "Package " + apkPath + " has no certificates at entry " + entry.getName());  }  final Signature[] entrySignatures = convertToSignatures(entryCerts);  ……

接着是逐项验证前面罗列出的apk中的各个文件。对每个文件,都接着调用了PackageParser.loadCertificates函数:

private static Certificate[][] loadCertificates(StrictJarFile jarFile, ZipEntry entry) throws ackageParserException{InputStream is = null;  try {  is = jarFile.getInputStream(entry);  readFullyIgnoringContents(is);  return jarFile.getCertificateChains(entry);  …

方法内对apk内的文件创建了一个输入流,并且通过函数PackageParser.readFullyIgnoringContents全读了一遍,而且通过函数名可以看出,具体读出什么内容并不重要。我们先来看看StrictJarFile.getInputStream函数:

public InputStream getInputStream(ZipEntry ze) {  final InputStream is = getZipInputStream(ze);    if (isSigned) {  JarVerifier.VerifierEntry entry = verifier.initEntry(ze.getName());  if (entry == null) {  return is;  }    return new JarFile.JarFileInputStream(is, ze.getSize(), entry);  }    return is;
}  

重点要关注两个函数调用,一是JarVerifier.initEntry,二是JarFile.JarFileInputStream

initEntry函数主要的用途就是构造一个JarVerifer.VerifierEntry对象:要构造这个对象,必须事先准备好参数。第一个参数很简单,就是要验证的文件名,直接将name传进来就好了。第二个参数是计算摘要的对象,可以通过MessageDigest.getInstance获得,不过要先告知到底要用哪个摘要算法,同样也是通过查看MANIFEST.MF文件中对应名字的属性值来决定的。本例中的MANIFEST.MF文件格式大致如下:

所以可以知道所用的摘要算法是SHA1。第三个参数是对应文件的摘要值,这是通过读取MANIFEST.MF文件获得的。第四个参数是证书链,即对该apk文件签名的所有证书链信息。为什么是二维数组呢?这是因为Android允许用多个证书对apk进行签名,但是它们的证书文件名必须不同。最后一个参数是已经验证过的文件列表,VerifierEntry在完成了对指定文件的摘要验证之后会将该文件的信息加到其中。

通过digest就可以算出apk内指定文件的真实摘要值。而记录在MANIFEST.MF文件中对应该文件的摘要值,也在构造JarVerifier.VerifierEntry时传递给了hash变量。不过这个hash值是经过Base64编码的。所以在比较之前,必须通过Base64解码。如果不一致的话,会抛出SecurityException异常:

至此,最后一步验证,即apk内所有文件的摘要值要和在MANIFEST.MF文件中记录的一致,也已经完成了。这还没完,PackageParser.collectCertificates还要接着验证apk文件中的每个文件对应的签名要和第一个文件一致:

……
if (pkg.mCertificates == null) {  pkg.mCertificates = entryCerts;  pkg.mSignatures = entrySignatures;  pkg.mSigningKeys = new ArraySet<PublicKey>();  for (int i=0; i < entryCerts.length; i++) {  pkg.mSigningKeys.add(entryCerts[i][0].getPublicKey());  }
}
……

最后将证书信息以Certificates, mSignatures, SigningKeys的形式保存到pkg中,并最终写入packages.xml供下次开机使用。

到这里,apk安装时的签名验证过程都已经分析完了,来总结一下:

1. 所有有关apk文件的签名验证工作都是在JarVerifier里面做的,一共分成三步;

2. JarVeirifer.verifyCertificate主要做了两步。首先,使用证书文件(在META-INF目录下,以.DSA.RSA或者.EC结尾的文件)检验签名文件(在META-INF目录下,和证书文件同名,但扩展名为.SF的文件)是没有被修改过的。然后,使用签名文件,检验MANIFEST.MF文件中的内容也没有被篡改过;

3. JarVerifier.VerifierEntry.verify做了最后一步验证,即保证apk文件中包含的所有文件,对应的摘要值与MANIFEST.MF文件中记录的一致。

PKMS包管理服务分析-证书校验流程04相关推荐

  1. Ukey证书校验流程和使用注意事项

    目录: 一.什么是证书吊销列表(CSR) 二.证书四验是哪四验 三.企业Ukey证书制作流程 四.Ukey证书校验流程 五.Ukey证书使用需要注意的问题 六.Ukey相关资料包 内容: 一.什么是证 ...

  2. 抓包:Android对抗证书校验

    Android客户端单向验证 客户端单向验证手段很多,可以参考JustTrustMe,SSLKiller 之类的Hook框架源码去探索,这里选取几个常用的API讲解. 这里我们依旧拿 https:// ...

  3. GitHub推出包管理服务,npm与Nuget全支持

    GitHub 今天推出了一项名为 GitHub Package Registry 的新产品,它提供了软件包管理服务,开发者通过它可发布公共或私有软件包. 官方介绍,GitHub Package Reg ...

  4. npm 卸载_完全免费!GitHub发布软件包管理服务:NPM瑟瑟发抖

    包栗子 发自 凹非寺 量子位 出品 | 公众号 QbitAI 今天,GitHub发布了全新的软件包管理服务,叫GitHub Package Registry,完全免费. 有了它,用户可以把自己的软件包 ...

  5. Azure DevOps —— Azure Artifacts 包管理平台

    Azure Artifacts 其实就是你自己的包管理服务.就好比现有的 maven(java).nuget(.net).pip(python).npm(javascript) 等等这种包管理服务. ...

  6. 华为内支付流程以及服务端php校验

    一.简介 华为应用内支付服务(In-App Purchases,IAP)为App提供便捷的应用内支付体验和简便的接入流程.App通过集成华为应用内支付SDK,再调用SDK接口启动IAP收银台,即可实现 ...

  7. pip包管理工具-install执行流程简单查看

    pip概述 pip是python提供的包管理工具,该工具提供了对python包的查找.下载.安装与卸载等功能的工具,当前是python中比较主流的管理工具. pip下载安装包的概述 pip工具的本质通 ...

  8. Android程序包管理(1)--PKMS启动过程

    一.PKMS启动过程 1.重要文件目录介绍 1.目录: /system/ect/permissions/xxx.xml:加载系统feature /system/ect/permissions/plat ...

  9. 2023爱分析· 云管理服务(MSP)市场厂商评估报告:华创方舟

    目录 1.   研究范围定义 2.   云管理服务(MSP)市场定义 3.   厂商评估:华创方舟 4.   入选证书 1.    研究范围定义 数字化时代,应用成为企业开展各项业务的落脚点.随着业务 ...

最新文章

  1. STC89C52单片机 使用定时器使LED灯闪烁
  2. Swift:在Safari中打开App
  3. c# winform实现2048游戏
  4. java 生成 tar.gz_一文教您如何通过 Java 压缩文件,打包一个 tar.gz Filebeat 采集器包...
  5. C# 5.0新加特性
  6. java比较时间sql_如何正确比较日期 java.sql.Date
  7. java基础学习笔记(二)
  8. nginx 的源码安装
  9. cmos和ttl_【转】CMOS与TTL电路的区别
  10. Qt---MaintenanceTool
  11. 第一个Spring冲刺周期团队进展报告
  12. H3CTE讲师分享H3C认证培训实验9 IP基础
  13. IPFS矿机托管的优势与劣势
  14. PWM驱动MOS管H桥电路
  15. Android应用程序四大组件分别是什么?各个组件所起到的作用是什么?
  16. chrome java过期_解决ubuntu的chrome浏览器的flash过期问题
  17. 视频转换成gif动图的方法步骤
  18. 阿里的素质在线测评2020春招Java实习
  19. 安全L1-AD.3-DNS代理原理及配置
  20. 【深度观察】深度学习技术其实没那么美好

热门文章

  1. ORACLE数据库应用开发三十忌
  2. (234)Verilog HDL:与门激励
  3. python里isalpha_python isdigit()、isalpha()、isalnum() 三个函数
  4. 从键盘上输入一个小于1000的正数,要求输出他的平方根。
  5. 勒索病毒频发下如何防勒索病毒
  6. webgis 计算机网络原理(3)Web GIS技术原理
  7. 五个最佳的免费网络记事本
  8. Python基础之列表和元组
  9. TDD 的原理和场景
  10. 什么是字节?字节的大小以及常用数据类型所占的字节