原文:https://blog.csdn.net/gulinxieying/article/details/78677487
目 录
一、签名与校验原理概要 2
1、数字签名简介 2
2、CMS简介 2
二、signapk工具签名过程 4
三、OTA校验过程 6
Android签名与校验过程详解
一、签名与校验原理概要
1、数字签名简介
在日常生活中,签名通常被做为个人身份的凭证。当一份文件上有某个人的签名时,便相信此份文件确实由此人审阅过了。与之类似,在数字安全领域中,数字签名也起着类似的作用。
首先,数字签名证实了一份数字信息确实来自于某个实体。因为基于非对称加密的原理,用私钥加密的消息只能用对应的公钥解密,反之亦然。如图 1 所示,签名是由该实体的私钥生成,而私钥只由签名方持有。因此在图 2 中,只能用签名方的公钥对签名进行解密。而当解密成功时,便可相信是签名方生成了此消息。
其次,数字签名可以确保消息在传递过程中未被篡改。如图 1 所示,数字签名是由特定算法的哈希值加密而来。使用 MD5、SHA 等哈希算法可以确保消息哈希值的唯一性。因此在图 2 中,可以通过用同样的算法重新计算消息的哈希值,与签名中的哈希值对比。若结果一致,则可信任该消息在发出后未被篡改。
图一、数字签名的生成过程 图二、数字签名的校验过程
2、CMS简介
由于我们的签名规范源于CMS,这里简单介绍下。
CMS(Crypto Message Syntax)是由互联网工程任务组(IETF)制定的安全消息规范 (RFC 5652)。该规范定义多种消息格式,分别用于对任意消息进行数字签名、哈希、认证和加密。
该规范所规定的数字签名格式可以包含如下内容:哈希值生成算法、签名方的数字证书(包含签名方的公钥)、签名方的基本信息、消息原文、签名等。对比上一节数字签名的生成和验证过程可以发现,CMS 规范所规定格式已经包含了数字签名所有必需的信息。
同时,对于开发者而言,可以利用开源工具方便的生成 CMS 格式的数字签名。例如 Linux 下的 openssl 命令以及 Java 的开源类库 BouncyCastle,都提供了针对 CMS 格式数字签名的生成和验证功能。
由于我们使用的签名工具signapk使用java接口所写,我们所关心的就是如何用Java实现CMS数字签名的生成与验证。签名过程大同小异,在代码实现上也是如此。这里生成数字签名的实例在网上摘录如下:
【生成CMS数字签名】
public String sign(X509CertificateHolder signCert, KeyPair signKP) {
List certList = new ArrayList();
certList.add(signCert);
Store certs = new JcaCertStore(certList);
CMSTypedData msg = new CMSProcessableByteArray("Hello world!".getBytes());
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA1withRSA").setProvider("BC").build(signKP.getPrivate());
gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder().setProvider("BC").build()).build(sha1Signer, signCert));
gen.addCertificates(certs);
CMSSignedData sigData = gen.generate(msg, true);
String signature = new String(sigData.getEncoded());
Return signature;
}
如上所展示的是为"Hello World"这一字符串生成 CMS 数字签名的过程。其中 certs 代表签名方的证书,signKP.getPrivate() 代表签名方的私钥,SHA1withRSA 代表消息的哈希算法是 SHA1,生成公钥私钥的算法是 RSA。这些要素与前文介绍的数字签名的生成过程相吻合。最后得到的 signature 就是 CMS 格式的数字签名字符串。对比signapk.java中的writeSignatureBlock函数,在操作步骤上基本一致。
最后值得注意的是 gen.generate(msg, true),这里的第二个参数代表是否将消息原文封装到签名当中。若这一参数为 false,通常称生成的是 detached signature,表示签名和消息是分开存放的。
【验证CMS数字签名】
public String verify(String signature){
CMSSignedData s = new CMSSignedData(signature.getBytes());
CertStore certs = s.getCertificatesAndCRLs("Collection", "BC");
SignerInformationStore signers = s.getSignerInfos();
boolean verified = false;
for (Iterator i = signers.getSigners().iterator(); i.hasNext(); ) {
SignerInformation signer = (SignerInformation) i.next();
Collection<? extends Certificate> certCollection =
certs.getCertificates(signer.getSID());
if (!certCollection.isEmpty()) {
X509Certificate cert =
(X509Certificate) certCollection.iterator().next();
if (signer.verify(cert.getPublicKey(), "BC")) {
verified = true;
}
}
}
CMSProcessable signedContent = s.getSignedContent() ;
byte[] originalContent = (byte[]) signedContent.getContent();
return new String(originalContent);
}
这里展示了如何验证在上述生成的数字签名。因为CMS数字签名支持多个实体对消息进行签名,因此这里可以对每个签名方逐一进行验证。签名方的证书已经包含在签名中,而证书包含验证签名所需的签名方的公钥。recovery中verifier.cpp中的verify_file函数解析步骤可以类比上述内容。
二、signapk工具签名过程
signapk对zip包的签名指令如下:
java-Xmx1024m -jarout/host/linux-x86/framework/signapk.jar -w ./testkey.x509.pem./testkey.pk8 update.zip update_signed.zip
从指令中可以看出,真个签名过程是基于signapk.jar工具包进行的,对应源码位于build\tools\signapk\SignApk.java中,具体流程可以从main函数分析,这里只关注比较核心的部分——CMSSigner类中的write函数,该函数中包含对update_signed.zip包中CERT.SF、CERT.RSA、MANIFEST.MF等的签名与生成过程(下面是网友总结,与自己对代码的理解基本吻合):
1. 对jar包中的各文件进行sha1hash,生成manifest对象,除META-INF文件夹下MANIFEST.MF、CERT.SF、CERT.RSA、com/android/otacert外。
Manifest manifest = addDigestsToManifest(inputJar, hash);
2. 将manifest对象中描述的各文件copy到新jar包中;
copyFiles(manifest, inputJar, outputJar, timestamp, 0);
3. 如果-w整包签,则将证书.x509.pem复制到META-INF/com/android/otacert,并在manifest对象中增加META-INF/com/android/otacert的SHA1摘要;
addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
这里是在signapk.java中main函数中获取传参后根据是否有"-w"所决定的:
while (argstart < args.length && args[argstart].startsWith("-")) {
if ("-w".equals(args[argstart])) {
signWholeFile = true;
++argstart;
} else if
.....
这里如果看到"-w"参数,则将signWholeFil标志设置为true。接着走如下流程:
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, alignment);
signFile(manifest, inputJar, publicKey, privateKey, outputJar);
outputJar.close();
}
.....
跟踪signWholeFile函数会走到上述提到的CMSSigner类中的write函数中来(这里从代码上不知为何并没有走通,应该和CMS协议规范有关吧。不过,本人通过打log追踪,具体流程确实如上所说)。可以看出,signWholeFile为false的情况下,会走else分支,这里并没有addOtacert操作。
4. 将manifest对象写入新jar包中META-INF/MANIFEST.MF文件;
现在开始的操作主要就是在signFile函数中完成的。该步骤具体内容如下:
// MANIFEST.MF
JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
manifest.write(outputJar);
5. 生成签名文件META-INF/CERT.SF和CERT.RSA;
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, getDigestAlgorithm(publicKey[k]));
byte[] signedData = baos.toByteArray();
outputJar.write(signedData);
// CERT.{EC,RSA} / CERT#.{EC,RSA}
final String keyType = publicKey[k].getPublicKey().getAlgorithm();
je = new JarEntry(numKeys == 1 ?
(String.format(CERT_SIG_NAME, keyType)) :
(String.format(CERT_SIG_MULTI_NAME, k, keyType)));
je.setTime(timestamp);
outputJar.putNextEntry(je);
writeSignatureBlock(new CMSProcessableByteArray(signedData),
publicKey[k], privateKey[k], outputJar);
}
对于CERT.SF文件,是对manifest中(每一项文件名称、sha1摘要)做sha1摘要, 生成新的Manifest对象,具体见writeSignatureFile函数;而CERT.RSA文件,privateKey对signedData加密生成签名,然后把签名和公钥证书一起保存到CERT.RSA中,是PKCS#7格式签名/加密信息(对CERT.SF进行SHA1withRSA,并将证书.pem附在其中)。具体见writeSignatureBlock函数。
7. 如果-w整包签,则在jar/zip文件
找到'End of central directory signature'
(一般zip如果无Comment length时,EOCD标记距尾部22Bytes)
[End of central directory record]格式
Offset Bytes Description[18]
0 4End of central directory signature | 核心目录结束标记(0x06054b50)
4 2Number of this disk | 当前磁盘编号
6 2Disk where central directory starts | 核心目录开始位置的磁盘编号
8 2Number of central directory records on this disk | 该磁盘上所记录的核心目录数量
10 2Total number of central directory records | 核心目录结构总数
12 4Size of central directory (bytes) | 核心目录的大小
16 4Offset of start of central directory,relative to start of archive | 核心目录开始位置相对于archive开始的位移
20 2Comment length (n)
注释长度
22 nComment(注释内容)
在其后写入Archive Comment:
signature_start = Comment_Length - len('signed by SignApk') - 1
(PKCS#7_SIG)是对对整个zip包(从ZIP头到<EOCD.CommentLength>之前)数据生成sha1,再对sha1用私钥加密生成签名放在公钥证书尾部整个Comment为PKCS#7格式(类似于CERT.RSA,只不过是对整个zip包数据做签名)
OTA包校验时也是先对ZIP包数据生成sha1,然后从ZIP尾部EOCD中取出Comment中的签名数据(SHA1WithRSA),用公钥解开再和sha1对比,一致则验证通过,具体实现在recovery\verifier.cpp中的verify_file()函数中。
三、OTA校验过程
OTA在升级前,拿到一个ota zip包之后,在真正安装前会对其进行签名校验。具体流程在recovery/install.zpp文件中,主要涉及到load_keys以及verify_file函数。load_keys主要是用来load /res/keys文件,并从中解析出公钥publicKey。下面重点关注verify_file:
首先声明的是,在整个校验期间系统并没有加压ota的zip包,是直接根据zip的压缩格式中的关键标志进行对zip包的解析。函数开头有说明:
// An archive with a whole-file signature will end in six bytes:
//
// (2-byte signature start) $ff $ff (2-byte comment size)
//
// (As far as the ZIP format is concerned, these are part of the
// archive comment.) We start by reading this footer, this tells
// us how far back from the end we have to start reading to find
// the whole comment.
简单来说就是:一个被整包签名做的压缩文件总是使用6个特定byte结尾,具体格式为:
(2-byte signature start) $ff $ff (2-byte comment size)
函数开始开头就会从zip文件结尾,根据这6个byte进行其他标志bytes的查找个定位。具体code不再列出,本人根据各标志位位置,对ota的某zip包做了一个解析,相关签名部分以十六进制显示如下:
经过verify_file函数定位解析后,主要标志位置大致如下图所示:
清楚了上面的zip文件中签名所存放的位置后,接着就可以将Signature块取出来:
size_t signature_size = signature_start - FOOTER_SIZE;//1720-6=1714
if (!read_pkcs7(eocd + eocd_size - signature_start, signature_size, &sig_der,
&sig_der_length)) {
LOGE("Could not find signature DER block\n");
return VERIFY_FAILURE;
}
接着就可以根据签名时的加密算法进行校验了,我们的加密算法是RSA,这里只看RSA分支部分:
// The 6 bytes is the "(signature_start) $ff $ff (comment_size)" that
// the signing tool appends after the signature itself.
if (pKeys[i].key_type == Certificate::RSA) {
if (sig_der_length < RSANUMBYTES) {
// "signature" block isn't big enough to contain an RSA block.
LOGI("signature is too short for RSA key %zu\n", i);
continue;
}
if (!RSA_verify(pKeys[i].rsa, sig_der, RSANUMBYTES,
hash, pKeys[i].hash_len)) {
LOGI("failed to verify against RSA key %zu\n", i);
continue;
}
LOGI("whole-file signature verified against RSA key %zu\n", i);
free(sig_der);
return VERIFY_SUCCESS;
} else if
分析上述代码可知,整个过程分两个步骤:
1、判断sig_der_length 长度是否比RSANUMBYTES(签名部分中的publicKey内容)要大,如果小于,则提示" signature is too short for RSA key ",继续校验其他publicKey,如果没有其他的publicKey,或者其他的publicKey也是这种情况,则continue到校验失败;
2、通过函数RSA_verify函数对RSANUMBYTES进行真正的校验,校验通过则成功,否则失败。