APK签名原理

网上已有多篇分析签名的类似文章,但是都有一个共同的问题,就是概念混乱,混乱的一塌糊涂。
在了解APK签名原理之前,首先澄清几个概念:


消息摘要 -Message Digest

简称摘要,不是签名,网上几乎所有APK签名分析的文章都混淆了这两个概念。

摘要的链接http://en.wikipedia.org/wiki/Message_digest

简单的说,消息摘要就是在消息数据上,执行一个单向的Hash函数,生成一个固定长度的Hash值,这个Hash值即是消息摘要,也称为数字指纹:

消息摘要有以下特点:

1. 通过摘要无法推算得出消息本身

2. 如果修改了消息,那么摘要一定会变化(实际上,由于长明文生成短摘要的Hash必然会产生碰撞),所以,这句话并不准确,我们可以改为:很难找到一种模式,修改了消息,而它的摘要不会变化。


消息摘要的这种特性,很适合来验证数据的完整性,比如在网络传输过程中,下载一个大文件BigFile,我们会同时从网络下载BigFile和BigFile.md5,BigFile.md5保存BigFile的摘要,我们在本地生成BigFile的消息摘要,和BigFile.md5比较,如果内容相同,则表示下载过程正确。

注意,消息摘要只能保证消息的完整性,并不能保证消息的不可篡改性。


MD5/SHA-0 SHA-1

这些都是摘要生成算法,和签名没有半毛钱关系。如果非要说他们和签名有关系,那就是签名是要借助于摘要技术。


数字签名 - Signature

数字签名百度百科对数字签名有非常清楚的介绍。我这里再罗嗦一下,不懂的去看百度百科。

数字签名,就是信息的发送者,用自己的私钥对消息摘要加密,产生一个字符串;加密算法确保别人无法伪造这段字符串,这段数字串也是对信息的发送者(发送信息)真实性的一个有效证明。

数字签名是 非对称加密技术 + 数字摘要技术 的结合。

数字签名技术,是将信息摘要用发送者的私钥加密所产生的签名,与原文一起传送给接收者。接收者只有用发送者的公钥,才能解密被加密的信息摘要,然后接收者用相同的Hash函数,对收到的原文产生一个信息摘要,与解密的信息摘要做比对。如果相同,则说明收到的信息是完整的,在传输过程中没有被修改;否则,则说明信息被修改过,因此,数字签名能保证信息的完整性。并且,由于只有发送者才有加密摘要的私钥,所以,我们可以确定信息一定是发送者发送的。


数字证书 - Certificate

数字证书是一个经证书授权中心数字签名的,包含公钥拥有者信息以及公钥的文件。CERT.RSA包含了一个数字签名以及一个数字证书。

需要注意的是Android APK中的CERT.RSA证书是自签名的,并不需要这个证书是第三方权威机构发布或者认证的,用户可以在本地机器自行生成这个自签名证书。


APK签名过程分析

摘要和签名的概念清楚后,我们就可以分析APK 签名过程了。Android提供了APK的签名工具signapk ,使用方法如下:

[html] view plain copy 在CODE上查看代码片 派生到我的代码片
  1. signapk[-w]publickey.x509[.pem]privatekey.pk8input.jaroutput.jar

publickey.x509.pem包含证书和证书链,证书和证书链中包含了公钥和加密算法;privatekey.pk8是私钥;input.jar是需要签名的jar;output.jar是签名结果


signapk的实现在android/build/tools/signapk/SignApk.java中,主函数main实现如下

