Android 签名过程的理解

转载
数据指纹,签名文件,证书文件

类目说明
数据指纹对一个数据源做 摘要算法(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文件之间可以互相转化
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值