VasDolly对V1签名机制的支持

V1的签名机制

V1签名主要就是依靠在META-INF目录下的MANIFEST.MFCERT.SFCERT.RSA

生成签名的流程

签名可以使用2个工具:

  • jarsigner
  • signapk

前者是java原生自带的工具,签名时使用.keystore.jks文件;而signapk则是android专门为自己定制的签名工具,签名时会用到2个文件.pk8.x509[.pem],他们各有自己的作用。两者的签名算法其实没啥差别。

Eclispe里一般还是采用jarsign工具进行签名,跟这个配套的还有keytool工具,常用的命令有:

【生成】证书库
keytool -genkey -v -keystore strange.keystore -alias strange -keyalg RSA -keysize 2048 -validity 10000

【查看】证书库
keytool -list -keystore stange.keystore

【签名】签名
jarsigner -verbose -sigalg SHA1withDSA -digestalg SHA1  -keystore D:\stange.keystore -storepass 123456 D:\123.apk stange

AS里现在都是采用的signapk的签名方式,.x509[.pem]包含证书的信息,包括证书链,公钥和加密算法;.pk8存放的就是私钥。跟signapk配套的生成工具是openssl,常用的命令有:

【生成】密钥
openssl genrsa -out key.pem 2048

【生成】证书请求
openssl req -new -key key.pem -out request.pem

【生成】 pem格式的 x.509 证书
openssl x509 -req -days 10000 -in request.pem -signkey key.pem -out certificate.pem -sha256

【生成】 pk8 格式密钥
openssl pkcs8 -topk8 -outform DER -in key.pem -inform PEM -out key.pk8 -nocrypt

【查看】pem证书
openssl x509 -in publicKey.x509.pem -text -noout

【签名】签名
java -jar signapk.jar [-w] publickey.x509[.pem] privatekey.pk8 input.jar output.jar

为啥AS采用signapk签名,但是放入keystore也是可以的?

原因是keystore和pk8、x509(pem)是可以相关转换的。这个在AS打包的时候其实已经做了的。

SignApk.java源码分析

1. 从publickey.x509[.pem]中读出公钥

private static X509Certificate readPublicKey(File file)
            throws IOException, GeneralSecurityException {
        FileInputStream input = new FileInputStream(file);
        try {
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            return (X509Certificate) cf.generateCertificate(input);
        } finally {
            input.close();
        }
    }

2. 从privatekey.pk8中读取私钥

    private static PrivateKey readPrivateKey(File file)
            throws IOException, GeneralSecurityException {
        DataInputStream input = new DataInputStream(new FileInputStream(file));
        try {
            byte[] bytes = new byte[(int) file.length()];
            input.read(bytes);
            KeySpec spec = decryptPrivateKey(bytes, file);
            if (spec == null) {
                spec = new PKCS8EncodedKeySpec(bytes);
            }
            try {
                return KeyFactory.getInstance("RSA").generatePrivate(spec);
            } catch (InvalidKeySpecException ex) {
                return KeyFactory.getInstance("DSA").generatePrivate(spec);
            }
        } finally {
            input.close();
        }
    }

3. 写MANIFEST.MF

private static Manifest addDigestsToManifest(JarFile jar)
            throws IOException, GeneralSecurityException {
        Manifest input = jar.getManifest();
        Manifest output = new Manifest();
        Attributes main = output.getMainAttributes();
        if (input != null) {
            main.putAll(input.getMainAttributes());
        } else {
            main.putValue("Manifest-Version", "1.0");
            main.putValue("Created-By", "1.0 (Android SignApk)");
        }
        MessageDigest md = MessageDigest.getInstance("SHA1");
        byte[] buffer = new byte[4096];
        int num;
        // We sort the input entries by name, and add them to the
        // output manifest in sorted order.  We expect that the output
        // map will be deterministic.
        TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
        for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
            JarEntry entry = e.nextElement();
            byName.put(entry.getName(), entry);
        }
        for (JarEntry entry: byName.values()) {
            String name = entry.getName();
            if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&
                    !name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) &&
                    !name.equals(OTACERT_NAME) &&
                    (stripPattern == null ||
                            !stripPattern.matcher(name).matches())) {
                InputStream data = jar.getInputStream(entry);
                while ((num = data.read(buffer)) > 0) {
                    md.update(buffer, 0, num);
                }
                Attributes attr = null;
                if (input != null) attr = input.getAttributes(name);
                attr = attr != null ? new Attributes(attr) : new Attributes();
                attr.putValue("SHA1-Digest",
                        new String(Base64.encode(md.digest()), "ASCII"));
                output.getEntries().put(name, attr);
            }
        }
        return output;
    }

