最近,我需要移植一些 C 加密代码才能在 ARMv8-A(aarch64) 处理器上运行。问题在于代码使用了一些 x86 AES 内部函数,编译器在面向 ARM 体系结构时无法识别这些内部函数。ARMv8-A确实有一个可选的加密扩展,其中包括几个 AES 指令,但它们的语义与 x86 指令略有不同。我对 AES 没有太多经验,最初发现这非常令人困惑,因为我假设所有 AES 实现都需要以相同的方式工作(毕竟 AES 是一个标准!事实证明,这两种方法都足以实现 AES,但 x86 和 ARM 选择以不同的方式解决问题。
关于 AES 的背景资料
高级加密标准(AES) 是常见的对称加密算法,它使用密钥来加密和解密数据。AES 一次加密 16 个字节,并使用 128 位到 256 位的密钥大小。16 字节数据块通过一系列被称为轮次 的步骤进行转换。步骤的顺序是固定的,但轮次数可能因密钥大小而异。例如,AES 标准规定,对于 128 位密钥大小,AES 加密算法将迭代 10 轮。
AES 每一轮步骤定义为对 16 字节的 $4 \times 4$ 矩阵执行的操作,如下所示:
|b0 b4 b8 b12|
|b1 b5 b9 b13|
|b2 b6 b10 b14|
|b3 b7 b11 b15|
其中 bN 是 16 字节数据块的第 N 个字节。
AES 每一轮的操作定义如下:
SubBytes— 使用查找表将每个字节映射到唯一的字节值ShiftRows— 将每行中的字节循环左移不同的值MixColumns— 通过组合每列中的四个字节进行列混淆AddRoundKey— 将矩阵中的每个字节和轮密钥进行异或
Intel 与 ARM 中 AES 加密的比较
Intel 在 x86 中提供了两条用于加密的 AES 指令,它们与 AES 轮次非常匹配:
AESENC— AES Encrypt(Normal Round)a. ShiftRows
b. SubBytes
c. MixColumns
d. AddRoundKey
AESENCLAST— AES Encrypt(Last Round, No MixColumns)a. ShiftRows
b. SubBytes
c. AddRoundKey
(你可能会注意到这里的 ShiftRows 和 SubBytes 与 AES 标准定义中的位置进行了交换。这没关系,因为这两种操作的位置交换不会改变最终结果。)
ARM 还提供了两个用于加密的 AES 指令,但稍微模糊了不同加密轮次之间的界限:
AESE— AES Encrypt(AddRoundKey is first, No MixColumns)a. AddRoundKey
b. ShiftRows
c. SubBytes
AESMC— AES MixCoumnsa. MixColumns
(有关 ARM 指令的详细信息,请参阅ARM 体系结构手册
以下是 Intel 和 ARM 如何实现三轮 AES 加密:
| ROUND | AES STEPS | INTEL | ARM |
|---|---|---|---|
| Round 1 | AddRoundKey | XOR | AESE |
| SubBytes | AESENC | ||
| ShiftRows | |||
| MixColumns | AESMC | ||
| AddRoundKey | AESE | ||
| Round 2 | SubBytes | AESENC | |
| ShiftRows | |||
| MixColumns | AESMC | ||
| AddRoundKey | AESE | ||
| Round 3 | SubBytes | AESENCLAST | |
| ShiftRows | |||
| AddRoundKey | XOR |
使用 ARM 指令实现 AESENC
我想避免重写我正在移植的算法,所以我决定坚持使用 Intel 语义,并使用 ARM NEON 内部函数和 GCC 矢量扩展重新实现 x86 内部函数。x86 中的内部函数 AESENC 和 AESENCLAST 具有以下原型:
1 | __m128i _mm_aesenc_si128 (__m128i a, __m128i RoundKey); |
实现这一目标的第一步是在 ARM 中为 __m128i 定义等效的类型,我将其映射为 NEON 类型 uint8x16_t:
1 |
|
接下来,我需要想出一系列可用于模拟 x86 AESENC 语义的 ARM 指令。 使用 AESE+AESMC+XOR 将使我们能够接近这个目标,除了 ARM AESE在开始时有一个在 x86 AESENC 中不存在额外的 AddRoundKey。但是,由于 AddRoundKey 只需将密钥与数据进行简单地 XOR,因此密钥值为零会将这一步转换为 NOP。这是最终的实现:
1 | __m128i _mm_aesenc_si128 (__m128i a, __m128i RoundKey) |
1 | # clang-6.0 -target aarch64-none-linux -march=armv8+crypto -O3 |
使用 ARM 指令实现 AESDEC
有两种方法可以实现 AES 解密算法。第一种称为 “Inverse Cipher”,这种方法只是简单地颠倒加密的步骤顺序。换句话说,不使用 ShiftRows、 SubBytes 和 MixColumns,解密算法使用逆变换 InvShiftRows、InvSubBytes 和 InvMixColumns。第二种方法是一种称为 “Equivalent Inverse Cipher” 的技术,它以不同的方式生成解密密钥,但允许以在硬件中更快地实现的方式对解密步骤进行重新排序。x86 和 ARMv8-A 中的 AES 指令被设计用于第二种解密算法。您可以在Intel 白皮书中阅读有关它的更多信息。
Intel 在 x86 中提供了 AESDEC 和 AESDECLAST 指令以帮助实现 AES 解密算法,而 ARM 则提供了 AESD 和 AESIMC 指令。就像加密一样,与其他架构相比,这些指令的语义略有不同。幸运的是,仍然可以使用用一系列 ARM 内部函数替换 Intel 内部函数。
1 | __m128i _mm_aesdec_si128 (__m128i a, __m128i RoundKey) |
1 | # clang-6.0 -target aarch64-none-linux -march=armv8+crypto -O3 |
使用 ARM 指令实现 AESKEYGENASSIST
我掩盖了 AES 的一个部分是如何为 AddRoundKey 步骤生成轮函数。AES 标准定义了一种密钥生成算法,Intel 通过 AESKEYGENASSIT 指令实现了密钥生成算法。不幸的是,ARM 没有为 ARMv8-A 提供等效的指令,所以我们必须手动实现。
Intel 文档为 AESKEYGENASSIT 指令的功能提供了相当精确的定义:
1 | X3[31:0] := a[127:96] |
唯一真正棘手的部分是 SubWord() 函数,它使用与 AES 步骤相同的查表算法。实现自定义查找表不是很有效,因此使用 AESE 指令实现查找表会很方便。
就像使用 AES 加密一样,我使用了一个清零的轮密钥来跳过 AddRoundKey这一步。AESE 指令便只剩下使用SubBytes 和 ShiftRows 步骤用来对输入进行变换:
|b0 b4 b8 b12| |sub(b0) sub(b4) sub(b8) sub(b12)|
|b1 b5 b9 b13| AESE |sub(b5) sub(b9) sub(b13) sub(b1) |
|b2 b6 b10 b14| ====> |sub(b10) sub(b14) sub(b2) sub(b6) |
|b3 b7 b11 b15| |sub(b15) sub(b3) sub(b7) sub(b11)|
使用 NEON TBL 指令,我可以提取所需的字节来构建一个新的向量。在左侧,X1 是 b4、b5、b6、b7,在右侧,这些字节已移动到位置 4、1、14 和 11。同样,X3 是 b12、b13、b14、b15,这些字节已移至位置 9、6、3、12。
1 | __m128i dest = { |
下一步是旋转 X1 和 X3 的字节。没有一个很好的指令来做到这一点,但是由于我已经打乱了 AESE 的输出,我可以再打乱一点来执行旋转:
1 | __m128i dest = { |
最后,RCON 值需要是与 X1 和 X3 进行 XOR。这是最终的实现:
1 | __m128i _mm_aeskeygenassist_si128 (__m128i a, const int imm8) |
1 | # clang-6.0 -target aarch64-none-linux -march=armv8+crypto -O3 |