Android 安装APK完整性校验(V1签名)

app在安装是会采集签名信息来验证apk的完成性

apk经过v1签名,解压后会得到META-INF文件夹,里面主要有三个文件CERT.RSA CERT.SF MANIFEST.MF,三者的介绍参考https://www.iteye.com/blog/myeyeofjava-2125348

http://www.blogjava.net/zh-weir/archive/2011/07/19/354663.html

APK安装时签名校验过程

在apk安装时会先对安装包惊醒解析,PackageParser.java类就是负责解析apk的,其中的collectCertificates函数开始收集签名信息,代码如下

private static void collectCertificates(Package pkg, File apkFile, boolean skipVerify)
            throws PackageParserException {
        final String apkPath = apkFile.getAbsolutePath();

        int minSignatureScheme = SigningDetails.SignatureSchemeVersion.JAR;
        if (pkg.applicationInfo.isStaticSharedLibrary()) {
            // must use v2 signing scheme
            minSignatureScheme = SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V2;
        }
        SigningDetails verified;
        if (skipVerify) {
            // systemDir APKs are already trusted, save time by not verifying
            verified = ApkSignatureVerifier.plsCertsNoVerifyOnlyCerts(
                        apkPath, minSignatureScheme);
        } else {
            verified = ApkSignatureVerifier.verify(apkPath, minSignatureScheme);
        }

        // Verify that entries are signed consistently with the first pkg
        // we encountered. Note that for splits, certificates may have
        // already been populated during an earlier parse of a base APK.
        if (pkg.mSigningDetails == SigningDetails.UNKNOWN) {
            pkg.mSigningDetails = verified;
        } else {
            if (!Signature.areExactMatch(pkg.mSigningDetails.signatures, verified.signatures)) {
                throw new PackageParserException(
                        INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
                        apkPath + " has mismatched certificates");
            }
        }
    }

安装过程中 skipVerify 当然是false,所以调用

ApkSignatureVerifier.verify(apkPath, minSignatureScheme);
public static PackageParser.SigningDetails verify(String apkPath,
            @SignatureSchemeVersion int minSignatureSchemeVersion)
            throws PackageParserException {

        if (minSignatureSchemeVersion > SignatureSchemeVersion.SIGNING_BLOCK_V3) {
            // V3 and before are older than the requested minimum signing version
            throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                    "No signature found in package of version " + minSignatureSchemeVersion
            + " or newer for package " + apkPath);
        }

        // first try v3
        Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV3");
        try {
            ApkSignatureSchemeV3Verifier.VerifiedSigner vSigner =
                    ApkSignatureSchemeV3Verifier.verify(apkPath);
            Certificate[][] signerCerts = new Certificate[][] { vSigner.certs };
            Signature[] signerSigs = convertToSignatures(signerCerts);
            Signature[] pastSignerSigs = null;
            int[] pastSignerSigsFlags = null;
            if (vSigner.por != null) {
                // populate proof-of-rotation information
                pastSignerSigs = new Signature[vSigner.por.certs.size()];
                pastSignerSigsFlags = new int[vSigner.por.flagsList.size()];
                for (int i = 0; i < pastSignerSigs.length; i++) {
                    pastSignerSigs[i] = new Signature(vSigner.por.certs.get(i).getEncoded());
                    pastSignerSigsFlags[i] = vSigner.por.flagsList.get(i);
                }
            }
            return new PackageParser.SigningDetails(
                    signerSigs, SignatureSchemeVersion.SIGNING_BLOCK_V3,
                    pastSignerSigs, pastSignerSigsFlags);
        } catch (SignatureNotFoundException e) {
            // not signed with v3, try older if allowed
            if (minSignatureSchemeVersion >= SignatureSchemeVersion.SIGNING_BLOCK_V3) {
                throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                        "No APK Signature Scheme v3 signature in package " + apkPath, e);
            }
        } catch (Exception e) {
            // APK Signature Scheme v2 signature found but did not verify
            throw new  PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                    "Failed to collect certificates from " + apkPath
                            + " using APK Signature Scheme v3", e);
        } finally {
            Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
        }

        // redundant, protective version check
        if (minSignatureSchemeVersion > SignatureSchemeVersion.SIGNING_BLOCK_V2) {
            // V2 and before are older than the requested minimum signing version
            throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                    "No signature found in package of version " + minSignatureSchemeVersion
                            + " or newer for package " + apkPath);
        }

        // try v2
        Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV2");
        try {
            Certificate[][] signerCerts = ApkSignatureSchemeV2Verifier.verify(apkPath);
            Signature[] signerSigs = convertToSignatures(signerCerts);

            return new PackageParser.SigningDetails(
                    signerSigs, SignatureSchemeVersion.SIGNING_BLOCK_V2);
        } catch (SignatureNotFoundException e) {
            // not signed with v2, try older if allowed
            if (minSignatureSchemeVersion >= SignatureSchemeVersion.SIGNING_BLOCK_V2) {
                throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                        "No APK Signature Scheme v2 signature in package " + apkPath, e);
            }
        } catch (Exception e) {
            // APK Signature Scheme v2 signature found but did not verify
            throw new  PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                    "Failed to collect certificates from " + apkPath
                            + " using APK Signature Scheme v2", e);
        } finally {
            Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
        }

        // redundant, protective version check
        if (minSignatureSchemeVersion > SignatureSchemeVersion.JAR) {
            // V1 and is older than the requested minimum signing version
            throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                    "No signature found in package of version " + minSignatureSchemeVersion
                            + " or newer for package " + apkPath);
        }

        // v2 didn't work, try jarsigner
        return verifyV1Signature(apkPath, true);
    }

