插件化阿里Atlas之插件热补丁安全校验

通过云端下发插件补丁的方式,我们可以很方便的用来修复线上bug和发布新功能。但是如何保证接收到的补丁是安全的呢?

很多人想到使用类似校验md5的方式,但是md5值也是通过接口下发,本来就是不安全的,使用fiddler mock接口,md5值想返回什么都行。

其实安卓的apk也是有签名的,假如补丁包也能使用apk相同的证书签名,然后对比下发补丁的签名和当前运行的apk签名是否一致就行了。

1、对补丁进行zip压缩

atlas的插件补丁文件有2个,一个是update.json,另一个是tpatch文件。对这两个文件进行压缩得到的zip文件名是patch.zip

2、使用apk的release证书对patch.zip签名

执行以下签名命令:

jarsigner -verbose -keystore KEYSTORE_FILEPATH -signedjar patch_v.zip patch.zip  dengyin2000

-keystore 后面输入apk证书的路径
-signedjar 后面输入签名完成后的zip文件名,然后是需要签名的文件名,然后是证书的别名
签名成功后,你就能看到patch_v.zip文件了。

3、使用代码进行验证,SecurityChecker.java

看看代码:

package com.iteye.dengyin2000.android.securitychecker;


import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.util.Log;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import javax.security.auth.x500.X500Principal;

/**
 * Created by denny on 2017/5/26.
 */
public class SecurityChecker {
    private static final String TAG = "SecurityChecker";

    private static final String CLASSES_DEX = "classes.dex";

    private static final X500Principal DEBUG_DN = new X500Principal(
            "CN=Android Debug,O=Android,C=US");

    private final Context mContext;
    /**
     * host publickey
     */
    private PublicKey mPublicKey;
    /**
     * host debuggable
     */
    private boolean mDebuggable;

    public SecurityChecker(Context context) {
        mContext = context;
        init(mContext);
    }

    public boolean verifyAtlasPatch(File path) {

        JarFile jarFile = null;
        try {
            jarFile = new JarFile(path);
            Enumeration entries = jarFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry jarEntry = entries.nextElement();
                if ("update.json".equals(jarEntry.getName()) || jarEntry.getName().endsWith(".tpatch")) {
                    loadDigestes(jarFile, jarEntry);
                    Certificate[] certs = jarEntry.getCertificates();
                    if (certs == null) {
                        return false;
                    }
                    if (!check(path, certs)) {
                        return false;
                    }
                    Log.i(TAG, "File: " + jarEntry.getName() + " is verified");
                }
            }
            return true;
        } catch (IOException e) {
            Log.e(TAG, path.getAbsolutePath(), e);
            return false;
        } finally {
            try {
                if (jarFile != null) {
                    jarFile.close();
                }
            } catch (IOException e) {
                Log.e(TAG, path.getAbsolutePath(), e);
            }
        }
    }

    /**
     * @param path Apk file
     * @return true if verify apk success
     */
    public boolean verifyApk(File path) {
        if (mDebuggable) {
            Log.d(TAG, "mDebuggable = true");
            return true;
        }

        JarFile jarFile = null;
        try {
            jarFile = new JarFile(path);

            JarEntry jarEntry = jarFile.getJarEntry(CLASSES_DEX);
            if (null == jarEntry) {// no code
                return false;
            }
            loadDigestes(jarFile, jarEntry);
            Certificate[] certs = jarEntry.getCertificates();
            return certs != null && check(path, certs);
        } catch (IOException e) {
            Log.e(TAG, path.getAbsolutePath(), e);
            return false;
        } finally {
            try {
                if (jarFile != null) {
                    jarFile.close();
                }
            } catch (IOException e) {
                Log.e(TAG, path.getAbsolutePath(), e);
            }
        }
    }

    private void loadDigestes(JarFile jarFile, JarEntry je) throws IOException {
        InputStream is = null;
        try {
            is = jarFile.getInputStream(je);
            byte[] bytes = new byte[8192];
            while (is.read(bytes) > 0) {
            }
        } finally {
            if (is != null) {
                is.close();
            }
        }
    }

    // verify the signature of the Apk
    private boolean check(File path, Certificate[] certs) {
        if (certs.length > 0) {
            for (int i = certs.length - 1; i >= 0; i--) {
                try {
                    certs[i].verify(mPublicKey);
                    return true;
                } catch (Exception e) {
                    Log.e(TAG, path.getAbsolutePath(), e);
                }
            }
        }
        return false;
    }

    // initialize,and check debuggable
    private void init(Context context) {
        try {
            PackageManager pm = context.getPackageManager();
            String packageName = context.getPackageName();

            PackageInfo packageInfo = pm.getPackageInfo(packageName,
                    PackageManager.GET_SIGNATURES);
            CertificateFactory certFactory = CertificateFactory
                    .getInstance("X.509");
            ByteArrayInputStream stream = new ByteArrayInputStream(
                    packageInfo.signatures[0].toByteArray());
            X509Certificate cert = (X509Certificate) certFactory
                    .generateCertificate(stream);
            mDebuggable = cert.getSubjectX500Principal().equals(DEBUG_DN);
            mPublicKey = cert.getPublicKey();
        } catch (NameNotFoundException e) {
            Log.e(TAG, "init", e);
        } catch (CertificateException e) {
            Log.e(TAG, "init", e);
        }
    }

}

init方法,获得运行apk的签名publicKey。
verifyAtlasPatch方法,验证zip包里面的update.json和.tpatch文件的签名是否跟apk的签名一致。所以验证补丁包是否合法调用这个方法就行了。:)

你可能感兴趣的:(插件化阿里Atlas之插件热补丁安全校验)