欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android数字签名机制和应用场景

程序员文章站 2022-07-14 23:49:53
...

目录

1 数字签名介绍

1.1 背景

1.2 机制

1.3 使用

1.3.1 ssh-******

1.3.2 keytool

2 Android使用数字签名的场景

2.1 未签名的APK VS 签名后的APK

2.2 应用层面:Android对APK的签名要求

2.2.1 Android拒绝安装没有签名的APK

2.2.2 Android不校验证书的合法性

2.2.3 当签名不匹配时,APK升级会失败

2.3 系统层面:Android基于数字签名的一些机制

2.3.1 四种不同类型的签名

2.3.2 应用的授权

2.3.3 SELinux根据签名给APK打标签(Labling)

3 总结


本文以Android 7.1的代码为蓝本,部分源码在不同Android版本上略有区别。

1 数字签名介绍

本节的内容概要如下:

  • 首先,介绍数字签名的产生背景,有什么用途以及怎么用,涉及到经常被拿出来比对的概念:数字签名(Digital Signature)电子签名(Electronic Signature)

  • 然后,介绍数字签名的运行机制,数据加密和认证的过程,涉及到的重要概念:**对(Key Pair),即私钥(Private Key)和公钥(Public Key)

  • 最后,通过介绍两个与数字签名相关的工具ssh-******keytool,一个**管理文件keystore,引申出几个重要的概念:证书(Certificate)证书链(Certificate Chain)证书认证机构(Certificate Authority)

1.1 背景

Digital Signature直译成中文就是数字签名,是Whitfield Diffie和Martin Hellman早在1976年就提出的概念,以他们名字命名的Diffie-Hellman Key Exchange算法可以用于在公开网络上传送**。 随后,由Ron Rivest, Adi Shamir和Leonard Adleman三人发明的RSA算法,被广泛运用到数字签名中。首个投放到市场的数字签名应用在1989年发布,采用的算法就是RSA。

签名有什么用呢?

生活中,大家一定有签名表身份的经历,譬如:在刷银行卡消费时,需要签上自己的大名,收银员有时还会对比一下你签的名与银行卡上签名栏是否匹配;在签署合同时,往往需要潇洒的挥上自己的大名,甚至手抹红印,按下指纹。这些都是为了确定行为人的身份,作为后续问题追溯的依据。数字签名的签名两字正是源于生活,意在确定身份,而数字两字表明其是签名的一种类型:基于密码学的签名方式。被数字签名广泛采用的RSA算法就是一种加密算法。

数字签名怎么用呢?

在网络发送信息时,存在安全问题,因为在发送者和接收者之间可能存在第三者,截获发送者信息,进行篡改后,再发送给接收者。为了解决这类安全问题,发送者可以对数据进行数字签名,当接收者拿到一堆签名后的数据时,可以对数据进行校验,一旦校验成功,接收者可以信心满满的说:

  • 这些数据一定是某人发送过来的,可以通过其他机构确定发送人的资质。
  • 发送者不可抵赖这是自己发送的,一旦接收者一口咬定数据是来源于某位发送者,那发送者只能心服首肯。
  • 这些数据一定没有被篡改,否则不会通过校验。

这其实就是数字签名的三大特性:可认证性(authentication)、不可抵赖性(non-repudiation)和完整性(integrity)。即便收到被第三者篡改的数据,接收者也可以确定数据不是来源于受信的发送者,而且数据肯定被动过。

自古以来,手写签名、按压指纹这种传统的签名方式都是具有法律效应的。在电子信息如此发达的今天,电子签名(Electronic Signature)也是被很多国家和地区(欧盟、美国)也被立法保护。读者在接触数字签名的概念时,往往会关联到电子签名的概念,前者属于技术实现的范畴,后者属于人文律法的范畴。

人们认可电子签名在法律上的有效性,而数字签名采用一定的加密算法,来保障电子签名的这种有效性。诚然,加密算法总有过时的一天,譬如广泛应用的低位数的RSA算法就已经被**了,这时候,电子签名的有效性就消失了,需要进一步更新数字签名的算法,继续维持电子签名的法律有效性。

这里,还需要说明一点,并非所有的电子签名实现都是采用数字签名。

1.2 机制

数字签名是如何保证可认证、不可抵赖和完整性的呢?这得从数字签名的算法说起。数字签名的实现包含三个算法:

  1. **生成算法。数字签名采用的是非对称**生成算法,即会生成一对**:私钥和公钥。用私钥加密的数据,只能用对应的公钥解密。私钥是应该要保护起来,不会泄露给其他人;公钥是完全公开的,会随着加密数据一起在公开网络上传送。

  2. 签名算法。给定私钥(private key)和数据(message),可以生成一个签名(signature)。

  3. 签名验证算法。给定数据(message)、公钥(public key)和签名(signature),可以解密数据并验证数据的来源和完整性。

**有对称(symmetric)和非对称(public)之分。对称**只有一个,同时用于加密和解密,就像我们用同一把钥匙开门和锁门,谁拿到钥匙,谁就掌握了房间的使用权。DES和AES这种加密算法采用的是对称**,而上文提到的RSA算法采用的是非对称**。

