android ota 版本校验,OTA升级签名校验简析
1. 概要
如果進行過OTA升級的開發者,都或多或少有這樣的疑問,如何確定該OTA升級包是可以信任的呢?這其中其實涉及到一個簽名驗證的流程。
2. 簽名生成
在生成正規的固件時,一般會運行生成新key的腳本,并重新修改key中的信息。以網上常用的生成key的腳本為例:
#!/bin/sh
AUTH='/C=CN/ST=xxxx/L=xxxxx/O=xxxxxx/OU=xxxxx/CN=China/emailAddress=xxxxxxx@com'
openssl genrsa -3 -out $1.pem 2048
openssl req -new -x509 -key $1.pem -out $1.x509.pem -days 10000 \
-subj "$AUTH"
echo "Please enter the password for this key:"
openssl pkcs8 -in $1.pem -topk8 -outform DER -out $1.pk8 -passout stdin
其中openssl通過genrsa標準命令生成私鑰,默認大小為2048:
openssl genrsa -3 -out $1.pem 2048
生成私鑰后,生成證書簽署請求,即公鑰:
openssl req -new -x509 -key $1.pem -out $1.x509.pem -days 365 \
-subj "$AUTH"
-new: new request
-x509: output a x509 structure instead of a cert. req.該選項說明生成一個自簽名的證書。
-keyfile: use the private key contained in file
-days: number of days a certificate generated by -x509 is valid for.
最后通過私鑰pem文件生成PKCS8私鑰文件
openssl pkcs8 -in $1.pem -topk8 -outform DER -out $1.pk8 -passout stdin
也可以參考android原生的生成key的流程,位于android/development/tools下的make_key腳本i。
自此后,生成了一對公私鑰用于簽名校驗。
3. sign_target_files_apks.py
3.1 對Apk進行重簽名流程
sign_target_files_apks在本人之前的用法都局限于將targetfile里的apk進行重簽名,其流程如下:
1.獲取腳本輸入參數
2.獲取輸入文件以及輸出文件參數,讀取misc_info文件
input_zip = zipfile.ZipFile(args[0], "r")
output_zip = zipfile.ZipFile(args[1], "w")
misc_info = common.LoadInfoDict(input_zip)
misc_info記錄了一些參數,如下:
其中這里關注的是默認簽名的路徑:
default_system_dev_certificate=build/target/product/security/testkey
3.建立key映射
假如在調用腳本時未指定-d.-k參數,那么默認使用的正是系統自帶的testkey。否則,將會映射到指定key目錄下的Key
def BuildKeyMap(misc_info, key_mapping_options):
for s, d in key_mapping_options:
if s is None: # -d option
devkey = misc_info.get("default_system_dev_certificate",
"build/target/product/security/testkey")
devkeydir = os.path.dirname(devkey)
OPTIONS.key_map.update({
devkeydir + "/testkey": d + "/releasekey",
devkeydir + "/devkey": d + "/releasekey",
devkeydir + "/media": d + "/media",
devkeydir + "/shared": d + "/shared",
devkeydir + "/platform": d + "/platform",
})
else:
OPTIONS.key_map[s] = d
4.讀取targetfile中的證書文件
apk_key_map = GetApkCerts(input_zip)
GetApkCerts的實現如下,其實質是讀取了targetfile中/META/apkcerts.txt文件
def GetApkCerts(tf_zip):
certmap = common.ReadApkCerts(tf_zip)
# apply the key remapping to the contents of the file
for apk, cert in certmap.iteritems():
certmap[apk] = OPTIONS.key_map.get(cert, cert)
# apply all the -e options, overriding anything in the file
for apk, cert in OPTIONS.extra_apks.iteritems():
if not cert:
cert = "PRESIGNED"
certmap[apk] = OPTIONS.key_map.get(cert, cert)
return certmap
def ReadApkCerts(tf_zip):
"""Given a target_files ZipFile, parse the META/apkcerts.txt file
and return a {package: cert} dict."""
certmap = {}
for line in tf_zip.read("META/apkcerts.txt").split("\n"):
line = line.strip()
if not line:
continue
m = re.match(r'^name="(.*)"\s+certificate="(.*)"\s+'
r'private_key="(.*)"$', line)
if m:
name, cert, privkey = m.groups()
public_key_suffix_len = len(OPTIONS.public_key_suffix)
private_key_suffix_len = len(OPTIONS.private_key_suffix)
if cert in SPECIAL_CERT_STRINGS and not privkey:
certmap[name] = cert
elif (cert.endswith(OPTIONS.public_key_suffix) and
privkey.endswith(OPTIONS.private_key_suffix) and
cert[:-public_key_suffix_len] == privkey[:-private_key_suffix_len]):
certmap[name] = cert[:-public_key_suffix_len]
else:
raise ValueError("failed to parse line from apkcerts.txt:\n" + line)
return certmap
在這里可以分析下apkcerts文件,其內容格式如下:
name="RecoveryLocalizer.apk" certificate="build/target/product/security/testkey.x509.pem" private_key="build/target/product/security/testkey.pk8"
name="CtsVerifier.apk" certificate="build/target/product/security/testkey.x509.pem" private_key="build/target/product/security/testkey.pk8"
....
ame="CtsShimPrivUpgradePrebuilt.apk" certificate="PRESIGNED" private_key=""
name="CtsShimPrivUpgradeWrongSHAPrebuilt.apk" certificate="PRESIGNED" private_key=""
...
可以看出每一個apk中,都指定了證書的位置以及私鑰文件路徑,可以對比出,當應用的Android.mk中以platform簽名,其格式為:
LOCAL_CERTIFICATE := platform
name="HdmiCts.apk" certificate="build/target/product/security/platform.x509.pem" private_key="build/target/product/security/platform.pk8"
一般apk以presigned簽名的,則為:
LOCAL_CERTIFICATE := PRESIGNED
name="AllCast.apk" certificate="PRESIGNED" private_key=""
以media簽名的,則為:
LOCAL_CERTIFICATE := media
name="Gallery.apk" certificate="build/target/product/security/media.x509.pem" private_key="build/target/product/security/media.pk8"
所以該文件定義了每個文件的證書以及簽名情況,如果回到編譯系統,可以看出該文件的編譯規則:
APKCERTS_FILE := $(intermediates)/$(name).txt
$(APKCERTS_FILE):
@echo APK certs list: $@
@mkdir -p $(dir $@)
@rm -f $@
$(foreach p,$(PACKAGES),\
$(if $(PACKAGES.$(p).EXTERNAL_KEY),\
$(call _apkcerts_echo_with_newline,\
'name="$(p).apk" certificate="EXTERNAL" \
private_key=""' >> $@),\
$(call _apkcerts_echo_with_newline,\
'name="$(p).apk" certificate="$(PACKAGES.$(p).CERTIFICATE)" \
private_key="$(PACKAGES.$(p).PRIVATE_KEY)"' >> $@)))
# In case value of PACKAGES is empty.
$(hide) touch $@
.PHONY: apkcerts-list
apkcerts-list: $(APKCERTS_FILE)
可以看出編譯偽目標apkcerts-list時,編譯系統就會遍歷$(PACKAGES),并將apk的信息記錄在apkcerts.txt文檔里。
在編譯每一個Apk時,package_internal.mk會讀取LOCAL_CERTIFICATE參數,并記錄信息如下:
PACKAGES.$(LOCAL_PACKAGE_NAME).PRIVATE_KEY := $(private_key)
PACKAGES.$(LOCAL_PACKAGE_NAME).CERTIFICATE := $(certificate)
在編譯targetfiles的時候,會編譯該文件:
$(BUILT_TARGET_FILES_PACKAGE): \
$(INSTALLED_BOOTIMAGE_TARGET) \
$(INSTALLED_RADIOIMAGE_TARGET) \
$(INSTALLED_RECOVERYIMAGE_TARGET) \
$(INSTALLED_SYSTEMIMAGE) \
$(INSTALLED_USERDATAIMAGE_TARGET) \
$(INSTALLED_CACHEIMAGE_TARGET) \
$(INSTALLED_VENDORIMAGE_TARGET) \
$(INSTALLED_ANDROID_INFO_TXT_TARGET) \
$(SELINUX_FC) \
$(APKCERTS_FILE) \
$(HOST_OUT_EXECUTABLES)/fs_config \
| $(ACP)
5.處理targetfile中文件
對于targetfile中文件,核心部分調用如下方法:
ProcessTargetFiles(input_zip, output_zip, misc_info,
apk_key_map, key_passwords,
platform_api_level,
codename_to_api_level_map)
主要關心Apk部分:
for info in input_tf_zip.infolist():
if info.filename.startswith("IMAGES/"):
continue
...
# Sign APKs.
if info.filename.endswith(".apk"):
name = os.path.basename(info.filename)
key = apk_key_map[name]
if key not in common.SPECIAL_CERT_STRINGS:
print " signing: %-*s (%s)" % (maxsize, name, key)
signed_data = SignApk(data, key, key_passwords[key], platform_api_level,
codename_to_api_level_map)
common.ZipWriteStr(output_tf_zip, out_info, signed_data)
else:
# an APK we're not supposed to sign.
print "NOT signing: %s" % (name,)
common.ZipWriteStr(output_tf_zip, out_info, data)
...
只要apk的key不是以"PRESIGNED"或者"EXTERNAL"簽名的,都會去重新簽名:
SPECIAL_CERT_STRINGS = ("PRESIGNED", "EXTERNAL")
3.2 更新system,recovery簽名
當sign_target_files_apks指定了-O參數時,將會執行如下邏輯:
if OPTIONS.replace_ota_keys:
new_recovery_keys = ReplaceOtaKeys(input_tf_zip, output_tf_zip, misc_info)
if new_recovery_keys:
write_to_temp("RECOVERY/RAMDISK/res/keys", 0o755 << 16, new_recovery_keys)
即當制定了-O參數時,將會調用ReplaceOtaKeys方法。
獲取/META/otakeys.txt
當方案中定義了PRODUCT_OTA_PUBLIC_KEYS時,在編譯時會將內容寫入otakeys.txt文件中
try:
keylist = input_tf_zip.read("META/otakeys.txt").split()
except KeyError:
raise common.ExternalError("can't read META/otakeys.txt from input")
2.獲取recovery的證書
同理,如果在方案中制定了extra_recovery_keys,也會從misc_info中找證書
extra_recovry_keys = misc_info.get("extra_recovery_keys", None)
if extra_recovery_keys:
extra_recovery_keys = [OPTIONS.key_map.get(k, k) + ".x509.pem"
for k in extra_recovery_keys.split()]
if extra_recovery_keys:
print "extra recovery-only key(s): " + ", ".join(extra_recovery_keys)
else:
extra_recovery_keys = []
3.對mapped_keys賦值
mapped_keys = []
for k in keylist:
m = re.match(r"^(.*)\.x509\.pem$", k)
if not m:
raise common.ExternalError(
"can't parse \"%s\" from META/otakeys.txt" % (k,))
k = m.group(1)
mapped_keys.append(OPTIONS.key_map.get(k, k) + ".x509.pem")
if mapped_keys:
print "using:\n ", "\n ".join(mapped_keys)
print "for OTA package verification"
else:
devkey = misc_info.get("default_system_dev_certificate",
"build/target/product/security/testkey")
mapped_keys.append(
OPTIONS.key_map.get(devkey, devkey) + ".x509.pem")
print "META/otakeys.txt has no keys; using", mapped_keys[0]
假如otakey文件中有內容,則將第一個key添加到mapped_keys中。否則就默認為系統的testkey。
4.利用dumpkey.jar為recovery創建新的key
p = common.Run(["java", "-jar",
os.path.join(OPTIONS.search_path, "framework", "dumpkey.jar")]
+ mapped_keys + extra_recovery_keys,
stdout=subprocess.PIPE)
new_recovery_keys, _ = p.communicate()
if p.returncode != 0:
raise common.ExternalError("failed to run dumpkeys")
common.ZipWriteStr(output_tf_zip, "RECOVERY/RAMDISK/res/keys",
new_recovery_keys)
通過之前的extra_recovery_keys作為參數,將公鑰內容打印出來,dumpkey.jar的內容如下:
5.更新otacerts.zip
最后會將mapped_keys中的文件寫入otacerts.zip中。如果otakey為空,則默認為testkey.x509.pem,如果指定了key的路徑(-d),以及設置了-O,那么由于testkey通過方法BuildKeyMap綁定了releasekey,因此會替換為releasekey.x509.pem
temp_file = cStringIO.StringIO()
certs_zip = zipfile.ZipFile(temp_file, "w")
for k in mapped_keys:
common.ZipWrite(certs_zip, k)
common.ZipClose(certs_zip)
common.ZipWriteStr(output_tf_zip, "SYSTEM/etc/security/otacerts.zip",
temp_file.getvalue())
4. ota_from_target_files.py
在上述步驟中,通過對targetfiles中的otacerts.zip以及recovery的/res/keys進行更新后,生成出來的固件假如與后續的ota包簽名不符,那么在校驗的時候也是會失敗,所以在生成ota包時,也必須指定相應的公鑰。
在ota_from_target_files中有如下的解析:
-k (--package_key) Key to use to sign the package (default is
the value of default_system_dev_certificate from the input
target-files's META/misc_info.txt, or
"build/target/product/security/testkey" if that value is not
specified).
并且在選項中假如未定義"--no_signing",而且-k未指定,將會使用原生的的testkey。
# Use the default key to sign the package if not specified with package_key.
if not OPTIONS.no_signing:
if OPTIONS.package_key is None:
OPTIONS.package_key = OPTIONS.info_dict.get(
"default_system_dev_certificate",
"build/target/product/security/testkey")
當OTA包完成所有的打包工作后,最終會調用到如下方法,表明要對整包進行簽名。
# Sign the whole package to comply with the Android OTA package format.
def SignOutput(temp_zip_name, output_zip_name)
其實現如下:
def SignOutput(temp_zip_name, output_zip_name):
key_passwords = common.GetKeyPasswords([OPTIONS.package_key])
pw = key_passwords[OPTIONS.package_key]
common.SignFile(temp_zip_name, output_zip_name, OPTIONS.package_key, pw,
whole_file=True)
common.GetKeyPasswords將會提示用戶輸入密碼,假如密碼與之前創建公鑰私鑰的密碼不對,則提示錯誤,匹配才能往下進行,并返回更新后{key:password}
根據ota_from_targetfiles中指定的package_key,找到其密碼pw
調用SignFile對OTA包進行整包簽名
def SignFile(input_name, output_name, key, password, min_api_level=None,
codename_to_api_level_map=dict(),
whole_file=False):
"""Sign the input_name zip/jar/apk, producing output_name. Use the
given key and password (the latter may be None if the key does not
have a password.
If whole_file is true, use the "-w" option to SignApk to embed a
signature that covers the whole file in the archive comment of the
zip file.
min_api_level is the API Level (int) of the oldest platform this file may end
up on. If not specified for an APK, the API Level is obtained by interpreting
the minSdkVersion attribute of the APK's AndroidManifest.xml.
codename_to_api_level_map is needed to translate the codename which may be
encountered as the APK's minSdkVersion.
"""
java_library_path = os.path.join(
OPTIONS.search_path, OPTIONS.signapk_shared_library_path)
cmd = [OPTIONS.java_path, OPTIONS.java_args,
"-Djava.library.path=" + java_library_path,
"-jar",
os.path.join(OPTIONS.search_path, OPTIONS.signapk_path)]
cmd.extend(OPTIONS.extra_signapk_args)
if whole_file:
cmd.append("-w")
min_sdk_version = min_api_level
if min_sdk_version is None:
if not whole_file:
min_sdk_version = GetMinSdkVersionInt(
input_name, codename_to_api_level_map)
if min_sdk_version is not None:
cmd.extend(["--min-sdk-version", str(min_sdk_version)])
cmd.extend([key + OPTIONS.public_key_suffix,
key + OPTIONS.private_key_suffix,
input_name, output_name])
p = Run(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
if password is not None:
password += "\n"
p.communicate(password)
if p.returncode != 0:
raise ExternalError("signapk.jar failed: return code %s" % (p.returncode,))
這里實際進行的命令是:
java -Xmx2048m -Djava.library.path=$ANDROID_BUILD_TOP/out/host/linux-x86/lib64 -jar $ANDROID_BUILD_TOP/out/host/linux-x86/framework/signapk.jar -w $ANDROID_BUILD_TOP/build/target/product/security/testkey.x509.pem $ANDROID_BUILD_TOP/build/target/product/security/testkey.pk8 $1 $2
通過反編譯可以看出對ota包的簽名實際操作如下:
計算出key的個數,這里由于公鑰私鑰成對出現,而且指定輸入輸出文件,那么key的對數即為在-w之后的個數除以2,再減去1,ota簽名時numKeys為1.
int numKeys = (args.length - argstart) / 2 - 1;
if ((signWholeFile) && (numKeys > 1)) {//這里證明了可以的對數只能為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)];
inputJar = new JarFile(new File(inputFilename), false);
outputFile = new FileOutputStream(outputFilename);
讀取公鑰內容
File firstPublicKeyFile = new File(args[(argstart + 0)]);//獲取公鑰文件
X509Certificate[] publicKey = new X509Certificate[numKeys];//新建X509證書
try {
for (int i = 0; i < numKeys; i++) {
int argNum = argstart + i * 2;
publicKey[i] = readPublicKey(new File(args[argNum]));//將公鑰讀取到X509結構的publicKey中
hashes |= getDigestAlgorithm(publicKey[i], minSdkVersion);//計算摘要
}
} catch (IllegalArgumentException e) {
System.err.println(e);
System.exit(1);
}
讀取私鑰內容
timestamp -= TimeZone.getDefault().getOffset(timestamp);
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]));
}
進行簽名
signWholeFile(inputJar, firstPublicKeyFile, publicKey[0], privateKey[0], timestamp, minSdkVersion, outputFile);
其實現如下:
CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile, publicKey, privateKey, timestamp, minSdkVersion, outputStream);
ByteArrayOutputStream temp = new ByteArrayOutputStream();
byte[] message = "signed by SignApk".getBytes("UTF-8");
temp.write(message);//將message寫入輸出流
temp.write(0);
cmsOut.writeSignatureBlock(temp);
byte[] zipData = cmsOut.getSigner().getTail();
//檢查zip格式核心目錄結束標識是否為504B0506
if ((zipData[(zipData.length - 22)] != 80) || (zipData[(zipData.length - 21)] != 75) || (zipData[(zipData.length - 20)] != 5) || (zipData[(zipData.length - 19)] != 6))
{
throw new IllegalArgumentException("zip data already has an archive comment");
}
int total_size = temp.size() + 6;
//檢查簽名大小
if (total_size > 65535) {
throw new IllegalArgumentException("signature is too big for ZIP file comment");
}
//結尾格式以2字節`signature_staret` ff ff 2字節`total_size`結尾
int signature_start = total_size - message.length - 1;
temp.write(signature_start & 0xFF);
temp.write(signature_start >> 8 & 0xFF);
temp.write(255);
temp.write(255);
temp.write(total_size & 0xFF);
temp.write(total_size >> 8 & 0xFF);
temp.flush();
//檢查temp流的結尾格式
byte[] b = temp.toByteArray();
for (int i = 0; i < b.length - 3; i++) {
if ((b[i] == 80) && (b[(i + 1)] == 75) && (b[(i + 2)] == 5) && (b[(i + 3)] == 6)) {
throw new IllegalArgumentException("found spurious EOCD header at " + i);
}
}
outputStream.write(total_size & 0xFF);
outputStream.write(total_size >> 8 & 0xFF);
temp.writeTo(outputStream);//將temp流寫到輸出文件中
5. 校驗OTA包
5.1 RecoverySystem校驗OTA包
RecoverySystem中有校驗OTA包的接口
public static void verifyPackage(File packageFile,
ProgressListener listener,
File deviceCertsZipFile)
其校驗流程如下:
1.獲取OTA包
final RandomAccessFile raf = new RandomAccessFile(packageFile, "r");
2.校驗OTA包的尾部
raf.seek(fileLen - 6);
byte[] footer = new byte[6];
raf.readFully(footer);
//校驗后6字節中間的兩個字節是否為ff,與signapk.jar邏輯相同
if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) {
throw new SignatureException("no signature in file (no footer)");
}
//獲取commentSize以及signatureStart
final int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8);
final int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8);
byte[] eocd = new byte[commentSize + 22];
raf.seek(fileLen - (commentSize + 22));
raf.readFully(eocd);
// Check that we have found the start of the
// end-of-central-directory record.
//檢查核心標識是否為504b0506
if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b ||
eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) {
throw new SignatureException("no signature in file (bad footer)");
}
//檢查eocd后四個字節是否是EOCD標識,如果是則報錯
for (int i = 4; i < eocd.length-3; ++i) {
if (eocd[i ] == (byte)0x50 && eocd[i+1] == (byte)0x4b &&
eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) {
throw new SignatureException("EOCD marker found after start of EOCD");
}
}
3.從OT包中獲取證書
// Parse the signature
PKCS7 block =
new PKCS7(new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart));
//獲取證書
// Take the first certificate from the signature (packages
// should contain only one).
X509Certificate[] certificates = block.getCertificates();
if (certificates == null || certificates.length == 0) {
throw new SignatureException("signature contains no certificates");
}
X509Certificate cert = certificates[0];
//獲取公鑰
PublicKey signatureKey = cert.getPublicKey();
SignerInfo[] signerInfos = block.getSignerInfos();
if (signerInfos == null || signerInfos.length == 0) {
throw new SignatureException("signature contains no signedData");
}
SignerInfo signerInfo = signerInfos[0];
4.對比公鑰信息
// Check that the public key of the certificate contained
// in the package equals one of our trusted public keys.
boolean verified = false;
HashSet trusted = getTrustedCerts(
deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile);
for (X509Certificate c : trusted) {
if (c.getPublicKey().equals(signatureKey)) {
verified = true;
break;
}
}
if (!verified) {
throw new SignatureException("signature doesn't match any trusted key");
}
確保OTA包中獲取的公鑰是在信任的keylist之中,信任的keylist是從以下目錄獲取的:
private static final File DEFAULT_KEYSTORE =
new File("/system/etc/security/otacerts.zip");
這個文件就是在sign_target_files_apks.py中通過指定-O選項時更新的。假如沒有指定新的簽名目錄,那么使用原生的testkey作為密鑰。所以從校驗可以看出,至少對ota整包的簽名的公鑰信息必須要與待OTA升級的system/etc/otacerts.zip中的公鑰信息是要一致的,否則將在校驗時出錯。
這個校驗流程與常規的簽名校驗一致,假如加密文件,這里OTA包,使用了私鑰進行簽名,并在尾部附上公鑰,那么正常而言,使用該公鑰即可對其進行驗證,但是這里就多了一個流程是公鑰必須要和需要升級的固件是一致的,相當于CA的一個作用證明該公鑰是有效的,才會繼續使用公鑰去計算出OTA包中的摘要,然后通過該摘要值與給到的OTA包進行計算的摘要值進行對比,保證該OTA包是沒有經過修改的。
5.對比摘要值
SignerInfo verifyResult = block.verify(signerInfo, new InputStream
這里的veriry中還實現了一個read方法,應當是對ota包中的內容進行讀取,并與signerinfo的信息進行比較,其中read的內容范圍為:
// The signature covers all of the OTA package except the
// archive comment and its 2-byte length.
long toRead = fileLen - commentSize - 2;
當校驗結果為null時出錯
if (verifyResult == null) {
throw new SignatureException("signature digest verification failed");
}
5.2 recovery校驗OTA包
recovery的校驗流程與RecoverySystem中相仿。
6. 結論
一般而言,正常編譯OTA包與編譯固件,均是默認使用testkey進行簽名
假如指定了新的簽名目錄,那么必須保證,待升級的固件中的公鑰信息(包括system以及recovery)都要與升級的OTA包中的公鑰信息等匹配,即使用同一對密鑰對。
總結
以上是生活随笔為你收集整理的android ota 版本校验,OTA升级签名校验简析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: android 如何去掉自定义标签页,A
- 下一篇: android 代码设置alignlef