栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Android Update Engine 分析(十二) 验证 payload 签名

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Android Update Engine 分析(十二) 验证 payload 签名

相关文章:

Android A/B System 分析系列

Android A/B System OTA 分析(一)概览Android A/B System OTA 分析(二)系统image的生成Android A/B System OTA 分析(三)主系统和bootloader的通信Android A/B System OTA 分析(四)系统的启动和升级Android A/B System OTA 分析(五)客户端参数Android A/B System OTA 分析(六)如何获取 payload 的 offset 和 size

Android Update Engine 分析系列

Android Update Engine 分析(一)MakefileAndroid Update Engine 分析(二)Protobuf和AIDL文件Android Update Engine 分析(三)客户端进程Android Update Engine 分析(四)服务端进程Android Update Engine 分析(五)服务端核心之Action机制Android Update Engine 分析(六)服务端核心之Action详解Android Update Engine 分析(七)DownloadAction之FileWriterAndroid Update Engine 分析(八)升级包制作脚本分析Android Update Engine 分析(九)delta_generator 工具的 6 种操作Android Update Engine 分析(十)生成 payload 和 metadata 的哈希Android Update Engine 分析(十一) 更新 payload 签名Android Update Engine 分析(十二) 验证 payload 签名

在《Android Update Engine 分析(九)delta_generator 工具的 6 种操作》中提到了,delta_generator 工具提供的 6 种操作,分别是:

    生成 payload 和 metadata 数据的哈希值更新 payload 和 metadata 数据的签名使用公钥验证 payload 和 metadata 数据的签名提取 payload 文件的 properties数据对 old image 打 delta 补丁生成 payload 数据

上两篇详细分析了 metadata 和 payload 的哈希如何生成,如何签名,如何更新到 payload 文件中,本篇继续分析代码是如何验证 payload 签名的,包括代码中验证签名的流程和签名在命令行的手动验证。

本篇主要内容有三点:

    如何提供公钥和验证签名总结 payload 的处理流程手动在命令行提取 payload 的数据进行验证

如果只想知道验证的公钥是如何生成的,请参考 “1.2 如何生成验证的公钥”。如果只想简单看下 payload 和 metadata 签名的调用流程,请参考 “4.1 验证 payload 和 metadata 签名的流程”。如果只想知道如何在命令行提取和验证 payload 和 metadata 签名,请参考 “4.2 手动签名验证总结”

本文涉及的Android代码版本:android‐7.1.1_r23 (NMF27D)

1. 使用公钥验证 payload 签名 1.1 使用公钥验证签名

上一篇《Android Update Engine 分析(十一) 更新 payload 签名》的第 1 节又一次总结了 payload 数据的处理流程,这里不再重复,直接进入本文的重点。

密码学上,签名和验证使用非对称密钥,其中公开的密钥叫公钥,没有公开的密钥叫私钥。签名时使用私钥,验证签名(验签)时使用公钥,即所谓的私钥签名,公钥验证。

如果调用 delta_generator 工具时提供了 --public_key 参数,则会使用这个 key 来验证 payload 文件的签名,例如:

$ delta_generator --in_file=payload.bin --public_key=key-pub.key
[0121/095347:INFO:generate_delta_main.cc(172)] Verifying signed payload.
[0121/095348:INFO:payload_verifier.cc(93)] signature blob size = 264
[0121/095348:INFO:payload_verifier.cc(112)] Verified correct signature 1 out of 1 signatures.
[0121/095348:INFO:payload_verifier.cc(93)] signature blob size = 264
[0121/095348:INFO:payload_verifier.cc(112)] Verified correct signature 1 out of 1 signatures.
[0121/095348:INFO:generate_delta_main.cc(178)] Done verifying signed payload.

关于关于参数的传递和解析,这里不再详细分析,不清楚的请转到《Android Update Engine 分析(九)delta_generator 工具的 6 种操作》 第 2.2 节,查看参数到底是如何解析的。

1.2 如何生成验证的公钥

这里验证的公钥到底是怎么来的?

在对 payload 进行签名时,如果没有提供签名用的 key,则默认从 build/target/product/security/testkey.pk8 文件中导出一个 RSA 的私钥用于签名,如下:

# 如果没有指定签名使用的 key, 则基于 testkey.pk8 生成一个临时 key 用于签名
openssl pkcs8 -in build/target/product/security/testkey.pk8 -inform DER -nocrypt -out /tmp/key-temp.key

Android 默认附带的 test key 文件 build/target/product/security/testkey.pk8 是一个按照 PKCS#8 格式的存储的私钥,而签名时需要的是 PKCS#1 格式的密钥,因此这里的 openssl pkcs8 命令就是将 PKCS#8 格式的私钥转换成 PKCS#1 格式的私钥。

这里导出的 PKCS#1 格式的私钥可用于签名,如果要验证签名的话,还需要从私钥中提取 PEM 格式的公钥:

