浏览器通过https访问指定网址时, 需要验证网站的证书, 浏览器中通常内置常用的CA(Certificate Authority)根证书,而网站的证书一般都是由根CA或者其子证书签名的.如果网站没有这些知名CA签名, 则需要将网站自己的根证书导入浏览器才能进行https访问.Android系统中的APK安装包的签名使用的是自签名,与浏览器中的场景是不同的,因此,在APK包的安装过程中,是不需要知名CA证书的.在Android系统中, APK包中的所有文件都必须由一套证书进行签名(于此不同的是, java的jar包中的文件,可以由不同的证书进行签名). APK包签名的证书可以是一个证书链,尽管很少有APK这样签名,但是确有一些APK使用证书链签名.

对于证书链存在2个角色,签发者和被签发者,二者存在的关系为:

  • 签发者使用其私钥签名被签发者的证书.
  • 被签发者证书的issuer等于签发者证书的subject.这里讨论的证书限于X509格式.因为subject和issuer只是X509证书中的一段字符串,是可以伪造的,所以,该特征只是构成证书链的必要条件.

当我们对APK包中的证书链进行验证时,首先根据第二个特征获得证书链,然后根据第一个特征验证证书链.如下代码是验证APK包的证书是否是一个真正有效的证书链(对于非证书链的情况也是适用的).

public class VerifyCertsChain {

    public static boolean verify(String apkFile) {
        final Certificate[] certificates = getCertsFromApk(apkFile);
        if (certificates == null || certificates.length == 0) return false;
        return verifyCerts(certificates);
    }

    private static Certificate[] getCertsFromApk(String apkFile) {
        InputStream inputStream = null;
        try {
            final JarFile jarFile = new JarFile(apkFile);
            final Enumeration<JarEntry> jarEntries = jarFile.entries();
            while (jarEntries.hasMoreElements()) {
                final JarEntry jarEntry = jarEntries.nextElement();
                if (jarEntry.isDirectory() || jarEntry.getName().startsWith("META-INF/")) {
                    continue;
                }
                final byte[] buffer = new byte[4096];
                inputStream = jarFile.getInputStream(jarEntry);
                while (inputStream.read(buffer, 0, buffer.length) != -1);
                return jarEntry.getCertificates();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                 inputStream.close();
                } catch (Exception e) {
                }
            }
        }

        return null;
    }

    private static boolean verifyCerts(Certificate[] certificates) {
        final int size = certificates.length;
        int index = 0;
        while (index < size) {
            final X509Certificate[] certsChain = getCertsChain(certificates, index);
            if (!verifyCertsChain(certsChain)) {
                return false;
            }
            index += certsChain.length;
        }

        return true;
    }

    private static X509Certificate[] getCertsChain(Certificate[] certificates, int start) {
        int index;
        for (index = start; index < certificates.length - 1; index++) {
            if (!((X509Certificate) certificates[index]).getIssuerX500Principal().equals(
                    ((X509Certificate) certificates[index+1]).getSubjectX500Principal())) {
                break;
            }
        }

        final int certsChainSize = index - start + 1;
        X509Certificate[] x509Certificates = new X509Certificate[certsChainSize];
        for (int i = 0; i < certsChainSize; i++) {
            x509Certificates[i] = (X509Certificate) certificates[start+i];
        }
        return x509Certificates;
    }

    private static boolean verifyCertsChain(X509Certificate[] x509Certificates) {
        final int size = x509Certificates.length;
        for (int i = 0; i < size - 1; i++) {
            final X509Certificate recipient = x509Certificates[i];
            final X509Certificate issuer = x509Certificates[i+1];

            try {
                recipient.verify(issuer.getPublicKey());
            } catch (CertificateException | NoSuchAlgorithmException |
                    InvalidKeyException | NoSuchProviderException |
                    SignatureException e) {
                e.printStackTrace();
                return false;
            }
        }

        return isSelfSigned(x509Certificates[size-1]);
    }

    private static boolean isSelfIssued(X509Certificate x509Certificate) {
        final X500Principal subject = x509Certificate.getSubjectX500Principal();
        final X500Principal issuer = x509Certificate.getIssuerX500Principal();
        return subject.equals(issuer);
    }

    private static boolean isSelfSigned(X509Certificate x509Certificate) {
        if (isSelfIssued(x509Certificate)) {
            try {
                x509Certificate.verify(x509Certificate.getPublicKey());
                return true;
            } catch (CertificateException | NoSuchAlgorithmException |
                    InvalidKeyException | NoSuchProviderException |
                    SignatureException e) {
                e.printStackTrace();
            }
        }
        return false;
    }
}

需要注意的是,从jar文件获得的证书列表jarEntry.getCertificates()中,如果存在证书链,则子证书在前,其签发证书在后,位置是连续的.每个证书链的最后一个证书是该链的根证书, 根证书是自签名的.自签名的证书中,其subject和issuer字符串标识是相同的.另外, Android系统中校验证书时, 不需要校验其有效期,Android系统对证书是否过期并不检查.