Android打包流程-签名

Android打包流程-签名编译任务 META-INFO主要用来存储当前的apk的签名信息, 与AP相关的签名信息有下面两个: 校验签名文件 会先通过validateSigningDebug任务来校验当前是否有签名文件,如果没有

编译任务

META-INF主要用来存储当前的apk的签名信息, 与AP相关的签名信息有下面两个:

validateSigningDebug
packageDebug

校验签名文件

会先通过validateSigningDebug任务来校验当前是否有签名文件,如果没有配置签名文件会报异常。

APK打包

因为签名需要基于全量的APK文件,所以整个APK签名的时机会在整个APK打包流程的最后阶段执行。

基础储备

在了解整体的APK的签名流程前,需要先部分常见的

数据摘要(数据指纹)

表示对一个数据源进行一个算法之后得到一个摘要,消息摘要算法的主要特征是加密过程不需要秘钥的参与,并且被加密过的数据无法被解密。只有输入相同的明文数据经过相同的数据摘要算法才能得到相同的密文。

消息摘要特点:

  • 无论输入的数据多长,计算出来的消息摘要的大小总是固定的。比如MD5算法摘要的的消息有128位,用SHA-1算法摘要的消息有160位. SHA-1变体可以产生192位以及256位的消息摘要。
  • 消息摘要的结果是伪随机的。
  • 消息摘要是不可逆的

数字签名

用私钥对数据摘要进行加密

证书

公开秘钥认证,又称数字证书,本质上是一种电子文档,是由CA中心颁布的较为权威与公正的证书。

数字证书的基本架构是公开秘钥PKI,即利用一对秘钥实施加密和解密。其中秘钥包括公钥和私钥,私钥用来签名和解密,由用户定义,只有用户知道。公钥用于签名验证和加密,可被多人共享。

此文件需要包含下面几个内容:

  • 公钥信息
  • 拥有者信息
  • 发行者对这个文件的数字签名

认证机构用自己的私钥对需要认证的人(或组织机构)的公钥施加数字签名并生成证书,

即证书的本质就是对公钥施加数字签名。

自签名证书

自签名证书(self-signed certificate)是用自己的私钥签署的数字证书。是有效且无成本的。自签证书和CA签名证书一样可以用来加密数据。

自签名证书优势:免费、随时签发、方便

自签名证书缺点:不受浏览器信任、不安全

X.509格式证书