$ openssl rsa -in key-temp.key -pubout -outform PEM -out key-pub.key
$ cat key-pub.key 
-----BEGIN PUBLIC KEY-----
MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKCAQEA1pMZBN7GCySx7cdi4NnY
JT4+zWzrHeL/Boyo6LyozWvTeG6nCqds5g67D5k1Wf/ZPnepQ+foPUtkuOT+otPm
VvHiZ6gbv7IwtXjCBEO+THIYuEb1IRWG8DihTonCvjh/jr7Pj8rD2h7jMMnqk9Cn
w9xK81AiDVAIBzLggJcX7moFM1nmppTsLLPyhKCkZsh6lNg7MQk6ZzcuL2QSwG5t
QvFYGN/+A4HMDNRE2mzdw7gkWBlIAbMlZBNPv96YySh3SNv1Z2pUDYFUyLvKB7ni
R1UzEcRrmvdv3uzMjmnnyKLQjngmIJQ/mXJ9PAT+cpkdmd+brjigshd/ox1bav7p
HwIBAw==
-----END PUBLIC KEY-----

说下这里的参数:

-in key-temp.key, 指定用于处理的私钥文件-pubout, 即 public key out,输出公钥文件-outform PEM,输出格式为 PEM (有多种格式可选,比如 PEM,DER 等,这里使用最常用的 PEM 格式)-out key-pub.key, 指定输出的公钥文件名字 1.3 关于公钥 brillo-update-payload-key.pub.pem

仔细观察下 update_engine 代码的目录结构,发现系统在 update_payload_key 下提供了一个公钥:

$ ls -lh update_payload_key/
total 8.0K
-rw-r--r-- 1 rg935739 users 138 Mar 31  2017 README
-rw-r--r-- 1 rg935739 users 451 Mar 31  2017 brillo-update-payload-key.pub.pem
$ cat update_payload_key/brillo-update-payload-key.pub.pem 
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxAxPqfII4vIe3cqKzdvl
gwjBhj9kyF+6ig73yZq0o4wLOq3nsRUToaIOtQmcjr1G+hhSXBU3WTbfZLlm07Fb
B535o2zhYghs8Br7xobjX+gikEnxnFuTtB2sB4Gpan4hKwU+BuZhJDSl1oZwUJJ4
eiGJpH5xJswbyO/bA81BCMjU3rm+G6SzOLQTK0YEnhn7bB69UucM57GM7l+dCl8r
RhKjbpP7E1fVtgX++BGs6pKciPLxYfXVup0MgH0h8VdSDMiHkshIXYvcCV1KOBFX
9GrYvXLtq41Hm5hC5l48mwLi0ALdIfbPQ5oHLl2u+etLmGwbMpzhybTCZQA/SgEl
HwIDAQAB
-----END PUBLIC KEY-----

这里的 brillo-update-payload-key.pub.pem 是 PEM 格式的公钥, 在 Makefile 中是这样引用的:

# Update payload signing public key.
# ========================================================
ifdef BRILLO
include $(CLEAR_VARS)
LOCAL_MODULE := brillo-update-payload-key
LOCAL_MODULE_CLASS := ETC
LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)/update_engine
LOCAL_MODULE_STEM := update-payload-key.pub.pem
LOCAL_SRC_FILES := update_payload_key/brillo-update-payload-key.pub.pem
LOCAL_BUILT_MODULE_STEM := update_payload_key/brillo-update-payload-key.pub.pem
include $(BUILD_PREBUILT)
endif  # BRILLO

只不过我们在《Android Update Engine 分析(一)Makefile》分析过,Android 系统中 BRILLO 没有被定义,所以这个公钥不会被使用。忙活了半天,空欢喜一场。

2. payload 签名验证流程

上一节演示了如何使用命令 delta_generator --in_file=payload.bin --public_key=key-pub.key 来验证签名,本节从代码角度分析整个签名验证的过程。

2.1 Main 函数调用 VerifySignedPayload

在 generate_delta_main.cc 文件的 Main 函数中,检测到提供了 --public_key 参数,则调用 VerifySignedPayload 去验证签名:

// 文件: system/update_engine/payload_generator/generate_delta_main.cc
  if (!FLAGS_public_key.empty()) {
    LOG_IF(WARNING, FLAGS_public_key_version != -1)
        << "--public_key_version is deprecated and ignored.";
    // 需要提供 "--in_file" 和 "--public_key" 参数
    VerifySignedPayload(FLAGS_in_file, FLAGS_public_key);
    return 0;
  }

Main 函数直接将接收到的 --in_file 和 --public_key 参数转发给 VerifySignedPaload 函数验证签名:

