数据摘要
签名文件和证书
成对出现,不可分离,消息发送者,除了要要发送原始消息。
1)对要发送的原始消息提取消息摘要
2)对提取的信息摘要用自己的私钥加密
就得到了原始信息的数字签名。
接收者:
1)原始消息提取消息摘要
2)对数字签名使用发送者的公钥解密
3)比较是否一致。
数字签名,就是发送者才能产生别人无法伪造的一段数字串,是对发送消息真实性的一个证明.
这里有个前提:接收者实现必须得到正确的公钥,如果保证公钥安全可信?就靠数字证书来解决了
数字证书 包含以下内容:
数字证书也用到了数字签名技术,只不过要签名的内容是发送方的公钥,及其他信息。数字证书的签名者,一定是有一定公信力的机构。数字证书主要用来解决公钥的安全发放问题。
jarsign和signapk工具
jarsign是Java本生自带的一个工具,他可以对jar进行签名的。而signapk是后面专门为了Android应用程序apk进行签名的工具,他们两的签名算法没什么区别,主要是签名时使用的文件不一样,这个就要引出第三个问题了
keystore文件和pk8,x509.pem区别
我们上面了解到了jarsign和signapk两个工具都可以进行Android中的签名,那么他们的区别在于签名时使用的文件不一样
jarsign工具签名时使用的是keystore文件
signapk工具签名时使用的是pk8,x509.pem文件
手动签名
这里需要两个文件:.pk8和.x509.pem这两个文件
pk8是私钥文件
x509.pem是含有公钥的文件
三、分析Android中签名流程机制
https://blog.csdn.net/jiangwei0910410003/article/details/50402000
https://www.cnblogs.com/blogs-of-lxl/p/9233285.html
1. 如果生成本地key,build->generate siged apk...->create new ...最终生成jks文件
2. 如果生成系统平台key,使用sdk的security文件,生成对应平台的key
./keytool-importkeypair -k [jks文件名] -p [jks的密码] -pk8 platform.pk8 -cert platform.x509.pem -alias [jks的别名]
如:./keytool-importkeypair -k ./SignDemo.jks -p 123456 -pk8 platform.pk8 -cert platform.x509.pem -alias SignDemo
java -jar apksigner.jar sign //执行签名操作
--ks 你的jks路径 //jks签名证书路径
--ks-key-alias 你的alias //生成jks时指定的alias
--ks-pass pass:你的密码 //KeyStore密码
--key-pass pass:你的密码 //签署者的密码,即生成jks时指定alias对应的密码
--out output.apk //输出路径
input.apk //被签名的apk
每次生成签名apk都要generate siged apk…很麻烦,还有简化方法,生成.jks文件后在app目录下的build.gradle配置,
1. File -> Project structure -> Signing:
2. File -> Project structure -> Flavors: phone signing config选择release
3. File -> Project structure -> Build Types:signing config选择release
通过以上配置可以看出build.gradle增加了如下配置,当然也可以手动输入配置信息
signingConfigs {
release {
keyAlias 'SignDemo'
keyPassword '123456'
storeFile file('E:/project/androidStudio/signAPK/SignDemo.jks')
storePassword '1234546'
}
}
//
buildTypes {
release { //生成release apk
zipAlignEnabled true //4字节对齐,减少运行内存消耗
minifyEnabled true //false = 关闭混淆
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
debug { //生成debug apk
zipAlignEnabled true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
然后通过Build Variants选择对应的项目版本,直接编译生成签名apk,省去之前每次选择key麻烦.
然而对一些开源项目,build.gradle配置key,密码信息就暴露了。可以把信息定义local.properties中,local.properties存储的是本地环境资源的一些相关信息,默认不加入版本管理,然后在build.gradle中引用起变量即可。
ndk.dir=C\:\\Users\\Administrator\\AppData\\Local\\Android\\Sdk\\ndk-bundle
sdk.dir=C\:\\Users\\Administrator\\AppData\\Local\\Android\\Sdk
keystore.path=E\:/project/androidStudio/signAPK/SignDemo.jks
keystore.password=123456
keystore.alias=SignDemo
keystore.alias_password=123456
build.gradle中引用其定义的资源
signingConfigs {
release {
//加载资源
Properties properties = new Properties()
InputStream inputStream = project.rootProject.file('local.properties').newDataInputStream()
properties.load(inputStream)
//读取文件
def sdkDir = properties.getProperty('keystore.path')
storeFile file(sdkDir)
//读取字段
def key_keyAlias = properties.getProperty('keystore.alias')
def key_keyPassword = properties.getProperty('keystore.password')
def key_storePassword = properties.getProperty('keystore.alias_password')
keyAlias key_keyAlias
keyPassword key_keyPassword
storePassword key_storePassword
}
}
////
buildTypes {
release { //生成release apk
zipAlignEnabled true //4字节对齐,减少运行内存消耗
minifyEnabled true //false = 关闭混淆
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
通过以上方法可以隐式的配置key,Project Structure中就不用配置key相关信息了。
平台签名的apk,如果 AndroidManifest.xml 中指定是 android:sharedUserId=“android.uid.system”,即为system app:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := app //要签名的apk名称
LOCAL_SRC_FILES := app.apk //apk文件
LOCAL_MODULE_CLASS := APPS
LOCAL_MODULE_SUFFIX := .apk
LOCAL_BUILT_MODULE_STEM := package.apk
LOCAL_CERTIFICATE := platform //系统签名
LOCAL_DEX_PREOPT := false
LOCAL_PRIVILEGED_MODULE := true
include $(BUILD_PREBUILT)
注:LOCAL_CERTIFICATE := platform 表示使用系统签名
LOCAL_DEX_PREOPT := false 不提前优化,无oat文件
系统签名,机制后续研究
上述使用的都是apksign.jar.
下述有误,还需要了解学习补充完善,
Android中签名的两个工具:jarsign和signapk,jarsign是Java本生自带的一个工具,他可以对jar进行签名的。而signapk是后面专门为了Android应用程序apk进行签名的工具,
生成keystore
keytool -genkey -v -keystore app.keystore -alias alias_name -keyalg RSA -validity 20000
-alias 后面跟的是别名这里是alias_name
-keyalg 是加密方式这里是RSA
-validity 是有效期这里是20000
-keystore 就是要生成的keystore的名称这里是app.keystore
jarsigner -verbose -keystore app.keystore -signedjar app_signed.apk app.apk alias_name
-keystore: keystore的名称
-signedjar app_signed.apk: 指定签名后生成的APK名称
app.apk: 目标APK
然后按回车:会要求输入刚才设置的密码,输入后按回车就开始签名了
Android源码的 build/target/product/security/ 目录下有 media.pk8、media.x509.pem、platform.pk8、platform.x509.pem、shared.pk8、shared.x509.pem、testkey.pk8、testkey.x509.pem等签名文件,不同的签名文件对应不同的权限,Android默认的签名文件为testkey.pk8、testkey.x509.pem。
Android SDK中的签名工具为 signapk.jar,具体路径:out/host/linux-x86/framework/signapk.jar,签名指令如下:
java -jar signapk.jar platform.x509.pem platform.pk8 old.apk new.apk
看看signapk的源码吧:
源码位置:com/android/signapk/sign.java
通过上面的签名时我们可以看到,Android签名apk之后,会有一个META-INF文件夹,这里有三个文件:
public static void main(String[] args) {
if (args.length != 4) {
System.err.println("Usage: signapk " +
"publickey.x509[.pem] privatekey.pk8 " +
"input.jar output.jar");
System.exit(2);
}
JarFile inputJar = null;
JarOutputStream outputJar = null;
try {
X509Certificate publicKey = readPublicKey(new File(args[0]));
// Assume the certificate is valid for at least an hour.
long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
PrivateKey privateKey = readPrivateKey(new File(args[1]));
inputJar = new JarFile(new File(args[2]), false); // Don't verify.
outputJar = new JarOutputStream(new FileOutputStream(args[3]));
outputJar.setLevel(9);
JarEntry je;
// MANIFEST.MF
Manifest manifest = addDigestsToManifest(inputJar);
je = new JarEntry(JarFile.MANIFEST_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
manifest.write(outputJar);
// CERT.SF
Signature signature = Signature.getInstance("SHA1withRSA");
signature.initSign(privateKey);
je = new JarEntry(CERT_SF_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
writeSignatureFile(manifest,
new SignatureOutputStream(outputJar, signature));
// CERT.RSA
je = new JarEntry(CERT_RSA_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
writeSignatureBlock(signature, publicKey, outputJar);
// Everything else
copyFiles(manifest, inputJar, outputJar, timestamp);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
} finally {
try {
if (inputJar != null) inputJar.close();
if (outputJar != null) outputJar.close();
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
}
//////////////////////
/** 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)");
}
BASE64Encoder base64 = new BASE64Encoder();
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) &&
(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", base64.encode(md.digest()));
output.getEntries().put(name, attr);
}
}
return output;
}
三个文件
MANIFEST.MF中,计算出每个文件的摘要信息,然后用Base64编码存储。
CERT.SF文件做了什么
计算这个MANIFEST.MF文件的整体SHA1值,再经过BASE64编码后,记录在CERT.SF主属性块(在文件头上)的“SHA1-Digest-Manifest”属性值值下
逐条计算MANIFEST.MF文件中每一个块的SHA1,并经过BASE64编码后,记录在CERT.SF中的同名块中,属性的名字是“SHA1-Digest
CERT.RSA文件二进制文件,因为RSA文件加密了,所以我们需要用openssl命令才能查看其内容.就是把之前生成的 CERT.SF文件, 用私钥计算出签名, 然后将签名以及包含公钥信息的数字证书一同写入 CERT.RSA 中保存。CERT.RSA是一个满足PKCS7格式的文件。
安装应用时PackageManagerService会对APK进行签名检查,具体分为以下几步
#为什么要这么签名
上面我们就介绍了签名apk之后的三个文件的详细内容,那么下面来总结一下,Android中为何要用这种方式进行加密签名,这种方加密是不是最安全的呢?下面我们来分析一下,如果apk文件被篡改后会发生什么。
首先,如果你改变了apk包中的任何文件,那么在apk安装校验时,改变后的文件摘要信息与MANIFEST.MF的检验信息不同,于是验证失败,程序就不能成功安装。
其次,如果你对更改的过的文件相应的算出新的摘要值,然后更改MANIFEST.MF文件里面对应的属性值,那么必定与CERT.SF文件中算出的摘要值不一样,照样验证失败。
最后,如果你还不死心,继续计算MANIFEST.MF的摘要值,相应的更改CERT.SF里面的值,那么数字签名值必定与CERT.RSA文件中记录的不一样,还是失败。
那么能不能继续伪造数字签名呢?不可能,因为没有数字证书对应的私钥。
所以,如果要重新打包后的应用程序能再Android设备上安装,必须对其进行重签名。
从上面的分析可以得出,只要修改了Apk中的任何内容,就必须重新签名,不然会提示安装失败,当然这里不会分析,后面一篇文章会注重分析为何会提示安装失败。