可以看出来,MANIFEST.MF里的数据是除了三个签名文件外的所有文件签名记录,而签名的策略是对每个文件的内容采用SHA1,然后做base64。

4. 写CERT.MF

private static void writeSignatureFile(Manifest manifest, OutputStream out)
            throws IOException, GeneralSecurityException {
        Manifest sf = new Manifest();
        Attributes main = sf.getMainAttributes();
        main.putValue("Signature-Version", "1.0");
        main.putValue("Created-By", "1.0 (Android SignApk)");
        MessageDigest md = MessageDigest.getInstance("SHA1");
        PrintStream print = new PrintStream(
                new DigestOutputStream(new ByteArrayOutputStream(), md),
                true, "UTF-8");
        // Digest of the entire manifest
        manifest.write(print);
        print.flush();
        main.putValue("SHA1-Digest-Manifest",
                new String(Base64.encode(md.digest()), "ASCII"));
        Map<String, Attributes> entries = manifest.getEntries();
        for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
            // Digest of the manifest stanza for this entry.
            print.print("Name: " + entry.getKey() + "\r\n");
            for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
                print.print(att.getKey() + ": " + att.getValue() + "\r\n");
            }
            print.print("\r\n");
            print.flush();
            Attributes sfAttr = new Attributes();
            sfAttr.putValue("SHA1-Digest",
                    new String(Base64.encode(md.digest()), "ASCII"));
            sf.getEntries().put(entry.getKey(), sfAttr);
        }
        CountOutputStream cout = new CountOutputStream(out);
        sf.write(cout);
    }

它首先是将上面的MANIFEST.MF文件整体的做了SHA1+base64的处理,然后对文件里的每个entry块(注意大循环里面还有个小的循环)进行SHA1+base64的处理,最后将这些信息写入到CERT.MF中。

5. 写CERT.RSA

RSA文件是二进制文件,已经加密过,无法正常查看,需要用命令:

openssl pkcs7 -inform DER -in CERT.RSA -text -print_certs

代码如下:

private static void writeSignatureBlock(
        Signature signature, X509Certificate publicKey, OutputStream out)
        throws IOException, GeneralSecurityException {
    SignerInfo signerInfo = new SignerInfo(
            new X500Name(publicKey.getIssuerX500Principal().getName()),
            publicKey.getSerialNumber(),
            AlgorithmId.get("SHA1"),
            AlgorithmId.get("RSA"),
            signature.sign());

    PKCS7 pkcs7 = new PKCS7(
            new AlgorithmId[] { AlgorithmId.get("SHA1") },
            new ContentInfo(ContentInfo.DATA_OID, null),
            new X509Certificate[] { publicKey },
            new SignerInfo[] { signerInfo });

    pkcs7.encodeSignedData(out);
}

这里会把之前生成的CERT.SF文件,用私钥计算出签名, 然后将签名以及包含公钥信息的数字证书一同写入CERT.RSA中保存。

验证签名的流程

代码在PackageParser.java里,在安装APK时,Android系统会校验签名,检查APK是否被篡改。