一般遵从X.509格式规范的证书,会有以下的内容(from wiki),它们以字段的方式表示:

  • 版本:现行通用版本是 V3

  • 序号:用以识别每一张证书,特别在撤消证书的时候有用

  • 主体:拥有此证书的所有人,

    • 国家(C,Country)
    • 州/省(S,State)
    • 地域/城市(L,Location)
    • 组织/单位(O,Organization)
    • 通用名称(CN,Common Name):在TLS应用上,此字段一般是网域
  • 发行者:以数字签名形式签署此证书的数字证书认证机构

  • 有效期开始时间:此证书的有效开始时间,在此前该证书并未生效

  • 有效期结束时间:此证书的有效结束时间,在此后该证书作废

  • 公开密钥用途:指定证书上公钥的用途,例如数字签名、服务器验证、客户端验证等

  • 公开密钥

  • 公开密钥指纹

  • 数字签名

  • 主体别名(英语:Subject Alternative Name :例如一个网站可能会有多个网域(www.wikipedia.org, zh.wikipedia.org, zh.m.wikipedia.org 都是维基百科)、一个组织可能会有多个网站(*.wikipedia.org, *.wikibooks.org, *.wikidata.org 都是维基媒体基金会旗下的网域),不同的网域可以一并使用同一张证书,方便实现应用及管理

证书格式

.jks

二进制格式文件,同时包含证书和私钥,一般有密码保护,只能存储非对称密钥对(私钥 + x509公钥证书)

PKCS#12格式

公钥加密标准,通用格式(rsa公司标准)。包含所有私钥、公钥和证书。其以二进制格式存储,也称为 PFX 文件,在windows中可以直接导入到密钥区,注意,PKCS#12的密钥库保护密码同时也用于保护Key。

JKS和PKCS12之间的最大区别是JKS是Java专用的格式,而PKCS12是存储加密的私钥和证书的标准化且与语言无关的方式。

pkcs#7

二进制格式,其中可以包含:x509公钥证书,公钥,消息体,数字签名等信息。

JKS文件

JKS文件申请流程

  1. 在菜单栏中,依次点击 Build > Generate Signed Bundle/APK

  2. Generate Signed Bundle or APK 对话框中,选择 Android App BundleAPK,然后点击 Next

  3. Key store path 字段下,点击 Create new

  4. New Key Store 窗口中,为您的密钥库和密钥提供以下信息,如图 2 所示。

    img

    图 2. 在 Android Studio 中创建新的上传密钥和密钥库。

  5. 密钥库

    • Key store path:选择创建密钥库的位置。 此外,还应在位置路径末尾添加一个扩展名为 .jks 的文件名。
    • Password:为您的密钥库创建并确认一个安全的密码。
  6. 密钥

    • Alias:为您的密钥输入一个标识名。
    • Password:为您的密钥创建并确认一个安全的密码。它应该与密钥库密码相同。(如需了解详情,请参阅已知问题
    • Validity (years) :以年为单位设置密钥的有效时长。密钥的有效期应至少为 25 年,以便您可以在应用的整个生命期内使用同一密钥为应用更新签名。
    • Certificate:为证书输入一些关于您本人的信息。此信息不会显示在应用中,但会作为 APK 的一部分包含在您的证书中。
  7. 填写完表单后,请点击 OK

jks文件的内容

查看jks内容

通过keytool命令可以查看jks文件的内容。

keytool -list -v -keystore /xx/xx/xx.jks

具体内容如下所示:

密钥库类型: PKCS12
密钥库提供方: SunJSSE
​
您的密钥库包含 1 个条目
​
别名: my-test
创建日期: 2022-5-2
条目类型: PrivateKeyEntry
证书链长度: 1
证书[1]:
所有者: CN=xie, OU=xie, O=xie, L=xie, ST=xie, C=xie
发布者: CN=xie, OU=xie, O=xie, L=xie, ST=xie, C=xie
序列号: 7e3d8370
生效时间: Mon May 02 12:10:42 CST 2022, 失效时间: Fri Apr 26 12:10:42 CST 2047
证书指纹:
   SHA1: 50:07:59:83:AB:45:13:36:9F:59:73:75:83:60:97:B9:54:3A:4E:90
   SHA256: 24:7E:31:43:EC:7E:38:AF:AD:23:42:30:FF:A2:FE:53:FD:D5:EB:6C:66:AB:92:F2:AF:33:F3:F7:F7:2C:C7:04
签名算法名称: SHA256withRSA
主体公共密钥算法: 2048RSA 密钥
版本: 3
....
jks文件提取公钥

可以使用keytool命令查看jks文件携带的公钥信息

keytool -list -rfc -keystore /Users/xiejinlong/mytest

结果如下:

密钥库类型: PKCS12
密钥库提供方: SunJSSE
​
您的密钥库包含 1 个条目
​
别名: my-test
创建日期: 2022-5-2
条目类型: PrivateKeyEntry
证书链长度: 1
证书[1]:
-----BEGIN CERTIFICATE-----
MIIDRzCCAi+gAwIBAgIEfj2DcDANBgkqhkiG9w0BAQsFADBUMQwwCgYDVQQGEwN4
aWUxDDAKBgNVBAgTA3hpZTEMMAoGA1UEBxMDeGllMQwwCgYDVQQKEwN4aWUxDDAK
BgNVBAsTA3hpZTEMMAoGA1UEAxMDeGllMB4XDTIyMDUwMjA0MTA0MloXDTQ3MDQy
NjA0MTA0MlowVDEMMAoGA1UEBhMDeGllMQwwCgYDVQQIEwN4aWUxDDAKBgNVBAcT
A3hpZTEMMAoGA1UEChMDeGllMQwwCgYDVQQLEwN4aWUxDDAKBgNVBAMTA3hpZTCC
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJsj/1+jXDHxpTg/jqlvdmIn
y2ZZPhIju+Zd//plCEWwI1jL10/m8WvSrIAFxolf6LKICSQMSbE7aG9j4z6Z3RTN
rkuYHilgsE3yxGlHfGPi8bAbf/WTWktlqb+V2bR6kFVtu52BqG0k5Lb1A/5lyYZF
daIO+wS7NrWSBqkl6PdeDA8E74x7tph2UxwqUFr/W77Jn4J4qizEtlpewcwxDYe4
HRWQq8Z1xlCnlEZjY8IRH7s27KQfmRfdz29AfyXnu7Jk1CWE1D2RQgZMDtvBMqQi
aai7HpgTU6NyGX2mySeiZz/htBoPaUAtVfErtKGc4b2GLZ81Ht+ng3mezngWDdEC
AwEAAaMhMB8wHQYDVR0OBBYEFDrH49s0dsLegwr4J+GeTlPSfA+kMA0GCSqGSIb3
DQEBCwUAA4IBAQB1VK7NhWTo/sh8H224OLz4kQC9ZGvBAWYKpRoR3w95SwUHaonu
Ylp3qIfjPDSMUNmYaCjMA01naVVUe33aQOIc4Fx45rU3G0aNCtoo3TDSy6mCtBed
ie81aTOnWYCYvEpPE581t7uLAnYVHy9KddXfZPP8301GYu7LbpGEK3TDfxFQnq6T
xCH1ih/9fCqUuSXtIi8hM/1C/H3U8pRwtPSEQEL6fg0knp5HO798BXjP96/mUCqP
YAuqbOf2sXQFhtN4n4kuLFMQJ+H4EzFrjLyYoUkUWU255zyu9zzOasx3RFZj/Scs
VOMCxBPfF/zyB8ZISAQ7XSCM/MIqVxgCVVaN
-----END CERTIFICATE-----
​
​
******************************************* 
jks文件提取私钥信息

无法直接从jks文件中拿到私钥信息,但是可以把jks证书转化为pfx证书,pfx就是PKCS#12格式的证书,再获取私钥。

  • jks文件转为fpx文件
keytool -v -importkeystore -srckeystore /Users/xiejinlong/mytest1 -srcstoretype jks -srcstorepass 111111 -destkeystore  mytest.pfx -deststoretype pkcs12 -deststorepass 111111 -destkeypass 111111
  • 从fpx证书文件中提取私钥并查看

    openssl pkcs12 -in mytest.pfx -nocerts -nodes -out server.key
    

    内容如下:

    Bag Attributes
        friendlyName: my-test
        localKeyID: 54 69 6D 65 20 31 36 35 31 34 37 38 38 34 31 30 36 36
    Key Attributes: <No Attributes>
    -----BEGIN PRIVATE KEY-----
    MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCbI/9fo1wx8aU4
    P46pb3ZiJ8tmWT4SI7vmXf/6ZQhFsCNYy9dP5vFr0qyABcaJX+iyiAkkDEmxO2hv
    Y+M+md0Uza5LmB4pYLBN8sRpR3xj4vGwG3/1k1pLZam/ldm0epBVbbudgahtJOS2
    9QP+ZcmGRXWiDvsEuza1kgapJej3XgwPBO+Me7aYdlMcKlBa/1u+yZ+CeKosxLZa
    XsHMMQ2HuB0VkKvGdcZQp5RGY2PCER+7NuykH5kX3c9vQH8l57uyZNQlhNQ9kUIG
    TA7bwTKkImmoux6YE1Ojchl9psknomc/4bQaD2lALVXxK7ShnOG9hi2fNR7fp4N5
    ns54Fg3RAgMBAAECggEARUPRJJX+9513sqFNxIArTq+NtGhruhWSMswNGXI6O0Lk
    xSRdQSNO7mDk+1OYzISxk+QAkMObszFe8zyZnL19Y2hhRQbpkHfGv0aAQrDT7JTK
    a2IbwzzCt57wJsV0qYt/HWUcurnExNYP9091NQOk8fnZBz3A/N/JEU/dAXAXjzkN
    GbTUVBwT0BjKRtaeUTAlmVml2BTJVaHvLaDcAErCuM/P9xWmaH2WATobmtktSsbk
    sgm6pSOuPU2PK4hBh+52Ao68/fXhkyrKSgFbLJKWMlwH8R1PiEp9PMCYu4ptySmU
    7bTUoEC8wGPFYZAIM9R/qxALtjoO+MPdCNC22OERcQKBgQDvfchVAf7uY+rOtgJj
    pi6pyGxmMGKlEHUyDEZ0eSx1zIsdlaqd22VcDUdjsMMCJZu1FULKAJ+hYXyqcNjo
    5rbxp0ses01Z815VbIWkh4FRg1Yr25FgKuZjsXBFFThZBR0NR+ZAm64ts0nM2UKQ
    dWRCAXVRSOTFdEmlhxv5hymu6wKBgQCl1bVKAW/wYTr4ItCQJFzd+ZcmejQ+n7G1
    y0cCG0d60+VsWT+QBEv1GuI61IItklfQaQDeOvHaGMGyKRILw4NCWkqIbzeESikl
    vj6eSDr0pd7N8TvL4tnyb83G3PE+X497boARWpD+gaZ8abhpHgGmEqZgduFUY+M5
    JHiLtt1fMwKBgQCV5TlNE4m3NaySotLOAgZ01/AY6bHkNG/V2l8CqZENTe/InZOD
    Z/2B5wHGwKzdnCJf7aW2/zrqbDT2DNmGtFjO2XGoDnuckDl76AehmfpzxWta2fuc
    oF1BCp8Fbimtdgjf9h+EUqxrCCtp3pXOCtusgHMoZCJj/8vUocL9o/6P7wKBgQCZ
    M/qcmQFSAHFbBcbKM128EDYMbP9RD3U8rsQ19P+vQI0F8Nmg8ec+VAzGLsbo1Pbd
    afRMWgZO52oiboDmb0QXC5UpwB140I58+OmejrowhAB3H7KPZE7XA2UGn90bM2s5
    q9cSsSmchihJbd3Y9sitJTOhkJIQxYsIomHfKRwQMwKBgQDIYAp+FcLZstFzyMJx
    yhohuXHlJBplOlZitWiUvlVcTtiMTpDr286Oh55r+0uL842g95xl8SkBe+0+C52y
    KGToqF/k37tVWBoY9wLwfrV3VCSyEGpQ+AUeZbmDXqeHDj/opi63bn4MLAkayWlS
    sOjlPqW1aoUGDzTUyVUfzGbMqQ==
    -----END PRIVATE KEY-----
    

Android签名

android目前支持了V1,V2已经V3签名。V1签名基于JAR签名,V2签名在Android 7.0中引入,V3签名在Android9引入。

为了最大限度地提高兼容性,需要 v1、v2、v3 的先后顺序采用所有方案对应用进行签名。与只通过 v1 方案签名的应用相比,还通过 v2+ 方案签名的应用能够更快速地安装到 Android 7.0 及更高版本的设备上。更低版本的 Android 平台会忽略 v2+ 签名,这就需要应用包含 v1 签名。

v1签名

签名工具

  • jarsigner:jdk自带的签名工具,可以对jar进行签名。并不是针对Android设计的,对于其签名,并不知道关于APK的任何东西。

    • 不能做apk的V2签名
    • 需要在 API 级别 17 或更低级别上运行的 APK,不能够使用SHA-256 摘要,而jarsigner仍然会使用,导致异常
    • 使用 keystore 文件进行签名。生成的签名文件默认使用 keystore 的别名命名
    • 文档传送门:jarSignerGuide
  • apkSigner:Android sdk 提供的专门用于 Android 应用的签名工具。

    • 使用 pk8、x509.pem 文件进行签名。其中 pk8 是私钥文件,x509.pem 是含有公钥的文件。
    • 生成的签名文件统一使用“CERT”命名。
    • 文档传送门:apkSignerGuide

签名过程

签名流程如下所示:

  • 遍历 APK 中的所有文件(此时代码文件已经编译成dex, 资源文件也经过aapt2的编译),即遍历整个ZIP文件。使用 SHA1(或者 SHA256)消息摘要算法提取出该文件的摘要然后进行 BASE64 编码后,作为「SHA1-Digest」属性的值写入到 MANIFEST.MF 文件中的一个块中。
  • 使用SHA1算法对MANIFEST.MF做二次摘要,生成CERT.SF
  • 使用私钥对CERT.SF签名,签名结果和公钥证书一起打包,生成CERT.RSA

MANIFEST.MF

关于MF文件的内容如下所示:

Manifest-Version: 1.0
Built-By: Signflinger
Created-By: Android Gradle 7.0.3Name: AndroidManifest.xml
SHA-256-Digest: MwoB+Vly5/E2An9xENA9ffJSV4eIwCexUWVj5pqFHxY=Name: META-INF/androidx.activity_activity.version
SHA-256-Digest: WYVJhIUxBN9cNT4vaBoV/HkkdC+aLkaMKa8kjc5FzgM=

每一个文件都对应了一个NameSHA-256-Digest,对应的生成代码如下所示:

      String entryDigestAttributeName = getEntryDigestAttributeName(jarEntryDigestAlgorithm);
      for (String entryName : sortedEntryNames) {
            byte[] entryDigest = jarEntryDigests.get(entryName);
            Attributes entryAttrs = new Attributes();
            entryAttrs.putValue(
                    entryDigestAttributeName,
                    Base64.getEncoder().encodeToString(entryDigest));
            ByteArrayOutputStream sectionOut = new ByteArrayOutputStream();
            byte[] sectionBytes;
            try {
                ManifestWriter.writeIndividualSection(sectionOut, entryName, entryAttrs);
                sectionBytes = sectionOut.toByteArray();
                manifestOut.write(sectionBytes);
            } catch (IOException e) {
                throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e);
            }
            invidualSectionsContents.put(entryName, sectionBytes);
        }

其中,

  • jarEntryDigests:提前从APK内读取出来的所有文件计算出来的摘要
  • entryDigestAttributeName: 表示当前使用的摘要算法名称

从上面代码可知,SHA-256-Digest的value值是通过Base64的转换的。

CERT.SF

Signature-Version: 1.0
Created-By: Android Gradle 7.0.3
SHA-256-Digest-Manifest: LrRyCxVO4OGTy0IRq4A24Rek5Tb3OlyCPa+5VDDJvN8=
X-Android-APK-Signed: 2Name: AndroidManifest.xml
SHA-256-Digest: +y53ibnUTz+B9Ji6/nlGHX8JqjaR992RjUAX1is/fCQ=Name: META-INF/androidx.activity_activity.version
SHA-256-Digest: Yu1eiqd7wti3kPabgLC0lsO+1ns/UAhiPGUExHOxH/w=
  • SHA-256-Digest-Manifest:表示的是Manifest.SF整个内容的摘要计算。

            // Add main attribute containing the digest of MANIFEST.MF.
            MessageDigest md = getMessageDigestInstance(manifestDigestAlgorithm);
            mainAttrs.putValue(
                    getManifestDigestAttributeName(manifestDigestAlgorithm),
                    Base64.getEncoder().encodeToString(md.digest(manifest.contents)));
    
  • 下面的NameSHA-256-Digest是针对于Manifest.SF每一个内容的摘要的二次摘要

       for (Map.Entry<String, byte[]> manifestSection
                    : manifest.individualSectionsContents.entrySet()) {
                String sectionName = manifestSection.getKey();
                byte[] sectionContents = manifestSection.getValue();
                byte[] sectionDigest = md.digest(sectionContents);
                Attributes attrs = new Attributes();
                attrs.putValue(
                        entryDigestAttributeName,
                        Base64.getEncoder().encodeToString(sectionDigest));
    

CERT.RSA

尝试打开CERT.RSA,如下所示:

3082 048b 0609 2a86 4886 f70d 0107 02a0
8204 7c30 8204 7802 0101 310f 300d 0609
6086 4801 6503 0402 0105 0030 0b06 092a
8648 86f7 0d01 0701 a082 02e8 3082 02e4
3082 01cc 0201 0130 0d06 092a 8648 86f7
0d01 0105 0500 3037 3116 3014 0603 5504
030c 0d41 6e64 726f 6964 2044 6562 7567
3110 300e 0603 5504 0a0c 0741 6e64 726f

可以看到其实二进制格式的文件,无法直接以文本的形式阅读。

我们看先看它的生成代码如下所示(部分异常抛出已经被过滤):

 private static byte[] generateSignatureBlock(
            SignerConfig signerConfig, byte[] signatureFileBytes) {
        // Obtain relevant bits of signing configuration
        List<X509Certificate> signerCerts = signerConfig.certificates;
        X509Certificate signingCert = signerCerts.get(0);
        PublicKey publicKey = signingCert.getPublicKey();
        DigestAlgorithm digestAlgorithm = signerConfig.signatureDigestAlgorithm;
        Pair<String, AlgorithmIdentifier> signatureAlgs =
                getSignerInfoSignatureAlgorithm(publicKey, digestAlgorithm,
                        signerConfig.deterministicDsaSigning);
        String jcaSignatureAlgorithm = signatureAlgs.getFirst();
        Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
        signature.initSign(signerConfig.privateKey);
        signature.update(signatureFileBytes);
        byte[] signatureBytes = signature.sign();
        AlgorithmIdentifier digestAlgorithmId =
                getSignerInfoDigestAlgorithmOid(digestAlgorithm);
        AlgorithmIdentifier signatureAlgorithmId = signatureAlgs.getSecond();
        return ApkSigningBlockUtils.generatePkcs7DerEncodedMessage(
                signatureBytes,
                null,
                signerCerts, digestAlgorithmId,
                signatureAlgorithmId);
    }
  • 获取当前项目中配置的证书,以X509格式读取
  • 通过配置的证书的私钥和算法, 对SF文件的内容做加密。
  • 将加密数据、证书转化为pkcs7格式的二进制数据

根据上面的第三点,CERT.RSA其实是pkcs7格式的二进制数据,对于pkcs7数据,可以通过openssl来阅读

具体命令如下:

openssl pkcs7 -inform DER -in /Users/xiejinlong/AndroidStudioProjects/MyTest/app/build/outputs/apk/debug/app-debug/META-INF/CERT.RSA   -print_certs -text

内容如下:

Certificate:
    Data:
        Version: 1 (0x0)
        Serial Number: 1 (0x1)
    Signature Algorithm: sha1WithRSAEncryption
        Issuer: CN=Android Debug, O=Android, C=US
        Validity
            Not Before: Feb 18 09:19:15 2022 GMT
            Not After : Feb 11 09:19:15 2052 GMT
        Subject: CN=Android Debug, O=Android, C=US
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:df:e8:96:0b:80:c0:9e:9b:53:24:5f:d2:a1:ae:
                    f3:ed:7d:52:a7:52:89:ab:4b:c7:4f:55:e2:95:be:
                    6f:f5:39:0a:b3:8b:f2:b3:05:4a:6f:d2:fc:23:fa:
                    0d:1b:1a:4c:78:3c:a0:df:28:57:b3:b4:43:ea:77:
                    15:cf:78:eb:0a:91:0a:54:35:b0:5a:62:b4:cb:13:
                    53:53:eb:71:3b:fe:53:c1:6c:96:4d:f2:28:c3:68:
                    6f:29:28:59:30:3e:e7:7b:3a:90:78:c3:d3:10:02:
                    0a:45:1b:4f:b3:a0:39:2d:d7:e8:e1:0b:95:2d:f5:
                    34:7d:ce:d0:4f:71:1f:2a:ee:4f:ee:db:3e:79:3f:
                    68:ef:b4:c3:45:63:f6:f5:4a:96:2d:c2:78:3d:0e:
                    3a:95:c3:bf:fc:15:ad:ab:79:05:1a:f0:68:5e:95:
                    74:10:be:d7:96:a6:f1:90:51:32:83:84:32:81:b8:
                    2c:88:85:e6:12:25:08:03:89:38:0b:73:58:25:a0:
                    f5:9b:11:8b:ba:a1:84:10:d3:eb:43:15:de:2f:85:
                    f6:5f:a6:ed:40:9a:10:8c:dc:a4:de:dd:e0:28:9a:
                    af:0f:da:10:47:28:b4:28:26:5e:08:7b:0d:b5:fa:
                    d0:d9:61:97:2e:47:d5:c3:88:3b:bb:e5:ab:3a:70:
                    43:dd
                Exponent: 65537 (0x10001)
    Signature Algorithm: sha1WithRSAEncryption
         09:53:f4:6b:9e:2c:50:f0:c6:92:26:c1:a6:35:d7:68:b3:f5:
         42:9a:de:43:93:a1:17:6a:c8:f6:11:ef:de:62:72:80:14:3e:
         4a:36:a2:a5:47:19:cb:61:8e:b4:20:c7:84:9e:4f:3f:56:3a:
         fa:c8:2b:c4:a4:e4:2c:10:10:dc:9b:09:8d:d8:5a:24:ca:6f:
         b2:f2:cc:1c:d1:7b:c6:86:ae:96:6a:36:57:8a:ce:ae:23:f5:
         df:56:4d:4f:5e:2c:37:5a:b6:31:3b:f6:11:f6:9c:7b:33:9a:
         c4:d3:50:ef:c3:22:a9:da:de:7d:d1:56:fe:83:7d:27:91:07:
         cf:70:37:86:26:1d:e2:c2:16:79:45:54:67:eb:ad:13:c3:e1:
         2f:2b:3b:e8:07:95:8e:20:03:13:01:e3:c3:48:68:09:1a:fd:
         10:5a:28:08:60:64:b3:46:85:40:8c:43:0e:87:ce:60:e8:e1:
         98:68:69:2e:30:96:2a:79:a8:23:a8:cb:36:73:b8:32:40:a6:
         bd:e4:56:d4:ad:ea:da:9e:a0:bc:e3:54:00:67:a5:01:ea:cb:
         27:c9:15:df:6f:6e:04:09:6d:83:99:0a:56:8d:60:d5:2d:8b:
         3f:c1:10:3d:a1:1a:77:54:1d:43:76:f0:68:27:8c:b5:7a:57:
         a2:5b:0d:e0
-----BEGIN CERTIFICATE-----
MIIC5DCCAcwCAQEwDQYJKoZIhvcNAQEFBQAwNzEWMBQGA1UEAwwNQW5kcm9pZCBE
ZWJ1ZzEQMA4GA1UECgwHQW5kcm9pZDELMAkGA1UEBhMCVVMwIBcNMjIwMjE4MDkx
OTE1WhgPMjA1MjAyMTEwOTE5MTVaMDcxFjAUBgNVBAMMDUFuZHJvaWQgRGVidWcx
EDAOBgNVBAoMB0FuZHJvaWQxCzAJBgNVBAYTAlVTMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEA3+iWC4DAnptTJF/Soa7z7X1Sp1KJq0vHT1Xilb5v9TkK
s4vyswVKb9L8I/oNGxpMeDyg3yhXs7RD6ncVz3jrCpEKVDWwWmK0yxNTU+txO/5T
wWyWTfIow2hvKShZMD7nezqQeMPTEAIKRRtPs6A5Ldfo4QuVLfU0fc7QT3EfKu5P
7ts+eT9o77TDRWP29UqWLcJ4PQ46lcO//BWtq3kFGvBoXpV0EL7XlqbxkFEyg4Qy gbgsiIXmEiUIA4k4C3NYJaD1mxGLuqGEENPrQxXeL4X2X6btQJoQjNyk3t3gKJqv D9oQRyi0KCZeCHsNtfrQ2WGXLkfVw4g7u+WrOnBD3QIDAQABMA0GCSqGSIb3DQEB BQUAA4IBAQAJU/RrnixQ8MaSJsGmNddos/VCmt5Dk6EXasj2Ee/eYnKAFD5KNqKl
RxnLYY60IMeEnk8/Vjr6yCvEpOQsEBDcmwmN2Fokym+y8swc0XvGhq6WajZXis6u
I/XfVk1PXiw3WrYxO/YR9px7M5rE01DvwyKp2t590Vb+g30nkQfPcDeGJh3iwhZ5
RVRn660Tw+EvKzvoB5WOIAMTAePDSGgJGv0QWigIYGSzRoVAjEMOh85g6OGYaGku
MJYqeagjqMs2c7gyQKa95FbUreranqC841QAZ6UB6ssnyRXfb24ECW2DmQpWjWDV
LYs/wRA9oRp3VB1DdvBoJ4y1eleiWw3g
-----END CERTIFICATE-----

以上就是签名APK签名的过程。对应的图如下所示:

Android打包流程-签名

验证过程

相对应的安装APK时,如果在V2、V3的APK检验有问题或者在Android7以下的系统,都会使用V1签名校验

  1. 从待安装的apk文件中,收集MF、SF和RSA文件
  2. 使用CERT.RSA检验CERT.SF没有被修改过。
  3. 使用CERT.SF 检验 MANIFEST.MF 文件没有被修改过。
  4. 检查 APK 中包含的所有文件,对应的摘要值与 MANIFEST.MF 文件中记录的值一致。

收集签名文件

CentralDirectoryRecord manifestEntry = null;
            Map<String, CentralDirectoryRecord> sigFileEntries = new HashMap<>(1);
            List<CentralDirectoryRecord> sigBlockEntries = new ArrayList<>(1);
            for (CentralDirectoryRecord cdRecord : cdRecords) {
                String entryName = cdRecord.getName();
                if (!entryName.startsWith("META-INF/")) {
                    continue;
                }
                if ((manifestEntry == null) && (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals(
                        entryName))) {
                    manifestEntry = cdRecord;
                    continue;
                }
                if (entryName.endsWith(".SF")) {
                    sigFileEntries.put(entryName, cdRecord);
                    continue;
                }
                if ((entryName.endsWith(".RSA"))
                        || (entryName.endsWith(".DSA"))
                        || (entryName.endsWith(".EC"))) {
                    sigBlockEntries.add(cdRecord);
                    continue;
                }
            }

校验CERT.SF

    int extensionDelimiterIndex = sigBlockEntryName.lastIndexOf('.');
    String sigFileEntryName = sigBlockEntryName.substring(0, extensionDelimiterIndex) + ".SF";
    CentralDirectoryRecord sigFileEntry = sigFileEntries.get(sigFileEntryName);
    Signer signer = new Signer(signerName, sigBlockEntry, sigFileEntry, signerInfo);
    signer.verifySigBlockAgainstSigFile(apk, cdStartOffset, minSdkVersion, maxSdkVersion);

具体的验证逻辑是:

  • 按照当前的SF内容重新加密出一份数据
  • 当前的RSA的数字签名做比对。

校验MANIFEST.MF

    signer.verifySigFileAgainstManifest(
                        manifestBytes,
                        manifestMainSection,
                        entryNameToManifestSection,
                        supportedApkSigSchemeNames,
                        foundApkSigSchemeIds,
                        minSdkVersion,
                        maxSdkVersion);

具体的验证逻辑:

  • 验证MIFEST.MF文件的的全量内容是否被修改
  • 验证MANIFEST.MF文件中的每一条记录是否被修改

校验APK内文件列表

  Set<Signer> apkSigners = verifyJarEntriesAgainstManifestAndSigners(
                            apk,
                            cdStartOffset,
                            cdRecords,
                            entryNameToManifestSection,
                            signers,
                            minSdkVersion,
                            maxSdkVersion,
                            result);
                            
                                       Set<String> signatureEntryNames = new HashSet<>(1 + result.signers.size() * 2);
            signatureEntryNames.add(manifestEntry.getName());
            for (Signer signer : apkSigners) {
                signatureEntryNames.add(signer.getSignatureBlockEntryName());
                signatureEntryNames.add(signer.getSignatureFileEntryName());
            }
            for (CentralDirectoryRecord cdRecord : cdRecords) {
                String entryName = cdRecord.getName();
                if ((entryName.startsWith("META-INF/"))
                        && (!entryName.endsWith("/"))
                        && (!signatureEntryNames.contains(entryName))) {
                    result.addWarning(Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY, entryName);
                }
            }

如果所有文件都验证通过,就说明当前的V1签名验证通过。

对应的流程如下图所示:

Android打包流程-签名

v2签名

在Android7.0之后开始支持了V2签名。因为V2签名会对原始的APK结构有新增内容,需要先简单了解一下APK的结构,也就是ZIP文件的结构。

先看看ZIP结构的图例:

Android打包流程-签名

v2 签名会在原先 APK 块中增加了一个新的块APK Signing Block(签名块)。

Android打包流程-签名

新的块存储了签名、摘要、签名算法、证书链和额外属性等信息,这个块有特定的格式。

最终的签名APK其实就有四块:头文件区、V2签名块、中央目录、尾部。

V2签名块的内容如下:

  • block块长度:整个签名块的长度

  • ID-Value的序列:签名数据列表

    • SignerData(签名者数据):主要包括签名者的证书,整个APK完整性校验hash,以及一些必要信息
    • Signature(签名):开发者对SignerData部分数据的签名数据
    • PublicKey(公钥):用于验签的公钥数据

    对应的数据结构结构为:

    private static final class V2SignatureSchemeBlock {
            private static final class Signer {
                public byte[] signedData;
                public List<Pair<Integer, byte[]>> signatures;
                public byte[] publicKey;
            }
    ​
            private static final class SignedData {
                public List<Pair<Integer, byte[]>> digests;
                public List<byte[]> certificates;
                public byte[] additionalAttributes;
            }
        }
    
  • block块长度:整个签名块的长度

  • magic: 魔数

签名流程

整体的签名流程如下所示:

Android打包流程-签名

把APK按照1M大小进行分割,分别计算这些分段的摘要,最后把这些分段的摘要再进行计算得到最终的摘要也就是 APK 的摘要。然后将 APK 的摘要 + 数字证书 + 其他属性生成签名数据写入到 APK Signing Block 区块。

static void computeOneMbChunkContentDigests(
            RunnablesExecutor executor,
            Set<ContentDigestAlgorithm> digestAlgorithms,
            DataSource[] contents,
            Map<ContentDigestAlgorithm, byte[]> outputContentDigests)
            throws NoSuchAlgorithmException, DigestException {
        long chunkCountLong = 0;
        for (DataSource input : contents) {
            chunkCountLong += getChunkCount(input.size(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
        }
        int chunkCount = (int) chunkCountLong;
​
        List<ChunkDigests> chunkDigestsList = new ArrayList<>(digestAlgorithms.size());
        for (ContentDigestAlgorithm algorithms : digestAlgorithms) {
            chunkDigestsList.add(new ChunkDigests(algorithms, chunkCount));
        }
​
        ChunkSupplier chunkSupplier = new ChunkSupplier(contents);
        executor.execute(() -> new ChunkDigester(chunkSupplier, chunkDigestsList));
​
        // Compute and write out final digest for each algorithm.
        for (ChunkDigests chunkDigests : chunkDigestsList) {
            MessageDigest messageDigest = chunkDigests.createMessageDigest();
            outputContentDigests.put(
                    chunkDigests.algorithm,
                    messageDigest.digest(chunkDigests.concatOfDigestsOfChunks));
        }
    }
  • 首先计算出当前的内容有个Chunk, CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES表示的大小为1024*1024
  • 根据chunk,计算出来各个chunk的摘要。

最终,将这个摘要写入到zip的signBlock中。

    ApkSignerEngine.OutputApkSigningBlockRequest2 outputApkSigningBlockRequest =
                signerEngine.outputZipSections2(
                        outputApkIn,
                        outputCentralDirDataSource,
                        DataSources.asDataSource(outputEocd));
​
        if (outputApkSigningBlockRequest != null) {
            int padding = outputApkSigningBlockRequest.getPaddingSizeBeforeApkSigningBlock();
            outputApkOut.consume(ByteBuffer.allocate(padding));
            byte[] outputApkSigningBlock = outputApkSigningBlockRequest.getApkSigningBlock();
            outputApkOut.consume(outputApkSigningBlock, 0, outputApkSigningBlock.length);
            ZipUtils.setZipEocdCentralDirectoryOffset(
                    outputEocd,
                    outputCentralDirStartOffset + padding + outputApkSigningBlock.length);
            outputApkSigningBlockRequest.done();
        }

校验流程:

解密流程如下图所示:

Android打包流程-签名

  • 找到APK Signing Block块,并验证下面几个内容

    • 验证签名模块中的两个block块长度是否一致
    • 验证ZIP是否是否正确
  • 利用公钥对signerData数据进行解密,得到signerData的明文数据

  • 验证SignedData中digests列表的ID与signatures中的ID是否一致

  • 使用签名算法所用的同一种摘要算法计算APK内容的摘要

V3签名

Android 9 新增了对 APK Signature Scheme v3 的支持。V3签名在V2签名的基础上,实现了新旧秘钥的的替换。通过在ID-Value的序列中添加ID为0xf05368c0标识为V3签名。在APK没有替换签名的场景下,仅使用V2就可以满足需求。

签名流程

V3签名的主要逻辑和V2签名基本一致,仍然采用检查整个压缩包的校验方式。

不同的是在signedData之上新增了Attribute块。在这个块中,可以记录不同版本的签名文件,用来做签名的替换和升级。

    public static byte[] generateV3SignerAttribute(
            SigningCertificateLineage signingCertificateLineage) {
        byte[] encodedLineage = signingCertificateLineage.encodeSigningCertificateLineage();
        int payloadSize = 4 + 4 + encodedLineage.length;
        ByteBuffer result = ByteBuffer.allocate(payloadSize);
        result.order(ByteOrder.LITTLE_ENDIAN);
        result.putInt(4 + encodedLineage.length);
        result.putInt(V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID);
        result.put(encodedLineage);
        return result.array();
    }
  • PROOF_OF_ROTATION_ATTR_ID:表示当前value紧跟的是签名列表。

验证流程

Android打包流程-签名

V4签名

Android 11 通过 APK 签名方案 v4 支持与流式传输兼容的签名方案。v4 签名基于根据 APK 的所有字节计算得出的 Merkle 哈希树。

主要的使用场景是ADB的增量APK安装

ADB 增量 APK 安装
在设备上安装大型(2GB 以上)APK 可能需要很长的时间,即使应用只是稍作更改也是如此。ADB(Android 调试桥)增量 APK 安装可以安装足够的 APK 以启动应用,同时在后台流式传输剩余数据,从而加速这一过程。如果设备支持该功能,并且您安装了最新的 SDK 平台工具,adb install 将自动使用此功能。如果不支持,系统会自动使用默认安装方法。
https://developer.android.google.cn/about/versions/11/features
​
运行以下 adb 命令以使用该功能。如果设备不支持增量安装,该命令将会失败并输出详细的解释。
​
​
adb install --incremental
​
​
在运行 ADB 增量 APK 安装之前,您必须先为 APK 签名并创建一个 APK 签名方案 v4 文件。必须将 v4 签名文件放在 APK 旁边,才能使此功能正常运行。
https://developer.android.google.cn/about/versions/11/features

Android 11 将签名存储在单独的 <apk name>.apk.idsig 文件中。v4 签名需要 v2v3 签名作为补充。

V4签名文件内容:

struct V4Signature {
        int32 version; // only version 2 is supported as of now
        sized_bytes<int32> hashing_info;
        sized_bytes<int32> signing_info;
        sized_bytes<int32> merkle_tree;  // optional
};
  • hashing_info: 用于Hash文件的生成

    struct hashing_info.bytes {
        int32 hash_algorithm;    // only 1 == SHA256 supported
        int8 log2_blocksize;     // only 12 (block size 4096) supported now
        sized_bytes<int32> salt; // used exactly as in fs-verity, 32 bytes max
        sized_bytes<int32> raw_root_hash; // salted digest of the first Merkle tree page
    };
    
  • signing_info: 摘要、签名相关信息

    struct signing_info.bytes {
        sized_bytes<int32> apk_digest;  // used to match with the corresponding APK
        sized_bytes<int32> x509_certificate; // ASN.1 DER form
        sized_bytes<int32> additional_data; // a free-form binary data blob
        sized_bytes<int32> public_key; // ASN.1 DER, must match the x509_certificate
        int32 signature_algorithm_id; // see the APK v2 doc for the list
        sized_bytes<int32> signature;
    };
    

签名流程

  • 源码传送门:V4签名流程
  • 根据 APK 的所有字节计算得出的 Merkle 哈希树
  • 生成签名文件中的hashing_info
  • 生成签名文件中的signing_info

验证流程

如图所示:

Android打包流程-签名

多渠道打包

在有了APK签名下,如何不通过重签名实现多渠道打包就显得格外重要了。

V1签名多渠道打包

前面说到,V1签名是对整个APK文件做数据摘要,意味着我们不能去修改APK的文件内容,那是不是就没有办法在APK内加入渠道信息了呢?其实不是的,在前面说到,整个APK文件是一个ZIP结构。

Android打包流程-签名

  • Contents of Zip entries: 称为内容块,主要用来存放数据
  • Central Directory: 称为中央目录,主要用来记录数据压缩算法以及数据偏移量,用于快速定位
  • End of Central Directory: 称为EOCD, 主要记录中央目录大小、偏移量和 ZIP 注释信息等

在V1签名中,只有内容块(Contents of Zip entries)会计算摘要,也就是说并不会校验后面两部分内容,所以可以把渠道信息放在后面两块区域中。大部分的多渠道打包工具都会把渠道信息写入到EOCD的注释信息中。

V2签名多渠道打包

Android打包流程-签名

如上图所示,在V2签名中,通过把APK分为各个chunk,对每一个chunk做摘要来保证数据不被修改,内容块、中央目录、EOCD都是受保护的模块,也就意味着我们给V1签名加渠道信息的方案无法直接应用到V2签名上。但是V2签名仍然有个模块时不会校验的,也就是存储V2签名信息的APK Signing Block块。这个块中存储了ID-Value的键值对列表,可以把渠道信息追加到ID-Value的键值对列表之后。

本文属于学习过程中的记录,有不对的地方请谅解下可以提出来

参考资料:

  1. 为应用签名:developer.android.com/studio/publ…
  2. source.android.com/security/ap…
  3. 提取jks证书的公钥和私钥: blog.csdn.net/u013412772/…
  4. zhuanlan.zhihu.com/p/89126018
  5. xuanxuanblingbling.github.io/ctf/android…
  6. cloud.tencent.com/developer/a…

今天的文章Android打包流程-签名分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/21881.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注