下图示意了数字签名的运行机制:

Android数字签名机制和应用场景

  • 对待发送的数据明文进行Hash,通常可采用MD5或SHA算法,然后采用私钥对Hash值进行加密,得到签名。将数据明文和签名一同发送出去。为什么要先对原始数据进行Hash后再用私钥加密呢?因为原数据可能比较大,直接使用私钥加密将会非常耗时。

  • 接收数据以后,会经过签名验证,其实就是比较两个Hash值:采用同样的Hash算法对数据明文进行解密,得到一个Hash值;采用公钥对签名进行解密后,得到原始的Hash值。如果两个Hash值相同,则说明数据没有被篡改而且来源可信:

    • 如果用公钥解密成功,则说明公钥与签名的私钥是唯一配对的,即一定是某个私钥的签名,这就保障了来源不可抵赖。

    • 如果数据被篡改,则接收到数据的Hash值与解密后的Hash值不会相同,这就保障了完整性。

    • 接收到的公钥是可以由第三方权威机构(Certificate Authority)认证的,因此接收方可以验证来源是否可靠,这就可以保障来源的可认证性。关于CA认证我们后文再展开。

说了这么多,其实数字签名最神奇的地方就在于**对:用私钥加密后的数据只能用对应的公钥解密。 RSA算法的理论根基是欧拉定理,可以参见RSA算法原理介绍。 

 

1.3 使用

了解数字签名的基本概念之后,就需要进一步了解一些数字签名的使用工具和衍生概念。

1.3.1 ssh-******

在使用SSH(Secure SHell)进行远程登录或其他网络请求时(譬如下载git库),会接触到ssh-******。相比Telent, FTP这些协议,SSH是安全的,因为可以使用ssh-******生成一对**,对通信过程进行加密,防止第三方攻击。

举一个最常见的例子,github上的开源项目均支持通过SSH协议进行下载,这就需要在github上添加一个SSH Key,否则无法正常下载github上的项目。

首先,会先在本地使用ssh-******命令生成一个**对:

