浏览器通过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系统对证书是否过期并不检查.