网上已有多篇分析签名的类似文章,但是都有一个共同的问题,就是概念混乱,混乱的一塌糊涂。
在了解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 ,使用方法如下:
- 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实现如下
- publicstaticvoidmain(String[]args){
- if(args.length!=4&&args.length!=5){
- System.err.println("Usage:signapk[-w]"+
- "publickey.x509[.pem]privatekey.pk8"+
- "input.jaroutput.jar");
- System.exit(2);
- }
- sBouncyCastleProvider=newBouncyCastleProvider();
- Security.addProvider(sBouncyCastleProvider);
- booleansignWholeFile=false;
- intargstart=0;
- if(args[0].equals("-w")){
- signWholeFile=true;
- argstart=1;
- }
- JarFileinputJar=null;
- JarOutputStreamoutputJar=null;
- FileOutputStreamoutputFile=null;
- try{
- FilepublicKeyFile=newFile(args[argstart+0]);
- X509CertificatepublicKey=readPublicKey(publicKeyFile);
- //Assumethecertificateisvalidforatleastanhour.
- longtimestamp=publicKey.getNotBefore().getTime()+3600L*1000;
- PrivateKeyprivateKey=readPrivateKey(newFile(args[argstart+1]));
- inputJar=newJarFile(newFile(args[argstart+2]),false);//Don'tverify.
- OutputStreamoutputStream=null;
- if(signWholeFile){
- outputStream=newByteArrayOutputStream();
- }else{
- outputStream=outputFile=newFileOutputStream(args[argstart+3]);
- }
- outputJar=newJarOutputStream(outputStream);
- //Forsigning.apks,usethemaximumcompressiontomake
- //themassmallaspossible(sincetheyliveforeveron
- //thesystempartition).ForOTApackages,usethe
- //defaultcompressionlevel,whichismuchmuchfaster
- //andproducesoutputthatisonlyatinybitlarger
- //(~0.1%onfullOTApackagesItested).
- if(!signWholeFile){
- outputJar.setLevel(9);
- }
- JarEntryje;
- Manifestmanifest=addDigestsToManifest(inputJar);
- //Everythingelse
- copyFiles(manifest,inputJar,outputJar,timestamp);
- //otacert
- if(signWholeFile){
- addOtacert(outputJar,publicKeyFile,timestamp,manifest);
- }
- //MANIFEST.MF
- je=newJarEntry(JarFile.MANIFEST_NAME);
- je.setTime(timestamp);
- outputJar.putNextEntry(je);
- manifest.write(outputJar);
- //CERT.SF
- je=newJarEntry(CERT_SF_NAME);
- je.setTime(timestamp);
- outputJar.putNextEntry(je);
- ByteArrayOutputStreambaos=newByteArrayOutputStream();
- writeSignatureFile(manifest,baos);
- byte[]signedData=baos.toByteArray();
- outputJar.write(signedData);
- //CERT.RSA
- je=newJarEntry(CERT_RSA_NAME);
- je.setTime(timestamp);
- outputJar.putNextEntry(je);
- writeSignatureBlock(newCMSProcessableByteArray(signedData),
- publicKey,privateKey,outputJar);
- outputJar.close();
- outputJar=null;
- outputStream.flush();
- if(signWholeFile){
- outputFile=newFileOutputStream(args[argstart+3]);
- signWholeOutputFile(((ByteArrayOutputStream)outputStream).toByteArray(),
- outputFile,publicKey,privateKey);
- }
- }catch(Exceptione){
- e.printStackTrace();
- System.exit(1);
- }finally{
- try{
- if(inputJar!=null)inputJar.close();
- if(outputFile!=null)outputFile.close();
- }catch(IOExceptione){
- e.printStackTrace();
- System.exit(1);
- }
- }
- }
生成MAINFEST.MF文件
- Manifestmanifest=addDigestsToManifest(inputJar);
遍历inputJar中的每一个文件,利用SHA1算法生成这些文件的信息摘要。
- //MANIFEST.MF
- je=newJarEntry(JarFile.MANIFEST_NAME);
- je.setTime(timestamp);
- outputJar.putNextEntry(je);
- manifest.write(outputJar);
生成MAINFEST.MF文件,这个文件包含了inputJar包内所有文件内容的摘要值。注意,不会生成下面三个文件的摘要值:MANIFEST.MF,CERT.SF和CERT.RSA
生成CERT.SF
- //CERT.SF
- je=newJarEntry(CERT_SF_NAME);
- je.setTime(timestamp);
- outputJar.putNextEntry(je);
- ByteArrayOutputStreambaos=newByteArrayOutputStream();
- writeSignatureFile(manifest,baos);
- byte[]signedData=baos.toByteArray();
- 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内容。
- //CERT.RSA
- je=newJarEntry(CERT_RSA_NAME);
- je.setTime(timestamp);
- outputJar.putNextEntry(je);
- writeSignatureBlock(newCMSProcessableByteArray(signedData),
- publicKey,privateKey,outputJar);
signedData这个数据会作为签名用到的摘要,writeSignatureBlock函数用privateKey对signedData加密生成签名,然后把签名和公钥证书一起保存到CERT.RSA中。
- /**Signdataandwritethedigitalsignatureto'out'.*/
- privatestaticvoidwriteSignatureBlock(
- CMSTypedDatadata,X509CertificatepublicKey,PrivateKeyprivateKey,
- OutputStreamout)
- throwsIOException,
- CertificateEncodingException,
- OperatorCreationException,
- CMSException{
- ArrayList<X509Certificate>certList=newArrayList<X509Certificate>(1);
- certList.add(publicKey);
- JcaCertStorecerts=newJcaCertStore(certList);
- CMSSignedDataGeneratorgen=newCMSSignedDataGenerator();
- ContentSignersha1Signer=newJcaContentSignerBuilder("SHA1withRSA")
- .setProvider(sBouncyCastleProvider)
- .build(privateKey);
- gen.addSignerInfoGenerator(
- newJcaSignerInfoGeneratorBuilder(
- newJcaDigestCalculatorProviderBuilder()
- .setProvider(sBouncyCastleProvider)
- .build())
- .setDirectSignature(true)
- .build(sha1Signer,publicKey));
- gen.addCertificates(certs);
- CMSSignedDatasigData=gen.generate(data,false);
- ASN1InputStreamasn1=newASN1InputStream(sigData.getEncoded());
- DEROutputStreamdos=newDEROutputStream(out);
- dos.writeObject(asn1.readObject());
- }
翻译下这个函数的注释:对参数data进行签名,然后把生成的数字签名写入参数out中
@data是生成签名的摘要
@publicKey; 是签名用到的私钥对应的证书
@privateKey: 是签名时用到的私钥
@out: 输出文件,也就是CERT.RSA
最终保存在CERT.RSA中的是CERT.SF的数字签名,签名使用privateKey生成的,签名算法会在publicKey中定义。同时还会把publicKey存放在CERT.RSA中,也就是说CERT.RSA包含了签名和签名用到的证书。并且要求这个证书是自签名的。