最近做的一款产品,由于前期失误使用的是 官方的testkey 签名rom包。所以别人rom可以随意刷我们产品盒子,为了改正这个bug我们使用了新的签名刷自己产品。防止别人盗刷我们盒子。
要用新签名rom包,先要理解android升级原理,
第一步:
当我们手动u盘升级或者ota升级下载好update.zip包后。当我们点击升级后,android framework的代码
./base/core/java/android/os/RecoverySystem.java
中函数会
public static void verifyPackage(File packageFile,ProgressListener listener,File deviceCertsZipFile)
这个函数 会被上层 应用 调用用于检测 rom 包签名
RecoverySystem.verifyPackage(verifyPackage, listener,
new File(“/system/etc/security/otacerts.zip”)); 而otacerts.zip 就是我们的密钥,解压后发现就是 testkey.x509.pem,如果我们rom签名的密钥 用的是 testkey.x509.pem 和testkey.pk8 ,现在用testkey.x509.pem 检测update.zip就能通过。
第二步:
在 recovery 模式升级的时候。我们recovery模式会再一次验证update.zip签名。具体代码在install.cpp 中函数 really_install_package 中
int numKeys;RSAPublicKey* loadedKeys = load_keys(PUBLIC_KEYS_FILE, &numKeys);if (loadedKeys == NULL) {LOGE("Failed to load keys\n");return INSTALL_CORRUPT;}LOGI("%d key(s) loaded from %s\n", numKeys, PUBLIC_KEYS_FILE);// Give verification half the progress bar...ui->Print("Verifying update package...\n");ui->SetProgressType(RecoveryUI::DETERMINATE);ui->ShowProgress(VERIFICATION_PROGRESS_FRACTION, VERIFICATION_PROGRESS_TIME);int err;err = verify_file(path, loadedKeys, numKeys);free(loadedKeys);LOGI("verify_file returned %d\n", err);if (err != VERIFY_SUCCESS) {LOGE("signature verification failed\n");return INSTALL_CORRUPT;}
首先 获得 public key 公钥,load_keys 返回值就是我们的 key指针地址 ,numKeys
是keys 数量。支持多个key。
load_keys(const char* filename, int* numKeys) {RSAPublicKey* out = NULL;*numKeys = 0;FILE* f = fopen(filename, "r");if (f == NULL) {LOGE("opening %s: %s\n", filename, strerror(errno));goto exit;}{int i;bool done = false;while (!done) {++*numKeys;out = (RSAPublicKey*)realloc(out, *numKeys * sizeof(RSAPublicKey));RSAPublicKey* key = out + (*numKeys - 1);char start_char;if (fscanf(f, " %c", &start_char) != 1) goto exit;if (start_char == '{') {// a version 1 key has no version specifier.key->exponent = 3;} else if (start_char == 'v') {int version;if (fscanf(f, "%d {", &version) != 1) goto exit;if (version == 2) {key->exponent = 65537;} else {goto exit;}}if (fscanf(f, " %i , 0x%x , { %u",&(key->len), &(key->n0inv), &(key->n[0])) != 3) {goto exit;}if (key->len != RSANUMWORDS) {LOGE("key length (%d) does not match expected size\n", key->len);goto exit;}for (i = 1; i < key->len; ++i) {if (fscanf(f, " , %u", &(key->n[i])) != 1) goto exit;}if (fscanf(f, " } , { %u", &(key->rr[0])) != 1) goto exit;for (i = 1; i < key->len; ++i) {if (fscanf(f, " , %u", &(key->rr[i])) != 1) goto exit;}fscanf(f, " } } ");// if the line ends in a comma, this file has more keys.switch (fgetc(f)) {case ',':// more keys to come.break;case EOF:done = true;break;default:LOGE("unexpected character between keys\n");goto exit;}LOGI("read key e=%d\n", key->exponent);}}
PUBLIC_KEYS_FILE = /res/keys
在recovery 下面用 busybox cat /res/keys 发现都是数字
{64,0xc926ad21,{1795090719,2141396315,950055447,2581568430,4268923165,1920809988,546586521,3498997798,1776797858,3740060814,1805317999,1429410244,129622599,1422441418,1783893377,1222374759,2563319927,323993566,28517732,609753416,1826472888,215237850,4261642700,4049082591,3228462402,774857746,154822455,2497198897,2758199418,3019015328,2794777644,87251430,2534927978,120774784,571297800,3695899472,2479925187,3811625450,3401832990,2394869647,3267246207,950095497,555058928,414729973,1136544882,3044590084,465547824,4058146728,2731796054,1689838846,3890756939,1048029507,895090649,247140249,178744550,3547885223,3165179243,109881576,3944604415,1044303212,3772373029,2985150306,3737520932,3599964420},{3437017481,3784475129,2800224972,3086222688,251333580,2131931323,512774938,325948880,2657486437,2102694287,3820568226,792812816,1026422502,2053275343,2800889200,3113586810,165549746,4273519969,4065247892,1902789247,772932719,3941848426,3652744109,216871947,3164400649,1942378755,3996765851,1055777370,964047799,629391717,2232744317,3910558992,191868569,2758883837,3682816752,2997714732,2702529250,3570700455,3776873832,3924067546,3555689545,2758825434,1323144535,61311905,1997411085,376844204,213777604,4077323584,9135381,1625809335,2804742137,2952293945,1117190829,4237312782,1825108855,3013147971,1111251351,2568837572,1684324211,2520978805,367251975,810756730,2353784344,1175080310}}
可能别的keys值和这个不一样。这没关系。
这个keys怎么得来的呢??
在 源码目录build/target/product/security下执行命令
java -jar ../../../../out/host/linux-x86/framework/dumpkey.jar testkey.x509.pem > keys
具体文件 在 build/core/Makefile 中
$(RECOVERY_INSTALL_OTA_KEYS): $(OTA_PUBLIC_KEYS) $(DUMPKEY_JAR) $(extra_keys)@echo "DumpPublicKey: $@ <= $(PRIVATE_OTA_PUBLIC_KEYS) $(extra_keys)"@rm -rf $@@mkdir -p $(dir $@)java -jar $(DUMPKEY_JAR) $(PRIVATE_OTA_PUBLIC_KEYS) $(extra_keys) > $@
上面源码就是 build/core/Makefile
RECOVERY_INSTALL_OTA_KEYS 就是 /res/keys
OTA_PUBLIC_KEYS = $(SRC_TARGET_DIR)/product/security/testkey.x509.pemDUMPKEY_JAR := $(HOST_OUT_JAVA_LIBRARIES)/dumpkey.jar
load_keys获得 RSAPublicKey之后,我们通过 verify_file(path, loadedKeys, numKeys); 验证 update.zip签名。
验证通过就升级update.zip,不通过就失败。
用新的签名当然先新建新的签名:
文件 development/tools/make_key 就是用于做新签名的工具
执行 命令
./make_key onekey '/C=CN/ST=ShangHai/L=ShangHai/O=PPTV/OU=Department/CN=Your Name/emailAddress=YourE-mailAddress'
key的名字很好理解,就是前面提到的4中类型的key,公司信息的参数比较多,它们的含义如下: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
执行上面命令提示你输入密码。输入你的密码后就会在当前目录下生成 onekey.x509.pem 和 onekey.pk8 就是新签名 密钥。
原来签名 用的是 testkey签名 update.zip,现在我们要用 onekey 签名。所有我们要有一个过度updat.zip包,update.zip 签名是用 testkey 签名。但是这个update.zip 包含的签名文件(otacert.zip 和 recovery 模式下的res/keys)都是 onekey 的。
打开 build/core/Makefile
DEFAULT_KEY_CERT_PAIR := $(DEFAULT_SYSTEM_DEV_CERTIFICATE)
DEFAULT_KEY_CERT_PAIR 是用于签名 update.zip 文件。
java -Xmx1536m -jar out/host/linux-x86/framework/signapk.jar -w build/target/product/security/
testkey.x509.pem build/target/product/security/testkey.pk8 /tmp/tmpg95cOx out/target/product/g18ref/g18ref-ota-
20150508.zip
就是上面的testkey.pk8 和testkey.x509.pem
我们改为
DEFAULT_KEY_CERT_PAIR := $(DEFAULT_SYSTEM_DEV_CERTIFICATE)
DEFAULT_KEY_CERT_PAIR := $(SRC_TARGET_DIR)/product/security/onetkey $(DEFAULT_KEY_CERT_PAIR)
上面展开 makefile 就是 otakey 和 testkey
$(INTERNAL_OTA_PACKAGE_TARGET): KEY_CERT_PAIR := $(firstword $(DEFAULT_KEY_CERT_PAIR))
上面 签名时候选择 第一个签名 onetkey
$(TARGET_OUT_ETC)/security/otacerts.zip: $(addsuffix .x509.pem,$(DEFAULT_KEY_CERT_PAIR))$(hide) rm -f $@$(hide) mkdir -p $(dir $@)$(hide) zip -qj $@ $<
将最后一句改为
$(hide) zip -qj $@ $^
上面用于 生成 otacerts.zip,我们让 otacerts.zip 包含 testkey 和 onekey 新旧 update.zip 在 都能验证通过。
ifeq ($(BUILD_SECURE),true)OTA_PUBLIC_KEYS := device/*/$(TARGET_DEVICE)/releasekey.x509.pem
elseOTA_PUBLIC_KEYS := $(SRC_TARGET_DIR)/product/security/testkey.x509.pem
endif
后面添加这句
OTA_PUBLIC_KEYS := $(SRC_TARGET_DIR)/product/security/otakey.x509.pem
这句话用于 生成 recovery 模式下 /res/keys
在
cp $(RECOVERY_INSTALL_OTA_KEYS) $(TARGET_RECOVERY_ROOT_OUT)/res/keys
添加后面一句
cp $(TARGET_OUT_ETC)/security/otacerts.zip $(TARGET_RECOVERY_ROOT_OUT)/res/otacerts.zip
为什么加这句。比如我现在版本(老的签名)用新的签名updtae.zip包(不是过度包)升级之后(otacert.zip包含testkey和 onekey)。我又想回到这个老版本,升级后otacerts.zip 只包含testkey,这时候我再想升级新的update.zip包(新签名包),如果不加这句就会还要用过度包。这个 otacerts.zip 会被在recovery 使用。
在 文件 recovery.cpp 中 添加
static char *INSTALL_RECOVRY_SH = "/system/etc";
static char *SOURCE_OTACERTZIP = "/res/otacerts.zip";
static char *DEST_OTACERTZIP = "/system/etc/security/otacerts.zip"; int old_updatezip_flag = 0;
添加函数 用于 将 res目录下的 /res/otacerts.zip 拷贝到 /system/etc/security/otacerts.zip
void copy_otacert_file(const char* source, const char* destination) { FILE *des = fopen(destination, "w");FILE *tmplog = fopen(source, "r"); if (tmplog != NULL) { fseek(tmplog, 0, SEEK_SET); char buf[4096]; 456 while(fread(buf,1,sizeof(buf),tmplog)){fwrite(buf,sizeof(buf),1,des); } } fclose(tmplog); fclose(des); }
finish_recovery(send_intent); if(old_updatezip_flag==1){ if (ensure_path_mounted(INSTALL_RECOVRY_SH) == 0) { unlink("/system/etc/install-recovery.sh"); copy_otacert_file(SOURCE_OTACERTZIP,DEST_OTACERTZIP); } ensure_path_unmounted(INSTALL_RECOVRY_SH); }
如果是 在 新签名 recovery 下升级 旧签名 update.zip ,需要在升级完成后 删除 install-recovery.sh ,不然 在正常启动后,install-recovery.sh 会把 当面新签名recovery 换成老的recovery。
copy_otacert_file(SOURCE_OTACERTZIP,DEST_OTACERTZIP); 替换
旧的 otacerts.zip
if(old_updatezip_flag==1) 表示 当前要升级 旧签名的update.zip包。这个值确定在文件verifier.cpp中。
在 recovery 模式下。我们将我们之前 发布的 rom 的sha散列值 都计算 好保存到我们recovery 中。
head -c -1740 update.zip | sha1sum
用上面函数计算,去掉 尾部 1740个字节这些都是签名文件 自己加上去的。
size_t comment_size = footer[4] + (footer[5] << 8);size_t signature_start = footer[0] + (footer[1] << 8);LOGI("comment is %d bytes; signature %d bytes from end\n",
上面函数打印出signature = 1740个字节 ,所以计算 sha值时候去掉 1740
extern int old_updatezip_flag; 声明外部变量char *known_sha1s[] = {"01c041ee4803755cc4bc00e108e0a613dcc782ab","a23f051c805b586608ccd63d0257ad867f577bdf","5acb8d649a0416ae1730e2a3b45aafb1727bb9be","4bfe92f6f2bafb20979d881b773933dcc438f88b","82fb1103c4e8d281e891bae12d26283332cb74ac","6349f6ec3794e799b52c87eee674dc3ad2435c79","dc2a66de723aa1889eaa6b7cef2fb05d8d9a7590","9406448609c52c0e8109afbfb77110f838f41c4e","a589842eb4ab97e45dc1d68041e77dd72a71a63c","b6e27016ff3eb502c634b753427d9f179e15f2ff","a6277d7ef65dda2a061aa7cd1620d82e787f9de4","903f681c6c4e177007b1653128f61b19d247444f","3715d801b92a5f1edbbcc819b1f57f2def1c85de","a694007371fd0e11daef3a72b8fefe071ca61a28","a623c066541f0ea8476e8b39ccbd4dcc7dac96ec","9affed9d8ddf7e0748b4698080924f0e03aee405","6514aba7766a47a8c42cd72ca015879be6480742","bf7f2cfb83036679f322e3799c160ad05f8cf57d","96987b650e13add4afc91da7e41c0dba3edaf389","NULL"};
const uint8_t* sha1 = SHA_final(&ctx);for (i = 0; i < numKeys; ++i) {// The 6 bytes is the "(signature_start) $ff $ff (comment_size)" that// the signing tool appends after the signature itself.if (RSA_verify(pKeys+i, eocd + eocd_size - 6 - RSANUMBYTES,RSANUMBYTES, sha1)) {LOGI("whole-file signature verified against key %d\n", i);fclose(f);free(eocd);return VERIFY_SUCCESS;}}下面都是新加的代码 /*verify old public rom */char tmp_sha[42] = {0};char const * hex = "0123456789ABCDEF";for(i=0;i<20;i++){ tmp_sha[i*2] = hex[sha1[i] >> 4];tmp_sha[i*2 + 1] = hex[sha1[i] & 0x0f];}将 uint8_t* 16进制 字符转成字符串 char **p ;p= known_sha1s;while(strcasecmp(*p,"NULL")!=0){if(strcasecmp(*p,tmp_sha)==0){old_updatezip_flag =1;这个值等于1 表明是 旧签名的发布update包,我们在升级旧rom包 fclose(f); return VERIFY_SUCCESS;}p++;}先校验是不是 旧签名 发布 rom 包/*校验 过度 rom*/ 如果是过度 rom 我们 查找某个字符是否相等 就让过度包通过校验可以升级。if (fseek(f, -comment_size, SEEK_END) != 0) {LOGE("failed to seek in %s (%s)\n", path, strerror(errno));fclose(f);return VERIFY_FAILURE;}unsigned char sig[1];if (fread(sig, 1, 1, f) != 1) {LOGE("failed to read footer from %s (%s)\n", path, strerror(errno));fclose(f);return VERIFY_FAILURE;}if(sig[0]==83){fclose(f);return VERIFY_SUCCESS;}
上面思路是 先用新的签名 校验 rom 包 。不能通过再比较rom 包sha 散列值。不通过再检验是不是过度包。
如果编译不通过 可能错误原因 是
verifier_test.cpp 中少 old_updatezip_flag
在文件中加入 int old_updatezip_flag;
整个 防刷机原理是 过度包刷完之后,
正常模式下 可以让old 签名包 和别人家的包和我们新签名包都通过验证。因为我们 otacerts.zip 包含 新旧 key,但是 我们返回旧key 签名 rom public 包。升级完成后 我们删除 install-recovert.sh ,并将 recovery 下的otacerts.zip拷贝到 system 下面。这样旧 public rom 和 新签名包可以来回 刷。但是 别家的却不能再刷通过 我们盒子。因为我们recovery 模式下面没有包含他们 sha值 known_sha1s 。有关过度包我们在验证通过我们做了 校验 update.zip 1740 字节某个字符是否相等