此方法中会判断签名版本,此次主要分析V1,也就是

verifyV1Signature(apkPath, true);
private static PackageParser.SigningDetails verifyV1Signature(
            String apkPath, boolean verifyFull)
            throws PackageParserException {
        int objectNumber = verifyFull ? NUMBER_OF_CORES : 1;
        boolean isPerfLockAcquired = false;
        StrictJarFile[] jarFile = new StrictJarFile[objectNumber];
        final ArrayMap<String, StrictJarFile> strictJarFiles = new ArrayMap<String, StrictJarFile>();
        try {
            final Certificate[][] lastCerts;
            final Signature[] lastSigs;

            Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "strictJarFileCtor");

            if (sPerfBoost == null) {
                sPerfBoost = new BoostFramework();
            }
            if (sPerfBoost != null && !isPerfLockAcquired && verifyFull) {
                //Use big enough number here to hold the perflock for entire PackageInstall session
                sPerfBoost.perfHint(BoostFramework.VENDOR_HINT_PACKAGE_INSTALL_BOOST,
                        null, VERIFICATION_TIME_OUT, -1);
                Slog.d(TAG, " Perflock acquired for PackageInstall ");
                isPerfLockAcquired = true;
            }
            // we still pass verify = true to ctor to collect certs, even though we're not checking
            // the whole jar.
            for (int i = 0; i < objectNumber; i++) {
                jarFile[i] = new StrictJarFile(
                        apkPath,
                        true, // collect certs
                        verifyFull); // whether to reject APK with stripped v2 signatures (b/27887819)
            }
            final List<ZipEntry> toVerify = new ArrayList<>();

            // Gather certs from AndroidManifest.xml, which every APK must have, as an optimization
            // to not need to verify the whole APK when verifyFUll == false.
            final ZipEntry manifestEntry = jarFile[0].findEntry(
                    PackageParser.ANDROID_MANIFEST_FILENAME);
            if (manifestEntry == null) {
                throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,
                        "Package " + apkPath + " has no manifest");
            }
            lastCerts = loadCertificates(jarFile[0], manifestEntry);
            if (ArrayUtils.isEmpty(lastCerts)) {
                throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, "Package "
                        + apkPath + " has no certificates at entry "
                        + PackageParser.ANDROID_MANIFEST_FILENAME);
            }
            lastSigs = convertToSignatures(lastCerts);

            // fully verify all contents, except for AndroidManifest.xml  and the META-INF/ files.
            if (verifyFull) {
                final Iterator<ZipEntry> i = jarFile[0].iterator();
                while (i.hasNext()) {
                    final ZipEntry entry = i.next();
                    if (entry.isDirectory()) continue;

                    final String entryName = entry.getName();
                    if (entryName.startsWith("META-INF/")) continue;
                    if (entryName.equals(PackageParser.ANDROID_MANIFEST_FILENAME)) continue;

                    toVerify.add(entry);
                }
                class VerificationData {
                    public Exception exception;
                    public int exceptionFlag;
                    public boolean wait;
                    public int index;
                    public Object objWaitAll;
                    public boolean shutDown;
                }
                VerificationData vData = new VerificationData();
                vData.objWaitAll = new Object();
                final ThreadPoolExecutor verificationExecutor = new ThreadPoolExecutor(
                        NUMBER_OF_CORES,
                        NUMBER_OF_CORES,
                        1,/*keep alive time*/
                        TimeUnit.SECONDS,
                        new LinkedBlockingQueue<Runnable>());
                for (ZipEntry entry : toVerify) {
                    Runnable verifyTask = new Runnable(){
                        public void run() {
                            try {
                                if (vData.exceptionFlag != 0 ) {
                                    Slog.w(TAG, "VerifyV1 exit with exception " + vData.exceptionFlag);
                                    return;
                                }
                                String tid = Long.toString(Thread.currentThread().getId());
                                StrictJarFile tempJarFile;
                                synchronized (strictJarFiles) {
                                    tempJarFile = strictJarFiles.get(tid);
                                    if (tempJarFile == null) {
                                        if (vData.index >= NUMBER_OF_CORES) {
                                            vData.index = 0;
                                        }
                                        tempJarFile = jarFile[vData.index++];
                                        strictJarFiles.put(tid, tempJarFile);
                                    }
                                }
                                final Certificate[][] entryCerts = loadCertificates(tempJarFile, entry);
                                if (ArrayUtils.isEmpty(entryCerts)) {
                                    throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                                            "Package " + apkPath + " has no certificates at entry "
                                            + entry.getName());
                                }

                                // make sure all entries use the same signing certs
                                final Signature[] entrySigs = convertToSignatures(entryCerts);
                                if (!Signature.areExactMatch(lastSigs, entrySigs)) {
                                    throw new PackageParserException(
                                            INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
                                            "Package " + apkPath + " has mismatched certificates at entry "
                                            + entry.getName());
                                }
                            } catch (GeneralSecurityException e) {
                                synchronized (vData.objWaitAll) {
                                    vData.exceptionFlag = INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING;
                                    vData.exception = e;
                                }
                            } catch (PackageParserException e) {
                                synchronized (vData.objWaitAll) {
                                    vData.exceptionFlag = INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION;
                                    vData.exception = e;
                                }
                            }
                        }};
                    synchronized (vData.objWaitAll) {
                        if (vData.exceptionFlag == 0) {
                            verificationExecutor.execute(verifyTask);
                        }
                    }
                }
                vData.wait = true;
                verificationExecutor.shutdown();
                while (vData.wait) {
                    try {
                        if (vData.exceptionFlag != 0 && !vData.shutDown) {
                            Slog.w(TAG, "verifyV1 Exception " + vData.exceptionFlag);
                            verificationExecutor.shutdownNow();
                            vData.shutDown = true;
                        }
                        vData.wait = !verificationExecutor.awaitTermination(50,
                                TimeUnit.MILLISECONDS);
                    } catch (InterruptedException e) {
                        Slog.w(TAG,"VerifyV1 interrupted while awaiting all threads done...");
                    }
                }
                if (vData.exceptionFlag != 0)
                    throw new PackageParserException(vData.exceptionFlag,
                            "Failed to collect certificates from " + apkPath, vData.exception);
            }
            return new PackageParser.SigningDetails(lastSigs, SignatureSchemeVersion.JAR);
        } catch (GeneralSecurityException e) {
            throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
                    "Failed to collect certificates from " + apkPath, e);
        } catch (IOException | RuntimeException e) {
            throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                    "Failed to collect certificates from " + apkPath, e);
        } finally {
            if (isPerfLockAcquired && sPerfBoost != null) {
                sPerfBoost.perfLockRelease();
                isPerfLockAcquired = false;
                Slog.d(TAG, " Perflock released for PackageInstall ");
            }
            strictJarFiles.clear();
            Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
            for (int i = 0; i < objectNumber ; i++) {
                closeQuietly(jarFile[i]);
            }
        }
    }

