Android 签名/认证机制
Android为了确认apk开发者身份和防止内容的篡改,设计了一套apk签名的方案保证apk的安全性,即在打包时由开发者进行apk的签名,在安装apk时Android系统会有相应的开发者身份和内容正确性的验证,只有验证通过才可以安装apk,签名过程和验证的设计就是基于 非对称加密的思想。
Android在7.0以前使用的一套签名方案:在apk根目录下的META-INF/文件夹下生成签名文件,然后在安装时在系统的PackageManagerService里进行签名文件的验证。
从7.0开始,Android提供了新的V2签名方案:利用apk(zip)压缩文件的格式,在几个原始内容区之外增加了一块用于存放签名信息的数据区,然后同样在安装时在系统的PackageManagerService里进行V2版本的签名验证,V2方案会更安全、使校验更快安装更快。
当然V2签名方案会向后兼容,如果没有使用V2签名就会默认走V1签名方案的验证过程。
一.V1签名
(一)签名方式
有两个工具可以进行android的签名,jarsign和signapk。
jarsign是Java本生自带的一个工具,他可以对jar进行签名的;而signapk是门为了Android应用程序apk进行签名的工具,他们两的签名算法没什么区别,主要是签名时使用的文件不一样。
jarsign签名时使用的是keystore文件(就是我们AS里经常见得keystore文件,由我们指定的密码生成的密钥文件),signapk签名时使用的是pk8(私钥)和x509.pem(公钥)文件,因为两种方式签名都可行,所以说明两种文件可以相互转换。
而我们对apk进行签名过后,会在apk根目录的META-INF/目录下产生三个文件
下面就来看看AS打包时如何使用我们生成的密钥文件来签名(生成这三个文件)的,以signapk源码为例。
1.MANIFEST.MF
Manifest manifest = addDigestsToManifest(inputJar);//使用inputJar向Manifest里生成内容
je = new JarEntry(JarFile.MANIFEST_NAME);//输出文件为META-INF/MANIFEST.MF
je.setTime(timestamp);
outputJar.putNextEntry(je);
manifest.write(outputJar);//写到输出文件中
在signapk的main函数中,inputJar就是输入的apk文件,而je就是outputJar输出目标的文件—JarFile.MANIFEST_NAME,即META-INF/MANIFEST.MF。
也就是说,调用addDigestsToManifest方法,将生成的内容写到META-INF/MANIFEST.MF文件中,那么来看看addDigestsToManifest生成了什么内容。
private static Manifest addDigestsToManifest(JarFile jar)
throws IOException, GeneralSecurityException {
Manifest input = jar.getManifest();
Manifest output = new Manifest();
Attributes main = output.getMainAttributes();
...
main.putValue("Manifest-Version", "1.0"); //输出版本
main.putValue("Created-By", "1.0 (Android SignApk)");
BASE64Encoder base64 = new BASE64Encoder(); //Base64
MessageDigest md = MessageDigest.getInstance("SHA1");//SHA1加密
byte[] buffer = new byte[4096];
int num;
//记录apk中所有文件的名字和文件的映射
TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
JarEntry entry = e.nextElement();
byName.put(entry.getName(), entry);
}
for (JarEntry entry: byName.values()) {
String name = entry.getName();
//循环除了META-INF/下的MANIFEST.MF、CERT.SF、CERT.RSA的文件
if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&
!name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) &&
(stripPattern == null ||
!stripPattern.matcher(name).matches())) {
//读取文件内容并用SHA1读取摘要
InputStream data = jar.getInputStream(entry);
while ((num = data.read(buffer)) > 0) {
md.update(buffer, 0, num);
}
...
//将摘要再用base64编码,写入文件
attr.putValue("SHA1-Digest", base64.encode(md.digest()));
//将文件名和对应的编码摘要信息写入output中
output.getEntries().put(name, attr);
}
}
return output;
}
该方法其实就是将apk中除了要生成的这三个文件外的所有文件,生成对应的摘要并base64编码,然后写入到MANIFEST.MF中,每个文件的名字和编码摘要单独输出,就形成了上述MANIFEST.MF文件的样子。
2.CERT.SF
看文件内容感觉和MANIFEST.MF类似,我们来看看代码吧
Signature signature = Signature.getInstance("SHA1withRSA");
signature.initSign(privateKey);//签名对象,使用SHA1withRSA算法,用开发者私钥进行签名
je = new JarEntry(CERT_SF_NAME);//META-INF/CERT.SF
je.setTime(timestamp);
outputJar.putNextEntry(je);
writeSignatureFile(manifest,new SignatureOutputStream(outputJar, signature));//manifest就是上述生成的MANIFEST.MF文件内容
signapk的main函数中,生成完上述的MANIFEST.MF文件后,会调用writeSignatureFile并传入上述MANIFEST.MF文件内容,将生成的内容写到META-INF/CERT.SF文件中,下面来看看writeSignatureFile方法生成了什么
private static void writeSignatureFile(Manifest manifest, OutputStream out)
throws IOException, GeneralSecurityException {
Manifest sf = new Manifest();
Attributes main = sf.getMainAttributes();
//写入版本信息
main.putValue("Signature-Version", "1.0");
main.putValue("Created-By", "1.0 (Android SignApk)");
BASE64Encoder base64 = new BASE64Encoder();
MessageDigest md = MessageDigest.getInstance("SHA1");
PrintStream print = new PrintStream(
new DigestOutputStream(new ByteArrayOutputStream(), md),
true, "UTF-8");
//将整个MANIFEST.MF文件的内容做一次SHA1摘要提取并用base64编码,写入CERT.SF
manifest.write(print);
print.flush();
main.putValue("SHA1-Digest-Manifest", base64.encode(md.digest()));
//将MANIFEST.MF里的每个文件的编码摘要,在做一次摘要提取并base64编码,并与文件名映射输出到CERT.SF中
Map<String, Attributes> entries = manifest.getEntries();
for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
// Digest of the manifest stanza for this entry.
print.print("Name: " + entry.getKey() + "\r\n");
for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
print.print(att.getKey() + ": " + att.getValue() + "\r\n");
}
print.print("\r\n");
print.flush();
Attributes sfAttr = new Attributes();
sfAttr.putValue("SHA1-Digest", base64.encode(md.digest()));
sf.getEntries().put(entry.getKey(), sfAttr);
}
sf.write(out);
}
该方法其实就是先将整个MANIFEST.MF内容生成摘要进行编码输出,再把MANIFEST.MF中每个文件的编码摘要再进行一次编码摘要进行输出,形成了CERT.SF文件。
3.CERT.RSA
最后的这个CERT.RSA文件是二进制文件,是一个加密文件,我们来看看是什么内容
je = new JarEntry(CERT_RSA_NAME);//META-INF/CERT.RSA
je.setTime(timestamp);
outputJar.putNextEntry(je);
writeSignatureBlock(signature, publicKey, outputJar);
在signapk的main方法最后,会调用writeSignatureBlock方法,将生成内容输出到META-INF/CERT.RSA文件中
private static void writeSignatureBlock(
Signature signature, X509Certificate publicKey, OutputStream out)
throws IOException, GeneralSecurityException {
//用私钥签名CERT.SF文件生成数字签名
SignerInfo signerInfo = new SignerInfo(
new X500Name(publicKey.getIssuerX500Principal().getName()),
publicKey.getSerialNumber(),
AlgorithmId.get("SHA1"),
AlgorithmId.get("RSA"),
signature.sign());
//数字签名连同包含公钥的证书等信息一起生成PKCS7格式的文件
PKCS7 pkcs7 = new PKCS7(
new AlgorithmId[] {
AlgorithmId.get("SHA1") },
new ContentInfo(ContentInfo.DATA_OID, null),
new X509Certificate[] {
publicKey },
new SignerInfo[] {
signerInfo });
//写入META-INF/CERT.RSA
pkcs7