// 文件: system/update_engine/payload_generator/generate_delta_main.cc
void VerifySignedPayload(const string& in_file,
                         const string& public_key) {
  // 再次检查 in_file 和 public_key 参数
  LOG(INFO) << "Verifying signed payload.";
  LOG_IF(FATAL, in_file.empty())
      << "Must pass --in_file to verify signed payload.";
  LOG_IF(FATAL, public_key.empty())
      << "Must pass --public_key to verify signed payload.";
  // 转到 PayloadSigner 执行具体的验证
  CHECK(PayloadSigner::VerifySignedPayload(in_file, public_key));
  LOG(INFO) << "Done verifying signed payload.";
}

从上面可见,最终执行验证操作的是静态函数 PayloadSigner::VerifySignedPayload。

2.2 PayloadSigner::VerifySignedPayload 函数

PayloadSigner::VerifySignedPayload 函数完成了整个签名验证操作。

// 文件: system/update_engine/payload_generator/payload_signer.cc
bool PayloadSigner::VerifySignedPayload(const string& payload_path,
                                        const string& public_key_path) {
  DeltaArchiveManifest manifest;
  uint64_t metadata_size;
  uint32_t metadata_signature_size;
  //
  // 步骤 1. 准备工作: 解析 payload 的 metadata 数据找到签名的位置和内容
  //
  // 1a. 解析 payload 文件的 metadata 部分, 包括:
  // - magic
  // - file_format_version
  // - manifest_size
  // - metadata_signature_size
  // - manifest
  TEST_AND_RETURN_FALSE(LoadPayloadmetadata(payload_path,
                                            nullptr,
                                            &manifest,
                                            nullptr,
                                            &metadata_size,
                                            &metadata_signature_size));
  brillo::Blob payload;
  // 加载 payload 文件内容到 payload 缓存中
  TEST_AND_RETURN_FALSE(utils::ReadFile(payload_path, &payload));
  // 确保解析得到的 manifest 数据包含 signatures_offset 和 signature_size, 这两个成员指示了 payload 签名的位置和大小
  TEST_AND_RETURN_FALSE(manifest.has_signatures_offset() &&
                        manifest.has_signatures_size());
  // 计算 payload 签名的起始位置
  uint64_t signatures_offset = metadata_size + metadata_signature_size +
                               manifest.signatures_offset();
  CHECK_EQ(payload.size(), signatures_offset + manifest.signatures_size());
  brillo::Blob payload_hash, metadata_hash;
  // 1b. 计算 payload 文件的 metadata 数据和 payload 数据的哈希, 分别存放到 payload_hash 和 metadata_hash 中
  TEST_AND_RETURN_FALSE(CalculateHashFromPayload(payload,
                                                 metadata_size,
                                                 metadata_signature_size,
                                                 signatures_offset,
                                                 &payload_hash,
                                                 &metadata_hash));
  //
  // 步骤 2. 验证 payload 数据的签名
  //
  // 2a. 提取 payload 数据的签名到 signature_blob 中
  brillo::Blob signature_blob(payload.begin() + signatures_offset,
                              payload.end());
  // 2b. 对 payload 数据的 SHA256 的哈希结果进行填充,使其满足 PKCS1-v1_5 填充格式格式
  // 填充格式是固定的,如下:
  //    0x00 0x01 0xff ... 0xff 0x00  ASN1HEADER  SHA256HASH
  //   |--------------205-----------||----19----||----32----|
  // 这里填充的是前面 205 + 19 字节的部分
  TEST_AND_RETURN_FALSE(PayloadVerifier::PadRSA2048SHA256Hash(&payload_hash));

  // 2c. 验证 payload 数据的签名,操作原理是:
  // 第 1 步. 使用 public key 对提取的 signature_blob 数据进行解密
  // 第 2 步. 理论上步骤 1 解密的结果和这里填充后的 payload_hash 应该一样
  // 第 3 步. 比较步骤 1 中得到的哈希和 payload_hash,如果一样则签名验证成功,否则验证失败
  TEST_AND_RETURN_FALSE(PayloadVerifier::VerifySignature(
      signature_blob, public_key_path, payload_hash));

  //
  // 步骤 3. 验证 metadata 数据的签名
  //
  // 3a. 提取 metadata 数据的签名到 signature_blob 中
  if (metadata_signature_size) {
    signature_blob.assign(payload.begin() + metadata_size,
                          payload.begin() + metadata_size +
                          metadata_signature_size);
    // 3b. 对 metadata 数据的 SHA256 的哈希结果进行填充,使其满足 PKCS1-v1_5 填充格式格式
    TEST_AND_RETURN_FALSE(
        PayloadVerifier::PadRSA2048SHA256Hash(&metadata_hash));
    // 3c. 验证 metadata 数据的签名,其操作和验证 payload 数据签名一样
    TEST_AND_RETURN_FALSE(PayloadVerifier::VerifySignature(
        signature_blob, public_key_path, metadata_hash));
  }
  return true;
}