此方法比较长,先看

jarFile[i] = new StrictJarFile(
        apkPath,
        true, // collect certs
        verifyFull); // whether to reject APK with stripped v2 signatures (b/27887819)
private StrictJarFile(String name,
            FileDescriptor fd,
            boolean verify,
            boolean signatureSchemeRollbackProtectionsEnforced)
                    throws IOException, SecurityException {
        this.nativeHandle = nativeOpenJarFile(name, fd.getInt$());
        this.fd = fd;

        try {
            // Read the MANIFEST and signature files up front and try to
            // parse them. We never want to accept a JAR File with broken signatures`
            // or manifests, so it's best to throw as early as possible.
            if (verify) {
                HashMap<String, byte[]> metaEntries = getMetaEntries();
                this.manifest = new StrictJarManifest(metaEntries.get(JarFile.MANIFEST_NAME), true);
                this.verifier =
                        new StrictJarVerifier(
                                name,
                                manifest,
                                metaEntries,
                                signatureSchemeRollbackProtectionsEnforced);
                Set<String> files = manifest.getEntries().keySet();
                for (String file : files) {
                    if (findEntry(file) == null) {
                        throw new SecurityException("File " + file + " in manifest does not exist");
                    }
                }

                isSigned = verifier.readCertificates() && verifier.isSignedJar();
            } else {
                isSigned = false;
                this.manifest = null;
                this.verifier = null;
            }
        } catch (IOException | SecurityException e) {
            nativeClose(this.nativeHandle);
            IoUtils.closeQuietly(fd);
            closed = true;
            throw e;
        }

        guard.open("close");
    }
