1. 程式人生 > >如何驗證Android系統中APK證書鏈的有效性

如何驗證Android系統中APK證書鏈的有效性

瀏覽器通過https訪問指定網址時, 需要驗證網站的證書, 瀏覽器中通常內建常用的CA(Certificate Authority)根證書,而網站的證書一般都是由根CA或者子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系統對證書是否過期並不檢查.