在Android 系统中,所有安装到系统的应用程序都必有一个数字证书,这个数字证书就是这个应用的签名。此数字证书用于标识应用程序的作者和在应用程序之间建立信任关系,如果一个 permission的protectionLevel为signature,那么就只有那些跟该permission所在的程序拥有同一个数字证书的应 用程序才能取得该权限。这个数字证书并不需要权威的数字证书签名机构认证,它只是用来让应用程序包自我认证的。
为了保证每个应用程序开发商合法ID,防止部分开放商可能通过使用相同的Package Name来混淆替换已经安装的程序,冒充原来的应哟功能。我们需要对我们发布的APK文件进行唯一签名,保证我们每次发布的版本的一致性(如自动更新不会因为版本不一致而无法安装)。
概括起来有以下几点:
(1 ) 发送者的身份认证
(2)保证输入信息的完整性
(3)防止交易中的抵赖发生
创建key,需要用到 keytool,使用产生的key对apk签名用到的是jarsigner。
keytool -genkey -alias demo.keystore -keyalg RSA -validity 40000 -keystore demo.keystore
/*说明:-genkey 产生密钥
-alias demo.keystore 别名 demo.keystore
-keyalg RSA 使用RSA算法对签名加密
-validity 40000 有效期限4000天
-keystore demo.keystore */
jarsigner -verbose -keystore demo.keystore -signedjar demo_signed.apk demo.apk demo.keystore
说明:-verbose 输出签名的详细信息
-keystore demo.keystore 密钥库位置
-signedjar demor_signed.apk demo.apk demo.keystore 正式签名,三个参数中依次为签名后产生的文件demo_signed,要签名的文件demo.apk和密钥库demo.keystore.
查看密钥信息
keytool -printcert -v -file miyao.cer
keytool -list -keystore debug.keystore
java -jar signapk.jar platform.x509.pem platform.pk8 MyDemo.apk MyDemo_signed.apk
signapk的参数分别为公钥,私钥,需要签名的apk,签名后的apk
使用源码内置的签名可以获取系统的不同权限。
签名之后,可以用zipalign(压缩对齐)优化APK文件
zipalign -v 4 demo_signed.apk final.apk
使用andorid源码打包apk是使用signapk签名的,我们分析一下这个过程
程序遍历app包中的所有文件(entry),对非文件夹非签名文件的文件,逐个生成SHA1的数字签名信息,再用Base64进行编码。具体代码见这个方法:
private static Manifest addDigestsToManifest(JarFile jar)
/** Add the SHA1 of every file to the manifest, creating it if necessary. */ 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() && (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", new String(Base64.encode(md.digest()), "ASCII")); output.getEntries().put(name, attr); } } return output; }
之后将生成的签名写入MANIFEST.MF文件。关键代码如下:
private static void signFile(Manifest manifest, JarFile inputJar, X509Certificate[] publicKey, PrivateKey[] privateKey, JarOutputStream outputJar) throws Exception { // Assume the certificate is valid for at least an hour. long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000; JarEntry je; // Everything else copyFiles(manifest, inputJar, outputJar, timestamp); // MANIFEST.MF je = new JarEntry(JarFile.MANIFEST_NAME); je.setTime(timestamp); outputJar.putNextEntry(je); manifest.write(outputJar); int numKeys = publicKey.length; for (int k = 0; k < numKeys; ++k) { // CERT.SF / CERT#.SF je = new JarEntry(numKeys == 1 ? CERT_SF_NAME : (String.format(CERT_SF_MULTI_NAME, k))); je.setTime(timestamp); outputJar.putNextEntry(je); ByteArrayOutputStream baos = new ByteArrayOutputStream(); writeSignatureFile(manifest, baos); byte[] signedData = baos.toByteArray(); outputJar.write(signedData); // CERT.RSA / CERT#.RSA je = new JarEntry(numKeys == 1 ? CERT_RSA_NAME : (String.format(CERT_RSA_MULTI_NAME, k))); je.setTime(timestamp); outputJar.putNextEntry(je); writeSignatureBlock(new CMSProcessableByteArray(signedData), publicKey[k], privateKey[k], outputJar); } }
SHA1数字签名。简 单地说,它就是一种安全哈希算法,类似于MD5算法。它把任意长度的输入,通过散列算法变成固定长度的输出(这里我们称作“摘要信息”)。你不能仅通过这 个摘要信息复原原来的信息。另外,它保证不同信息的摘要信息彼此不同。因此,如果你改变了apk包中的文件,那么在apk安装校验时,改变后的文件摘要信息与MANIFEST.MF的检验信息不同,于是程序就不能成功安装。
对前一步生成的Manifest,使用SHA1-RSA算法,用私钥进行签名。关键代码如下:
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"); // Digest of the entire manifest manifest.write(print); print.flush(); main.putValue("SHA1-Digest-Manifest", new String(Base64.encode(md.digest()), "ASCII")); 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'); } }
RSA是一种非对称加密算法。用私钥通过RSA算法对摘要信息进行加密。在安装时只能使用公钥才能解密它。解密之后,将它与未加密的摘要信息进行对比,如果相符,则表明内容没有被异常修改。
生成MANIFEST.MF没有使用密钥信息,生成CERT.SF文件使用了私钥文件。那么我们可以很容易猜测到,CERT.RSA文件的生成肯定和公钥相关。
CERT.RSA文件中保存了公钥、所采用的加密算法等信息。
生成CERT.RSA文件:核心代码如下:
// CERT.RSA / CERT#.RSA je = new JarEntry(numKeys == 1 ? CERT_RSA_NAME : (String.format(CERT_RSA_MULTI_NAME, k))); je.setTime(timestamp); outputJar.putNextEntry(je); writeSignatureBlock(new CMSProcessableByteArray(signedData), publicKey[k], privateKey[k], outputJar);
分根据APK包的签名流程,我们可以意识到:
1、 Android签名机制其实是对APK包完整性和发布机构唯一性的一种校验机制。
2、 Android签名机制不能阻止APK包被修改,但修改后的再签名无法与原先的签名保持一致。(拥有私钥的情况除外)。
3、 APK包加密的公钥就打包在APK包内,且不同的私钥对应不同的公钥。换句话言之,不同的私钥签名的APK公钥也必不相同。所以我们可以根据公钥的对比,来判断私钥是否一致。
4、签名是不对META-INF的文件校验的,可以利用这个漏洞做一些事情。比如快速动态做渠道标记
1) App升级。 使用相同签名的升级软件可以正常覆盖老版本的软件,否则系统比较发现新版本的签名证书和老版本的签名证书不一致,不会允许新版本安装成功的。
2) App模块化。android系统允许多个安装包的App运行在同一个进程中,如果运行在同一个进程中,则他们相当于同一个App,但是你可以单独对他们升级更新,这是一种App级别的模块化思路。
3) 允许代码和数据共享。android中提供了一个基于签名的Permission标签。通过允许的设置,我们可以实现对不同App之间的访问和共享
在app的AndroidManifest.xml中加上 android:sharedUserId="android.uid.system"然后使用目标系统的platform密钥来重新给apk文件签名。这首先找到密钥文件,在Android源码目录中的位置 是“build\target\product\security”,下面的platform.pk8和platform.x509.pem两个文件。然 后用Android提供的Signapk工具来签名,signapk的源代码是在“build\tools\signapk”下,用法为“signapk platform.x509.pem platform.pk8 input.apk output.apk”,文件名最好使用绝对路径防止找不到,也可以修改源代码直接使用。这样修改后的应用就会获取系统权限。
安装apk时,通过CERT.RSA查找公钥和算法,并对CERT.SF进行解密和签名验证,确认MANIFEST.MF,最终对每个文件签名校验。
升级时,android也会进行签名验证。如果遇到以下情况,都不能完成升级:
1) 两个应用,名字相同,签名不同
2) 升级时前一版本签名,后一版本没签名。
3) 升级时前一版本为DEBUG签名,后一个为自定义签名。
4) 升级时前一版本为Android源码中的签名,后一个为DEBUG签名或自定义签名。
5) 安装未签名的程序。
6) 安装升级已过有效期的程序。