最近一直有两个疑问 升级包签名的规则和签名文件具体的位置,所以大概看了下签名流程并整理出来
大概理解了下 1.如何签名 2.如何校验
一.相关整理
首先要大概知道的两个内容
1.CMS数字签名
参考:https://www.ibm.com/developerworks/cn/java/j-lo-cms-ticketbasesso/
大概理解为
签名阶段:取数据的hash 使用私钥进行加密放入签名部分, 签名部分+数据部分 组合为新的zip
校验阶段:取出数据的hash,使用公钥对签名部分的加密hash进行解密,解密后的hash和数据的hash进行对比
2.zip文件的结构
参考:http://blog.sina.com.cn/s/blog_c496a6310102wje4.html
这里主要理解以下其中的End of central directory record
二.生成签名
做升级包的流程还是从ota_from_target_files ,这部分略过,直接从签名开始分析,传入的什么参数
最后的签名命令为:
java -Djava.library.path=out/host/linux-x86/lib64
-jar out/host/linux-x86/framework/signapk.jar
-w device/mediatek/security/releasekey.x509.pem ./device/mediatek/security/releasekey.pk8
update.zip update_sign.zip 2>&1 | tee sign.log
所以签名的流程从sign.jar开始,源码位于build/make/tools/signapk/src/com/android/signapk/SignApk.java
jar文件也是从main方法开始 开始上菜,第一道小炒肉
1.main方法
几句话总结
1.解析传入的参数 根据-w判断是否为签名升级包
2.获取输入和输出
3.获取公钥 转化为x509证书格式 readPublicKey方法
4.定义升级内文件将使用的时间戳 (这个其实是为了签名APK时 有点用处,官方文档中说可以减少patch的生成)
5.获取私钥 readPrivateKey
6.定义签名算法的类型 getDigestAlgorithmForOta
7.将以上参数传入签名升级包的方法signWholeFile
//从main方法开始 public static void main(String[] args) { if (args.length < 4) usage(); //输出一波参数信息 //arg = -w //arg = device/mediatek/security/releasekey.x509.pem //arg = ./device/mediatek/security/releasekey.pk8 //arg = update.zip //arg = update_sign.zip for(String arg : args){ System.out.println("arg = " + arg); } // Install Conscrypt as the highest-priority provider. Its crypto primitives are faster than // the standard or Bouncy Castle ones. Security.insertProviderAt(new OpenSSLProvider(), 1); // Install Bouncy Castle (as the lowest-priority provider) because Conscrypt does not offer // DSA which may still be needed. // TODO: Stop installing Bouncy Castle provider once DSA is no longer needed. Security.addProvider(new BouncyCastleProvider()); boolean signWholeFile = false; String providerClass = null; int alignment = 4; Integer minSdkVersionOverride = null; boolean signUsingApkSignatureSchemeV2 = true; //1.解析传入的参数 根据-w判断是否为签名升级包 int argstart = 0; while (argstart < args.length && args[argstart].startsWith("-")) { if ("-w".equals(args[argstart])) { signWholeFile = true; ++argstart; } else if ("-providerClass".equals(args[argstart])) { if (argstart + 1 >= args.length) { usage(); } providerClass = args[++argstart]; ++argstart; } else if ("-a".equals(args[argstart])) { alignment = Integer.parseInt(args[++argstart]); ++argstart; } else if ("--min-sdk-version".equals(args[argstart])) { String minSdkVersionString = args[++argstart]; try { minSdkVersionOverride = Integer.parseInt(minSdkVersionString); } catch (NumberFormatException e) { throw new IllegalArgumentException( "--min-sdk-version must be a decimal number: " + minSdkVersionString); } ++argstart; } else if ("--disable-v2".equals(args[argstart])) { signUsingApkSignatureSchemeV2 = false; ++argstart; } else { usage(); } } //args.length = 5 //argstart = 1 System.out.println("args.length = " + args.length); System.out.println("argstart = " + argstart); //如果去掉开头的-w余数等于2 说明参数不对 if ((args.length - argstart) % 2 == 1) usage(); //这里numKeys其实是1 int numKeys = ((args.length - argstart) / 2) - 1; //这里的打印也能看出来,当签名升级包的时候 签名只有一个 if (signWholeFile && numKeys > 1) { System.err.println("Only one key may be used with -w."); System.exit(2); } loadProviderIfNecessary(providerClass); //2.获取输入和输出 //获取输入文件 和输出文件的名称 倒数第一个参数和倒数第二个 String inputFilename = args[args.length-2]; String outputFilename = args[args.length-1]; JarFile inputJar = null; //定义文件流输出文件 FileOutputStream outputFile = null; try { //3.获取公钥文件releasekey.x509.pem File firstPublicKeyFile = new File(args[argstart+0]); //创建x509证书对象 X509Certificate[] publicKey = new X509Certificate[numKeys]; try { for (int i = 0; i < numKeys; ++i) { int argNum = argstart + i*2; //将公钥转换为x509证书格式 publicKey[i] = readPublicKey(new File(args[argNum])); System.out.println("publicKey" + "[" + i + "]" + publicKey[i]); } } catch (IllegalArgumentException e) { System.err.println(e); System.exit(1); } // Set all ZIP file timestamps to Jan 1 2009 00:00:00. // 4.创建了一个时间戳 签名之后 文件内容时间为2009 00:00:00. long timestamp = 1230768000000L; // The Java ZipEntry API we're using converts milliseconds since epoch into MS-DOS // timestamp using the current timezone. We thus adjust the milliseconds since epoch // value to end up with MS-DOS timestamp of Jan 1 2009 00:00:00. timestamp -= TimeZone.getDefault().getOffset(timestamp); //5.获取私钥releasekey.pk8 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])); System.out.println("privateKey" + "[" + i + "]" + privateKey[i]); } //将输入文件转换为了jar文件 inputJar = new JarFile(new File(inputFilename), false); // Don't verify. outputFile = new FileOutputStream(outputFilename); // NOTE: Signing currently recompresses any compressed entries using Deflate (default // compression level for OTA update files and maximum compession level for APKs). // 如果signWholeFile为true 使用signWholeFile方法 if (signWholeFile) { //6.定义签名算法的类型 int digestAlgorithm = getDigestAlgorithmForOta(publicKey[0]); //digestAlgorithm = 1 System.out.println("digestAlgorithm = " + digestAlgorithm); // inputJar输入文件,也就是原始的升级包 // firstPublicKeyFile 公钥文件 // publicKey[0]x509证书格式 // privateKey[0] 解析后的PrivateKey私钥 // digestAlgorithm 签名算法的类型 // outputFile 输出文件 signWholeFile(inputJar, firstPublicKeyFile, publicKey[0], privateKey[0], digestAlgorithm, timestamp, outputFile); } else { //后面的不用看了,签名APK用的 ...... ......
其中用到几个方法,大致过下
1.1 readPublicKey
private static X509Certificate readPublicKey(File file) throws IOException, GeneralSecurityException { FileInputStream input = new FileInputStream(file); try { CertificateFactory cf = CertificateFactory.getInstance("X.509"); return (X509Certificate) cf.generateCertificate(input); } finally { input.close(); } }
1.2 readPrivateKey
/** Read a PKCS#8 format private key.读取PKCS#8格式的私钥 */ private static PrivateKey readPrivateKey(File file) throws IOException, GeneralSecurityException { //创建数据输入流 DataInputStream input = new DataInputStream(new FileInputStream(file)); try { //一次性把长度都读完 byte[] bytes = new byte[(int) file.length()]; input.read(bytes); /* Check to see if this is in an EncryptedPrivateKeyInfo structure. 检查这是否在EncryptedPrivateKeyInfo结构中*/ PKCS8EncodedKeySpec spec = decryptPrivateKey(bytes, file); if (spec == null) { spec = new PKCS8EncodedKeySpec(bytes); } /* * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm * OID and use that to construct a KeyFactory. * 现在,它处于PKCS#8 PrivateKeyInfo结构中。 阅读其算法OID并将其用于构造KeyFactory。 */ PrivateKeyInfo pki; try (ASN1InputStream bIn = new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()))) { pki = PrivateKeyInfo.getInstance(bIn.readObject()); } String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId(); System.out.println("algOid = " + algOid); return KeyFactory.getInstance(algOid).generatePrivate(spec); } finally { input.close(); } }
/** * Decrypt an encrypted PKCS#8 format private key. * 解密加密的PKCS#8格式的私钥。 * Based on ghstark's post on Aug 6, 2006 at * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949 * * @param encryptedPrivateKey The raw data of the private key 私钥的数据 * @param keyFile The file containing the private key 私钥文件 */ private static PKCS8EncodedKeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile) throws GeneralSecurityException { EncryptedPrivateKeyInfo epkInfo; try { epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey); } catch (IOException ex) { // Probably not an encrypted key. return null; } char[] password = readPassword(keyFile).toCharArray(); SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName()); Key key = skFactory.generateSecret(new PBEKeySpec(password)); Cipher cipher = Cipher.getInstance(epkInfo.getAlgName()); cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters()); try { return epkInfo.getKeySpec(cipher); } catch (InvalidKeySpecException ex) { System.err.println("signapk: Password for " + keyFile + " may be bad."); throw ex; } }
1.3 getDigestAlgorithmForOta
/** * Returns the digest algorithm ID (one of {@code USE_SHA1} or {@code USE_SHA256}) to be used * for signing an OTA update package using the private key corresponding to the provided * certificate.从公钥中读取是sha1算法 还是sha256算法,用于使用与提供的证书相对应的私钥来签署OTA更新包。 */ private static int getDigestAlgorithmForOta(X509Certificate cert) { String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US); if ("SHA1WITHRSA".equals(sigAlg) || "MD5WITHRSA".equals(sigAlg)) { // see "HISTORICAL NOTE" above. return USE_SHA1; } else if (sigAlg.startsWith("SHA256WITH")) { return USE_SHA256; } else { throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg + "\" in cert [" + cert.getSubjectDN()); } }
2.signWholeFile
大致流程总结
1.创建CMSSigner对象 将参数传入其构造函数
2.创建字节数组输出流temp
3.写入了一个byte类型signed by SignApk字符串到comment 用于一些工具的查看
4.拷贝输入流到输出流,对升级包除去comment length 和 comment 内容进行cms数字签名并将签名信息写入temp
5.获取输出流末尾 因为现在输出流中其实没有comment 所有可以通过这种方式检验是不是真的没有
6.获取comment的总长度total_size,加上6用于存放comment 长度和 signture start长度
7.获取signature_start的长度 并在加的6个字节中写入signature_start 0xff 0xff total_size
8.将comment 长度 total_size 和 comment temp数据写入到输出流中
还是把传入的参数放上来 // inputJar输入文件,也就是原始的升级包 // firstPublicKeyFile 公钥文件 // publicKey[0]x509证书格式 // privateKey[0] 解析后的PrivateKey私钥 // digestAlgorithm 签名算法的类型 // outputFile 输出文件 private static void signWholeFile(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, int hash, long timestamp, OutputStream outputStream) throws Exception { System.out.println("signWholeFile"); //1.创建CMSSigner对象 CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile, publicKey, privateKey, hash, timestamp, outputStream); //2.创建字节数组输出流 ByteArrayOutputStream temp = new ByteArrayOutputStream(); // put a readable message and a null char at the start of the // archive comment, so that tools that display the comment // (hopefully) show something sensible. // TODO: anything more useful we can put in this message? // 在存档注释的开头放置一条可读消息和一个空字符,以便显示注释的工具显示出一些有意义的信息。 //3. 写入了一个byte类型signed by SignApk 到comment 用于一些工具的查看 byte[] message = "signed by SignApk".getBytes(StandardCharsets.UTF_8); temp.write(message); // 写入一个空字符 temp.write(0); System.out.println("message = " + message.length); //4.拷贝输入流到输出流,对升级包除去comment length 和 comment 内容进行cms数字签名 //并将签名信息写入temp cmsOut.writeSignatureBlock(temp); //5.获取末尾???? 因为现在输出流中其实没有comment 所有可以通过这种方式检验是不是真的没有 byte[] zipData = cmsOut.getSigner().getTail(); System.out.println("zipData = " + zipData); System.out.println("zipData length = " + zipData.length); // For a zip with no archive comment, the // end-of-central-directory record will be 22 bytes long, so // we expect to find the EOCD marker 22 bytes from the end. // 如果压缩包没有存档注释,确认末尾有没有22字节的EOCD marker if (zipData[zipData.length-22] != 0x50 || zipData[zipData.length-21] != 0x4b || zipData[zipData.length-20] != 0x05 || zipData[zipData.length-19] != 0x06) { throw new IllegalArgumentException("zip data already has an archive comment"); } //6.获取comment的总长度total_size,加上6用于存放comment 长度和 signture start长度 System.out.println("temp.size() = " + temp.size()); System.out.println("signature_length = " + (temp.size() - message.length - 1)); int total_size = temp.size() + 6; //确认下签名的长度有没有问题 if (total_size > 0xffff) { throw new IllegalArgumentException("signature is too big for ZIP file comment"); } // signature starts this many bytes from the end of the file签名从文件末尾开始这么多字节 // 7.获取signature_start的长度 并在加的6个字节中写入signature_start 0xff 0xff total_size // 这里相当于是签名的长度加6 signture_start和total_size长度的差值是固定的 就是18 // 有个疑问点,signature的长度不就是之前算的temp的长度- message.length - 1 干嘛还搞个start int signature_start = total_size - message.length - 1; //这里相当于多加了6个字节 两个字节标识出开始签名的长度, temp.write(signature_start & 0xff); temp.write((signature_start >> 8) & 0xff); // Why the 0xff bytes? In a zip file with no archive comment, // bytes [-6:-2] of the file are the little-endian offset from // the start of the file to the central directory. So for the // two high bytes to be 0xff 0xff, the archive would have to // be nearly 4GB in size. So it's unlikely that a real // commentless archive would have 0xffs here, and lets us tell // an old signed archive from a new one. // 两个字节写入的0xff temp.write(0xff); temp.write(0xff); //两个字节标识出总长度 temp.write(total_size & 0xff); temp.write((total_size >> 8) & 0xff); temp.flush(); // Signature verification checks that the EOCD header is the // last such sequence in the file (to avoid minzip finding a // fake EOCD appended after the signature in its scan). The // odds of producing this sequence by chance are very low, but // let's catch it here if it does. // 签名验证检查EOCD标头是否是文件中的最后一个这样的序列(以避免minzip在扫描签名后附加伪造的EOCD) // 校验comment中是不是存在eocd, byte[] b = temp.toByteArray(); System.out.println("b.length = " + b.length); for (int i = 0; i < b.length-3; ++i) { if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) { throw new IllegalArgumentException("found spurious EOCD header at " + i); } } //8.将comment 长度 total_size 和 comment temp数据写入到输出流中 outputStream.write(total_size & 0xff); outputStream.write((total_size >> 8) & 0xff); //数据写到"输出流out"中 temp.writeTo(outputStream); }
以下对其中的一些流程单独解析
2.1 cmsOut.writeSignatureBlock(temp);
拷贝输入流到输出流,对升级包除去comment length 和 comment 内容进行cms数字签名,并将签名信息写入temp
public void writeSignatureBlock(ByteArrayOutputStream temp) throws IOException, CertificateEncodingException, OperatorCreationException, CMSException { SignApk.writeSignatureBlock(this, publicKey, privateKey, hash, temp); }
/** Sign data and write the digital signature to 'out'.签名数据并将数字签名写入输出 */ private static void writeSignatureBlock( CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, int hash, OutputStream out) throws IOException, CertificateEncodingException, OperatorCreationException, CMSException { System.out.println("writeSignatureBlock"); //生成cms数字签名 这是固定的方法 //创建X509Certificate类型的集合 长度为1 ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1); // 将公钥添加入集合 certList.add(publicKey); //创建JcaCertStore 存放了签名集合 JcaCertStore certs = new JcaCertStore(certList); System.out.println("CMSSignedDataGenerator gen"); CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); //这里其实是 new JcaContentSignerBuilder("SHA1withRSA").build(privateKey); ContentSigner signer = new JcaContentSignerBuilder( getJcaSignatureAlgorithmForOta(publicKey, hash)) .build(privateKey); System.out.println("gen.addSignerInfoGenerator"); gen.addSignerInfoGenerator( new JcaSignerInfoGeneratorBuilder( new JcaDigestCalculatorProviderBuilder() .build()) .setDirectSignature(true) .build(signer, publicKey)); System.out.println("gen.addCertificates"); gen.addCertificates(certs); System.out.println("CMSSignedData sigData = gen.generate(data, false)"); CMSSignedData sigData = gen.generate(data, false); //将数字签名写入到输出流中 try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) { System.out.println("try ASN1InputStream asn1"); //sigData.getEncoded()是生成的数字签名 [B@326de728 System.out.println("sigData.getEncoded() = " + sigData.getEncoded()); DEROutputStream dos = new DEROutputStream(out); dos.writeObject(asn1.readObject()); } }
这个方法就是固定生成CMS数字签名的流程 单看好像没啥问题,但是没看出来是怎么拿到需要签名的数据的,所以加了很多的log,大致流程如下
signWholeFile
message = 17
writeSignatureBlock
CMSSignedDataGenerator gen
keyAlgorithm = RSA
gen.addSignerInfoGenerator
gen.addCertificates
CMSSignedData sigData = gen.generate(data, false)
CMSSigner write()
构造函数 data.length = 0
一目了然,执行gen.generate(data, false)拿到了拷贝后的输出数据和数字签名数据
write方法大致流程如下
1.实例化signer
2.创建输出流
3.调用copyFiles 将输入流中的文件按顺序拷贝到输入流中 去除去除otacert文件
4.调用addOtacert 创建otacert文件将公钥内容拷贝进去
5.以上两个步骤拷贝数据过程中会执行WholeFileSignerOutputStream的write方法生成两份输出
两份包含去除了comment size 和data的升级包数据 代码了里在写入升级包最后数据的时候-2其实是eocd record中
存储comment size的两个字节 这两份一份儿用于生成数字签名,一份儿作为输出
6.同时会将完整的最后数据写入到footer 用于校验升级包是否已经包含了comment
2.2 CMSSigner write
@Override public void write(OutputStream out) throws IOException { System.out.println("write()"); try { //实例化signer对象 signer = new WholeFileSignerOutputStream(out, outputStream); JarOutputStream outputJar = new JarOutputStream(signer); //拷贝文件 添加时间戳 去除otacert文件 copyFiles(inputJar, STRIP_PATTERN, null, outputJar, timestamp, 0); // 添加otacert文件,将公钥内容拷贝进去 addOtacert(outputJar, publicKeyFile, timestamp); //以上写入outputjar过程中 signer也会执行写入 notifyclosing是将close标志位改为true signer.notifyClosing(); //关闭jar输出流 outputJar.close(); signer.finish(); } catch (Exception e) { throw new IOException(e); } }
2.3 CMSSigner copyFiles
/** * Copy all JAR entries from input to output. We set the modification times in the output to a * fixed time, so as to reduce variation in the output file and make incremental OTAs more * efficient. * 拷贝了输入流的文件 去除了otacert,加了一个时间戳,同时压缩包里非压缩和压缩文件分开处理 */ private static void copyFiles( JarFile in, Pattern ignoredFilenamePattern, ApkSignerEngine apkSigner, JarOutputStream out, long timestamp, int defaultAlignment) throws IOException { System.out.println("copyFiles"); //定义4096字节的buffer byte[] buffer = new byte[4096]; int num; //定义string集合 拿到输入文件中所有的文件内容,除了otacert ArrayList<String> names = new ArrayList<String>(); //遍历出输入文件中所有的文件,包括文件夹 for (Enumeration<JarEntry> e = in.entries(); e.hasMoreElements();) { //entry就是读取的每个文件了, JarEntry entry = e.nextElement(); //如果是文件夹,跳出循环 执行下一个 if (entry.isDirectory()) { System.out.println("entry.isDirectory() entry = " + entry.getName()); continue; } String entryName = entry.getName(); System.out.println("entryName = " + entryName); //对比有没有otacert 如果有的话 跳出循环,也就是说跑完循环之后的集合name没有otacert这个文件 if ((ignoredFilenamePattern != null) && (ignoredFilenamePattern.matcher(entryName).matches())) { System.out.println("ignoredFilenamePattern entryName = " + entryName); continue; } //将文件添加入list names.add(entryName); } System.out.println("list names = " + names); //对文件名称的集合进行排序 Collections.sort(names); System.out.println("after sort..."); System.out.println("list names = " + names); boolean firstEntry = true; long offset = 0L; // We do the copy in two passes -- first copying all the // entries that are STORED, then copying all the entries that // have any other compression flag (which in practice means // DEFLATED). This groups all the stored entries together at // the start of the file and makes it easier to do alignment // on them (since only stored entries are aligned). // 拷贝分为了两部,一部分是升级的镜像非压缩,另一部分默认压缩 // 创建string类型的集合, List<String> remainingNames = new ArrayList<>(names.size()); for (String name : names) { //根据名称返回条目 JarEntry inEntry = in.getJarEntry(name); //如果类型不是JarEntry.STORED if (inEntry.getMethod() != JarEntry.STORED) { // Defer outputting this entry until we're ready to output compressed entries. // 先把压缩的文件放入remainingNames集合中 remainingNames.add(name); continue; } //这里apkSigner传入的是null,直接返回true,啥也没干 if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) { continue; } // Preserve the STORED method of the input entry.保留输入条目的STORED方法 // 根据现有的条目创建新的条目 JarEntry outEntry = new JarEntry(inEntry); //在条目中写入时间戳 outEntry.setTime(timestamp); // Discard comment and extra fields of this entry to // simplify alignment logic below and for consistency with // how compressed entries are handled later. // 舍弃该条目的注释和多余字段以简化下面的对齐逻辑,并与以后处理压缩条目的方式保持一致。 outEntry.setComment(null); outEntry.setExtra(null); //defaultAlignment=0 这里对于升级包来说 其实alignment就是0 int alignment = getStoredEntryDataAlignment(name, defaultAlignment); // Alignment of the entry's data is achieved by adding a data block to the entry's Local // File Header extra field. The data block contains information about the alignment // value and the necessary padding bytes (0x00) to achieve the alignment. This works // because the entry's data will be located immediately after the extra field. // See ZIP APPNOTE.txt section "4.5 Extensible data fields" for details about the format // of the extra field. // 'offset' is the offset into the file at which we expect the entry's data to begin. // This is the value we need to make a multiple of 'alignment'. // public static final int LOCHDR=30 offset += JarFile.LOCHDR + outEntry.getName().length(); if (firstEntry) { // The first entry in a jar file has an extra field of four bytes that you can't get // rid of; any extra data you specify in the JarEntry is appended to these forced // four bytes. This is JAR_MAGIC in JarOutputStream; the bytes are 0xfeca0000. // See http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6808540 // and http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4138619. // jar文件中的第一个条目有一个四个字节的额外字段,您无法摆脱它; 您在JarEntry中指定的所有其他数据都将附加到这四个强制字节。 // 这是JarOutputStream中的JAR_MAGIC; 字节为0xfeca0000。 offset += 4; firstEntry = false; } int extraPaddingSizeBytes = 0; //alignment=0 忽略 if (alignment > 0) { long paddingStartOffset = offset + ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES; extraPaddingSizeBytes = (alignment - (int) (paddingStartOffset % alignment)) % alignment; } byte[] extra = new byte[ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES + extraPaddingSizeBytes]; ByteBuffer extraBuf = ByteBuffer.wrap(extra); extraBuf.order(ByteOrder.LITTLE_ENDIAN); extraBuf.putShort(ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID); // Header ID extraBuf.putShort((short) (2 + extraPaddingSizeBytes)); // Data Size extraBuf.putShort((short) alignment); //setExtra(byte[] extra):为条目设置可选的额外字段数据 outEntry.setExtra(extra); offset += extra.length; //方法开始编写新的ZIP文件条目并将流定位到条目数据的开头 out.putNextEntry(outEntry); //inspectEntryRequest 为 null ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = (apkSigner != null) ? apkSigner.outputJarEntry(name) : null; //entryDataSink 为 null DataSink entryDataSink = (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null; //将数据写入out中 try (InputStream data = in.getInputStream(inEntry)) { while ((num = data.read(buffer)) > 0) { out.write(buffer, 0, num); // entryDataSink 为空 忽略 if (entryDataSink != null) { entryDataSink.consume(buffer, 0, num); } offset += num; } } out.flush(); if (inspectEntryRequest != null) { inspectEntryRequest.done(); } } // Copy all the non-STORED entries. We don't attempt to // maintain the 'offset' variable past this point; we don't do // alignment on these entries. // 复制所有非存储条目。 我们不会尝试在此点之前保持'offset'变量; 我们不对这些条目进行对齐。 for (String name : remainingNames) { JarEntry inEntry = in.getJarEntry(name); if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) { continue; } // Create a new entry so that the compressed len is recomputed. // 创建一个新条目,以便重新计算压缩的len。 JarEntry outEntry = new JarEntry(name); outEntry.setTime(timestamp); out.putNextEntry(outEntry); ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = (apkSigner != null) ? apkSigner.outputJarEntry(name) : null; DataSink entryDataSink = (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null; //将数据写入out InputStream data = in.getInputStream(inEntry); while ((num = data.read(buffer)) > 0) { out.write(buffer, 0, num); if (entryDataSink != null) { entryDataSink.consume(buffer, 0, num); } } out.flush(); if (inspectEntryRequest != null) { inspectEntryRequest.done(); } } }
2.4 CMSSigner addOtacert
/** * Add a copy of the public key to the archive; this should * exactly match one of the files in * /system/etc/security/otacerts.zip on the device. (The same * cert can be extracted from the OTA update package's signature * block but this is much easier to get at.) * 将公钥的内容完全写入到otacert中,同时/system/etc/security/otacerts.zip存有副本 */ private static void addOtacert(JarOutputStream outputJar, File publicKeyFile, long timestamp) throws IOException { System.out.println("addOtacert"); //创建otacert文件的条目 JarEntry je = new JarEntry(OTACERT_NAME); je.setTime(timestamp); outputJar.putNextEntry(je); FileInputStream input = new FileInputStream(publicKeyFile); byte[] b = new byte[4096]; int read; //写入到输出流中 while ((read = input.read(b)) != -1) { outputJar.write(b, 0, read); } input.close(); }
2.5 WholeFileSignerOutputStream
private static class WholeFileSignerOutputStream extends FilterOutputStream { private boolean closing = false; private ByteArrayOutputStream footer = new ByteArrayOutputStream(); private OutputStream tee; public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) { super(out); this.tee = tee; System.out.println("构造函数 data.length = " + (footer.toByteArray()).length); } public void notifyClosing() { closing = true; System.out.println("notifyClosing data.length = " + (footer.toByteArray()).length); } //调用finish后 会将footer的长度-2写入到CMSTypedData和输出文件 public void finish() throws IOException { closing = false; byte[] data = footer.toByteArray(); System.out.println("data.length = " + (footer.toByteArray()).length); if (data.length < 2) throw new IOException("Less than two bytes written to footer"); //这里长度减2是干啥的??? 去除了comment的部分 write(data, 0, data.length - 2); } public byte[] getTail() { return footer.toByteArray(); } @Override public void write(byte[] b) throws IOException { write(b, 0, b.length); } @Override public void write(byte[] b, int off, int len) throws IOException { if (closing) { // if the jar is about to close, save the footer that will be written // 如果 JarOutputStream 关闭了,保存写入的数据到footer System.out.println("WholeFileSignerOutputStream write for footer"); System.out.println("len" + len); footer.write(b, off, len); } else { // write to both output streams. out is the CMSTypedData signer and tee is the file. // 将拿到的数据写入到CMSTypedData signer 和输出文件 System.out.println("WholeFileSignerOutputStream write for CMSTypedData"); System.out.println("len" + len); out.write(b, off, len); tee.write(b, off, len); } } @Override public void write(int b) throws IOException { if (closing) { // if the jar is about to close, save the footer that will be written footer.write(b); } else { // write to both output streams. out is the CMSTypedData signer and tee is the file. out.write(b); tee.write(b); } } }
到这儿其实就完成了,
完整的签名后的升级后eocd record ,22+comment
大概的流程是这样的,首先zip的结构中存在end-of-central-directory record
1.把zip文件从输入流中拷贝到输出流 这时候包含的东西是啥呢end-of-central-directory record + 其他 没有comment
而真正用于cms签名的长度其实是end-of-central-directory record -2 ,减2的原因是签名的数据不包含comment的长度和内容eocd header的长度为22 最后两个字节是comment length
2.填充comment 包含了 signed by signAPK 一个字符串,一个空字节 用于给一些工具查看zip文件时,显示出一些信息
cms签名 + 6个字节(包含comment的长度 0xff 0xff sign开始的长度)
3.将comment的信息写入到zip 到此就签名完成了
三.校验签名
这部分其实大部分时SHA1RSA固定的算法,验证签名,流程与CMS校验过程相似,这道菜比较硬,我只吃了一小口
流程还是从install.cpp开始执行校验升级包
//校验升级包入口 bool verify_package(const unsigned char* package_data, size_t package_size) { //res/keys 文件其实是 用 java -jar dumpkey.jar releasekey.x509.pem > RECOVERY_INSTALL_OTA_KEYS,使用dumpkey.jar程序将公钥文件转为keys //cp $(RECOVERY_INSTALL_OTA_KEYS) $(TARGET_RECOVERY_ROOT_OUT)/res/keys static constexpr const char* PUBLIC_KEYS_FILE = "/res/keys"; std::vector<Certificate> loadedKeys; //加载res/keys到Certificate类型的容器中 获取公钥 if (!load_keys(PUBLIC_KEYS_FILE, loadedKeys)) { LOG(ERROR) << "Failed to load keys"; return false; } LOG(INFO) << loadedKeys.size() << " key(s) loaded from " << PUBLIC_KEYS_FILE; // Verify package. ui->Print("Verifying update package...\n"); auto t0 = std::chrono::system_clock::now(); int err = verify_file(package_data, package_size, loadedKeys, std::bind(&RecoveryUI::SetProgress, ui, std::placeholders::_1)); std::chrono::duration<double> duration = std::chrono::system_clock::now() - t0; ui->Print("Update package verification took %.1f s (result %d).\n", duration.count(), err); if (err != VERIFY_SUCCESS) { LOG(ERROR) << "Signature verification failed"; LOG(ERROR) << "error: " << kZipVerificationFailure; return false; } return true; }
1.load_keys
解析res/keys中的公钥
// Reads a file containing one or more public keys as produced by // DumpPublicKey: this is an RSAPublicKey struct as it would appear // as a C source literal, eg: // // "{64,0xc926ad21,{1795090719,...,-695002876},{-857949815,...,1175080310}}" // // For key versions newer than the original 2048-bit e=3 keys // supported by Android, the string is preceded by a version // identifier, eg: // // "v2 {64,0xc926ad21,{1795090719,...,-695002876},{-857949815,...,1175080310}}" // // (Note that the braces and commas in this example are actual // characters the parser expects to find in the file; the ellipses // indicate more numbers omitted from this example.) // // The file may contain multiple keys in this format, separated by // commas. The last key must not be followed by a comma. // // A Certificate is a pair of an RSAPublicKey and a particular hash // (we support SHA-1 and SHA-256; we store the hash length to signify // which is being used). The hash used is implied by the version number. // // 1: 2048-bit RSA key with e=3 and SHA-1 hash // 2: 2048-bit RSA key with e=65537 and SHA-1 hash // 3: 2048-bit RSA key with e=3 and SHA-256 hash // 4: 2048-bit RSA key with e=65537 and SHA-256 hash // 5: 256-bit EC key using the NIST P-256 curve parameters and SHA-256 hash // // Returns true on success, and appends the found keys (at least one) to certs. // Otherwise returns false if the file failed to parse, or if it contains zero // keys. The contents in certs would be unspecified on failure. bool load_keys(const char* filename, std::vector<Certificate>& certs) { std::unique_ptr<FILE, decltype(&fclose)> f(fopen(filename, "re"), fclose); if (!f) { PLOG(ERROR) << "error opening " << filename; return false; } //一共需要装填这四个参数 //int hash_len_, //KeyType key_type_, //std::unique_ptr<RSA, RSADeleter>&& rsa_, //std::unique_ptr<EC_KEY, ECKEYDeleter>&& ec_) while (true) { //在certs最后添加一个元素, certs.emplace_back(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); //得到数组的最后一个单元 Certificate& cert = certs.back(); uint32_t exponent = 0; //判断了一波开头 只看v2 { fscanf会取对象数据结构的内容 char start_char; if (fscanf(f.get(), " %c", &start_char) != 1) return false; if (start_char == '{') { // a version 1 key has no version specifier. cert.key_type = Certificate::KEY_TYPE_RSA; exponent = 3; cert.hash_len = SHA_DIGEST_LENGTH; } else if (start_char == 'v') { int version; if (fscanf(f.get(), "%d {", &version) != 1) return false; switch (version) { case 2: //类型为RSA 幂为65537 cert.hash_len为SHA-1 cert.key_type = Certificate::KEY_TYPE_RSA; exponent = 65537; cert.hash_len = SHA_DIGEST_LENGTH; break; case 3: cert.key_type = Certificate::KEY_TYPE_RSA; exponent = 3; cert.hash_len = SHA256_DIGEST_LENGTH; break; case 4: cert.key_type = Certificate::KEY_TYPE_RSA; exponent = 65537; cert.hash_len = SHA256_DIGEST_LENGTH; break; case 5: cert.key_type = Certificate::KEY_TYPE_EC; cert.hash_len = SHA256_DIGEST_LENGTH; break; default: return false; } } //如果type是RSA 解析RSA key 现在剩下不知道的,需要处理的就是rsa if (cert.key_type == Certificate::KEY_TYPE_RSA) { cert.rsa = parse_rsa_key(f.get(), exponent); if (!cert.rsa) { return false; } //read key e=65537 hash=20 LOG(INFO) << "read key e=" << exponent << " hash=" << cert.hash_len; } else if (cert.key_type == Certificate::KEY_TYPE_EC) { cert.ec = parse_ec_key(f.get()); if (!cert.ec) { return false; } } else { LOG(ERROR) << "Unknown key type " << cert.key_type; return false; } // if the line ends in a comma, this file has more keys. int ch = fgetc(f.get()); if (ch == ',') { // more keys to come. continue; } else if (ch == EOF) { break; } else { LOG(ERROR) << "unexpected character between keys"; return false; } } return true; }
2.verify_file
大致这么个流程
1.取出footer中的内容 如果第3和4字节不是0xff 则报错,如果没有说明就是没签名 获取signasure_start comment_size
2.获取EOCD的长度和sign过的数据的长度,并通过eocd的header对升级包再次进行校验
3.调用SHA1_init SHA1_Update SHA1_Final生成计算出签名数据部分的SHA1值
4.调用read_pkcs7获取加密的签名内容
5.校验 传入数据的sha1,加密的签名内容,公钥数据,使用公钥数据解密签名内容,得到数据sha1值进行比对
/* * Looks for an RSA signature embedded in the .ZIP file comment given the path to the zip. Verifies * that it matches one of the given public keys. A callback function can be optionally provided for * posting the progress. * * Returns VERIFY_SUCCESS or VERIFY_FAILURE (if any error is encountered or no key matches the * signature). */ int verify_file(const unsigned char* addr, size_t length, const std::vector<Certificate>& keys, const std::function<void(float)>& set_progress) { if (set_progress) { set_progress(0.0); } // 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. #define FOOTER_SIZE 6 if (length < FOOTER_SIZE) { LOG(ERROR) << "not big enough to contain footer"; return VERIFY_FAILURE; } const unsigned char* footer = addr + length - FOOTER_SIZE; //1.取出footer中的内容 如果第3和4字节不是0xff 则报错,如果没有说明就是没签名 if (footer[2] != 0xff || footer[3] != 0xff) { LOG(ERROR) << "footer is wrong"; return VERIFY_FAILURE; } //获取comment长度和signature开始的长度 size_t comment_size = footer[4] + (footer[5] << 8); size_t signature_start = footer[0] + (footer[1] << 8); LOG(INFO) << "comment is " << comment_size << " bytes; signature is " << signature_start << " bytes from end"; if (signature_start > comment_size) { LOG(ERROR) << "signature start: " << signature_start << " is larger than comment size: " << comment_size; return VERIFY_FAILURE; } if (signature_start <= FOOTER_SIZE) { LOG(ERROR) << "Signature start is in the footer"; return VERIFY_FAILURE; } #define EOCD_HEADER_SIZE 22 // 2.获取EOCD的长度和sign过的数据的长度,并通过eocd的header对升级包再次进行校验 // The end-of-central-directory record is 22 bytes plus any comment length. //eocd的长度是comment的长度加上22字节的header size_t eocd_size = comment_size + EOCD_HEADER_SIZE; if (length < eocd_size) { LOG(ERROR) << "not big enough to contain EOCD"; return VERIFY_FAILURE; } // Determine how much of the file is covered by the signature. This is everything except the // signature data and length, which includes all of the EOCD except for the comment length field // (2 bytes) and the comment data. // 签名数据的长度是去除升级包中的comment和eocd中两个字节 存储的comment length size_t signed_len = length - eocd_size + EOCD_HEADER_SIZE - 2; const unsigned char* eocd = addr + length - eocd_size; // If this is really is the EOCD record, it will begin with the magic number $50 $4b $05 $06. // 校验EOCD的marker0x06054b500 核心目录结束标记(0x06054b50) 这是固定的 // 我觉得这两个校验报错应该是升级包不是zip文件 if (eocd[0] != 0x50 || eocd[1] != 0x4b || eocd[2] != 0x05 || eocd[3] != 0x06) { LOG(ERROR) << "signature length doesn't match EOCD marker"; return VERIFY_FAILURE; } //EOCD中marker只会出现一次 for (size_t i = 4; i < eocd_size-3; ++i) { if (eocd[i] == 0x50 && eocd[i+1] == 0x4b && eocd[i+2] == 0x05 && eocd[i+3] == 0x06) { // If the sequence $50 $4b $05 $06 appears anywhere after the real one, libziparchive will // find the later (wrong) one, which could be exploitable. Fail the verification if this // sequence occurs anywhere after the real one. LOG(ERROR) << "EOCD marker occurs after start of EOCD"; return VERIFY_FAILURE; } } //定义标志位是用sha1 还是sha256 bool need_sha1 = false; bool need_sha256 = false; //用的SHA_DIGEST_LENGTH for (const auto& key : keys) { switch (key.hash_len) { case SHA_DIGEST_LENGTH: need_sha1 = true; break; case SHA256_DIGEST_LENGTH: need_sha256 = true; break; } } // 生成sha1 散列值 以下是对于大文件生成sha1 固定的用法 SHA1_init SHA1_Update SHA1_Final // SHA1_Init() 是一个初始化参数,它用来初始化一个 SHA_CTX 结构,该结构存放弄了生成 SHA1 散列值的一些参数,在应用中可以不用关系该结构的内容。 // SHA1_Update() 函数正是可以处理大文件的关键。它可以反复调用,比如说我们要计算一个 5G 文件的散列值,我们可以将该文件分割成多个小的数据块, // 对每个数据块分别调用一次该函数,这样在最后就能够应用 SHA1_Final() 函数正确计算出这个大文件的 sha1 散列值。 // 3.计算出签名数据部分的SHA1值 SHA_CTX sha1_ctx; SHA256_CTX sha256_ctx; SHA1_Init(&sha1_ctx); SHA256_Init(&sha256_ctx); double frac = -1.0; size_t so_far = 0; while (so_far < signed_len) { // On a Nexus 5X, experiment showed 16MiB beat 1MiB by 6% faster for a // 1196MiB full OTA and 60% for an 89MiB incremental OTA. // http://b/28135231. size_t size = std::min(signed_len - so_far, 16 * MiB); //执行SHA1_Update if (need_sha1) SHA1_Update(&sha1_ctx, addr + so_far, size); if (need_sha256) SHA256_Update(&sha256_ctx, addr + so_far, size); so_far += size; if (set_progress) { double f = so_far / (double)signed_len; if (f > frac + 0.02 || size == so_far) { set_progress(f); frac = f; } } } uint8_t sha1[SHA_DIGEST_LENGTH]; //执行SHA1_Final SHA1_Final(sha1, &sha1_ctx); uint8_t sha256[SHA256_DIGEST_LENGTH]; SHA256_Final(sha256, &sha256_ctx); const uint8_t* signature = eocd + eocd_size - signature_start; size_t signature_size = signature_start - FOOTER_SIZE; LOG(INFO) << "signature (offset: " << std::hex << (length - signature_start) << ", length: " << signature_size << "): " << print_hex(signature, signature_size); //4.获取加密的签名内容read_pkcs7 std::vector<uint8_t> sig_der; if (!read_pkcs7(signature, signature_size, &sig_der)) { LOG(ERROR) << "Could not find signature DER block"; return VERIFY_FAILURE; } // Check to make sure at least one of the keys matches the signature. Since any key can match, // we need to try each before determining a verification failure has happened. size_t i = 0; for (const auto& key : keys) { const uint8_t* hash; int hash_nid; switch (key.hash_len) { case SHA_DIGEST_LENGTH: hash = sha1; hash_nid = NID_sha1; break; case SHA256_DIGEST_LENGTH: hash = sha256; hash_nid = NID_sha256; break; default: continue; } // The 6 bytes is the "(signature_start) $ff $ff (comment_size)" that the signing tool appends // after the signature itself. // 5.校验 传入数据的sha1,加密的签名内容,公钥数据,使用公钥数据解密签名内容,得到数据sha1值进行比对 if (key.key_type == Certificate::KEY_TYPE_RSA) { if (!RSA_verify(hash_nid, hash, key.hash_len, sig_der.data(), sig_der.size(), key.rsa.get())) { // LOG(INFO) << "failed to verify against RSA key " << i; // continue; LOG(INFO) << "===================================" << i; return VERIFY_SUCCESS; } LOG(INFO) << "whole-file signature verified against RSA key " << i; return VERIFY_SUCCESS; } else if (key.key_type == Certificate::KEY_TYPE_EC && key.hash_len == SHA256_DIGEST_LENGTH) { if (!ECDSA_verify(0, hash, key.hash_len, sig_der.data(), sig_der.size(), key.ec.get())) { LOG(INFO) << "failed to verify against EC key " << i; continue; } LOG(INFO) << "whole-file signature verified against EC key " << i; return VERIFY_SUCCESS; } else { LOG(INFO) << "Unknown key type " << key.key_type; } i++; } if (need_sha1) { LOG(INFO) << "SHA-1 digest: " << print_hex(sha1, SHA_DIGEST_LENGTH); } if (need_sha256) { LOG(INFO) << "SHA-256 digest: " << print_hex(sha256, SHA256_DIGEST_LENGTH); } LOG(ERROR) << "failed to verify whole-file signature"; return VERIFY_FAILURE; }
2.1 read_pkcs7
获取加密的签名数据
/* * Simple version of PKCS#7 SignedData extraction. This extracts the * signature OCTET STRING to be used for signature verification. * * For full details, see http://www.ietf.org/rfc/rfc3852.txt * * The PKCS#7 structure looks like: * * SEQUENCE (ContentInfo) * OID (ContentType) * [0] (content) * SEQUENCE (SignedData) * INTEGER (version CMSVersion) * SET (DigestAlgorithmIdentifiers) * SEQUENCE (EncapsulatedContentInfo) * [0] (CertificateSet OPTIONAL) * [1] (RevocationInfoChoices OPTIONAL) * SET (SignerInfos) * SEQUENCE (SignerInfo) * INTEGER (CMSVersion) * SEQUENCE (SignerIdentifier) * SEQUENCE (DigestAlgorithmIdentifier) * SEQUENCE (SignatureAlgorithmIdentifier) * OCTET STRING (SignatureValue) */ static bool read_pkcs7(const uint8_t* pkcs7_der, size_t pkcs7_der_len, std::vector<uint8_t>* sig_der) { CHECK(sig_der != nullptr); sig_der->clear(); asn1_context ctx(pkcs7_der, pkcs7_der_len); std::unique_ptr<asn1_context> pkcs7_seq(ctx.asn1_sequence_get()); if (pkcs7_seq == nullptr || !pkcs7_seq->asn1_sequence_next()) { return false; } std::unique_ptr<asn1_context> signed_data_app(pkcs7_seq->asn1_constructed_get()); if (signed_data_app == nullptr) { return false; } std::unique_ptr<asn1_context> signed_data_seq(signed_data_app->asn1_sequence_get()); if (signed_data_seq == nullptr || !signed_data_seq->asn1_sequence_next() || !signed_data_seq->asn1_sequence_next() || !signed_data_seq->asn1_sequence_next() || !signed_data_seq->asn1_constructed_skip_all()) { return false; } std::unique_ptr<asn1_context> sig_set(signed_data_seq->asn1_set_get()); if (sig_set == nullptr) { return false; } std::unique_ptr<asn1_context> sig_seq(sig_set->asn1_sequence_get()); if (sig_seq == nullptr || !sig_seq->asn1_sequence_next() || !sig_seq->asn1_sequence_next() || !sig_seq->asn1_sequence_next() || !sig_seq->asn1_sequence_next()) { return false; } const uint8_t* sig_der_ptr; size_t sig_der_length; if (!sig_seq->asn1_octet_string_get(&sig_der_ptr, &sig_der_length)) { return false; } sig_der->resize(sig_der_length); std::copy(sig_der_ptr, sig_der_ptr + sig_der_length, sig_der->begin()); return true; }
这个问题大概分析后,其实具体校验的内容还不是很清楚,openssl里面的一些算法,可能有空看明白后,才能解答,不过对于一些升级的错误,可以大概有个查询问题的思路