$ ssh-******
Generating public/private rsa key pair.
Enter file in which to save the key (~/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in ~/.ssh/id_rsa.
Your public key has been saved in ~/.ssh/id_rsa.pub.
The key fingerprint is:
ac:70:ea:54:20:75:ce:8e:ec:ed:36:8f:b2:4a:3d:26 aaa@qq.com
The key's randomart image is:
+--[ RSA 2048]----+
|    . .          |
|   . +           |
|  . . o          |
|   o + .         |
|    + + S        |
|   o * .         |
|  E B o          |
| . =.oo.         |
|  ..o+oo.        |
+-----------------+

 

该命令执行完后,会在用户目录下的.ssh子目录生成两个文件:id_rsaid_rsa.pub

然后,便是将id_rsa.pub的文件内容粘贴到github的SSH Key中。完成配置后便可通过SSH下载远程代码,知其然却不知其所以然。在理解了数字签名的基本概念后,再来看这个过程:

  • ssh-******默认采用RSA算法生成了私钥(id_rsa)和公钥(id_rsa.pub)。上文中采用的是RSA 2048位的算法,因为256,512位的RSA算法已经被**了。这里需要提出的是,除了RSA,还有其他**对的生成算法,譬如DSA(Digital Signature Algorithm)、EdDSA(Edwards-curve Digital Signature Algorithm)等,可以通过ssh-******-t参数指定**对的生成算法。

    使用ssh-******生成**对时,会提示输入一个口令(passphrase),用来保护私钥。口令采用的对称**加密算法,简单来说,口令就像银行卡密码一样,需要记在脑子里。

    公钥通常比较长,上文中RSA算法生成的公钥是2048位,为了便于描述和比对,采用MD5算法将公钥映射成128位的Hash值,通常用16进制表示,这一串Hash值就叫做指纹(Fingerprint)

  • 将公钥粘贴到github的SSH Key配置项中,能够免去不断输入用户名和密码的烦恼;同时,这说明公钥是完全公开的,但私钥(id_rsa)仍然是保存在本地的。当github收到从客户端发起请求时,会利用客户端的公钥对一串随机数进行加密操作,然后将密文发送给客户端;客户端在收到密文后,会用配对的私钥进行解密,如果解密成功,便可建立一个受信的连接,不用再通过用户名和密码鉴权。

除了github,还有很多代码管理工具都支持SSH,譬如gitlab,oschina,gerrit等,其操作过程都是一致的。因为SSH利用数字签名,建立了安全的连接。

1.3.2 keytool

keytool是JDK的一个工具,用于**和证书的管理。keytool的主要操作对象是keystore文件,该文件一般以.keystore或.jks(Java KeyStore)为后缀名,用于**和证书的存储,其存储结构如下图所示:

Android数字签名机制和应用场景

keystore可以存储多个**对(Key Pair),每一个**对包含私钥(Private Key)和多个证书(Certificate)。想必诸位读者一定心生怪异了,上文说的**对都是一个私钥和一个公钥配对,然而keystore中存储的却是私钥和多个证书的配对,到底**对是怎么一样对应关系呢?这里有必要把证书和公钥的关系说明清楚了。

证书(Certificate)

如其名,用来证明真伪,就像一家企业的营业执照、大学生的毕业证一样,可用来证明其标的物的合法性,数字签名中的证书就是用于证明公钥的合法性。前文中提过,数字签名算法具备可认证性,然而公钥是完全公开的,很容易被伪造,这样一来,给定一个公钥是无法认证其合法性的,因此,需要给公钥“颁发”一个证书,其实现手段就是给公钥再签名一次,得到的结果就是证书。上图中,展示了一个完整的证书所包含的属性:Version、Subject、Issuer、Valid From、Valid Until、Public Key(待签名的公钥)、 Signature Algorithm(对公钥签名采用的数字签名算法)、Fingerprint(对公钥签名的公钥)。

至此,我们知道证书和公钥的关系了:证书就是对公钥再次签名产生的结果,证书包含被签名的公钥、公钥拥有者的信息以及对公钥进行签名的公钥。 接下来,还有两个重要的概念:证书认证机构CA(Certificate Authority)证书链(Certificate Chain)。这两个概念在数字签名体系中经常出现,但并不容易理解。

证书认证机构CA(Certificate Authority)证书链(Certificate Chain)

上述证书的生成过程会导致一个无限的链条:要得到证书,就需要给公钥进行数字签名,这就有两个公钥了:证书本身的公钥(即证书中的Public Key,用公钥A表示)和数字签名算法的公钥(即证书中的Fingerprint,用公钥B表示),要保证公钥B的合法性,我们可以对公钥B再进行数字签名,就又会引入一个新的公钥C,由此类推,还会有公钥D、E、F…,公钥合法性的保证就像链条一样:层层相扣,无穷无尽。怎么办呢?要打破这个无限的链条,就需要有一个权威机构,来统一颁发证书,这个机构就是CA(Certificate Authority),只要是CA颁发的证书,就不需要再签名了,就是权威的。然而,全世界需要的证书太多了,光靠一个CA是忙不过来的,CA得授权一些代理,这些代理也可以颁发证书,代理颁发的证书也被认为是合法可信的,这就形成了一个证书认证的链条,即证书链。我们再回过来头来看一下keystore文件的结构:一个私钥(Private Key)与多个证书(Certificates)组成**对(Key Pair),其实多个证书的结构就是证书链

要理解CA证书链,还是可以举毕业证的例子,我们的*就可以看做一个CA,*授权给教育部,教育部具备颁发和认证毕业证的职能;教育部再授权给各个高校,高校具备颁发自己学校毕业证的职能,教育部和高校就是不同级别的代理,授权过程就像链条一样。当我们要认证一个毕业证的真伪时,先拿到所属高校认证一下,如果高校无法确定合法,就拿到教育部认证一下,这种对毕业证的颁发和逐级认证的过程,就可以理解为证书链

 

有了这么多概念储备后,就可以轻松理解keytool这个工具的使用了。如果将keystore看做**和证书管理的数据库,那么keytool就是这个数据库增、删、改、查的接口。 笔者在网上搜索keytool的使用,基本都是罗列keytool命令的各中参数,真心记不住啊!好在Eclipse/Android Studio这些IDE都集成了keysotre的图形化工具,为开发人员带来了在签名的便利。

当然,除了Eclipse/Android Studio里面的keystore操作工具,还有很多其他好用的工具,笔者通过KeyStore Explorer工具打开Android的debug.keystore文件(位于~/.android/,存储了Android应用的默认签名),可以看到如下信息:

Android数字签名机制和应用场景

通过keytool命令可以显示出相同的内容:

aaa@qq.com:~$ keytool -list -v -keystore ~/.android/debug.keystore
Enter keystore password: # 此处需要输入keystore的口令: android
 
Keystore type: JKS
Keystore provider: SUN
 
Your keystore contains 1 entry
 
Alias name: androiddebugkey
Creation date: Jul 1, 2015
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: CN=Android Debug, O=Android, C=US
Issuer: CN=Android Debug, O=Android, C=US
Serial number: 588a6601
Valid from: Wed Aug 18 08:03:38 CST 2017 until: Fri Aug 11 08:03:38 CST 2047
Certificate fingerprints:
      MD5:  DD:89:BB:7A:E2:49:66:E0:B5:2F:9C:CF:DB:3B:AB:26
      SHA1: 7B:DE:EF:7E:4A:25:8E:F8:08:0B:8D:28:32:0A:0A:3C:C6:50:D5:CB
      SHA256:
27:E0:B8:9C:6B:4F:C6:88:25:7E:34:6E:93:7D:6C:F2:58:AC:64:02:2A:44:BB:17:23:19:E8:F4:8C:17:E6:30
      Signature algorithm name: SHA256withRSA
      Version: 3
 
Extensions:
 
#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
  KeyIdentifier [
  0000: 34 D2 8F 7A D4 AE FB 04   A8 0D 0E 7A D6 8C 3C 00 4..z.......z..<.
  0010: 9B 71 5A 61                                        .qZa
  ]
]

*******************************************
*******************************************