通过 getMetaEntries() 读取 META-INF/ 文件夹下的所有文件,其中包括签名信息等,然后将所有文件信息传递给StrictJarVerifier,之后开始校验
isSigned = verifier.readCertificates() && verifier.isSignedJar();

查看readCertificates方法

synchronized boolean readCertificates() {
        if (metaEntries.isEmpty()) {
            return false;
        }

        Iterator<String> it = metaEntries.keySet().iterator();
        while (it.hasNext()) {
            String key = it.next();
            Log.d("readCertificates","key="+key);
            if (key.endsWith(".DSA") || key.endsWith(".RSA") || key.endsWith(".EC")) {
                verifyCertificate(key);
                it.remove();
            }
        }
        return true;
    }

可以看到检验只支持三种加密方法DSA RSA EC,之后查看verifyCertificate方法

private void verifyCertificate(String certFile) {
        Log.d("StrictJarVerifier","liyang--verifyCertificate:"+certFile);
        // Found Digital Sig, .SF should already have been read
        String signatureFile = certFile.substring(0, certFile.lastIndexOf('.')) + ".SF";
        byte[] sfBytes = metaEntries.get(signatureFile);
        if (sfBytes == null) {
            return;
        }

        byte[] manifestBytes = metaEntries.get(JarFile.MANIFEST_NAME);
        // Manifest entry is required for any verifications.
        if (manifestBytes == null) {
            return;
        }

        byte[] sBlockBytes = metaEntries.get(certFile);
        try {
            Certificate[] signerCertChain = verifyBytes(sBlockBytes, sfBytes);
            if (signerCertChain != null) {
                certificates.put(signatureFile, signerCertChain);
            }
        } catch (GeneralSecurityException e) {
          throw failedVerification(jarName, signatureFile, e);
        }

        // Verify manifest hash in .sf file
        Attributes attributes = new Attributes();
        HashMap<String, Attributes> entries = new HashMap<String, Attributes>();
        try {
            StrictJarManifestReader im = new StrictJarManifestReader(sfBytes, attributes);
            im.readEntries(entries, null);
        } catch (IOException e) {
            return;
        }

        // If requested, check whether a newer APK Signature Scheme signature was stripped.
        if (signatureSchemeRollbackProtectionsEnforced) {
            String apkSignatureSchemeIdList =
                    attributes.getValue(SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME);
            if (apkSignatureSchemeIdList != null) {
                // This field contains a comma-separated list of APK signature scheme IDs which
                // were used to sign this APK. If an ID is known to us, it means signatures of that
                // scheme were stripped from the APK because otherwise we wouldn't have fallen back
                // to verifying the APK using the JAR signature scheme.
                boolean v2SignatureGenerated = false;
                boolean v3SignatureGenerated = false;
                StringTokenizer tokenizer = new StringTokenizer(apkSignatureSchemeIdList, ",");
                while (tokenizer.hasMoreTokens()) {
                    String idText = tokenizer.nextToken().trim();
                    if (idText.isEmpty()) {
                        continue;
                    }
                    int id;
                    try {
                        id = Integer.parseInt(idText);
                    } catch (Exception ignored) {
                        continue;
                    }
                    if (id == ApkSignatureSchemeV2Verifier.SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID) {
                        // This APK was supposed to be signed with APK Signature Scheme v2 but no
                        // such signature was found.
                        v2SignatureGenerated = true;
                        break;
                    }
                    if (id == ApkSignatureSchemeV3Verifier.SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID) {
                        // This APK was supposed to be signed with APK Signature Scheme v3 but no
                        // such signature was found.
                        v3SignatureGenerated = true;
                        break;
                    }
                }

                if (v2SignatureGenerated) {
                    throw new SecurityException(signatureFile + " indicates " + jarName
                            + " is signed using APK Signature Scheme v2, but no such signature was"
                            + " found. Signature stripped?");
                }
                if (v3SignatureGenerated) {
                    throw new SecurityException(signatureFile + " indicates " + jarName
                            + " is signed using APK Signature Scheme v3, but no such signature was"
                            + " found. Signature stripped?");
                }
            }
        }

        // Do we actually have any signatures to look at?
        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;
        }

        // Use .SF to verify the mainAttributes of the manifest
        // If there is no -Digest-Manifest-Main-Attributes entry in .SF
        // file, such as those created before java 1.5, then we ignore
        // such verification.
        Log.d("StrictJarVerifier","liyang--verifyCertificate:mainAttributesEnd="+mainAttributesEnd+", createdBySigntool="+createdBySigntool);
        if (mainAttributesEnd > 0 && !createdBySigntool) {
            String digestAttribute = "-Digest-Manifest-Main-Attributes";
            if (!verify(attributes, digestAttribute, manifestBytes, 0, mainAttributesEnd, false, true)) {
                throw failedVerification(jarName, signatureFile);
            }
        }

        // Use .SF to verify the whole manifest.
        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();
                StrictJarManifest.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);
    }

此方法是最主要的.开始读取摘要信息并进行解密比对,代码很好理解,重点关注verify方法,使用到了MessageDigest验证摘要信息

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值