android7.0及以上版本签名校验过程详解
来源:互联网 发布:网络门禁模块 编辑:程序博客网 时间:2024/06/05 06:06
对于新的签名方案APK Signature Scheme v2,在这篇文章中已经有详细的介绍http://www.tuicool.com/articles/bURRVrj。从这篇文章中可以知道,新的签名方案与旧的签名方案之间的对比是:
图1
新的签名方案生成与旧的签名方案相比,在zip文件中新增了一个APK Signing Block区块。使用新的签名方案以后,在apk下所有的文件中所做的修改,都会导致在android7.0系统上安装失败。那么android7.0上的签名校验过程是怎样的呢?为什么apk下任意文件修改都会导致签名校验失败呢?网上很多文章都对7.0之前的签名校验过程进行了讲解,想了解7.0之前的签名校验过程可以参考文章http://blog.csdn.net/roland_sun/article/details/42029019。但是很少有文章对7.0及更高版本上的签名校验过程进行分析,所以在这篇文章中,主要针对7.0及以上的签名校验过程进行分析,通过分析7.0系统源码的方式,来向大家描述一下这一过程。
(1) Android平台上所有应用程序安装都是由PackageManangerService(代码位于frameworks\base\services\core\java\com\android\server\pm\PackageManagerService.java)来管理的,apk的安装流程与签名验证相关的步骤位于installPackageLI函数中:
private void installPackageLI(InstallArgs args, PackageInstalledInfo res) { …… PackageParser pp = new PackageParser(); …… try { pp.collectCertificates(pkg, parseFlags); pp.collectManifestDigest(pkg); } catch (PackageParserException e) { res.setError("Failed collect during installPackageLI", e); return; } ……
(2) 在这个函数中,会用到PackageParser这个类(代码位于frameworks\base\core\java\android\content\pm\PackageParser.java,编译后存在于framework.jar文件中)是一个apk包的解析器,接下来我们来看其collectCertificates函数的实现:
public static void collectCertificates(Package pkg, int parseFlags) throws PackageParserException { collectCertificatesInternal(pkg, parseFlags); final int childCount = (pkg.childPackages != null) ? pkg.childPackages.size() : 0; for (int i = 0; i < childCount; i++) { Package childPkg = pkg.childPackages.get(i); childPkg.mCertificates = pkg.mCertificates; childPkg.mSignatures = pkg.mSignatures; childPkg.mSigningKeys = pkg.mSigningKeys; } }
(3) 在collectCertificates函数中,调用了 collectCertificatesInternal函数。所以接下来我们看看 collectCertificatesInternal的函数实现:
private static void collectCertificatesInternal(Package pkg, int parseFlags) throws PackageParserException { pkg.mCertificates = null; pkg.mSignatures = null; pkg.mSigningKeys = null; Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "collectCertificates"); try { collectCertificates(pkg, new File(pkg.baseCodePath), parseFlags); if (!ArrayUtils.isEmpty(pkg.splitCodePaths)) { for (int i = 0; i < pkg.splitCodePaths.length; i++) { collectCertificates(pkg, new File(pkg.splitCodePaths[i]), parseFlags); } } } finally { Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); } }
(4)在函数collectCertificatesInternal中调用了collectCertificates函数的重载函数,在7.0之前,是在collectCertificates函数中直接调用其重载函数,中间是没有collectCertificatesInternal函数的。collectCertificates的重载函数是一个很重要的函数,接下来我们看看在这个函数中做了什么操作:
private static void collectCertificates(Package pkg, File apkFile, int parseFlags)1 throws PackageParserException {1151 final String apkPath = apkFile.getAbsolutePath();11521153 // Try to verify the APK using APK Signature Scheme v2.1154 boolean verified = false;1155 {1156 Certificate[][] allSignersCerts = null;1157 Signature[] signatures = null;1158 try {1159 Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV2");1160 allSignersCerts = ApkSignatureSchemeV2Verifier.verify(apkPath);1161 signatures = convertToSignatures(allSignersCerts);1162 // APK verified using APK Signature Scheme v2.1163 verified = true;1164 } catch (ApkSignatureSchemeV2Verifier.SignatureNotFoundException e) {1165 // No APK Signature Scheme v2 signature found1166 } catch (Exception e) {1167 // APK Signature Scheme v2 signature was found but did not verify1168 throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,1169 "Failed to collect certificates from " + apkPath1170 + " using APK Signature Scheme v2",1171 e);1172 } finally {1173 Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);1174 } ……
在这个函数中,首先对签名apk做了v2方式的签名校验(代码从1154-1174)。也就是说首先用针对v2方式的签名方式来做签名校验,如果校验成功verified = true。如果在校验的过程中抛出了异常,那么有两种可能:1.apk没有用v2签名方式进行签名;2.apk用了v2签名方式进行签名,但是签名检验没有成功。
(5)我们接下来继续看 private static void collectCertificates(Package pkg, File apkFile, int parseFlags)函数中的内容:
StrictJarFile jarFile = null;1199 try {1200 Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "strictJarFileCtor");1201 // Ignore signature stripping protections when verifying APKs from system partition.1202 // For those APKs we only care about extracting signer certificates, and don't care1203 // about verifying integrity.1204 boolean signatureSchemeRollbackProtectionsEnforced =1205 (parseFlags & PARSE_IS_SYSTEM_DIR) == 0;1206 jarFile = new StrictJarFile(1207 apkPath,1208 !verified, // whether to verify JAR signature1209 signatureSchemeRollbackProtectionsEnforced);1210 Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);12111212 // Always verify manifest, regardless of source1213 final ZipEntry manifestEntry = jarFile.findEntry(ANDROID_MANIFEST_FILENAME);1214 if (manifestEntry == null) {1215 throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,1216 "Package " + apkPath + " has no manifest");1217 }12181219 // Optimization: early termination when APK already verified1220 if (verified) {1221 return;1222 } ……
(6)接下来,用到了 StrictJarFile这个类(代码位于/frameworks/base/core/java/android/util/jar/StrictJarFile.java)。可以看到在实例化该类时,传入的参数中有 !verified。
public StrictJarFile(String fileName,74 boolean verify,75 boolean signatureSchemeRollbackProtectionsEnforced)76 throws IOException, SecurityException {77 this.nativeHandle = nativeOpenJarFile(fileName);78 this.raf = new RandomAccessFile(fileName, "r");7980 try {81 // Read the MANIFEST and signature files up front and try to82 // parse them. We never want to accept a JAR File with broken signatures83 // or manifests, so it's best to throw as early as possible.84 if (verify) {85 HashMap<String, byte[]> metaEntries = getMetaEntries();86 this.manifest = new StrictJarManifest(metaEntries.get(JarFile.MANIFEST_NAME), true);87 this.verifier =88 new StrictJarVerifier(89 fileName,90 manifest,91 metaEntries,92 signatureSchemeRollbackProtectionsEnforced);93 Set<String> files = manifest.getEntries().keySet();94 for (String file : files) {95 if (findEntry(file) == null) {96 throw new SecurityException(fileName + ": File " + file + " in manifest does not exist");97 }98 }99100 isSigned = verifier.readCertificates() && verifier.isSignedJar();101 } else {102 isSigned = false;103 this.manifest = null;104 this.verifier = null;105 }106 } catch (IOException | SecurityException e) {107 nativeClose(this.nativeHandle);108 IoUtils.closeQuietly(this.raf);109 throw e;110 }
进入StrictJarFile中可以看出,当verified = true时(要记得在外面传入的是!verified,所以这里的verified = true表示的是v2签名校验验没有成功),所以在if(verify)判断条件成立的情况下(即v2签名校验失败),后面使用了StrictJarVerifie类,对apk使用7.0之前的签名校验方式进行签名校验,之后的流程就是7.0之前的签名校验过程了,这里就不在多说了;当verified = false时,只做了一些赋值操作。
然后回后到 private static void collectCertificates(Package pkg, File apkFile, int parseFlags)函数中的1212行,对apk中的manifest文件是否为空进行了校验,如果manifest校验通过。接下来verified = true(说明v2签名校验成功)就退出了该函数,这一次的校验结束。
到这里可以对7.0系统中的签名校验过程先做一个小结:7.0系统对apk进行签名校验,先对apk进行v2签名方案的签名校验,如果校验成功,之后还会再对manifest进行校验,签名校验到此结束; 如果v2签名校验没有成功,会走7.0之前的签名校验流程,之后还会再对manifest进行校验。
(7)在上面我们一直说,7.0上会先对apk做v2签名校验,那么接下来,我们就详细介绍一下v2签名方式校验的流程:
首先在 private static void collectCertificates(Package pkg, File apkFile, int parseFlags)方法中调用了ApkSignatureSchemeV2Verifier.verify方法来进行v2签名校验,我以我们先来看卡ApkSignatureSchemeV2Verifier中verify这个方法:
/**97 * Verifies APK Signature Scheme v2 signatures of the provided APK and returns the certificates98 * associated with each signer.99 *100 * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2.101 * @throws SecurityException if a APK Signature Scheme v2 signature of this APK does not verify.102 * @throws IOException if an I/O error occurs while reading the APK file.103 */104 public static X509Certificate[][] verify(String apkFile)105 throws SignatureNotFoundException, SecurityException, IOException {106 try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {107 return verify(apk);108 }109 }
在这个方法中,读取apk文件以后,其实是调用了verify的重载方法verify(RandomAccessFile apk),并将其结果直接返回,所以接下来我们看看verify(RandomAccessFile apk)方法的实现。
(8)private static X509Certificate[][] verify(RandomAccessFile apk)121 throws SignatureNotFoundException, SecurityException, IOException {122 SignatureInfo signatureInfo = findSignature(apk);123 return verify(apk.getFD(), signatureInfo);124 }
在这个方法中先通过 findSignature方法获取了apk的签名信息, findSignature这个方法的返回值是一个 SignatureInfo 类型的对象,通过 SignatureInfo 这个我们来看一下 findSignatur这个方法获取的签名信息中都包含哪些内容:
/**127 * APK Signature Scheme v2 block and additional information relevant to verifying the signatures128 * contained in the block against the file.129 */130 private static class SignatureInfo {131 /** Contents of APK Signature Scheme v2 block. */132 private final ByteBuffer signatureBlock;133134 /** Position of the APK Signing Block in the file. */135 private final long apkSigningBlockOffset;136137 /** Position of the ZIP Central Directory in the file. */138 private final long centralDirOffset;139140 /** Position of the ZIP End of Central Directory (EoCD) in the file. */141 private final long eocdOffset;142143 /** Contents of ZIP End of Central Directory (EoCD) of the file. */144 private final ByteBuffer eocd;145146 private SignatureInfo(147 ByteBuffer signatureBlock,148 long apkSigningBlockOffset,149 long centralDirOffset,150 long eocdOffset,151 ByteBuffer eocd) {152 this.signatureBlock = signatureBlock;153 this.apkSigningBlockOffset = apkSigningBlockOffset;154 this.centralDirOffset = centralDirOffset;155 this.eocdOffset = eocdOffset;156 this.eocd = eocd;157 }158 }
signatureInfo中包括整个签名块的内容,签名块的位置信息,核心目录块的的位置信息,目录结束标识块的位置及内容。获取apk文件中个区块信息之后,又调用了verify的另一个重载函数:
(10)private static X509Certificate[][] verify(203 FileDescriptor apkFileDescriptor,204 SignatureInfo signatureInfo) throws SecurityException {205 int signerCount = 0;206 Map<Integer, byte[]> contentDigests = new ArrayMap<>();207 List<X509Certificate[]> signerCerts = new ArrayList<>();208 CertificateFactory certFactory;209 try {210 certFactory = CertificateFactory.getInstance("X.509");211 } catch (CertificateException e) {212 throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);213 }214 ByteBuffer signers;215 try {216 signers = getLengthPrefixedSlice(signatureInfo.signatureBlock);217 } catch (IOException e) {218 throw new SecurityException("Failed to read list of signers", e);219 }220 while (signers.hasRemaining()) {221 signerCount++;222 try {223 ByteBuffer signer = getLengthPrefixedSlice(signers);224 X509Certificate[] certs = verifySigner(signer, contentDigests, certFactory);225 signerCerts.add(certs);226 } catch (IOException | BufferUnderflowException | SecurityException e) {227 throw new SecurityException(228 "Failed to parse/verify signer #" + signerCount + " block",229 e);230 }231 }232233 if (signerCount < 1) {234 throw new SecurityException("No signers found");235 }236237 if (contentDigests.isEmpty()) {238 throw new SecurityException("No content digests found");239 }240241 verifyIntegrity(242 contentDigests,243 apkFileDescriptor,244 signatureInfo.apkSigningBlockOffset,245 signatureInfo.centralDirOffset,246 signatureInfo.eocdOffset,247 signatureInfo.eocd);248249 return signerCerts.toArray(new X509Certificate[signerCerts.size()][]);250 }
在该函数中,会从签名块信息中获取sigers信息放在缓冲区中,sigers中可能包含多个signerBlock,循环调用getLengthPrefixedSlice(sigers)方法从signers中获取signerBlock,获取signerBlock以后,会调用 verifySigner方法获取signerBlock中的签名数据、签名和公钥,以下是 verifySigner方法中获取签名数据、签名和公钥的代码。
ByteBuffer signedData = getLengthPrefixedSlice(signerBlock); ByteBuffer signatures = getLengthPrefixedSlice(signerBlock); byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);
verifySigner中还对签名信息signatures 进行验证。并且由于signedData中包含摘要信息,所以在 verifySigner方法中还会从signerBlock的signedData 中获取摘要信息放在contentDigests中,以及获取并返回signerBlock的证书。
contentDigests是一个Map<Integer, byte[]>类型,其中使用以下函数 通过key值可以找到该摘要所使用的算法,byte[]是摘要。contentDigests将在 verifyIntegrity方法中用得到。
private static String getSignatureAlgorithmJcaKeyAlgorithm(int sigAlgorithm) {704 switch (sigAlgorithm) {705 case SIGNATURE_RSA_PSS_WITH_SHA256:706 case SIGNATURE_RSA_PSS_WITH_SHA512:707 case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:708 case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:709 return "RSA";710 case SIGNATURE_ECDSA_WITH_SHA256:711 case SIGNATURE_ECDSA_WITH_SHA512:712 return "EC";713 case SIGNATURE_DSA_WITH_SHA256:714 return "DSA";715 default:716 throw new IllegalArgumentException(717 "Unknown signature algorithm: 0x"718 + Long.toHexString(sigAlgorithm & 0xffffffff));719 }
接下来在verify( FileDescriptor apkFileDescriptor,SignatureInfo signatureInfo ) 方法校验了signerBlock的个数(不能小于1)和摘要信息(不能为空)。之后调用 verifyIntegrity方法进行继续校验,下面我们来看看 verifyIntegrity的实现过程:
(11)private static void verifyIntegrity(389 Map<Integer, byte[]> expectedDigests,390 FileDescriptor apkFileDescriptor,391 long apkSigningBlockOffset,392 long centralDirOffset,393 long eocdOffset,394 ByteBuffer eocdBuf) throws SecurityException {395396 if (expectedDigests.isEmpty()) {397 throw new SecurityException("No digests provided");398 }399400 // We need to verify the integrity of the following three sections of the file:401 // 1. Everything up to the start of the APK Signing Block.402 // 2. ZIP Central Directory.403 // 3. ZIP End of Central Directory (EoCD).404 // Each of these sections is represented as a separate DataSource instance below.405406 // To handle large APKs, these sections are read in 1 MB chunks using memory-mapped I/O to407 // avoid wasting physical memory. In most APK verification scenarios, the contents of the408 // APK are already there in the OS's page cache and thus mmap does not use additional409 // physical memory.410 DataSource beforeApkSigningBlock =411 new MemoryMappedFileDataSource(apkFileDescriptor, 0, apkSigningBlockOffset);412 DataSource centralDir =413 new MemoryMappedFileDataSource(414 apkFileDescriptor, centralDirOffset, eocdOffset - centralDirOffset);415416 // For the purposes of integrity verification, ZIP End of Central Directory's field Start of417 // Central Directory must be considered to point to the offset of the APK Signing Block.418 eocdBuf = eocdBuf.duplicate();419 eocdBuf.order(ByteOrder.LITTLE_ENDIAN);420 ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, apkSigningBlockOffset);421 DataSource eocd = new ByteBufferDataSource(eocdBuf);422423 int[] digestAlgorithms = new int[expectedDigests.size()];424 int digestAlgorithmCount = 0;425 for (int digestAlgorithm : expectedDigests.keySet()) {426 digestAlgorithms[digestAlgorithmCount] = digestAlgorithm;427 digestAlgorithmCount++;428 }429 byte[][] actualDigests;430 try {431 actualDigests =432 computeContentDigests(433 digestAlgorithms,434 new DataSource[] {beforeApkSigningBlock, centralDir, eocd});435 } catch (DigestException e) {436 throw new SecurityException("Failed to compute digest(s) of contents", e);437 }438 for (int i = 0; i < digestAlgorithms.length; i++) {439 int digestAlgorithm = digestAlgorithms[i];440 byte[] expectedDigest = expectedDigests.get(digestAlgorithm);441 byte[] actualDigest = actualDigests[i];442 if (!MessageDigest.isEqual(expectedDigest, actualDigest)) {443 throw new SecurityException(444 getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm)445 + " digest of contents did not verify");446 }447 }448 }
从注释可以看出,该函数会对除APK Signing Block以外的其他三个模块进行完整性校验,函数开始处分别获得了apk中其他三个模块的数据,第431行,会调用computeContentDigests方法来计算其他三个模块的摘要信息;获取摘要信息以后,到438行,有一个for循环,在for 循环中会获取保存在APK Signing Block模块中的每个模块的摘要信息,即第(10)步中说到的contentDigests的 value信息和计算得来的每个模块的摘要信息进行比较,如果不想等则完整性校验没有通过。
这个过程说明,在APK Signing Block中会保存Contents of ZIP entries 、Central Directory和End of Central Directory这三个模块的摘要信息,完整性校验,其实就是再计算一遍个模块的摘要信息和保存在APK Signing Block中的摘要信息进行比较,如果都相等说明apk没有被修改。
从这个过程也可以看出,没有对APK Signing Block模块进行完整性校验,所以对APK Signing Block模块进行修改不会影响apk在安装时的签名校验过程。这也就是为什么可以在改模块上加入渠道信息。
上面说到,会重新计算Contents of ZIP entries 、Central Directory和End of Central Directory这三个模块的摘要信息,下面简单说一下照耀信息的计算过程:
- 每个部分的内容分成连续的1 MB大小的块。最后一大块将是较短的,if段长度不是1 MB的倍数。没有块是为空(零长度)段制作的。
- 计算每个块的摘要信息
- 并将所有部分的摘要按照顺序连起来。
为了能够更好的理解android7.0签名校验过程,这里对zip文件的格式进行简单的介绍,想详细的了解zip的格式,可以点击以下网址https://my.oschina.net/dubenju/blog/514969进行详细的了解。
zip文件的结构:压缩源文件数据区+压缩源文件目录区(Central directory 核心目录)+压缩源文件目录结束标志(End of central directory record(EOCD) 目录结束标识)
具体点的zip格式:
[文件头+文件数据+数据描述符]{此处可重复n次}+核心目录+目录结束标识
每个块都有一个开始的标志及压缩后的大小:
文件头标识,值固定(0x04034b50)
核心目录文件header标识=(0x02014b50)
核心目录结束标记(0x06054b50)
在ZIP文件的第一部分,会保存每个文件的信息,所以修改apk中的任意文件,都会导致第一个模块完整性校验通不过,apk安装失败。
而且可以知道zip文件每个区块的数据是独立的。新版的v2签名,是在“压缩源文件数据区”这个块之后又增加了一个新的块,因为各模块是是独立的,以至于在APK Signing Block区块中增加内容不会对其他区块造成影响,所以在7.0系统上对其他三个模块做完整性校验是能够通过的。
若有不正之处请多多谅解,并欢迎批评指正。
请尊重作者劳动成果,转载请标明原文链接
阅读全文
1 0
- android7.0及以上版本签名校验过程详解
- 关于Android7.0及以上版本FileUriExposedException的问题
- 详解Android7.0及以上版本拍照或者相册选取照片包括裁剪照片时时App崩溃问题
- Android7.0及以上 拍照crash问题
- Android签名与校验过程详解
- 对XML文档进行Schema校验的方法(适用于Framework2.0及以上版本)
- 如何解决Android7.0及以上的权限崩溃问题
- 如何解决Android7.0及以上的权限崩溃问题
- 如何解决Android7.0及以上的权限崩溃问题
- myeclipse9.0以上版本出现的校验问题----structs2
- 解决Android7.0以上版本升级apk时报android.os.FileUriExposedException的问题
- MyEclipse10.0及以上版本破解过程,run.bat文件总是一闪而过的解决办法
- Visual SVN1.5以上版本下载及破解过程
- Visual SVN1.5以上版本下载及破解过程
- Apk去签名校验详解
- spring2.0和spring2.5及以上版本的jar包区别 spring jar 包详解
- spring2.0和spring2.5及以上版本的jar包区别 spring jar 包详解
- spring2.0和spring2.5及以上版本的jar包区别 spring jar 包详解
- install apache hadoop on ubuntu
- CSU-ACM2017暑期训练4-dfs F
- 医学图像涉及到的窗宽窗位 2
- Eclipse下如何配置Tomcat
- 02 Ext js学习之开发利器Maven的安装配置
- android7.0及以上版本签名校验过程详解
- 安卓·Butterknife使用
- 旋转数组
- 《JAVA编程思想》第一章总结
- 异常解决:util.NativeCodeLoader: Unable to load native-hadoop library for your platform
- 个人三观的东西(1)
- Linux下非交互式提权详解
- 树的创建与数据打印
- AngularJS 的Provider,Factory与Service实现依赖注入