可以为keystore文件设置一个口令,增强访问的安全限制。Android的debug.keystore口令就是android,我们通过Eclipse/Android Studio开发应用时,默认就会使用debug.keystore进行签名。该文件描述了以下信息:

  • keystore的文件格式为JSK(Java Key Store),技术提供商为SUN公司。

  • keystore存了一个记录:名为androiddebugkey的**对,其中有一个私钥和一条证书链,证书链中只有一个证书。

  • 证书的持有者和发布者都是CN=Android Debug, O=Android, C=US,Common Name是Android Debug,Organization Name是Android,Country是US。

  • 证书的有效期2017-8-18到2047-8-11;分别用MD5,SHA1,SHA256三种不同的Hash算法计算出了证书的指纹。

  • 证书中的公钥是采用RSA 1024算法生成的,格式是X.509,上述所定义的各种属性字段,就是X.509的标准。

本节不再罗列keytool命令的其他参数,后文中还会使用keytool查看APK的签名信息。其实,最重要的还是理解几个关键概念:证书证书链证书认证机构,在当下的安全体系中,这几个概念出现的相当多,最有名的当属HTTPS

2 Android使用数字签名的场景

笔者把Android中数字签名的应用场景分为应用和系统两个层面,先抛出具体的场景,后文中再通过源码分析这些场景的实现方式。

  • 应用层面:Android对APK的签名要求

    • Android拒绝安装没有签名的APK
    • Android并不校验证书的合法性
    • 当签名不匹配时,APK升级会失败
  • 系统层面:Android基于数字签名的一些机制

    • 编译Android系统时,会根据不同应用的类型进行签名
    • Android基于数字签名来判定是否给应用授权
    • Android基于数字签名来标记APK的SELinux Lable

2.1 未签名的APK VS 签名后的APK

Android中的数字签名主要是针对APK的,因此有必要先介绍APK签名前后的差异。使用JDK的工具jarsigner便可对APK进行签名。

aaa@qq.com:~$ jarsigner -verbose \
   -keystore ~/.android/debug.keystore \
   -signedjar app-signed.apk app-unsigned.apk \
   androiddebugkey
Enter Passphrase for keystore:  #此处输入**android
  updating: META-INF/MANIFEST.MF
    adding: META-INF/ANDROIDD.SF
    adding: META-INF/ANDROIDD.RSA
   signing: AndroidManifest.xml
   signing: classes.dex
   signing: res/anim-v21/design_bottom_sheet_slide_in.xml
   signing: res/anim-v21/design_bottom_sheet_slide_out.xml
   signing: res/anim/abc_fade_in.xml
   ... #此处省略很多对其他文件的signing日志
   signing: resources.arsc
jar signed.

该命令使用了前文介绍的Android Studio默认的keystore文件,其中存放了一对别名为androiddebugkey的**对。加上-verbose参数可以打印出具体对哪些文件进行过签名。

APK文件其实就是压缩包,可以通过zip或tar等工具直接解压打开,对比签名前后的APK内容如下:

app-unsigned                    app-signed
├── AndroidManifest.xml         ├── AndroidManifest.xml
├── classes.dex                 ├── classes.dex
├── META-INF                    ├── META-INF
│   │                           │   ├── ANDROIDD.RSA
│   │                           │   ├── ANDROIDD.SF
│   └── MANIFEST.MF             │   └── MANIFEST.MF
├── res                         ├── res
└── resources.arsc              └── resources.arsc

由此可见,APK签名前后,只有META-INF目录发生了变化,签名后会更新MANIFEST.MF文件,会增加ANDROIDD.SFANDROIDD.RSA这两个文件,这与使用jarsigner签名时的日志输出是一致的。那么,META-INF/目录下,变化的这三个文件到底是什么呢?

MANIFEST.MFANDROIDD.SF都是文本文件,直接打开可以看到其文件内容,通过bcompare对比工具查看其文件差异,截图片段如下图所示:

Android数字签名机制和应用场景

MANIFEST.MF是APK包含文件的清单列表,记录了APK中每一个文件的SHA256摘要,前文介绍数字签名的时候说过,签名算法很耗时,因此不是对原文件执行加密算法,而是先生成原文件的摘要,再对摘要进行签名。由此,可以推断出ANDROIDD.MF文件的内容就是摘要密文。

ANDROIDD.RSA其实是数字证书,RSA的文件后缀表示证书中包含了一个基于RSA算法生成的公钥。通过前文介绍的keytool命令,便可打印出该证书的信息,这与前文中keystore中打印的证书信息是一致的:

aaa@qq.com:~/app-signed/META-INF$ keytool --printcert
-file ANDROIDD.RSA
Owner: CN=Android Debug, O=Android, C=US
Issuer: CN=Android Debug, O=Android, C=US
Serial number: 588a6601
Valid from: Wed Aug 18 08:03:38 CST 2017 until: Fri Aug 11 08:03:38 CST 2047
Certificate fingerprints:
      MD5:  DD:89:BB:7A:E2:49:66:E0:B5:2F:9C:CF:DB:3B:AB:26
      SHA1: 7B:DE:EF:7E:4A:25:8E:F8:08:0B:8D:28:32:0A:0A:3C:C6:50:D5:CB
      SHA256:
27:E0:B8:9C:6B:4F:C6:88:25:7E:34:6E:93:7D:6C:F2:58:AC:64:02:2A:44:BB:17:23:19:E8:F4:8C:17:E6:30
      Signature algorithm name: SHA256withRSA
      Version: 3
 
Extensions:
 
#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 34 D2 8F 7A D4 AE FB 04   A8 0D 0E 7A D6 8C 3C 00 4..z.......z..<.
0010: 9B 71 5A 61                                        .qZa
]
]

至此,APK签名前后的差异已经分析完毕。签名会在APK中的META-INF/目录下添加证书ANDROIDD.RSA和密文ANDROIDD.SF,有了这两个文件,就可以对APK进行校验了。

2.2 应用层面:Android对APK的签名要求

2.2.1 Android拒绝安装没有签名的APK

通过命令行adb install安装一个没有签名的APK时,会提示INSTALL_PARSE_FAILED_NO_CERTIFICATES错误:

aaa@qq.com:$ adb install app-release-unsigned.apk
5927 KB/s (2357317 bytes in 0.388s)
Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES: Package /data/app/vmdl1995237626.tmp/base.apk has no certificates at entry AndroidManifest.xml]

 同时,会出现如下日志:

1034  1271 E PackageInstaller: Commit of session 1995237626 failed: Package /data/app/vmdl1995237626.tmp/base.apk has no certificates at entry AndroidManifest.xml
1034 11776 W System.err: java.lang.SecurityException: Caller has no access to session 1995237626
1034 11776 W System.err: at com.android.server.pm.PackageInstallerService.abandonSession(PackageInstallerService.java:737)
1034 11776 W System.err: at android.content.pm.IPackageInstaller$Stub.onTransact(IPackageInstaller.java:97)
1034 11776 W System.err: at android.os.Binder.execTransact(Binder.java:574)

要深入理解这些日志,需要对adb安装APK的原理有了解,然后才能分析Android是如何在APK安装路径上植入签名检查的。

通过adb安装APK的时序可以分为三步:创建安装Session拷贝APK到手机上安装APK。这里引入了Session(会话)的概念,之所以要用Session,是为了保证安装过程中的数据不会丢失:如果一个APK的安装因为某种原因中断,那么下次系统重启后仍可以继续之前的安装过程。了解Session的用意后,我们接着分析adb安装APK的时序:如下图所示:

Android数字签名机制和应用场景

1. 创建安装的Session。这里出现的关键类是Pm.java,它是包管理相关的命令解析器。通过命令行输入adb installadb shell package命令就会交由Pm进行解析,对输入的命令解析完后,真正创建Session的操作是通过跨进程调用到PackageInstallerService.java中完成的。PackageInstallerService是运行在系统进程的,伴随着PackageManagerService的创建而创建,它的主要职责就是为安装APK服务,提供了很多操作Session的接口,其内部会维护所有安装的Sesssion,并将其Session的信息保存到磁盘文件/data/system/install_sessions.xml中,如果有安装有中断,则下次从该文件中读取信息便可恢复Session。

Pm.run()
 └── Pm.runInstall()
     └── Pm.doCreateSession()
         └── IPackageInstaller.createSession()
             └── Binder.execTransact()
                 ...
                 └── PackageInstallerService.createSession()

 

2. 拷贝APK到手机。Session创建完后,要做的第一件事就是将待安装的APK拷贝到手机上,拷贝到手机上的哪个目录呢?创建Session的时候会根据APK的安装属性来做判定:如果安装到内置存储空间(通常就是Data分区),那会将APK拷贝到/data/app/vmdl+sessionId+.tmp/目录下;如果安装到外置存储空间(通常就是SD卡),那么将APK拷贝到外置存储空间/smdl+sessionId+.tmp/目录下,此处的sessionId是唯一的。在上文的日志中出现的/data/app/vmdl1995237626.tmp/base.apk其实表示当时安装的sessionId是1995237626

3. 安装APK。前面的步骤创建了一个Session,保存了一些安装信息,并将APK拷贝到手机上,这个状态称为Staged,是一个临时状态,在安装APK时可以看到很多与Stage相关的关键字。对于一个Session而言,需要进行提交(commit),交给系统完成后续的安装操作。上文的时序图并没有把完整的函数调用逻辑示意出来,其实Session的commit操作路径还是较长的,具体如下:

 Pm.run()
 └── Pm.runInstall()
     └── Pm.doCommitSession()
         └── PackageInstaller.commit()
             └── IPackageInstallerSession.commit()
                 └── Binder.execTransact()
                     ...
                     └── PackageInstallerSession.commit()
                         └── sendMessage(MSG_COMMIT)

 Handler.Callback.handleMessage(MSG_COMMIT)
 └── PackageInstallerSession.commitLocked()
     └── PackageInstallerSession.validateInstallLocked()
         └── PackageParser.parseApkLite()
             └── PackageParser.collectCertificates()
                 └── PackageParser.loadCertificates()
     └── PackageManagerService.installStage()

 本文不再赘述路径上各函数的功能,只提出一点:要想知道APK中是否包含签名,就需要对读取APK文件的内容,这是由PackageParser.parseApkLite()完成的,在读取APK时,就会去找签名,这是通过PackageParser.loadCertificates()完成的。如果找不到签名,则会INSTALL_PARSE_FAILED_NO_CERTIFICATES异常,中断安装过程;如果找到签名,则会继续调用**PackageManagerService.installStage()完成后续的安装操作,更多安装操作的细节,请参考Android包管理机制一文。

