转载
数据指纹,签名文件,证书文件
类目 | 说明 |
---|---|
数据指纹 | 对一个数据源做 摘要算法(SHA / MD5算法)得出的值,这个值是唯一的、固定长度的 |
签名文件 | 签名文件:用 RSA算法 的 私钥 对 数据指纹( 消息摘要 )加密,保存了加密数据的文件 签名文件、证书 是成对出现的 校验时( 比如,安装 apk 时),使用证书中的公钥对签名解密 使用 摘要算法 对源数据( 比如,apk) 获取摘要 获取的摘要 与 解密数据 对比,数据一致则校验通过 |
证书文件 | 证书文件中包含了 公钥信息、签名、其他信息 |
Apk的 签名文件 证书文件 | 在Android签名之后 META-INF 目录下 CERT.SF 保存了apk中所有文件(除了签名过程生成的文件)摘要信息 CERT.RSA 就是证书文件,内部包含公钥、签名、证书信息 可以使用 openssl 查看 CERT.RSA 文件中的证书信息、公钥信息 |
jarsign | 签名时使用的是 keystore 文件 Android 允许使用多个 keystore 对apk进行签名 |
signapk | 签名时使用的是 pk8、x509.pem文件 pk8 是私钥文件、x509.pem 是含有公钥的文件 signapk的源码:com/android/signapk/sign.java 签名后的 apk 的 META-INF 目录中 有三个相关文件: 1、MANIFEST.MF 保存了除(MANIFEST.MF、CERT.RSA、CERT.SF)这三个文件、目录之外 每个文件的 摘要的Base64编码值(对文件摘要算法、对摘要Base64编码) 2、CERT.SF 1)计算 MANIFEST.MF文件 的整体SHA1值,并BASE64编码,存入文件头“SHA1-Digest-Manifest”属性值下 2)逐条计算 MANIFEST.MF文件 中每一个块的 SHA1,然后 BASE64编码后 记录在 CERT.SF中的同名(与 MANIFEST.MF 中对应项同名)块中 3、CERT.RSA 文件 CERT.RSA是一个 RSA 加密的二进制文件 把 CERT.SF 文件, 用私钥计算出签名 然后将 签名、包含公钥信息的数字证书,一同写入 CERT.RSA 中保存 签名、包含公钥信息的数字证书,是写在 CERT.RSA 文件中的 CERT.RSA 文件是不会被校验的,walle项目对V2签名快速写入渠道,就是把渠道信息写入该文件中 |
一、准备知识
1、数据摘要(数据指纹)
对一个数据源进行一个算法之后得到一个摘要,也叫作数据指纹,不同的数据源,数据指纹肯定不一样。
消息摘要算法(Message Digest Algorithm)是一种能产生特殊输出格式的算法,
其原理是根据一定的运算规则对原始数据进行某种形式的信息提取,
被提取出的信息就被称作原始数据的消息摘要。
著名的摘要算法:MD5算法、SHA-1算法、及其大量的变体。
消息摘要的主要特点有:
1)无论输入的消息有多长,计算出来的【消息摘要的长度总是固定的】
用 MD5算法 摘要的消息有128个比特位。
用 SHA-1 算法摘要的消息最终有160比特位的输出。
2)一般来说(不考虑碰撞的情况下),只要输入的原始数据不同,
对其进行摘要以后产生的消息摘要也必不相同,即使原始数据稍有改变,
输出的消息摘要便完全不同。但是,相同的输入必会产生相同的输出。
3)具有不可逆性,即只能进行正向的信息摘要,而无法从摘要中恢复出任何的原始消息。
2、签名文件和证书
签名文件和证书是成对出现的,二者不可分离。
这两个文件名字是一样的,只是后缀名不一样。
[1]数字签名
是对 非对称加密技术 和 数字摘要技术 的具体应用。
1)消息的发送者,先要【生成一对公私钥对,将公钥给消息的接收者】。
2)消息的发送者给消息接收者发消息,在发送的信息中,
除了要包含原始的消息外,还要加上另外一段消息。
这段消息通过如下两步生成:
i.对要发送的原始消息【提取消息摘要】
ii.对提取的信息摘要【用自己的私钥加密】
(通过这两步得出的消息,就是所谓的原始信息的数字签名。)
3)信息的接收者来,所收到的信息,将包含两个部分,
一是原始的消息内容,二是附加的那段数字签名。
他将通过以下三步来验证消息的真伪:
i.对原始消息部分【提取消息摘要】,注意这里使用的消息摘要算法要和发送方使用的一致
ii.对附加上的那段数字签名,【使用预先得到的公钥解密】
iii.【比较前两步所得到的两段消息】是否一致。
一致,则表明消息确实是期望的发送者发的,且内容没有被篡改过;
不一致,则表明传送的过程中一定出了问题,消息不可信。
[2]数字证书
数字证书一般包含以下一些内容:
证书的发布机构(Issuer)
证书的有效期(Validity)
消息发送方的公钥
证书所有者(Subject)
数字签名所使用的算法
数字签名
数字证书其实也用到了数字签名技术,签名的内容是消息发送方的公钥,以及一些其它信息。
数字证书中签名者是要有一定公信力的机构。
一般来说,这些有公信力机构的根证书,已经在设备出厂前预先安装到了设备上了。
所以,数字证书 可以保证 数字证书里的公钥 确实是这个证书的所有者的。
数字证书主要是用来解决公钥的安全发放问题。
3、jarsign工具签名 和 signapk工具签名
1)jarsign 是Java本生自带的一个工具,他可以对jar进行签名。
2)signapk 专门为了Android应用程序apk进行签名的工具。
二者签名算法没区别,主要是签名时使用的文件不一样。
4、keystore文件和pk8文件,x509.pem文件的关系
jarsign 签名时使用的是 keystore文件
signapk 签名时使用的是 pk8,x509.pem文件
1)查看keystore文件的 MD5 和 SHA1 值:
keytool -list -keystore debug.keystore
2)手动的生成一个keystore文件:
keytool -genkeypair -v
-keyalg DSA
-keysize 1024
-sigalg SHA1withDSA
-validity 20000
-keystore D:\jiangwei.keystore
-alias jiangwei
-keypass jiangwei
-storepass jiangwei
-alias 是定义别名,这里为debug
-keyalg 是规定签名算法,这里是DSA,这里的算法直接关系到后面apk中签名文件的后缀名,
到后面会详细说明。
3)用jarsigner工具进行签名
jarsigner -verbose
-sigalg SHA1withDSA
-digestalg SHA1
-keystore D:\jiangwei.keystore
-storepass jiangwei D:\123.apk jiangwei
Android中是允许使用多个keystore 对apk进行签名的
4)用 signapk 进行签名
java -jar signapk.jar .testkey.x509.pem testkey.pk8 debug.apk debug.sig.apk
这里需要两个文件:.pk8 和 .x509.pem
pk8 是私钥文件
x509.pem 是含有公钥的文件
signapk签名之后的apk中的 META-INF文件夹中的三个文件的名字是这样的:
CERT.RSA
CERT.SF
MANIFEST.MF
因为signapk在前面的时候不像jarsigner会自动使用别名来命名文件,
这里就是写死了是CERT的名字,不过文件名不影响的,
Android中的Apk校验过程,只会通过后缀名来查找文件。
二、Android中签名流程
signapk的源码:com/android/signapk/sign.java
Android签名apk之后,会有一个 META-INF 文件夹,这里有三个文件:
MANIFEST.MF
CERT.RSA
CERT.SF
1、MANIFEST.MF
遍历apk里面的所有条目,
如果是目录 或者 这三个文件(MANIFEST.MF,CERT.RSA,CERT.SF)就跳过,
如果是一个文件,就用SHA1(或者SHA256)消息摘要算法提取出该文件的摘要
然后对摘要进行BASE64编码后,
作为“SHA1-Digest”属性的值写入到 MANIFEST.MF文件中的一个块中。
该块有一个“Name”属性,其值就是该文件在apk包中的路径。
1)文件内容:
Signature-Version: 1.0
Created-By: 1.0 (Android)
SHA1-Digest-Manifest: sUr+nU94gqk7SMH+P9VXqvVUR1s=
X-Android-APK-Signed: 2
Name: AndroidManifest.xml
SHA1-Digest: GtoDRfBq/vsn+gtrXrgZtDFmqqE=
Name: META-INF/CHANGES
SHA1-Digest: yzibkvU6k/HmnPHmyXXUnuCJzFg=
Name: assets/font/Roboto-Light.ttf
SHA1-Digest: +zRkYk07itSTQb1pRVZRQ6mU6a8=
Name: classes.dex
SHA1-Digest: o7wHLoxi2YryftRILwESL2hpHi0=
Name: com/google/api/client/googleapis/google.jks
SHA1-Digest: 2VFYHPhiUUfhcGkhs29aZWVIkFE=
...
2)签名过程 写 MANIFEST.MF 的代码
com/android/signapk/sign.java
public static void main(String[] args) {
...
//MANIFEST.MF
Manifest manifest = addDigestsToManifest(inputJar);
je = new JarEntry(JarFile.MANIFEST_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
manifest.write(outputJar);
...
}
/*除了三个文件(MANIFEST.MF,CERT.RSA,CERT.SF),
* 其他的文件都会对文件内容做一次SHA1算法,计算出文件的摘要信息,然后用Base64进行编码
*/
private static Manifest addDigestsToManifest(JarFile jar)... {
...
BASE64Encoder base64 = new BASE64Encoder();
MessageDigest md = MessageDigest.getInstance("SHA1");
byte[] buffer = new byte[4096];
int num;
...
for (JarEntry entry: byName.values()) {
String name = entry.getName();
if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&
!name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) &&
(stripPattern == null ||
!stripPattern.matcher(name).matches())) {
InputStream data = jar.getInputStream(entry);
while ((num = data.read(buffer)) > 0) {
md.update(buffer, 0, num);
}
Attributes attr = null;
if (input != null) attr = input.getAttributes(name);
attr = attr != null ? new Attributes(attr) : new Attributes();
attr.putValue("SHA1-Digest", base64.encode(md.digest()));
output.getEntries().put(name, attr);
}
}
return output;
}
2、CERT.SF文件
CERT.SF文件做了什么:
1》计算这个 MANIFEST.MF文件 的整体SHA1值,再经过BASE64编码后,
记录在 CERT.SF 主属性块(在文件头上)的“SHA1-Digest-Manifest”属性值值下
2》逐条计算 MANIFEST.MF文件 中每一个块的SHA1,并经过BASE64编码后,
记录在CERT.SF中的同名块中,属性的名字是“SHA1-Digest”
1)文件内容
Signature-Version: 1.0
Created-By: 1.0 (Android)
SHA1-Digest-Manifest: sUr+nU94gqk7SMH+P9VXqvVUR1s=
X-Android-APK-Signed: 2
Name: AndroidManifest.xml
SHA1-Digest: GtoDRfBq/vsn+gtrXrgZtDFmqqE=
Name: META-INF/CHANGES
SHA1-Digest: yzibkvU6k/HmnPHmyXXUnuCJzFg=
Name: assets/crashlytics-build.properties
SHA1-Digest: ggU4f5dX7MU7Rr/BULArGWGtibc=
Name: res/a4/qo.xml
SHA1-Digest: xCNbNXrxuiECc4fpeRqbXecJeUs=
...
2)签名过程 写 CERT.SF 的代码
public static void main(String[] args) {
...
//CERT.SF
Signature signature = Signature.getInstance("SHA1withRSA");
signature.initSign(privateKey);
je = new JarEntry(CERT_SF_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
//manifest变量就是刚刚写入了MANIFEST.MF文件的
writeSignatureFile(manifest ,new SignatureOutputStream(outputJar, signature));
...
}
/*需要对之前的 MANIFEST.MF文件 整个内容做一个SHA1放到SHA1-Digest-Manifest字段中
*
*/
private static void writeSignatureFile(Manifest manifest, OutputStream out)... {
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放到SHA1-Digest-Manifest字段中
* manifest变量就是刚刚写入了MANIFEST.MF文件的
*/
manifest.write(print);
print.flush();
main.putValue("SHA1-Digest-Manifest", base64.encode(md.digest()));
/*
* 还是用到了刚刚传入的 mainfest变量,遍历他的条目内容,
* 然后进行 SHA算法 计算再 Base64 一下:
*
* 其实就是对 MANIFEST.MF文件 中的每个条目内容做一次SHA,在保存一下即可
*/
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);
}
3、CERT.RSA 文件
这里会把之前生成的 CERT.SF 文件, 用私钥计算出签名,
然后将【签名、以及包含公钥信息的数字证书】一同写入 CERT.RSA 中保存。
CERT.RSA是一个满足PKCS7格式的文件。
签名、包含公钥信息的数字证书,是写再 CERT.RSA 文件中的。
1)文件内容
二进制文件,因为RSA文件加密了,所以我们需要用openssl命令才能查看其内容
openssl pkcs7 -inform DER -in CERT.RSA -noout -print_certs –text
2)签名过程 写 CERT.RSA 的代码
/** Write a .RSA file with a digital signature. */
private static void writeSignatureBlock(Signature signature, X509Certificate publicKey, OutputStream out)... {
SignerInfo signerInfo = new SignerInfo(
new X500Name(publicKey.getIssuerX500Principal().getName()),
publicKey.getSerialNumber(),
AlgorithmId.get("SHA1"),
AlgorithmId.get("RSA"),
signature.sign());
PKCS7 pkcs7 = new PKCS7(
new AlgorithmId[] { AlgorithmId.get("SHA1") },
new ContentInfo(ContentInfo.DATA_OID, null),
new X509Certificate[] { publicKey },
new SignerInfo[] { signerInfo });
pkcs7.encodeSignedData(out);
}
这里会把之前生成的 CERT.SF文件, 用私钥计算出签名,
然后将签名以及包含公钥信息的数字证书一同写入 CERT.RSA 中保存。
CERT.RSA是一个满足PKCS7格式的文件。
三、为何要这么来签名
如果apk文件被篡改后会发生什么。
首先,如果你改变了apk包中的任何文件(除了这三个文件之外 MANIFEST.MF,CERT.RSA,CERT.SF),
那么在apk安装校验时,
改变后的文件摘要信息与 MANIFEST.MF 的检验信息不同,于是验证失败,程序就不能成功安装。
其次,如果你对更改的过的文件相应的算出新的摘要值,
然后更改 MANIFEST.MF文件里面对应的属性值,那么必定与CERT.SF文件中算出的摘要值不一样,
照样验证失败。
最后,如果你还不死心,继续计算 MANIFEST.MF 的摘要值,相应的更改CERT.SF里面的值,
那么数字签名值必定与CERT.RSA文件中记录的不一样,还是失败。
那么能不能继续伪造数字签名呢?不可能,因为没有数字证书对应的私钥。
所以,如果要重新打包后的应用程序能再Android设备上安装,必须对其进行重签名。
从上面的分析可以得出,只要修改了Apk中的任何内容,就必须重新签名,不然会提示安装失败。
例外:v1签名时,zip文件的comment区不会校验。
v2签名时,apk的签名区块不会校验。
这是walle 实现Gradle 快速多渠道打包的依据。
四、知识点梳理
1、数据指纹,签名文件,证书文件的含义
1)数据指纹就是对一个数据源做SHA/MD5算法,这个值是唯一的
2)签名文件技术就是:数据指纹+RSA算法
3)证书文件中包含了公钥信息和其他信息
4)在Android签名之后,其中SF就是签名文件,
RSA就是证书文件我们可以使用openssl来查看RSA文件中的证书信息和公钥信息
2、我们了解了Android中的签名有两种方式:jarsigner和signapk
这两种方式的区别是:
1)jarsigner签名时,需要的是keystore文件,而signapk签名的时候是pk8,x509.pem文件
2)jarsigner签名之后的SF和RSA文件名默认是keystore的别名,
而signapk签名之后文件名是固定的:CERT
3)Eclipse中我们在跑Debug程序的时候,默认用的是jarsigner方式签名的,
用的也是系统默认的debug.keystore签名文件
4)keystore文件和pk8,x509.pem文件之间可以互相转化