android签名机制

非对称加密

android签名机制_第1张图片android签名机制_第2张图片

android签名

  1. 数据摘要

    • 对一个数据源,进行一个算法之后得到的一个摘要,也叫数据指纹。
    • 数据摘要算法:能产生特殊输出格式的算法,原理是根据一定的运算规则对原始数据进行某种形式的信息提取。给提取的信息就是原始数据的消息摘要。
    • 著名的摘要算法有RSA公司的MD5算法,SHA-1算法及其大量变体。
    • 主要特点
      • 长度固定,MD5 128比特位,SHA-1 160比特位
      • 相同的输入产生相同输入,
      • 不可逆
  2. 签名文件和证书

    成对出现,不可分离,消息发送者,除了要要发送原始消息。
    1)对要发送的原始消息提取消息摘要
    2)对提取的信息摘要用自己的私钥加密
    就得到了原始信息的数字签名
    接收者:
    1)原始消息提取消息摘要
    2)对数字签名使用发送者的公钥解密
    3)比较是否一致。
    数字签名,就是发送者才能产生别人无法伪造的一段数字串,是对发送消息真实性的一个证明.

    这里有个前提:接收者实现必须得到正确的公钥,如果保证公钥安全可信?就靠数字证书来解决了
    数字证书 包含以下内容:

    • 证书的发布机构 Issur
    • 有效期 validity
    • 发送方的公钥
    • 证书的所有者 subject
    • 数字签名使用的算法
    • 数字签名

    数字证书也用到了数字签名技术,只不过要签名的内容是发送方的公钥,及其他信息。数字证书的签名者,一定是有一定公信力的机构。数字证书主要用来解决公钥的安全发放问题。

  3. jarsign和signapk工具
    jarsign是Java本生自带的一个工具,他可以对jar进行签名的。而signapk是后面专门为了Android应用程序apk进行签名的工具,他们两的签名算法没什么区别,主要是签名时使用的文件不一样,这个就要引出第三个问题了

  4. keystore文件和pk8,x509.pem区别
    我们上面了解到了jarsign和signapk两个工具都可以进行Android中的签名,那么他们的区别在于签名时使用的文件不一样

jarsign工具签名时使用的是keystore文件

signapk工具签名时使用的是pk8,x509.pem文件

  1. 手动签名

    • 使用keytool和jarsigner来进行签名,需要创建keystore文件,
    • 使用signapk签名
      java -jar signapk.jar .testkey.x509.pem testkey.pk8 debug.apk debug.sig.apk

    这里需要两个文件:.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

一、常用的方法,andorid studio

配置key签名,
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
generate siged apk…窗口生成签名apk,实质上运行的是
		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:

二、使用Android.mk配置编译成签名apk:

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)
  1. 将apk放入.mk同目录(命名为app.apk),配置好sdk编译环境后执行mm指令编译,签名apk生成在:out/target/product/xxxx/system/priv-app/app/app.apk 。

注:LOCAL_CERTIFICATE := platform 表示使用系统签名
  LOCAL_DEX_PREOPT := false    不提前优化,无oat文件

三、使用Android SDK中的签名工具给apk签名:

系统签名,机制后续研究

上述使用的都是apksign.jar.

下述有误,还需要了解学习补充完善,

Android中签名的两个工具:jarsign和signapk,jarsign是Java本生自带的一个工具,他可以对jar进行签名的。而signapk是后面专门为了Android应用程序apk进行签名的工具,

jarsign工具签名时使用的是keystore文件,可以通过jdk/bin/下的keytool工具来创建自己的keystore

生成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

然后按回车:会要求输入刚才设置的密码,输入后按回车就开始签名了

signapk工具签名时使用的是pk8,x509.pem文件,
  1. 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。

  2. Android SDK中的签名工具为 signapk.jar,具体路径:out/host/linux-x86/framework/signapk.jar,签名指令如下:

    java -jar signapk.jar platform.x509.pem platform.pk8 old.apk new.apk

android签名机制

看看signapk的源码吧:

源码位置:com/android/signapk/sign.java

通过上面的签名时我们可以看到,Android签名apk之后,会有一个META-INF文件夹,这里有三个文件:

  • MANIFEST.MF
  • CERT.RSA
  • CERT.SF
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;
}





三个文件

  1. MANIFEST.MF中,计算出每个文件的摘要信息,然后用Base64编码存储。

  2. CERT.SF文件做了什么

    1. 计算这个MANIFEST.MF文件的整体SHA1值,再经过BASE64编码后,记录在CERT.SF主属性块(在文件头上)的“SHA1-Digest-Manifest”属性值值下

    2. 逐条计算MANIFEST.MF文件中每一个块的SHA1,并经过BASE64编码后,记录在CERT.SF中的同名块中,属性的名字是“SHA1-Digest

  3. CERT.RSA文件二进制文件,因为RSA文件加密了,所以我们需要用openssl命令才能查看其内容.就是把之前生成的 CERT.SF文件, 用私钥计算出签名, 然后将签名以及包含公钥信息的数字证书一同写入 CERT.RSA 中保存。CERT.RSA是一个满足PKCS7格式的文件。

安装应用时PackageManagerService会对APK进行签名检查,具体分为以下几步

  1. 读取CERT.RSA(证书信息包括公钥)、MANIFEST.MF、CERT.SF
  2. 通过公钥(‘CERT.RSA’)对CERT.SF解密,将解密结果和MANIFEST.MF进行比较,如果相同说明证书有效、MANIFEST.MF未被更改
  3. 对APK中所有文件内容分别进行Hash计算,将结果的BASE64编码和MANIFEST.MF里的相应内容进行比较,全部相同则APK的内容未被更改

#为什么要这么签名

上面我们就介绍了签名apk之后的三个文件的详细内容,那么下面来总结一下,Android中为何要用这种方式进行加密签名,这种方加密是不是最安全的呢?下面我们来分析一下,如果apk文件被篡改后会发生什么。

首先,如果你改变了apk包中的任何文件,那么在apk安装校验时,改变后的文件摘要信息与MANIFEST.MF的检验信息不同,于是验证失败,程序就不能成功安装。
其次,如果你对更改的过的文件相应的算出新的摘要值,然后更改MANIFEST.MF文件里面对应的属性值,那么必定与CERT.SF文件中算出的摘要值不一样,照样验证失败。
最后,如果你还不死心,继续计算MANIFEST.MF的摘要值,相应的更改CERT.SF里面的值,那么数字签名值必定与CERT.RSA文件中记录的不一样,还是失败。
那么能不能继续伪造数字签名呢?不可能,因为没有数字证书对应的私钥。
所以,如果要重新打包后的应用程序能再Android设备上安装,必须对其进行重签名。

从上面的分析可以得出,只要修改了Apk中的任何内容,就必须重新签名,不然会提示安装失败,当然这里不会分析,后面一篇文章会注重分析为何会提示安装失败。

你可能感兴趣的:(android,app)