前言
这些天有人问我关于APK或者ROM签名的原理,因为先前接触过签名的东西,就想当然地认为在META-INF下存在3个文件, 一个是清单文件MANIFEST.MF,一个是签名后的CERT.SF,一个是公钥文件CERT.RSA,网上不少资料也是这样的观点。后来查看了签名工具的源代码才发现大错特错,CERT.SF根本不是用私钥对MANIFSET.MF签名后的文件,只是对MANIFEST.MF的每个条目再次计算摘要后的文件。现在想想凡事不可轻易断言,还是实事求是才能找到真理。接下来将根据源码详细分析APK或者ROM签名的原理。
什么是签名
首先我们得知道什么是摘要,摘要是指采用单向Hash函数对数据进行计算生成的固定长度的Hash值,摘要算法有Md5,Sha1等,Md5生成的Hash值是128位的数字,即16个字节,用十六进制表示是32个字符,Sha1生成的Hash值是160位的数字,即20个字节,用十六进制表示是40个字符。我们是不能通过摘要推算出用于计算摘要的数据,如果修改了数据,那么它的摘要一定会变化(其实这句话并不正确,只是很难正好找到不同的数据,而他们的摘要值正好相等)。摘要经常用于验证数据的完整性,很多下载网站都会列出下载文件的md5值或者sha1值。
摘要和签名没有任何关系,网上常常将摘要和签名混为一谈,这是错误的。签名和数字签名是同一个概念,是指信息的发送者用自己的私钥对消息摘要加密产生一个字符串,加密算法确保别人无法伪造生成这段字符串,这段数字串也是对信息的发送者发送信息真实性的一个有效证明。其他发送者用他们的私钥对同一个消息摘要加密会得到不同的签名,接收者只有使用发送者签名时使用的私钥对应的公钥解密签名数据才能得到消息摘要,否则得到的不是正确的消息摘要。
数字签名是非对称密钥加密技术+数字摘要技术的结合。
数字签名技术是将信息摘要用发送者的私钥加密,和原文以及公钥一起传送给接收者。接收者只有用发送者的公钥才能解密被加密的信息摘要,然后接收者用相同的Hash函数对收到的原文产生一个信息摘要,与解密的信息摘要做比对。如果相同,则说明收到的信息是完整的,在传输过程中没有被修改;不同则说明信息被修改过,因此数字签名能保证信息的完整性。并且由于只有发送者才有加密摘要的私钥,所以我们可以确定信息一定是发送者发送的。
另外还需要理解一个概念:数字证书。数字证书是一个经证书授权中心数字签名的包含公钥及其拥有者信息的文件。数字证书的格式普遍采用的是X.509V3国际标准,一个标准的X.509数字证书包含以下一些内容:证书的版本信息:
1)证书的序列号,每个证书都有一个唯一的证书序列号; 2)证书所使用的签名算法; 3)证书的发行机构名称,命名规则一般采用X.500格式; 4)证书的有效期,通用的证书一般采用UTC时间格式,它的计时范围为1950-2049; 5)证书所有人的名称,命名规则一般采用X.500格式; 6)证书所有人的公开密钥; 7)证书发行者对证书的签名。
CERT.RSA包含了数字签名以及开发者的数字证书。CERT.RSA里的数字签名是指对CERT.SF的摘要采用私钥加密后的数据,Android系统安装apk时会对CERT.SF计算摘要,然后使用CERT.RSA里的公钥对CERT.RSA里的数字签名解密得到一个摘要,比较这两个摘要便可知道该apk是否有正确的签名,也就说如果其他人修改了apk并没有重新签名是会被检查出来的。
需注意Android平台的证书是自签名的,也就说不需要权威机构签发,数字证书的发行机构和所有人是相同的,都是开发者自己,开发者生成公私钥对后不需要提交到权威机构进行校验。
签名工具的使用
Android源码编译出来的signapk.jar既可给apk签名,也可给rom签名的。使用格式:
java –jar signapk.jar [-w] publickey.x509[.pem] privatekey.pk8 input.jar output.jar
-w 是指对ROM签名时需使用的参数
publickey.x509[.pem] 是公钥文件
privatekey.pk8 是指 私钥文件
input.jar 要签名的apk或者rom
output.jar 签名后生成的apk或者rom
signapk.java
1) main函数 main函数会生成公钥对象和私钥对象,并调用addDigestsToManifest函数生成清单对象Manifest后,再调用signFile签名。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static void main( String [ ] args) {
//...
boolean signWholeFile = false ;
int argstart = 0 ;
/*如果对ROM签名需传递-w参数*/
if ( args[ 0 ] .equals ( "-w" ) ) {
signWholeFile = true ;
argstart = 1 ;
}
// ...
try {
File publicKeyFile = new File ( args[ argstart+ 0 ] ) ;
X509Certificate publicKey = readPublicKey( publicKeyFile) ;
PrivateKey privateKey = readPrivateKey( new File ( args[ argstart+ 1 ] ) ) ;
inputJar = new JarFile ( new File ( args[ argstart+ 2 ] ) , false ) ;
outputFile = new FileOutputStream ( args[ argstart+ 3 ] ) ;
/*对ROM签名,读者可自行分析,和Apk饿签名类似,但是它会添加otacert文件*/
if ( signWholeFile) {
SignApk.signWholeFile ( inputJar, publicKeyFile, publicKey,
privateKey, outputFile) ;
}
else {
JarOutputStream outputJar = new JarOutputStream ( outputFile) ;
outputJar.setLevel ( 9 ) ;
/*addDigestsToManifest会生成Manifest对象,然后调用signFile进行签名*/
signFile( addDigestsToManifest( inputJar) , inputJar,
publicKeyFile, publicKey, privateKey, outputJar) ;
outputJar.close ( ) ;
}
} catch ( Exception e) {
e.printStackTrace ( ) ;
System .exit ( 1 ) ;
} finally {
//...
}
}
2) addDigestsToManifest 首先我们得理解Manifest文件的结构,Manifest文件里用空行分割成多个段,每个段由多个属性组成,第一个段的属性集合称为主属性集合,其它段称为普通属性集合,普通属性集合一般会有Name属性,作为该属性集合所在段的名字。Android的manifeset文件会为zip的所有文件各自建立一个段,这个段的Name属性的值就是该文件的path+文件名,另外还有一个SHA1-Digest的属性,该属性的值是对文件的sha1摘要用base64编码得到的字符串。
Manifest示例:
1
2
3
4
5
6
7
8
9
10
11
Manifest-Version: 1.0
Created-By: 1.6.0-rc (Sun Microsystems Inc.)
Name: res/drawable-hdpi/user_logout.png
SHA1-Digest: zkQSZbt3Tqc9myEVuxc1dzMDPCs=
Name: res/drawable-hdpi/contacts_cancel_btn_pressed.png
SHA1-Digest: mSVZvKpvKpmgUJ9oXDJaTWzhdic=
Name: res/drawable/main_head_backgroud.png
SHA1-Digest: fe1yzADfDGZvr0cyIdNpGf/ySio=
Manifest-Version属性和Created-By所在的段就是主属性集合,其它属性集合就是普通属性集合,这些普通属性集合都有Name属性,作为该段的名字。
addDigestsToManifest源代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
private static Manifest addDigestsToManifest( JarFile jar)
throws IOException , GeneralSecurityException {
Manifest input = jar.getManifest ( ) ;
Manifest output = new Manifest ( ) ;
Attributes main = output.getMainAttributes ( ) ;
if ( input != null ) {
main.putAll ( input.getMainAttributes ( ) ) ;
} else {
main.putValue ( "Manifest-Version" , "1.0" ) ;
main.putValue ( "Created-By" , "1.0 (Android SignApk)" ) ;
}
MessageDigest md = MessageDigest .getInstance ( "SHA1" ) ;
byte [ ] buffer = new byte [ 4096 ] ;
int num;
// We sort the input entries by name, and add them to the
// output manifest in sorted order. We expect that the output
// map will be deterministic.
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 ( ) ;
if ( ! entry.isDirectory ( ) && ! name.equals ( JarFile .MANIFEST_NAME ) &&
! name.equals ( CERT_SF_NAME) && ! name.equals ( CERT_RSA_NAME) &&
! name.equals ( OTACERT_NAME) &&
( stripPattern == null ||
! stripPattern.matcher ( name) .matches ( ) ) ) {
InputStream data = jar.getInputStream ( entry) ;
/*计算sha1*/
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 ( ) ;
/*base64编码sha1值得到SHA1-Digest属性的值*/
attr.putValue ( "SHA1-Digest" ,
new String ( Base64.encode ( md.digest ( ) ) , "ASCII" ) ) ;
output.getEntries ( ) .put ( name, attr) ;
}
}
return output;
}
3) signFile 先将inputjar的所有文件拷贝至outputjar,然后生成Manifest.MF,CERT.SF和CERT.RSA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static void signFile( Manifest manifest, JarFile inputJar,
File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey,
JarOutputStream outputJar) throws Exception {
// Assume the certificate is valid for at least an hour.
long timestamp = publicKey.getNotBefore ( ) .getTime ( ) + 3600L * 1000 ;
JarEntry je;
// 拷贝文件
copyFiles( manifest, inputJar, outputJar, timestamp) ;
// 生成MANIFEST.MF
je = new JarEntry ( JarFile .MANIFEST_NAME ) ;
je.setTime ( timestamp) ;
outputJar.putNextEntry ( je) ;
manifest.write ( outputJar) ;
// 调用writeSignatureFile 生成CERT.SF
je = new JarEntry ( CERT_SF_NAME) ;
je.setTime ( timestamp) ;
outputJar.putNextEntry ( je) ;
ByteArrayOutputStream baos = new ByteArrayOutputStream ( ) ;
writeSignatureFile( manifest, baos) ;
byte [ ] signedData = baos.toByteArray ( ) ;
outputJar.write ( signedData) ;
// 非常关键的一步 生成 CERT.RSA
je = new JarEntry ( CERT_RSA_NAME) ;
je.setTime ( timestamp) ;
outputJar.putNextEntry ( je) ;
writeSignatureBlock( new CMSProcessableByteArray( signedData) ,
publicKey, privateKey, outputJar) ;
}
4) writeSignatureFile 生成CERT.SF,其实是对MANIFEST.MF的各个段再次计算Sha1摘要得到CERT.SF。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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)" ) ;
MessageDigest md = MessageDigest .getInstance ( "SHA1" ) ;
PrintStream print = new PrintStream (
new DigestOutputStream ( new ByteArrayOutputStream ( ) , md) ,
true , "UTF-8" ) ;
// 添加Manifest.mf的sha1摘要
manifest.write ( print) ;
print.flush ( ) ;
main.putValue ( "SHA1-Digest-Manifest" ,
new String ( Base64.encode ( md.digest ( ) ) , "ASCII" ) ) ;
//对MANIFEST.MF的各个段计算sha1摘要
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" ,
new String ( Base64.encode ( md.digest ( ) ) , "ASCII" ) ) ;
sf.getEntries ( ) .put ( entry.getKey ( ) , sfAttr) ;
}
CountOutputStream cout = new CountOutputStream( out) ;
sf.write ( cout) ;
// A bug in the java.util.jar implementation of Android platforms
// up to version 1.6 will cause a spurious IOException to be thrown
// if the length of the signature file is a multiple of 1024 bytes.
// As a workaround, add an extra CRLF in this case.
if ( ( cout.size ( ) % 1024 ) == 0 ) {
cout.write ( '\r ' ) ;
cout.write ( '\n ' ) ;
}
}
5) writeSignatureBlock 采用SHA1withRSA算法对CERT.SF计算摘要并加密得到数字签名,使用的私钥是privateKey,然后将数字签名和公钥一起存入CERT.RSA。这里使用了开源库bouncycastle来签名。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private static void writeSignatureBlock(
CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
OutputStream out)
throws IOException ,
CertificateEncodingException ,
OperatorCreationException,
CMSException {
ArrayList< X509Certificate> certList = new ArrayList< X509Certificate> ( 1 ) ;
certList.add ( publicKey) ;
JcaCertStore certs = new JcaCertStore( certList) ;
CMSSignedDataGenerator gen = new CMSSignedDataGenerator( ) ;
//签名算法是SHA1withRSA
ContentSigner sha1Signer = new JcaContentSignerBuilder( "SHA1withRSA" )
.setProvider ( sBouncyCastleProvider)
.build ( privateKey) ;
gen.addSignerInfoGenerator (
new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder( )
.setProvider ( sBouncyCastleProvider)
.build ( ) )
.setDirectSignature ( true )
.build ( sha1Signer, publicKey) ) ;
gen.addCertificates ( certs) ;
CMSSignedData sigData = gen.generate ( data, false ) ;
ASN1InputStream asn1 = new ASN1InputStream( sigData.getEncoded ( ) ) ;
DEROutputStream dos = new DEROutputStream( out) ;
dos.writeObject ( asn1.readObject ( ) ) ;
}
总结 通过源码分析可知使用signpak.jar对某个apk或者rom签名后,将在apk或者rom的zip根目录添加META-INF目录,并在里面添加三个文件,清单文件MANIFEST.MF,对清单文件各个段计算sha1摘要得到的CERT.SF,存放数字签名和数字证书的CERT.RSA文件。如果是rom包还会添加文件META-INF/com/android/otacert。
很多公司都定制了signapk.jar工具,签名后放在META-INF下的文件不是CERT.SF和CERT.RSA,而是其它名字,比如微信,META-INF下的文件是COM_TENC.SF和COM_TENC.RSA。但内容和标准的CERT.SF,CERT.RSA其实是一样的。