什么是签名
实际生活中我们自己的签名是为了证明签名的材料是出自于你手,是否有人更改,是辨别真假的一种最简单直接的方式。那么什么是数字世界的签名呢?其实和现实世界的签名一样,是为了保证数字内容的完整性,保证传输的内容没有经过非法的更改。在弄清楚签名之前,需要知道什么只摘要和什么是非对称加密。
什么是摘要
摘要是指采用单向Hash函数对数据进行计算生成的固定长度的Hash值,摘要算法有Md5,Sha1等,Md5生成的Hash值是128位的数字,即16个字节,用十六进制表示是32个字符,Sha1生成的Hash值是160位的数字,即20个字节,用十六进制表示是40个字符。我们是不能通过摘要推算出用于计算摘要的数据,如果修改了数据,那么它的摘要一定会变化(其实这句话并不正确,只是很难正好找到不同的数据,而他们的摘要值正好相等)。摘要实际上是通过校验算法生成的校验值。
什么是非对称加密
对于一份数据,通过一种算法,基于传入的密钥(一串由数字或字符组成的字符串,也称“key”),将明文数据转换成了不可阅读的密文,这是众所周知的“加密”,同样的,密文到达目的地后,需要再以相应的算法,配合一个密钥,将密文再解密成明文,这就是“解密”。如果加密和解密使用的是同一个密钥,那么这就是“对称密钥加解密”(最常见的对称加密算法是DES)。如果加密和解密使用的是两个不同的密钥,那么这就是“非对称密钥加解密”(最常用的非对称加密算法是RSA)。这两个不同的密钥一个叫作公开密钥(publickey)另一个叫私有密钥(privatekey),公开密钥对外公开,任何人均可获取,而私有密钥则由自己保存,其实公钥和私钥并没有什么不同之处,公钥之所以成为公钥是因为它会被公开出来,产生任意份拷贝,供任何人获取,而只有服务主机持有唯一的一份私钥,从这种分发模式上看,我们不难看出其中的用意,这种分发模式实际上是Web站点多客户端(浏览器)与单一服务器的网络拓扑所决定的,多客户端意味着密钥能被复制和公开获取,单一服务器意味着密钥被严格控制,只能由本服务器持有,这实际上也是后面要提到的之所以能通过数据证书确定信任主机的重要原因之一。如果我们跳出web站点的拓扑环境,其实就没有什么公钥与私钥之分了,比如,对于那些使用以密钥为身份认证的SSH主机,往往是为每一个用户单独生成一个私钥分发给他们自己保存,SSH主机会保存一份公钥,公钥私钥各有一份,都不会公开传播。
数字签名
数字签名是非对称密钥加密技术+数字摘要技术的结合。
数字签名技术是将信息摘要用发送者的私钥加密,和原文以及公钥一起传送给接收者。接收者只有用发送者的公钥才能解密被加密的信息摘要,然后接收者用相同的Hash函数对收到的原文产生一个信息摘要,与解密的信息摘要做比对。如果相同,则说明收到的信息是完整的,在传输过程中没有被修改;不同则说明信息被修改过,因此数字签名能保证信息的完整性。数字证书
从上面的描述不知是否看出了数字签名的漏洞?漏洞确实是存在的,现在安卓的发布的app都需要签名,但是仍然阻止不了恶意用户更改app,在其中注入广告信息。主要原因是因为广大的用户不知道发布者的签名公匙,在发布过程中被恶意用户重现签名。这里的问题就是:对于接受方来说,它怎么能确定它所得到的公钥一定是从目标主机那里发布的,而且没有被篡改过呢?亦或者请求的目标主机本本身就从事窃取用户信息的不正当行为呢?这时候,我们需要有一个权威的值得信赖的第三方机构(一般是由政府审核并授权的机构)来统一对外发放主机机构的公钥,只要请求方这种机构获取公钥,就避免了上述问题的发生。这种机构被称为证书权威机构(Certificate Authority, CA),它们所发放的包含主机机构名称、公钥在内的文件就是人们所说的“数字证书”。
安卓app包中CERT.RSA包含了数字签名以及开发者的数字证书。CERT.RSA里的数字签名是指对CERT.SF的摘要采用私钥加密后的数据,Android系统安装apk时会对CERT.SF计算摘要,然后使用CERT.RSA里的公钥对CERT.RSA里的数字签名解密得到一个摘要,比较这两个摘要便可知道该apk是否有正确的签名,也就说如果其他人修改了apk并没有重新签名是会被检查出来的,所以只要是开发者的数字证书不是由权威机构发布的,那么app都有可能被恶意更改。
安卓的rom和app都会签名,但这里只介绍app的签名,rom的签名大同小异。
APK签名分析
签名的app用zip解压后在META-INF文件夹,可以看到3个文件:CERT.RSA,CERT.SF,MANIFEST.MF。其中CERT.RSA包含了公钥信息和发布机构信息;MANIFEST.MF中保存了除自身以外所有其他文件的SHA-1并进行base64编码后的值(注意:对于xml等文本格式的资源文件,系统先将这些文本文件编译成二进制文件,再获取二进制文件的SHA-1值并进行base64编码);CERT.SF的生成过程分两步:(1)读取MANIFEST.MF文件的SHA-1,然后将该SHA-1值base64编码后保存为SHA1-Digest-Manifest,所以与MANIFEST.MF文件相比,CERT.SF会多出一个SHA1-Digest-Manifest值,(2)然后再逐个读取MANIFEST.MF中每个资源文件的Name和SHA1-Digest值再添加2个空行后进行SHA-1,将该SHA-1值进行base64编码后依次写到CERT.SF中。下面小弟就举个栗子,说明MANIFEST.MF文件某个段落生成的过程。
- MANIFEST.MF文件某个段落生成的过程
Manifest-Version: 1.0
Created-By: 1.0 (Android)
Name: res/drawable-xhdpi/ic_launcher.png
SHA1-Digest: AfPh3OJoypH966MludSW6f1RHg4=
Name: res/menu/main.xml
SHA1-Digest: CMgiSm8dKeJbmAhowhxFsmNAHqg=
Name: AndroidManifest.xml
SHA1-Digest: y/7aEvobcE5aOBCKiY3/s2FZW+E=
Name: res/drawable-mdpi/ic_launcher.png
SHA1-Digest: RRxOSvpmhVfCwiprVV/wZlaqQpw=
Name: res/drawable-hdpi/ic_launcher.png SHA1-Digest: Nq8q3HeTluE5JNCBpVvNy3BXtJI= Name: res/layout/activity_main.xml SHA1-Digest: gRYsZVPTn8/DcnOy9mOzi9B6Xkk= Name: resources.arsc SHA1-Digest: gYQMdJlPGAPJ5/SozzlgV2Wq57c= Name: classes.dex SHA1-Digest: ILBp4DCBpOWwSXlwBHXMWy5qvuk= Name: res/drawable-xxhdpi/ic_launcher.png SHA1-Digest: GVIfdEOBv4gEny2T1jDhGGsZOBo=
我们主要看
Name: res/menu/main.xml
SHA1-Digest: CMgiSm8dKeJbmAhowhxFsmNAHqg=
SHA1-Digest是如何计算出来的,首先请百度下载HashTab插件,再在解压的apk目录找到res/menu/main.xml文件,查看属性
copy SHA-1的值08C8224A6F1D29E25B980868C21C45B263401EA8,打开在线base64编码输入值后点击convert,可以看到结果跟SHA1-Digest的值完全一致。
- CERT.SF段落生成过程
Signature-Version: 1.0
Created-By: 1.0 (Android)
SHA1-Digest-Manifest: ztxNXNSX0ROqR+h+Y/PEbSQlYV4=
Name: res/menu/main.xml
SHA1-Digest: phti43JrVCPvFyPVD+A0aH/F804=
Name: res/drawable-xhdpi/ic_launcher.png
SHA1-Digest: cIga++hy5wqjHl9IHSfbg8tqCug=
Name: AndroidManifest.xml
SHA1-Digest: iN6TTbqeTdXxrIq1mW2FGVdURKI=
Name: res/drawable-mdpi/ic_launcher.png
SHA1-Digest: VY7kOF8E3rn8EUTvQC/DcBEN6kQ=
Name: res/drawable-hdpi/ic_launcher.png SHA1-Digest: stS7pUucSY0GgAVoESyO3Y7SanU= Name: res/layout/activity_main.xml SHA1-Digest: 05FHZzNfWjGzOvVdzEN/0nKgob8= Name: resources.arsc SHA1-Digest: CEgnN+t+o8xQ29Z4eYnCBCE2P4k= Name: classes.dex SHA1-Digest: fPUytYQBSWwxJ+W4gUUivC8/Ez4= Name: res/drawable-xxhdpi/ic_launcher.png SHA1-Digest: KKqaLh/DVvFp+v1KoaDw7xETvrI=
下面我们主要看
Name: res/menu/main.xml
SHA1-Digest: phti43JrVCPvFyPVD+A0aH/F804=
是如何生成的。
将
Name: res/menu/main.xml
copy到新建文本中,并再添加两个空行,如下如所示:
SHA1-Digest: CMgiSm8dKeJbmAhowhxFsmNAHqg=
,
保存后查看HashTab属性
如下图所示:
copy sha-1值在线base64编码,如下图所示:
从结果可以看出完全和CERT.SF相关段落一致。
APK签名分析源码分析
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的实现过程:
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 {
//...
}
}
main函数会生成公钥对象和私钥对象,并调用addDigestsToManifest函数生成清单对象Manifest后,再调用signFile签名。上面已经较详细的介绍了Manifest文件的结构,Manifest文件里用空行分割成多个段,每个段由多个属性组成,第一个段的属性集合称为主属性集合,其它段称为普通属性集合,普通属性集合一般会有Name属性,作为该属性集合所在段的名字。Android的manifeset文件会为zip的所有文件各自建立一个段,这个段的Name属性的值就是该文件的path+文件名,另外还有一个SHA1-Digest的属性,该属性的值是对文件的sha1摘要用base64编码得到的字符串。
从上面的Manifest示例可以看出:
Manifest-Version属性和Created-By所在的段就是主属性集合,其它属性集合就是普通属性集合,这些普通属性集合都有Name属性,作为该段的名字。
addDigestsToManifest源代码分析:
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;
}
signFile源码分析:
先将inputjar的所有文件拷贝至outputjar,然后生成Manifest.MF,CERT.SF和CERT.RSA
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);
}
writeSignatureFile源码分析:
生成CERT.SF,其实是对MANIFEST.MF的各个段再次计算Sha1摘要得到CERT.SF。
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');
}
}
writeSignatureBlock源码分析:
采用SHA1withRSA算法对CERT.SF计算摘要并加密得到数字签名,使用的私钥是privateKey,然后将数字签名和公钥一起存入CERT.RSA。这里使用了开源库bouncycastle来签名。
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());
}