我们可以从数据库的事务操作来理解一个session,安装一个APK就可以理解为向系统添加一项APK记录的事务操作。创建一个事务(createSession),表达操作意图(writeSession),然后将事务提交(commitSession)。剩下的事情就交由系统完成了,系统进程会进入APK的安装阶段。

理解了APK安装过程后,我们再回过头来看一下上文中提示安装错误的日志:

1034  1271 E PackageInstaller: Commit of session 1995237626 failed: Package /data/app/vmdl1995237626.tmp/base.apk has no certificates at entry AndroidManifest.xml

对于1995237626这个Session而言,Commit的时候出现了错误,这表示Session已经进入到了Commit阶段,APK已经拷贝到手机上/data/app/vmdl1995237626.tmp/base.apk(取名为base.apk是与APK的拆分有关,如果没有拆分,那base.apk就是待安装的APK)。出错的原因是找不到AndroidManifest.xml这个文件的证书,其实就是没有APK中没有签名信息啦~

2.2.2 Android不校验证书的合法性

Android拒绝安装没有签名的APK,但Android并不会校验APK证书的有效性,即只要签名正确,不管证书是不是合法的,Android都会安装。我们可以使用jarsigner工具对APK的证书进行校验,会出现如下的警告,但APK依旧可以正常安装。

aaa@qq.com:~$ jarsigner -verify app-signed.apk
jar verified.

Warning:
This jar contains entries whose certificate chain is not validated.
This jar contains signatures that does not include a timestamp.
Without a timestamp, users may not be able to validate this jar after the signer certificate's expiration date (2047-08-11) or after any future revocation date.

由于Android并不校验证书的合法性,因此我们无法判定一个安装的应用是否经由正规的公司发布。譬如,我们可以给微信进行重新签上自己生成的签名,还是可以正常安装,但这已经不是腾讯发布的微信了,Android系统对此并不感知。当然,微信自己会进行签名校验,发现签名不对会选择自行退出,但对于市面上绝大多数应用而言,都没有自校验签名。因此也就出现了一些无良的开发者,窃取别人的劳动成果,在已有APK中植入广告,重签名进行发布。

数字签名的证书是可以交由第三方机构认证的,即由CA(Certifiacte Authoriy)颁发的证书是得到认可的。基于此,应用市场(譬如应用宝、豌豆荚等)可以对上架的应用进行证书的校验。

2.2.3 当签名不匹配时,APK升级会失败

如果一个APK的前后两个版本签名不一致,Android是拒绝升级的,这是一种基于数字签名对应用的保护机制。在此限定下,应用开发者需保持其签名一直不变。

本文不再细述APK升级安装的过程,这个过程由PMS处理,读者其实可以猜到,无非就是在安装多了一个环节:与已安装的同包名应用进行签名比对。具体的细节可以参考Android包管理机制文中的应用安装一节。

举一个例子,对一个应用签两次不同的名,通过adb install -r命令重复安装,便会升级签名不匹配的错误:

aaa@qq.com:~$ adb install -r app-signed-changed.apk
1904 KB/s (2407277 bytes in 1.234s)
Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE: Package com.xo.demo signatures do not match the previously installed version; ignoring!]

2.3 系统层面:Android基于数字签名的一些机制

2.3.1 四种不同类型的签名

Android预置的系统应用都是需要签名的,包括联系人、相机、设置等。Android设计了四种不同类型的签名:platformsharemediatestkey,默认置于在源码的build/target/product/security目录下,分别用于给不同类型的系统应用进行签名。从Android Lollipop开始,Android还对boot.img和system.img进行签名以防被篡改,所以在原来四组签名的基础上又增加了verity

build/target/product/security
├── (media.pk8, media.x509.pem)       #用于给MediaProvider, Gallery等签名
├── (platform.pk8, platform.x509.pem) #用于给Settings, Phone等签名
├── (shared.pk8, shared.x509.pem)     #用于给Launcher, Dailer等签名
├── (testkey.pk8, testkey.x509.pem)   #用于给一般应用签名
└── (verity.pk8, verity.x509.pem)     #用于给boot.img和system.img签名

 一共有五组**对,.pk8为私钥, .509.pem为证书(包含公钥),本质上,它们都是**对,并没有区别,只不过Android做了逻辑上的区分,后文我们再来介绍不同类型的签名各有什么用。先来看如何给系统应用设定签名类型,以下是packages/apps/Settings/Android.mk的内容片段,表示要编译Settings这个模块:

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
...
LOCAL_PACKAGE_NAME := Settings
LOCAL_CERTIFICATE := platform  #表示Settings需要采用platform**对进行签名
LOCAL_PRIVILEGED_MODULE := true
...
include $(BUILD_PACKAGE)

 