源码:

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

        // Try to verify the APK using APK Signature Scheme v2.
        boolean verified = false;
        {
            Certificate[][] allSignersCerts = null;
            Signature[] signatures = null;
            try {
                Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV2");
                allSignersCerts = ApkSignatureSchemeV2Verifier.verify(apkPath);
                signatures = convertToSignatures(allSignersCerts);
                // APK verified using APK Signature Scheme v2.
                verified = true;
            } catch (ApkSignatureSchemeV2Verifier.SignatureNotFoundException e) {
                // No APK Signature Scheme v2 signature found
                if ((parseFlags & PARSE_IS_EPHEMERAL) != 0) {
                    throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                        "No APK Signature Scheme v2 signature in ephemeral package " + apkPath,
                        e);
                }
                // Static shared libraries must use only the V2 signing scheme
                if (pkg.applicationInfo.isStaticSharedLibrary()) {
                    throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                            "Static shared libs must use v2 signature scheme " + apkPath);
                }
            } catch (Exception e) {
                // APK Signature Scheme v2 signature was 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);
            }

            if (verified) {
                if (pkg.mCertificates == null) {
                    pkg.mCertificates = allSignersCerts;
                    pkg.mSignatures = signatures;
                    pkg.mSigningKeys = new ArraySet<>(allSignersCerts.length);
                    for (int i = 0; i < allSignersCerts.length; i++) {
                        Certificate[] signerCerts = allSignersCerts[i];
                        Certificate signerCert = signerCerts[0];
                        pkg.mSigningKeys.add(signerCert.getPublicKey());
                    }
                } else {
                    if (!Signature.areExactMatch(pkg.mSignatures, signatures)) {
                        throw new PackageParserException(
                                INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
                                apkPath + " has mismatched certificates");
                    }
                }
                // Not yet done, because we need to confirm that AndroidManifest.xml exists and,
                // if requested, that classes.dex exists.
            }
        }

        StrictJarFile jarFile = null;
        try {
            Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "strictJarFileCtor");
            // Ignore signature stripping protections when verifying APKs from system partition.
            // For those APKs we only care about extracting signer certificates, and don't care
            // about verifying integrity.
            boolean signatureSchemeRollbackProtectionsEnforced =
                    (parseFlags & PARSE_IS_SYSTEM_DIR) == 0;
            jarFile = new StrictJarFile(
                    apkPath,
                    !verified, // whether to verify JAR signature
                    signatureSchemeRollbackProtectionsEnforced);
            Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);

            // Always verify manifest, regardless of source
            final ZipEntry manifestEntry = jarFile.findEntry(ANDROID_MANIFEST_FILENAME);
            if (manifestEntry == null) {
                throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,
                        "Package " + apkPath + " has no manifest");
            }

            // Optimization: early termination when APK already verified
            if (verified) {
                return;
            }

            // APK's integrity needs to be verified using JAR signature scheme.
            Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV1");
            final List<ZipEntry> toVerify = new ArrayList<>();
            toVerify.add(manifestEntry);

            // If we're parsing an untrusted package, verify all contents
            if ((parseFlags & PARSE_IS_SYSTEM_DIR) == 0) {
                final Iterator<ZipEntry> i = jarFile.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(ANDROID_MANIFEST_FILENAME)) continue;

                    toVerify.add(entry);
                }
            }

            // Verify that entries are signed consistently with the first entry
            // we encountered. Note that for splits, certificates may have
            // already been populated during an earlier parse of a base APK.
            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);

                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());
                    }
                } else {
                    if (!Signature.areExactMatch(pkg.mSignatures, entrySignatures)) {
                        throw new PackageParserException(
                                INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, "Package " + apkPath
                                        + " has mismatched certificates at entry "
                                        + entry.getName());
                    }
                }
            }
            Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
        } 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 {
            closeQuietly(jarFile);
        }
    }

可以看出,解析是同时针对V1和V2的。

VasDolly对的多渠道支持

结合上一篇的zip文件个是的分析,和现在V1签名的机制分析。我们发现其实可以在不重新签名的情况下,完成多个渠道的打包。

我们可以将各个渠道的数据写入到apk文件(即zip)的最后面,即EOCD里面,因为这个是不参与V1校验的。而里面的comment字段是最好的选择。

因此具体的方案是:在apk文件(即zip)的注释字段里,添加渠道信息。

  1. 拷贝原始apk文件
  2. 找到apk文件的EOCD数据块
  3. 修改EOCD数据块里的comment长度
  4. 增加对应的渠道信息
  5. 添加渠道信息的字段长度
  6. 添加魔数(用处是查找渠道信息时,更方便的定位)

