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 pkgFile apkFileint 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 pkgFile apkFileint 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 pkgFile apkFileint parseFlags)方法中调用了ApkSignatureSchemeV2Verifier.verify方法来进行v2签名校验,我以我们先来看卡ApkSignatureSchemeV2Verifierverify这个方法:
       /**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的重载方法verifyRandomAccessFile apk),并将其结果直接返回,所以接下来我们看看verifyRandomAccessFile 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这个方法获取的签名信息中都包含哪些内容:
(9)
        /**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        }
接下来在verifyFileDescriptor 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. 每个部分的内容分成连续的1 MB大小的块。最后一大块将是较短的,if段长度不是1 MB的倍数。没有块是为空(零长度)段制作的。
  2. 计算每个块的摘要信息
  3. 并将所有部分的摘要按照顺序连起来。

为了能够更好的理解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