总结下 PayloadSigner::VerifySignedPayload 函数的操作:

    解析 payload 头部的 metadata, 找到 payload 和 metadata 签名的位置和内容,同时根据文件内容重新计算 payload 和 metadata 的哈希使用公钥验证 payload 签名

    对 payload 哈希按照签名格式进行填充使用公钥解密 payload 签名数据, 将解密的签名数据和 payload 哈希填充内容进行比较 使用公钥验证 metadata 签名

    对 metadata 哈希按照签名格式进行填充使用公钥解密 metadata 签名数据, 将解密的签名数据和 payload 哈希填充内容进行比较

2.3 签名验证的底层细节

填充函数 PayloadVerifier::PadRSA2048SHA256Hash

签名验证时,第一步是计算哈希值并按照要求进行填充,我们来看下是如何填充的。

// 文件: system/update_engine/payload_consumer/payload_verifier.cc
bool PayloadVerifier::PadRSA2048SHA256Hash(brillo::Blob* hash) {
  TEST_AND_RETURN_FALSE(hash->size() == 32);
  // 将预先准备好的填充数据添加到哈希数据的前面
  hash->insert(hash->begin(),
               reinterpret_cast(kRSA2048SHA256Padding),
               reinterpret_cast(kRSA2048SHA256Padding +
                                             sizeof(kRSA2048SHA256Padding)));
  TEST_AND_RETURN_FALSE(hash->size() == 256);
  return true;
}

因为用 RSA 私钥签名时采用 PKCS-v1_5 格式的填充,使用的哈希算法为 SHA256。
哈希算法和填充格式确定后,填充的内容(包括长度和数据)就固定了:

 0x00 0x01 0xff ... 0xff 0x00  ASN1HEADER  SHA256HASH
|--------------205-----------||----19----||----32----|

所以这里预先准备好填充数据 kRSA2048SHA256Padding,直接和计算的哈希值连接在一起。

签名验证函数 PayloadVerifier::VerifySignature

// 文件: system/update_engine/payload_consumer/payload_verifier.cc
bool PayloadVerifier::VerifySignature(const brillo::Blob& signature_blob,
                                      const string& public_key_path,
                                      const brillo::Blob& hash_data) {
  TEST_AND_RETURN_FALSE(!public_key_path.empty());

  // 从 protobuf 中格式的数据中还原出原始的 signature 数据到 signatures 中
  Signatures signatures;
  LOG(INFO) << "signature blob size = " <<  signature_blob.size();
  TEST_AND_RETURN_FALSE(signatures.ParseFromArray(signature_blob.data(),
                                                  signature_blob.size()));

  if (!signatures.signatures_size()) {
    LOG(ERROR) << "No signatures stored in the blob.";
    return false;
  }

  std::vector tested_hashes;
  // Tries every signature in the signature blob.
  // 逐个验证每个签名数据
  for (int i = 0; i < signatures.signatures_size(); i++) {
    const Signatures_Signature& signature = signatures.signatures(i);
    // 提取签名的原始数据
    brillo::Blob sig_data(signature.data().begin(), signature.data().end());
    brillo::Blob sig_hash_data;
    // GetRawHashFromSignature 使用公钥解密签名数据(因为签名数据是用私钥加密生成的,只能使用公钥解密)
    if (!GetRawHashFromSignature(sig_data, public_key_path, &sig_hash_data))
      continue;

    // 将解密的数据,和传入的经过填充后的哈希数据相比较,如果一样,则签名验证通过
    if (hash_data == sig_hash_data) {
      LOG(INFO) << "Verified correct signature " << i + 1 << " out of "
                << signatures.signatures_size() << " signatures.";
      return true;
    }
    tested_hashes.push_back(sig_hash_data);
  }
  // 签名验证失败,输出计算得到的哈希值
  LOG(ERROR) << "None of the " << signatures.signatures_size()
             << " signatures is correct. Expected:";
  utils::HexDumpVector(hash_data);
  LOG(ERROR) << "But found decrypted hashes:";
  for (const auto& sig_hash_data: tested_hashes) {
    utils::HexDumpVector(sig_hash_data);
  }
  return false;
}

签名验证函数 PayloadVerifier::VerifySignature 的操作简单总结为:

    从 payload 文件中根据 probuf 格式还原出签名数据用公钥解密签名数据(理论上: 解密后的签名数据应该和哈希数据填充后的内容一样)将填充后的哈希数据和解密的签名数据比较,如果一样,则签名验证通过;否则提示签名验证失败。

相关资料:

关于使用私钥签名,公钥验证的更多细节,请参考《OpenSSL和Python实现RSA Key数字签名和验证》

底层的解密函数 GetRawHashFromSignature

使用 RSA 签名时,先计算哈希,然后对哈希值按照要求进行填充,再使用私钥加密得到签名。

因此,这里使用公钥解密签名,得到的就是填充后的数据。

GetRawHashFromSignature 函数用指定的公钥解密签名,得到填充后的数据:

bool PayloadVerifier::GetRawHashFromSignature(
    const brillo::Blob& sig_data,
    const string& public_key_path,
    brillo::Blob* out_hash_data) {
  TEST_AND_RETURN_FALSE(!public_key_path.empty());

  // The code below executes the equivalent of:
  //
  // openssl rsautl -verify -pubin -inkey |public_key_path|
  //   -in |sig_data| -out |out_hash_data|

  // 从传入的密钥文件中提取公钥数据
  // Loads the public key.
  FILE* fpubkey = fopen(public_key_path.c_str(), "rb");
  if (!fpubkey) {
    LOG(ERROR) << "Unable to open public key file: " << public_key_path;
    return false;
  }

  char dummy_password[] = { ' ', 0 };  // Ensure no password is read from stdin.
  RSA* rsa = PEM_read_RSA_PUBKEY(fpubkey, nullptr, nullptr, dummy_password);
  fclose(fpubkey);
  TEST_AND_RETURN_FALSE(rsa != nullptr);
  unsigned int keysize = RSA_size(rsa);
  if (sig_data.size() > 2 * keysize) {
    LOG(ERROR) << "Signature size is too big for public key size.";
    RSA_free(rsa);
    return false;
  }

  // 使用公钥解密签名数据
  // Decrypts the signature.
  brillo::Blob hash_data(keysize);
  int decrypt_size = RSA_public_decrypt(sig_data.size(),
                                        sig_data.data(),
                                        hash_data.data(),
                                        rsa,
                                        RSA_NO_PADDING);
  RSA_free(rsa);
  TEST_AND_RETURN_FALSE(decrypt_size > 0 &&
                        decrypt_size <= static_cast(hash_data.size()));
  hash_data.resize(decrypt_size);
  
  // 将解密数据存放到 out_hash_data 中返回
  out_hash_data->swap(hash_data);
  return true;
}
3. 手动使用命令行工具验证签名 3.1 准备验签的公钥

前面 1.2 节有提到如何生成验签的公钥,这里以 Android 默认的使用的 testkey 为例:

# 1. 将 PKCS#8 格式的私钥转换成 PKCS#1 格式的私钥
openssl pkcs8 -in build/target/product/security/testkey.pk8 -inform DER -nocrypt -out /tmp/key-temp.key

# 2. 从私钥中提取公钥
openssl rsa -in key-temp.key -pubout -outform PEM -out key-pub.key
3.2 从 payload.bin 中提取 metadata 签名数据

在《Android Update Engine 分析(十)生成 payload 和 metadata 的哈希》的第 4 节(“4. 命令行手工计算 payload 和 metadata 的哈希”) 详细演示过如何通过命令行提取 metadata 和 payload 的哈希,因此这里直接提取 metadata 和 payload 的签名数据进行验证。

payload 数据的头部 128 字节如下:

$ hexdump -Cv -n 128 payload.bin 
00000000  43 72 41 55 00 00 00 00  00 00 00 02 00 00 00 00  |CrAU............|
00000010  00 00 52 de 00 00 01 08  18 80 20 20 cb c2 e7 7d  |..R.......  ...}|
00000020  28 88 02 60 03 6a d1 07  0a 04 62 6f 6f 74 32 27  |(..`.j....boot2'|
00000030  08 80 c0 a3 09 12 20 b5  58 f2 3f 83 f2 01 ad 53  |...... .X.?....S|
00000040  09 88 ac 77 d3 48 1a bd  1f 76 6e 15 b4 8a d5 cf  |...w.H...vn.....|
00000050  ec 3f 56 b8 ef c7 90 3a  27 08 80 c0 a3 09 12 20  |.?V....:'...... |
00000060  7c ea 74 e8 96 57 ac bd  01 a1 fc eb 65 dc 1e 5e  ||.t..W......e..^|
00000070  a1 c7 7b 28 d0 c5 94 97  5c e9 84 aa db 82 71 41  |..{(.........qA|
00000080
$

这里直接说头部 metadata 数据分析的结论:

offset 0: 4 字节的 magic [00 00 00 00 00 00 00 02] “CrAU”offset 4: 8 字节的 file_format_version,[00 00 00 00 00 00 00 02], 大端格式,其值为 2offset 12: 8 字节的 manifest_size,[00 00 00 00 00 00 52 de], 大端格式,其值为 0x52de,即十进制的 21214offset 20: 4 字节的 metadata_signature_size,[00 00 01 08], 大端格式,其值为 0x0108,即十进制的 264


根据 payload.bin 文件的布局图,可以知道 metadata 数据的签名 metadata_signature_message 数据信息:

起始地址: header(24 bytes) + manifest(21214 bytes) = 21238大小: 264 bytes

有了起始地址和大小,我们就可以从 payload.bin 中提取 metadata 的签名了:

# 提取 metadata 签名
$ dd if=payload.bin skip=21238 bs=1 count=264 of=metadata-signature.bin

# 查看签名数据
$ xxd -g 1 metadata-signature.bin
00000000: 0a 85 02 08 01 12 80 02 98 23 7f ea e1 24 9f c0  .........#...$..
00000010: 6f 43 07 f1 52 de bb 96 61 29 4c 5e 6c ef 24 c9  oC..R...a)L^l.$.
00000020: a1 f0 00 6b 05 2d b7 90 36 a9 ce a2 08 20 b5 4e  ...k.-..6.... .N
00000030: 78 6b 05 69 6c b0 72 db 97 83 5d ee ec ea db 79  xk.il.r...]....y
00000040: 5f 60 5e 2e 02 f7 e6 23 37 ab 17 ab dc d8 19 62  _`^....#7......b
00000050: c8 e2 48 7d 5a 5e af 54 c1 23 a6 16 8e 59 df f0  ..H}Z^.T.#...Y..
00000060: 35 47 0a a4 5e 9e 0a 3a 94 04 9e c7 a0 71 d5 91  5G..^..:.....q..
00000070: 15 24 31 1b 80 a6 6f 42 8d 94 b4 30 45 1c a7 e8  .$1...oB...0E...
00000080: 75 5c cb ee 81 1f 40 76 ed ac 52 6c f9 4c 32 f9  u....@v..Rl.L2.
00000090: 36 be 55 a5 e5 76 dc 81 a8 2a 0d 1a 78 5f 04 de  6.U..v...*..x_..
000000a0: a9 5e d8 32 47 33 26 0f d2 c0 34 79 25 6b 06 bd  .^.2G3&...4y%k..
000000b0: 3e fc 72 43 af 7d c6 4d 32 4b 00 58 ba 0b 96 27  >.rC.}.M2K.X...'
000000c0: 0c d4 61 30 1b ea c6 1d c9 e0 f1 19 b8 51 95 12  ..a0.........Q..
000000d0: 45 f7 8a f6 96 e3 12 73 ba 08 58 ab 11 8d f1 f5  E......s..X.....
000000e0: 16 04 fb 65 36 0e 38 c5 44 81 48 4e 78 d0 89 23  ...e6.8.D.HNx..#
000000f0: a7 97 fa 02 4c 78 5d 53 35 30 89 57 ab db 0b 49  ....Lx]S50.W...I
00000100: 49 dc db f4 e6 8c d5 e8                          I.......
3.3 使用 protobuf 工具还原 metadata 签名数据