这样处理之后,apk里的EOCD块就成了这个样子:

查找就相对简单很多:

  1. 定位到魔数
  2. 向前读两个字节,确定渠道信息的长度LEN
  3. 继续向前读LEN字节,就是渠道信息了。

源码分析

1. 判断是否是V1签名

public static boolean containV1Signature(File file) {
        JarFile jarFile;
        try {
            jarFile = new JarFile(file);
            JarEntry manifestEntry = jarFile.getJarEntry("META-INF/MANIFEST.MF");
            JarEntry sfEntry = null;
            Enumeration<JarEntry> entries = jarFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();
                if (entry.getName().matches("META-INF/\\w+\\.SF")) {
                    sfEntry = jarFile.getJarEntry(entry.getName());
                    break;
                }
            }
            if (manifestEntry != null && sfEntry != null) {
                return true;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

2. apk文件的拷贝

public static void copyFileUsingNio(File source, File dest) throws IOException {
        FileChannel in = null;
        FileChannel out = null;
        FileInputStream inStream = null;
        FileOutputStream outStream = null;
        File parent = dest.getParentFile();
        if (parent != null && (!parent.exists())) {
            parent.mkdirs();
        }
        try {
            inStream = new FileInputStream(source);
            outStream = new FileOutputStream(dest, false);
            in = inStream.getChannel();
            out = outStream.getChannel();
            in.transferTo(0, in.size(), out);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inStream != null) {
                inStream.close();
            }
            if (outStream != null) {
                outStream.close();
            }
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.close();
            }
        }
    }

3. 得到EOCD块

public static Pair<ByteBuffer, Long> getEocd(File apk) throws IOException, ApkSignatureSchemeV2Verifier.SignatureNotFoundException {
        if (apk == null || !apk.exists() || !apk.isFile()) {
            return null;
        }
        RandomAccessFile raf = new RandomAccessFile(apk, "r");
        //find the EOCD
        Pair<ByteBuffer, Long> eocdAndOffsetInFile = ApkSignatureSchemeV2Verifier.getEocd(raf);
        if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(raf, eocdAndOffsetInFile.getSecond())) {
            throw new ApkSignatureSchemeV2Verifier.SignatureNotFoundException("ZIP64 APK not supported");
        }

        return eocdAndOffsetInFile;
    }

4. 写入

RandomAccessFile raf = new RandomAccessFile(file, "rw");
            //1.locate comment length field
            raf.seek(file.length() - ChannelConstants.SHORT_LENGTH);
            //2.write zip comment length (content field length + length field length + magic field length)
            writeShort(comment.length + ChannelConstants.SHORT_LENGTH + ChannelConstants.V1_MAGIC.length, raf);
            //3.write content
            raf.write(comment);
            //4.write content length
            writeShort(comment.length, raf);
            //5. write magic bytes
            raf.write(ChannelConstants.V1_MAGIC);
            raf.close();

6. 校验写入的渠道值是否正确

7. 校验V1签名是否正确

/**
     * verify V1 signature
     *
     * @param inputApk
     * @return
     * @throws NoSuchAlgorithmException
     * @throws IOException
     * @throws ZipFormatException
     * @throws ApkFormatException
     */
    public static boolean verifyV1Signature(File inputApk) throws NoSuchAlgorithmException, IOException, ZipFormatException, ApkFormatException {
        ApkVerifier.Builder apkVerifierBuilder = new ApkVerifier.Builder(inputApk);
        ApkVerifier apkVerifier = apkVerifierBuilder.build();
        ApkVerifier.Result result = apkVerifier.verify();
        boolean verified = result.isVerified();
        System.out.println("verified : " + verified);
        if (verified) {
            System.out.println("Verified using v1 scheme (JAR signing): " + result.isVerifiedUsingV1Scheme());
            System.out.println("Verified using v2 scheme (APK Signature Scheme v2): " + result.isVerifiedUsingV2Scheme());
            if (result.isVerifiedUsingV1Scheme()) {
                return true;
            }
        }
        return false;
    }

如何读取值呢?

read source...

results matching ""

    No results matching ""