RK完整的Secureboot包括两部分,第一部分为Linux的Secureboot,第二部分为Android特有的AVB(Android Verified Boot)。开启了Secureboot的设备,会在启动时逐级校验各分区,一旦某一级校验不通过,则设备就无法启动。
Secureboot分为安全性校验与完整性校验。
AVB阶段安全性校验和完整性校验需要依赖于vbmeta.img,相关的公钥及描述信息存储在vbmeta.img中。
Secureboot涉及到的两级:maskrom —> miniloader、miniloader —> uboot、uboot—> kernel,但在Android上Secureboot部分只实现前两级,uboot—> kernel以及之后的启动校验交由AVB进行处理。以下以maskrom —> miniloader为例讲解Secureboot流程。
(adsbygoogle=window.adsbygoogle||[]).push({})
使用rk提供的签名工具(rk_sign_tool)进行签名步骤及原理如下
1.该工具首先会产生一对密钥对,即:public key和privete key
2.使用SHA256计算镜像的hash,并使用privete key对镜像的hash进行RSA2048签名
3.使用SHA256计算出public key的hash
4.将镜像+第2步中签名+public key进行打包形成新的镜像
5.第3步中的hash将会烧写到efuse中
1.首先从新的镜像中获取public key计算hash值
2.从efuse中读取public key的hash值进行对比,如果相同则继续,否则启动失败
3.从镜像中获取签名,然后使用RSA2048计算hash
4.使用SHA256计算镜像的hash值,与第三步计算出来的hash进行对比,相同则继续,否则启动失败
AVB的核心结构为vbmeta,vbmeta分区存储了boot
分区的hash,而对于system
和vender
分区,哈希树紧随在各自的分区数据之后,vbmeta分区只保存哈希树描述符中哈希树的根哈希(root hash),盐(salt)和偏移量(offset)。
uboot启动后,首先需要进行vbmeta的合法性验证,即安全性校验,RK的做法是将验证vbmeta的公钥信息经过trust加密后存储在security分区,其中trust分区的安全性又是受efuse验证的Secureboot进行保证的。uboot启动kernel前先验签vbmeta,vbmeta可信后,再取出vbmeta中的相关信息来进行其他分区的校验。
AVB在验证system分区时采用了动态校验的方式进行完整性校验,所以采用了分块进行hash的方式来校验。那么如何存储该数据块的hash,直接采用最暴力的方式,自然而然想到的是使用一个hash列表来存储。但是使用Hash列表来保证数据块的正确性还不够,黑客修改数据的同时,如果将Hash列表也对应修改了,这就无法保证数据块的正确性了。所以需要引入一个顶层的hash,将hash列表里的每个hash字符串拼在一起后再做一次hash运算,最后的hash值称之为root hash,只要保证该root hash的正确性即可。
但是AVB并未采用该简单结构。假设system的大小为1GB,数据块大小为4KB,则有26万个数据块,对应着hash列表就有26万个元素。AVB进行运行时校验,设备运行时读到哪个块就会对哪个块校验,将需要校验的块进行hash后更新具有26万个元素的hash列表中的一个元素后计算root hash,再与vbmeta中root hash作对比来判断数据是否正确。这个效率可想而知非常糟糕,所以AVB采用了一种称为Merkle Tree的树结构。
Merkle Tree,通常也被称作Hash Tree,其叶子节点是数据块或者文件的hash值。非叶节点是其对应子节点串联字符串的hash。Hash 列表可以看作一种特殊的Merkle Tree,即树高为2的多叉Merkle Tree。
建树过程:
在树的最底层,和hash列表一样,将数据分成若干个小的数据块,有相应的hash与之对应。但是往上走,并不是直接去计算root hash,而是把相邻的两个hash合并成一个字符串,然后计算这个字符串的hash,将这个hash值作为两个节点的父节点。按照同样的方式,可以得到数目更少的新一级hash,最终必然形成一棵树,树的根节点即为root hash。
Merkle Tree的结构非常易于同步大文件或文件集合,按照查找树的查找思路,从root hash开始比对,依次往下查找到叶子节点即能找到需要重新同步或下载的数据块,其时间复杂度为O(logN),如果采用hash列表的方式,需要完整进行一遍遍历才能定位到不同的数据块,其时间复杂度为O(N)。Merkle Tree在数字签名、P2P网络、区块链等技术都有应用。回到本文介绍的AVB,AVB在运行时校验某一块时只需要更新Merkle Tree的一个分支即可计算出hash root,其运算时间比hash列表大大减少。在Android9上使用avbtool的python代码进行hash tree的生成,该算法跟上文描述略有不同,当1G的system进行4KB大小的划分,其生成的hash tree只有四层(包括root hash这一层),所以运行时计算hash只要沿着这个四层树的分支计算即可,可想而知效率大大提升。
以下分析一下Android9上hash tree的生成过程,涉及到用Python实现的avbtool源码的两个函数:calc_hash_level_offsets
,generate_hash_tree
calc_hash_level_offsets
def calc_hash_level_offsets(image_size, block_size, digest_size):
"""Calculate the offsets of all the hash-levels in a Merkle-tree.
Arguments:
image_size: The size of the image to calculate a Merkle-tree for.
block_size: The block size, e.g. 4096.
digest_size: The size of each hash, e.g. 32 for SHA-256.
Returns:
A tuple where the first argument is an array of offsets and the
second is size of the tree, in bytes.
"""
level_offsets = [] # 用来存储每一层在bytearray中的偏移
level_sizes = [] # 每一层占用的大小
tree_size = 0 # 树的大小
num_levels = 0 # 树的层数
# size用于计算时表示当前层的下一层的数据大小,从第0层(计算数据块hash)开始,
# 所以初始值为image的大小
size = image_size
while size > block_size:
# 计算当前层数据需要多少个块
num_blocks = (size + block_size - 1) / block_size
# round_to_multiple函数用来将第一个参数舍入到最接近第二个参数的倍数
# 在这里就是对齐到block_size的整数倍
# 计算当前层的hash digest需要占用的大小
level_size = round_to_multiple(num_blocks * digest_size, block_size)
level_sizes.append(level_size)
tree_size += level_size
num_levels += 1
# 循环往上计算,所以更新size为当前层,用于计算上一层
size = level_size
# 计算每一层在bytearray中的偏移
for n in range(0, num_levels):
offset = 0
for m in range(n + 1, num_levels):
offset += level_sizes[m]
level_offsets.append(offset)
return level_offsets, tree_size
Android9上将hash tree存储在bytearray中,所以需要事先计算好树的每一层在bytearray中的偏移,以及整个树需要多长的bytearray存储。注意,hash tree的建树过程上自下往上的。其实从calc_hash_level_offsets
函数就可大致看出Android上hash tree的存储形态了,但更为形象的存储结构还是需要看generate_hash_tree
函数。
generate_hash_tree
def generate_hash_tree(image, image_size, block_size, hash_alg_name, salt,
digest_padding, hash_level_offsets, tree_size):
"""Generates a Merkle-tree for a file.
Args:
image: The image, as a file.
image_size: The size of the image.
block_size: The block size, e.g. 4096.
hash_alg_name: The hash algorithm, e.g. 'sha256' or 'sha1'.
salt: The salt to use.
digest_padding: The padding for each digest.
hash_level_offsets: The offsets from calc_hash_level_offsets().
tree_size: The size of the tree, in number of bytes.
Returns:
A tuple where the first element is the top-level hash and the
second element is the hash-tree.
"""
hash_ret = bytearray(tree_size)
hash_src_offset = 0
hash_src_size = image_size
level_num = 0
while hash_src_size > block_size:
level_output = ''
remaining = hash_src_size
while remaining > 0:
hasher = hashlib.new(name=hash_alg_name, string=salt)
# Only read from the file for the first level - for subsequent
# levels, access the array we're building.
# 第0层直接按照block_size读取image来进行hash
if level_num == 0:
image.seek(hash_src_offset + hash_src_size - remaining)
data = image.read(min(remaining, block_size))
# 第0层之上的每一层都由取其下一层来进行hash,eg: 将第m-1层的数据分块hash后生成m层数据
else:
offset = hash_level_offsets[level_num - 1] + hash_src_size - remaining
# 以block_size为单位进行分块
data = hash_ret[offset:offset + block_size]
hasher.update(data)
remaining -= len(data)
if len(data) < block_size:
hasher.update('\0' * (block_size - len(data)))
level_output += hasher.digest()
if digest_padding > 0:
level_output += '\0' * digest_padding
padding_needed = (round_to_multiple(
len(level_output), block_size) - len(level_output))
level_output += '\0' * padding_needed
# Copy level-output into resulting tree.
offset = hash_level_offsets[level_num]
hash_ret[offset:offset + len(level_output)] = level_output
# Continue on to the next level.
hash_src_size = len(level_output)
level_num += 1
# 建树完成后,单独计算root hash
hasher = hashlib.new(name=hash_alg_name, string=salt)
hasher.update(level_output)
return hasher.digest(), hash_ret
通过calc_hash_level_offsets
函数计算好偏移和大小后,即可将参数传递给generate_hash_tree
函数来建树了。 从建树代码的循环过程可以看出,该树的实现是将生成的hash拼接在一起作为这一层的数据,然后分块进行hash后再拼接在一起给到父层,而不是之前的描述Merkle Tree的两两子节点合并后计算hash作为父节点。
本文作者: Ifan Tsai (菜菜)
本文链接: https://cloud.tencent.com/developer/article/2164593
版权声明: 本文采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!