对于采用 SHA256 哈希的 RSA 签名,其签名数据大小为 256 bytes, 但 payload 头部数据却显示为 264 bytes,这是为什么呢?我们使用 264 字节提取的签名数据应该怎么验证呢?

payload 文件的数据是按照 protobuf 格式组织,所以从 payload 中截取的签名 metadata-signature.bin 也就是 probobuf 编码格式,这里需要从 protobuf 格式中提取原始的签名数据。

关于 ProtoBuf 数据, 网上有很多文章介绍 ProtoBuf,我参考了这两篇:

深入 ProtoBuf - 简介深入 ProtoBuf - 编码

对于签名数据,其 protobuf 格式在 system/update_engine/update_metadata.proto 文件中是这样定义的:

message Signatures {
  message Signature {
    optional uint32 version = 1;
    optional bytes data = 2;
  }
  repeated Signature signatures = 1;
}

将这里的 Signatures 消息定义存储到 signature.proto 文件中,然后使用 out/host/linux-x86/bin 工具进行逆向解析还原:

$ ./out/host/linux-x86/bin/aprotoc --version
libprotoc 2.6.1
$
$ cat metadata-signature.bin | out/host/linux-x86/bin/aprotoc --decode=Signatures signature.proto
signatures {
  version: 1
  data: "230#177352341$237300oC07361R336273226a)L^l357$31124136000k05-267220625131624210 265Nxk05il260r333227203]356354352333y_`^.02367346#72532725333433031b310342H}Z^257T301#24626216Y3373605Gn244^236n:22404236307240q32522125$133200246oB2152242640E34247350u\31335620137@v355254Rl371L23716276U245345v334201250*r32x_04336251^3302G3&173223004y%k06275>374rC257}306M2K00X27213226'14324a0333523063531134036131270Q22522E36721236622634322s27210X253212153613652604373e6168305D201HNx320211#24722737202Lx]S50211W25333313II334333364346214325350"
}

这里使用 aprotoc 工具解析得到的 signatures 中就包含了 version 和 data 两个成员,其中 data 是一个 utf-8 编码的字符串,要是可以直接将消息存储为二进制文件就更好了。

因为没有找到合适的命令行工具将 utf-8 编码的字符串还原,这里手动检查,知道什么工具可以还原 utf-8 编码的请留言说一下啊,谢谢啦~

仔细观察下 data 数据编码的字符串,取前 4 个字符和最后 2 个字符分析:

前 4 个字符 “230#177352”

‘230’,八进制编码,十六进制为 0x98‘#’, ASCII 字符,十六进制为 0x23‘177’, 八进制格式,十六进制为 0x7f‘352’, 八进制格式,十六进制为 0xea 最后 2 个字符 “325350”