[java] view plain copy 在CODE上查看代码片 派生到我的代码片
  1. publicstaticvoidmain(String[]args){
  2. if(args.length!=4&&args.length!=5){
  3. System.err.println("Usage:signapk[-w]"+
  4. "publickey.x509[.pem]privatekey.pk8"+
  5. "input.jaroutput.jar");
  6. System.exit(2);
  7. }
  8. sBouncyCastleProvider=newBouncyCastleProvider();
  9. Security.addProvider(sBouncyCastleProvider);
  10. booleansignWholeFile=false;
  11. intargstart=0;
  12. if(args[0].equals("-w")){
  13. signWholeFile=true;
  14. argstart=1;
  15. }
  16. JarFileinputJar=null;
  17. JarOutputStreamoutputJar=null;
  18. FileOutputStreamoutputFile=null;
  19. try{
  20. FilepublicKeyFile=newFile(args[argstart+0]);
  21. X509CertificatepublicKey=readPublicKey(publicKeyFile);
  22. //Assumethecertificateisvalidforatleastanhour.
  23. longtimestamp=publicKey.getNotBefore().getTime()+3600L*1000;
  24. PrivateKeyprivateKey=readPrivateKey(newFile(args[argstart+1]));
  25. inputJar=newJarFile(newFile(args[argstart+2]),false);//Don'tverify.
  26. OutputStreamoutputStream=null;
  27. if(signWholeFile){
  28. outputStream=newByteArrayOutputStream();
  29. }else{
  30. outputStream=outputFile=newFileOutputStream(args[argstart+3]);
  31. }
  32. outputJar=newJarOutputStream(outputStream);
  33. //Forsigning.apks,usethemaximumcompressiontomake
  34. //themassmallaspossible(sincetheyliveforeveron
  35. //thesystempartition).ForOTApackages,usethe
  36. //defaultcompressionlevel,whichismuchmuchfaster
  37. //andproducesoutputthatisonlyatinybitlarger
  38. //(~0.1%onfullOTApackagesItested).
  39. if(!signWholeFile){
  40. outputJar.setLevel(9);
  41. }
  42. JarEntryje;
  43. Manifestmanifest=addDigestsToManifest(inputJar);
  44. //Everythingelse
  45. copyFiles(manifest,inputJar,outputJar,timestamp);
  46. //otacert
  47. if(signWholeFile){
  48. addOtacert(outputJar,publicKeyFile,timestamp,manifest);
  49. }
  50. //MANIFEST.MF
  51. je=newJarEntry(JarFile.MANIFEST_NAME);
  52. je.setTime(timestamp);
  53. outputJar.putNextEntry(je);
  54. manifest.write(outputJar);
  55. //CERT.SF
  56. je=newJarEntry(CERT_SF_NAME);
  57. je.setTime(timestamp);
  58. outputJar.putNextEntry(je);
  59. ByteArrayOutputStreambaos=newByteArrayOutputStream();
  60. writeSignatureFile(manifest,baos);
  61. byte[]signedData=baos.toByteArray();
  62. outputJar.write(signedData);
  63. //CERT.RSA
  64. je=newJarEntry(CERT_RSA_NAME);
  65. je.setTime(timestamp);
  66. outputJar.putNextEntry(je);
  67. writeSignatureBlock(newCMSProcessableByteArray(signedData),
  68. publicKey,privateKey,outputJar);
  69. outputJar.close();
  70. outputJar=null;
  71. outputStream.flush();
  72. if(signWholeFile){
  73. outputFile=newFileOutputStream(args[argstart+3]);
  74. signWholeOutputFile(((ByteArrayOutputStream)outputStream).toByteArray(),
  75. outputFile,publicKey,privateKey);
  76. }
  77. }catch(Exceptione){
  78. e.printStackTrace();
  79. System.exit(1);
  80. }finally{
  81. try{
  82. if(inputJar!=null)inputJar.close();
  83. if(outputFile!=null)outputFile.close();
  84. }catch(IOExceptione){
  85. e.printStackTrace();
  86. System.exit(1);
  87. }
  88. }
  89. }


生成MAINFEST.MF文件

[java] view plain copy 在CODE上查看代码片 派生到我的代码片
  1. Manifestmanifest=addDigestsToManifest(inputJar);

遍历inputJar中的每一个文件,利用SHA1算法生成这些文件的信息摘要。

[java] view plain copy 在CODE上查看代码片 派生到我的代码片
  1. //MANIFEST.MF
  2. je=newJarEntry(JarFile.MANIFEST_NAME);
  3. je.setTime(timestamp);
  4. outputJar.putNextEntry(je);
  5. manifest.write(outputJar);

