Android源码(7)-Android系统build阶段签名机制

本文介绍Android系统build阶段的签名机制。

一、系统build阶段签名机制

  1. 系统中有4组key用于build阶段对apk进行签名:
  • Media
  • Platform
  • Shared
  • Testkey

default key是放在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;
    其中,*.pk8文件为私钥,*.x509.pem文件为公钥,这需要去了解非对称加密方式。
  1. 在apk的android.mk文件中会指定LOCAL_CERTIFICATE 变量:

LOCAL_CERTIFICATE可设置的值如下:

1
2
3
4
5
LOCAL_CERTIFICATE := testkey   # 普通APK,默认情况下使用
LOCAL_CERTIFICATE := platform # 该APK完成一些系统的核心功能,这种方式编译出来的APK所在进程的UID为system
LOCAL_CERTIFICATE := shared # 该APK是media/download系统中的一环
LOCAL_CERTIFICATE := media # 该APK是media/download系统中的一环
如果不指定,默认使用testkey。

对应的,除了在Android.mk指定上述的值,还需要在APK源码的AndroidManifest.xml文件的manifest节点里面申明权限:

1
2
3
android:sharedUserId="android.uid.system"
android:sharedUserId="android.uid.shared"
android:sharedUserId="android.media"
  1. Build规则是Build/core/prebuilt.mk。
  2. 在build/core/config.mk中,DEFAULT_SYSTEM_DEV_CERTIFICATE可以通过PRODUCT_DEFAULT_DEV_CERTIFICATE去指定各家厂商的key path。

我们可以看到,默认为build/target/product/security/testkey。

1
2
3
4
5
6
# The default key if not set as LOCAL_CERTIFICATE
ifdef PRODUCT_DEFAULT_DEV_CERTIFICATE
DEFAULT_SYSTEM_DEV_CERTIFICATE := $(PRODUCT_DEFAULT_DEV_CERTIFICATE)
else
DEFAULT_SYSTEM_DEV_CERTIFICATE := build/target/product/security/testkey
endif

二、自定义系统签名的key

上面介绍了系统有默认四组key,那么如果我们要制作自己的key,需要怎么做呢?
在build/target/product/security/目录下有一个README,里面有说明怎么制作这些key并且使用。

  1. 进入Development/tools/ 目录

2

  1. 使用make_key工具生成签名文件:
    1
    sh make_key releasekey  '/C=CN/ST=Guangdong/L=Shenzhen/O=Mediatek/OU=MTK/CN=fzll/emailAddress=maoao530@foxmail.com'
    其中:

C : Country Name (2 letter code)
ST : State or Province Name (full name)
L : Locality Name (eg, city)
O : Organization Name (eg, company)
OU : Organizational Unit Name (eg, section)
CN : Common Name (eg, your name or your server’s hostname)
emailAddress : Contact email address
3. 用ls命令发现目录下多了两个文件:releasekey.x509.pem 和 releasekey.pk8

1

  1. 同样的步骤生成platform / shared / media

  2. 用自定义的key替换build/target/product/security/目录下面的key。

三、对APK进行系统签名
为了使apk有system权限,通常我们需要对其进行系统签名:

1、在应用程序的AndroidManifest.xml中的manifest节点中加入

1
android:sharedUserId="android.uid.system"这个属性。

2、修改它的Android.mk文件,加入

1
2
LOCAL_CERTIFICATE := platform
重新编译,生成的apk就有修改system权限了,我们通过ps命令查看APK所在进程的UID,发现值为system。