‘325’, 八进制格式,十六进制为 0xd5‘350’, 八进制格式, 十六进制为 0xe8

将这里得到的数据和我们前面显示的整个 metadata-signature.bin 的数据相比,可以知道原始的签名数据就是 metadata-signature.bin 的后 256 字节,只不过因为 protobuf 格式编码将 256 bytes 变成了 264 bytes。

从 protobuf 编码的 metadata-signature.bin 中提取后 256 字节得到原始的签名数据:

$ dd if=metadata-signature.bin skip=8 bs=1 count=256 of=metadata-signature-raw.bin
$ xxd -g 1 metadata-signature-raw.bin
00000000: 98 23 7f ea e1 24 9f c0 6f 43 07 f1 52 de bb 96  .#...$..oC..R...
00000010: 61 29 4c 5e 6c ef 24 c9 a1 f0 00 6b 05 2d b7 90  a)L^l.$....k.-..
00000020: 36 a9 ce a2 08 20 b5 4e 78 6b 05 69 6c b0 72 db  6.... .Nxk.il.r.
00000030: 97 83 5d ee ec ea db 79 5f 60 5e 2e 02 f7 e6 23  ..]....y_`^....#
00000040: 37 ab 17 ab dc d8 19 62 c8 e2 48 7d 5a 5e af 54  7......b..H}Z^.T
00000050: c1 23 a6 16 8e 59 df f0 35 47 0a a4 5e 9e 0a 3a  .#...Y..5G..^..:
00000060: 94 04 9e c7 a0 71 d5 91 15 24 31 1b 80 a6 6f 42  .....q...$1...oB
00000070: 8d 94 b4 30 45 1c a7 e8 75 5c cb ee 81 1f 40 76  ...0E...u....@v
00000080: ed ac 52 6c f9 4c 32 f9 36 be 55 a5 e5 76 dc 81  ..Rl.L2.6.U..v..
00000090: a8 2a 0d 1a 78 5f 04 de a9 5e d8 32 47 33 26 0f  .*..x_...^.2G3&.
000000a0: d2 c0 34 79 25 6b 06 bd 3e fc 72 43 af 7d c6 4d  ..4y%k..>.rC.}.M
000000b0: 32 4b 00 58 ba 0b 96 27 0c d4 61 30 1b ea c6 1d  2K.X...'..a0....
000000c0: c9 e0 f1 19 b8 51 95 12 45 f7 8a f6 96 e3 12 73  .....Q..E......s
000000d0: ba 08 58 ab 11 8d f1 f5 16 04 fb 65 36 0e 38 c5  ..X........e6.8.
000000e0: 44 81 48 4e 78 d0 89 23 a7 97 fa 02 4c 78 5d 53  D.HNx..#....Lx]S
000000f0: 35 30 89 57 ab db 0b 49 49 dc db f4 e6 8c d5 e8  50.W...II.......
3.3 使用公钥验证 metadata 签名

使用公钥验证签名我们采用两种方式,第一种是先将签名数据解密,然后检查解密后的数据; 第二种是直接用工具验证签名。

第一种,先解密签名数据,然后对比解密后的数据和填充数据

解密签名数据(不保留填充数据):

$ openssl rsautl -verify  -pubin -inkey key-pub.key -in metadata-signature-raw.bin | xxd -g1
00000000: 30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05  010...`.H.e.....
00000010: 00 04 20 ef e5 f7 0d 34 df 8a 97 af b5 5d aa 30  .. ....4.....].0
00000020: 4a a8 5b 9e fa 5e c1 60 24 66 5d d3 2b 23 58 d4  J.[..^.`$f].+#X.
00000030: 00 4c 83                                         .L.

解密签名数据(保留填充格式):

$ openssl rsautl -verify  -pubin -inkey key-pub.key -in metadata-signature-raw.bin -hexdump -raw
0000 - 00 01 ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
0010 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
0020 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
0030 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
0040 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
0050 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
0060 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
0070 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
0080 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
0090 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
00a0 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
00b0 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
00c0 - ff ff ff ff ff ff ff ff-ff ff ff ff 00 30 31 30   .............010
00d0 - 0d 06 09 60 86 48 01 65-03 04 02 01 05 00 04 20   ...`.H.e.......
00e0 - ef e5 f7 0d 34 df 8a 97-af b5 5d aa 30 4a a8 5b   ....4.....].0J.[
00f0 - 9e fa 5e c1 60 24 66 5d-d3 2b 23 58 d4 00 4c 83   ..^.`$f].+#X..L.

计算 metadata 数据的哈希:

$ dd if=payload.bin bs=1 count=21238 2>/dev/null | sha256sum | awk '{print $1}' | xxd -r -ps | xxd -g1
00000000: ef e5 f7 0d 34 df 8a 97 af b5 5d aa 30 4a a8 5b  ....4.....].0J.[
00000010: 9e fa 5e c1 60 24 66 5d d3 2b 23 58 d4 00 4c 83  ..^.`$f].+#X..L.