设置Makefile的LOCAL_CERTIFICATE变量,就可以指定APK的签名类型了,编译系统会根据设定的变量值选取**对。

在AOSP的源码中,上述的**对是完全公开的,只是为了完成签名操作,并没有安全性可言。OEM设备厂商都会自行生成**对,并将私钥(.pk8文件)保护起来。

2.3.2 应用的授权

Android包管理机制一文中,详细介绍了系统如何给应用授权,本文不再赘述,仅把与数字签名相关的应用授权提炼出来。

先来看一个特殊的APK:framework-res.apk,其包名为android,在它的AndroidManifest.xml文件中,定义了一些系统权限:

  <!-- 安装应用的权限 -->
  <permission android:name="android.permission.INSTALL_PACKAGES"
      android:protectionLevel="signature|privileged" />
  <!-- 卸载应用的权限 -->
  <permission android:name="android.permission.DELETE_PACKAGES"
      android:protectionLevel="signature|privileged" />

 

所有定义的权限都有一个protectionLevel属性,它表示一个权限的保护级别。其中部分权限的protectionLevel值包含signature,表示该权限的申请者必须与framework-res.apk的签名相同,系统才会授予申请者该权限。从framework-res.apk的Android.mk中,可以看出它签名类型为platform,这也就意味着如果想要获取系统中protectionLevel为signature的权限,就必须使用platform**对来签名应用。

除了framework-res.apk,其他应用也可以定义属于自己的权限,同样可以设定protectionLevel,通过Android的权限授予机制来保护API,防止滥用。譬如在packages/apps/Launcer3/AndroidManifest.xml中,就定义了如下权限:

<permission
    android:name="com.android.launcher3.permission.WRITE_SETTINGS"
    android:permissionGroup="android.permission-group.SYSTEM_TOOLS"
    android:protectionLevel="signatureOrSystem"
    android:label="@string/permlab_write_settings"
    android:description="@string/permdesc_write_settings"/>

这就表明要想获取com.android.launcher3.permission.WRITE_SETTINGS这个权限,申请者要么与Launcher3具有相同的签名,要么是一个系统应用。

正常情况下,普通的应用是无法获取一些受保护系统权限的,因为无法获得与系统应用相同的签名。试想如果一个恶意的应用获取了静默安装的系统权限,每天偷偷的下载安装应用,这是一件多么恐怖的事情。

Android基于数字签名设计的应用授权,是一种灵活的授权机制,因为仅仅通过是否为系统应用来判定是否授权,有很多局限,譬如一些受信的应用没有安装在系统分区,就无法获取系统权限;两个普通的应用之间也无法定义受保护的权限。 有了数字签名,就相当于多了一种受信机制。两个不同的APK,签名相同,意味着来源相同,彼此是受信的。

2.3.3 SELinux根据签名给APK打标签(Labling)

Android Lollipop强制使用SELinux后,极大的增强了Android系统的安全性,关于SELinux的运行机制,各位读者可以从网上搜罗很多,本节意不在此,仅是为了介绍Android如何结合SELinux和数字签名为APK打上标签(所谓“标签”,是为了后文中描述的方便,在SELinux的概念中,其实叫SELinux Context)。有一些必要基础的背景知识:

  • 在SELinux环境下,所有的文件和进程都有标签(通过ls -Zps -Z命令可以查看),这个标签称为SELinux Context,是SELinux的权限控制的基础。

  • 部分标签是事先定义好的,譬如system/sepolicy/file_contexts文件中所定义的一些系统文件和目录的标签;部分标签是根据规则生成的,譬如在fork子进程时,会根据TE(Type Enforcement)文件中定义的规则,为子进程打上标签。

以Settings为例,手机上的系统分区会存在Settings.apk这个静态的文件,运行ls -Z命令可以查看其SELinux标签:

angler:/system/priv-app/Settings # ls -Z
-rw-r--r-- 1 root root u:object_r:system_file:s0 Settings.apk

Settings.apk的标签为u:object_r:system_file:s0,表示:

  • 该文件属于SELinux的用户u,Android下的SELinux只定义了一个用户,就是u

  • 该文件的角色为object_r,SELinux的规则文件中为不同的角色定义了不同的权限

  • 该文件的域为system_file,域的概念不是很好理解,姑且将其理解为操作对象,还有其他的域,譬如system_data_file, apk_data_file等,SELinux的规则文件中定义了一个角色对不同对象的操作权限

  • MLS(Multi-Level Security)级别0,目前Android中所有Label的级别都是S0,此处不展开讨论

这个静态的Settings.apk文件的SELinux标签是怎么打上去的呢?在system/sepolicy/file_contexts文件中做了如下定义:

...
#############################
# System files
#
/system(/.*)?		u:object_r:system_file:s0
/system/bin/atrace	u:object_r:atrace_exec:s0
/system/bin/e2fsck	--	u:object_r:fsck_exec:s0
/system/bin/fsck\.f2fs	--	u:object_r:fsck_exec:s0
/system/bin/fsck_msdos	--	u:object_r:fsck_exec:s0
/system/bin/toolbox	--	u:object_r:toolbox_exec:s0
...

