如何驗證Android系統中APK證書鏈的有效性
阿新 • • 發佈:2018-12-19
瀏覽器通過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系統對證書是否過期並不檢查.