Android签名与校验过程详解

原文: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进行真正的校验,校验通过则成功,否则失败。
 

猜你喜欢

转载自blog.csdn.net/fengjinghuanian/article/details/86607374
今日推荐