上面的代码片段表示对于/system/目录下的所有文件,如果没有特殊的指定,其SELinux标签就为u:object_r:system_file:s0/system/子目录也可以再重新指定,譬如/system/bin/toolbox,虽然也在/system目录下,但具体指定了其标签为u:object_r:toolbox_exec:s0

当Settings运行起来后,系统中就存在一个Settings进程,通过运行ps -Z命令查看:

angler:/system/priv-app/Settings # ps -Z | grep settings
u:r:system_app:s0 system S com.android.settings

对于Settings进程而言,SELinux标签的角色为r,域为system_app,这个标签与静态的Settings.apk文件不同,它是依据APK的签名打上去的,Android设计了一套为应用进程打标签的机制:维护签名到SELinux标签的映射表,记录在system/sepolicy/mac_permissions.xml中,其初始内容如下所示:

<?xml version="1.0" encoding="utf-8"?>
<policy>
    <signer signature="@PLATFORM" >
      <seinfo value="platform" />
    </signer>
</policy>

以上内容只是一个模板,表达的意思是:签名为@PLATFORM的APK所在的进程,其seinfo为platform。@PLATFORM编译后会被替换成真实的platfrom类型的签名,以下是mac_permissions.xml经过编译后的结果:

<?xml version="1.0" encoding="iso-8859-1"?>
<policy>
  <signer signature="308204a83...8b1b357">
    <seinfo value="platform"/>
  </signer>
</policy>

对于在Android.mk中声明LOCAL_CERTIFICATE := platform的应用来说,其seinfo就被标记为platform了,那么seinfo又是什么呢?frameworks/base/core/java/android/content/pm/ApplicationInfo.java文件中有其定义:

    /**
     * String retrieved from the seinfo tag found in selinux policy. This value
     * can be overridden with a value set through the mac_permissions.xml policy
     * construct. This value is useful in setting an SELinux security context on
     * the process as well as its data directory. The String default is being used
     * here to represent a catchall label when no policy matches.
     *
     * {@hide}
     */
    public String seinfo = "default";

ApplicationInfo这个对象是表示一个应用程序的信息,其中一个属性就是seinfo,默认取值为default,最终取值会依据mac_permissions.xml而定。在system/sepolicy/seinfo_contexts文件中,定义了不同seinfo映射到的SELinux标签:

isSystemServer=true domain=system_server
user=system seinfo=platform domain=system_app type=system_app_data_file
user=bluetooth seinfo=platform domain=bluetooth type=bluetooth_data_file
user=nfc seinfo=platform domain=nfc type=nfc_data_file
user=radio seinfo=platform domain=radio type=radio_data_file
user=shared_relro domain=shared_relro
user=shell seinfo=platform domain=shell type=shell_data_file
user=_isolated domain=isolated_app levelFrom=user
user=_app seinfo=platform domain=platform_app type=app_data_file levelFrom=user
user=_app isAutoPlayApp=true domain=autoplay_app type=autoplay_data_file levelFrom=all
user=_app isPrivApp=true domain=priv_app type=app_data_file levelFrom=user
user=_app domain=untrusted_app type=app_data_file levelFrom=user

Settings进程的seinfo为platform,进程所属的用户为system,那就匹配到了“user=system seinfo=platform domain=system_app type=system_app_data_file”这一条,表示Settings进程的域为system_app,其可操作的文件类型为system_app_data_file

当APK的签名在mac_permissions.xml中没有匹配成功时,会默认设置seinfo为default,这样最终APK的应用进程的域就为untrusted_app。感兴趣的读者可以自行通过上文中的命令查看。

用下图总结一下为应用进程打标签的过程:

Android数字签名机制和应用场景

  • 对APK进行扫描的时候,根据mac_permissions.xml中定义的签名=>seinfo映射,设置APK所对应的应用进程的seinfo属性

  • 当APK的应用进程启动时,根据seapp_contexts中定义的seinfo=>SELinux Context映射,设置进程的SELinux标签

3 总结

本文的目的是为了介绍数字签名在Android中的应用场景。

首先,介绍了数字签名的背景,机制和常见的使用工具。对于数字签名(Digitial Signature)、电子签名(Electronic Signature)、证书(Certificate)、证书链(Certificate Chain)、证书认证机构(Certificate Authority)等概念做了比较通俗的解释。数字签名的实现基于非对称**算法,主要有签名和校验两个机制。使用数字签名可以保障可认证性(Authentication)、不可抵赖性(Non-repudiation)和完整性(Integrity),因此应用场景非常之广泛,譬如SSH、HTTPS、网银U盾、电子合同等。

然后,分析了Android基于数字签名实现的一些安全机制,包括拒绝安装没有签名的应用、拒绝升级签名不匹配的应用、基于签名给应用授权、SELinux环境下基于签名打标签。在Android源码中,都能够找到这些机制的实现,读者可以重点学习Android是如何将数字签名机制嵌入到已有的代码流程中的。

最后,数字签名还可以衍生出一些有意思的变种,譬如如何做到可抵赖(Repudiation),这在间谍领域是很有必要的,基于本文一开始提到的Diffie-Hellman Key Exchange算法就可以实现可抵赖性,有兴趣的读者可以继续研究。