生成MAINFEST.MF文件,这个文件包含了inputJar包内所有文件内容的摘要值。注意,不会生成下面三个文件的摘要值:MANIFEST.MF,CERT.SF和CERT.RSA


生成CERT.SF

[java] view plain copy 在CODE上查看代码片 派生到我的代码片
  1. //CERT.SF
  2. je=newJarEntry(CERT_SF_NAME);
  3. je.setTime(timestamp);
  4. outputJar.putNextEntry(je);
  5. ByteArrayOutputStreambaos=newByteArrayOutputStream();
  6. writeSignatureFile(manifest,baos);
  7. byte[]signedData=baos.toByteArray();
  8. outputJar.write(signedData);

虽然writeSignatureFile字面上看起来是写签名文件,但是CERT.SF的生成和私钥没有一分钱的关系,实际上也不应该有一分钱的关系,这个文件自然不保存任何签名内容。

CERT.SF中保存的是MANIFEST.MF的摘要值,以及MANIFEST.MF中每一个摘要项的摘要值。恕我愚顿,没搞清楚为什么要引入CERT.SF,实际上我觉得签名完全可以用MANIFEST.MF生成。

signedData就是CERT.SF的内容,这个信息摘要在制作签名的时候会用到。


生成CERT.RSA

CERT.RSA这个文件保存了签名公钥证书。签名的生成一定会有私钥参与,签名用到的信息摘要就是CERT.SF内容。

[java] view plain copy 在CODE上查看代码片 派生到我的代码片
  1. //CERT.RSA
  2. je=newJarEntry(CERT_RSA_NAME);
  3. je.setTime(timestamp);
  4. outputJar.putNextEntry(je);
  5. writeSignatureBlock(newCMSProcessableByteArray(signedData),
  6. publicKey,privateKey,outputJar);

signedData这个数据会作为签名用到的摘要,writeSignatureBlock函数用privateKey对signedData加密生成签名,然后把签名和公钥证书一起保存到CERT.RSA中。

[java] view plain copy 在CODE上查看代码片 派生到我的代码片
  1. /**Signdataandwritethedigitalsignatureto'out'.*/
  2. privatestaticvoidwriteSignatureBlock(
  3. CMSTypedDatadata,X509CertificatepublicKey,PrivateKeyprivateKey,
  4. OutputStreamout)
  5. throwsIOException,
  6. CertificateEncodingException,
  7. OperatorCreationException,
  8. CMSException{
  9. ArrayList<X509Certificate>certList=newArrayList<X509Certificate>(1);
  10. certList.add(publicKey);
  11. JcaCertStorecerts=newJcaCertStore(certList);
  12. CMSSignedDataGeneratorgen=newCMSSignedDataGenerator();
  13. ContentSignersha1Signer=newJcaContentSignerBuilder("SHA1withRSA")
  14. .setProvider(sBouncyCastleProvider)
  15. .build(privateKey);
  16. gen.addSignerInfoGenerator(
  17. newJcaSignerInfoGeneratorBuilder(
  18. newJcaDigestCalculatorProviderBuilder()
  19. .setProvider(sBouncyCastleProvider)
  20. .build())
  21. .setDirectSignature(true)
  22. .build(sha1Signer,publicKey));
  23. gen.addCertificates(certs);
  24. CMSSignedDatasigData=gen.generate(data,false);
  25. ASN1InputStreamasn1=newASN1InputStream(sigData.getEncoded());
  26. DEROutputStreamdos=newDEROutputStream(out);
  27. dos.writeObject(asn1.readObject());
  28. }

翻译下这个函数的注释:对参数data进行签名,然后把生成的数字签名写入参数out中

@data是生成签名的摘要

@publicKey; 是签名用到的私钥对应的证书

@privateKey: 是签名时用到的私钥

@out: 输出文件,也就是CERT.RSA


最终保存在CERT.RSA中的是CERT.SF的数字签名,签名使用privateKey生成的,签名算法会在publicKey中定义。同时还会把publicKey存放在CERT.RSA中,也就是说CERT.RSA包含了签名和签名用到的证书。并且要求这个证书是自签名的。


转自:http://blog.youkuaiyun.com/kickxxx/article/details/18252881
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值