若是进行过OTA升级的开发者,都或多或少有这样的疑问,如何肯定该OTA升级包是能够信任的呢?这其中其实涉及到一个签名验证的流程。java
在生成正规的固件时,通常会运行生成新key的脚本,并从新修改key中的信息。以网上经常使用的生成key的脚本为例:linux
#!/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:android
openssl genrsa -3 -out $1.pem 2048
生成私钥后,生成证书签署请求,即公钥:api
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.该选项说明生成一个自签名的证书。-key
file: use the private key contained in file-days
: number of days a certificate generated by -x509 is valid for.最后经过私钥pem文件生成PKCS8私钥文件app
openssl pkcs8 -in $1.pem -topk8 -outform DER -out $1.pk8 -passout stdin
也能够参考android原生的生成key的流程,位于android/development/tools下的make_key脚本i。
自此后,生成了一对公私钥用于签名校验。dom
sign_target_files_apks.py
sign_target_files_apks
在本人以前的用法都局限于将targetfile里的apk进行重签名,其流程以下:ide
1.获取脚本输入参数
2.获取输入文件以及输出文件参数,读取misc_info
文件ui
input_zip = zipfile.ZipFile(args[0], "r") output_zip = zipfile.ZipFile(args[1], "w") misc_info = common.LoadInfoDict(input_zip)
misc_info
记录了一些参数,以下:this
其中这里关注的是默认签名的路径:加密
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")
当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方法。
当方案中定义了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())
ota_from_target_files.py
在上述步骤中,经过对targetfiles中的otacerts.zip以及recovery的/res/keys进行更新后,生成出来的固件假如与后续的ota包签名不符,那么在校验的时候也是会失败,因此在生成ota包时,也必须指定相应的公钥。
在ota_from_target_files
中有以下的解析:
-k (--package_key) <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)
ota_from_targetfiles
中指定的package_key
,找到其密码pwdef 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包的签名实际操做以下:
-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流写到输出文件中
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<X509Certificate> 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"); }
recovery的校验流程与RecoverySystem中相仿。