对比 metadata 的哈希和解密签名数据的后 32 字节,其内容一样,签名验证通过。

第二种,直接验证签名

提取 metadata 数据到 metadata.bin 中:

dd if=payload.bin bs=1 count=21238 of=metadata.bin

根据原始数据 metadata.bin 和签名数据 metadata-signature-raw.bin,使用公钥 key-pub.key 进行验证:

$ openssl dgst -sha256 -verify key-pub.key -signature metadata-signature-raw.bin metadata.bin
Verified OK

这里验证的结果是 Verified OK,通过签名验证。

3.4 使用公钥验证 payload 签名

手动验证 payload 签名相比于验证 metadata 签名会麻烦很多,主要原因有两点:

    payload 签名计算哈希时需要跳过 metadata signature 区域提取 payload 签名的 offset 和 size 时需要解析 manifest 数据,而 manifest 数据在 payload 中是压缩存储的,因此比较麻烦。

因此,手工验证 payload 数据签名的部分待更新。

4. 总结 4.1 验证 payload 和 metadata 签名的流程

以下简单总结代码中验证 payload 和 metadata 签名的流程:

缩进关系表示下一级调用, 同样的缩进表明同一层次的调用

"delta_generator --in_file=payload.bin --public_key=key-pub.key", 验证签名的命令
   --> VerifySignedPayload
      --> PayloadSigner::VerifySignedPayload
         --> LoadPayloadmetadata, 解析 metadata 数据
         --> CalculateHashFromPayload, 计算 metadata 和 payload 哈希用于验证签名
         --> PayloadVerifier::PadRSA2048SHA256Hash(payload_hash), 按照签名格式填充 payload 哈希
         --> PayloadVerifier::VerifySignature(public_key, payload_hash), 验证 payload 哈希
            --> GetRawHashFromSignature(public_key, payload)
               --> PEM_read_RSA_PUBKEY(public_key)
               --> RSA_public_decrypt(public_key, payload_signature)
         --> PayloadVerifier::PadRSA2048SHA256Hash(metadata_hash), 按照签名格式填充 metadata 哈希
         --> PayloadVerifier::VerifySignature(public_key, metadata_hash), 验证 metdata 哈希
            --> GetRawHashFromSignature(public_key, metadata)
               --> PEM_read_RSA_PUBKEY(public_key)
               --> RSA_public_decrypt(public_key, metadata_signature)

这里重复使用了 LoadPayloadmetadata 和 CalculateHashFromPayload 函数,这两个函数在《Android Update Engine 分析(十)生成 payload 和 metadata 的哈希》中详细分析过。

4.2 手动签名验证总结
# 1. 将 PKCS#8 格式的私钥转换成 PKCS#1 格式的私钥
openssl pkcs8 -in build/target/product/security/testkey.pk8 -inform DER -nocrypt -out key-temp.key

# 2. 从私钥中提取公钥
openssl rsa -in key-temp.key -pubout -outform PEM -out key-pub.key

# 3. 根据 payload.bin 的头部 24 字节解析得到:
#              manifest_size: 0x52de, 即 21214
#    metadata_signature_size: 0x0108, 即 264
#    所以 metadata: 0~21238(24+21214), metadata_signature: 21238~21502(21238+264)

# 4. 提取 metadata 数据
$ dd if=payload.bin bs=1 count=21238 of=metadata.bin

# 5. 提取 metadata 签名 (这里提取原始签名时需要跳过签名头部的 8 个字节)
$ dd if=payload.bin skip=21246 bs=1 count=256 of=metadata-signature.bin

# 6. 使用公钥验证 metadata 签名
$ openssl dgst -sha256 -verify key-pub.key -signature metadata-signature-raw.bin metadata.bin
Verified OK

这里只验证了 metadata 数据签名,payload 数据验证过程后续补充。

5. 其它

洛奇工作中常常会遇到自己不熟悉的问题,这些问题可能并不难,但因为不了解,找不到人帮忙而瞎折腾,往往导致浪费几天甚至更久的时间。

所以我组建了几个微信讨论群(记得微信我说加哪个群,如何加微信见后面),欢迎一起讨论:

一个密码编码学讨论组,主要讨论各种加解密,签名校验等算法,请说明加密码学讨论群。一个Android OTA的讨论组,请说明加Android OTA群。一个git和repo的讨论组,请说明加git和repo群。

在工作之余,洛奇尽量写一些对大家有用的东西,如果洛奇的这篇文章让您有所收获,解决了您一直以来未能解决的问题,不妨赞赏一下洛奇,这也是对洛奇付出的最大鼓励。扫下面的二维码赞赏洛奇,金额随意:

洛奇自己维护了一个公众号“洛奇看世界”,一个很佛系的公众号,不定期瞎逼逼。公号也提供个人联系方式,一些资源,说不定会有意外的收获,详细内容见公号提示。扫下方二维码关注公众号:

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/711063.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号