三. Android签名机制之—签名过程详解

  1. 两种签名方式
    • jarsigner
    • signapk
  2. 签名流程机制
    源码位置:com/android/signapk/sign.java

    通过上面的签名时我们可以看到,Android签名apk之后,会有一个META-INF文件夹,这里有三个文件:
    MANIFEST.MF
    CERT.RSA
    CERT.SF
    下面来看看这三个文件到底是干啥的?
  • MANIFEST.MF
    1
    开下源码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    【main】
    public static void main(String[] args) {
    if (args.length < 4) usage();

    sBouncyCastleProvider = new BouncyCastleProvider();
    Security.addProvider(sBouncyCastleProvider);

    boolean signWholeFile = false;
    int argstart = 0;
    if (args[0].equals("-w")) {
    signWholeFile = true;
    argstart = 1;
    }

    if ((args.length - argstart) % 2 == 1) usage();
    int numKeys = ((args.length - argstart) / 2) - 1;
    if (signWholeFile && numKeys > 1) {
    System.err.println("Only one key may be used with -w.");
    System.exit(2);
    }

    String inputFilename = args[args.length-2];
    String outputFilename = args[args.length-1];

    JarFile inputJar = null;
    FileOutputStream outputFile = null;
    int hashes = 0;

    try {
    File firstPublicKeyFile = new File(args[argstart+0]);

    X509Certificate[] publicKey = new X509Certificate[numKeys];
    try {
    for (int i = 0; i < numKeys; ++i) {
    int argNum = argstart + i*2;
    publicKey[i] = readPublicKey(new File(args[argNum]));
    hashes |= getAlgorithm(publicKey[i]);
    }
    } catch (IllegalArgumentException e) {
    System.err.println(e);
    System.exit(1);
    }

    // Set the ZIP file timestamp to the starting valid time
    // of the 0th certificate plus one hour (to match what
    // we've historically done).
    long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;

    PrivateKey[] privateKey = new PrivateKey[numKeys];
    for (int i = 0; i < numKeys; ++i) {
    int argNum = argstart + i*2 + 1;
    privateKey[i] = readPrivateKey(new File(args[argNum]));
    }
    inputJar = new JarFile(new File(inputFilename), false); // Don't verify.

    outputFile = new FileOutputStream(outputFilename);


    if (signWholeFile) {
    SignApk.signWholeFile(inputJar, firstPublicKeyFile,
    publicKey[0], privateKey[0], outputFile);
    } else {
    JarOutputStream outputJar = new JarOutputStream(outputFile);

    // For signing .apks, use the maximum compression to make
    // them as small as possible (since they live forever on
    // the system partition). For OTA packages, use the
    // default compression level, which is much much faster
    // and produces output that is only a tiny bit larger
    // (~0.1% on full OTA packages I tested).
    outputJar.setLevel(9);

    Manifest manifest = addDigestsToManifest(inputJar, hashes);
    copyFiles(manifest, inputJar, outputJar, timestamp);
    signFile(manifest, inputJar, publicKey, privateKey, outputJar);
    outputJar.close();
    }
    } catch (Exception e) {
    e.printStackTrace();
    System.exit(1);
    } finally {
    try {
    if (inputJar != null) inputJar.close();
    if (outputFile != null) outputFile.close();
    } catch (IOException e) {
    e.printStackTrace();
    System.exit(1);
    }
    }
    }

    【signWholeFile中调用 signFile】
    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;

    // MANIFEST.MF
    JarEntry 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, getAlgorithm(publicKey[k]));
    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);
    }
    }

    【main函数中 addDigestsToManifest】
    /**
    * Add the hash(es) of every file to the manifest, creating it if
    * necessary.
    */
    private static Manifest addDigestsToManifest(JarFile jar, int hashes)
    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_sha1 = null;
    MessageDigest md_sha256 = null;
    if ((hashes & USE_SHA1) != 0) {
    md_sha1 = MessageDigest.getInstance("SHA1");
    }
    if ((hashes & USE_SHA256) != 0) {
    md_sha256 = MessageDigest.getInstance("SHA256");
    }

    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) {
    if (md_sha1 != null) md_sha1.update(buffer, 0, num);
    if (md_sha256 != null) md_sha256.update(buffer, 0, num);
    }

    Attributes attr = null;
    if (input != null) attr = input.getAttributes(name);
    attr = attr != null ? new Attributes(attr) : new Attributes();
    if (md_sha1 != null) {
    attr.putValue("SHA1-Digest",
    new String(Base64.encode(md_sha1.digest()), "ASCII"));
    }
    if (md_sha256 != null) {
    attr.putValue("SHA-256-Digest",
    new String(Base64.encode(md_sha256.digest()), "ASCII"));
    }
    output.getEntries().put(name, attr);
    }
    }

    return output;
    }
    总结:生成三个文件 MANIFEST.MF,CERT.SF,CERT.RSA
  1. // MANIFEST.MF
    JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
    je.setTime(timestamp);
    outputJar.putNextEntry(je);
    manifest.write(outputJar);

    除了三个文件(MANIFEST.MF,CERT.RSA,CERT.SF),其他的文件都会对文件内容做一次SHA1算法,就是计算出文件的摘要信息,然后用Base64进行编码即可,保存到MANIFEST.MF

  2. // 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, getAlgorithm(publicKey[k]));
byte[] signedData = baos.toByteArray();
outputJar.write(signedData);

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

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

  1. // 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);

    我们看到,这里会把之前生成的 CERT.SF文件, 用私钥计算出签名, 然后将签名以及包含公钥信息的数字证书一同写入 CERT.RSA 中保存。CERT.RSA是一个满足PKCS7格式的文件。

四. 画个图表示

signapk