DES 加密算法存在四个弱密钥,分别为:

  • 0x0101010101010101
  • 0xFEFEFEFEFEFEFEFE
  • 0xE0E0E0E0F1F1F1F1
  • 0x1F1F1F1F0E0E0E0E

它们均满足 $\forall x.\ \mathrm{DESEncrypt}(x, K_w) = \mathrm{DESDecrypt}(x, K_w)$。接下来我们将使用 Z3 求解所有的 $K_w$,以此证明弱密钥的存在性。

证明的第一步是将弱密钥存在性问题简化至对密钥编排(key schedule)性质的研究上。记16个轮密钥按顺序组成序列 $\mathrm{DESKeySched}(K) = \{ k_i \}$,我们首先证明以下引理:


引理1 $K$ 为弱密钥的充要条件为 $\mathrm{DESKeySched}(K)$ 构成回文。

证明 由于 DES 算法为 Feistel 结构,可将加密、解密过程改写为:

\[\begin{align} \mathrm{DESEncrypt}(x, K) &= \mathrm{DESNetwork}(x, \mathrm{DESKeySched}(K)) \nonumber \\ &= \mathrm{DESNetwork}(x, \{ k_i \}) \nonumber \\ \mathrm{DESDecrypt}(x, K) &= \mathrm{DESNetwork}(x, \overline{\mathrm{DESKeySched}(K)}) \nonumber \\ &= \mathrm{DESNetwork}(x, \overline{\{ k_i \}}) \nonumber \\ \end{align}\]

其中 $\overline{\{ k_i \}}$ 表示逆序。那么显然 $\forall x.\ \mathrm{DESEncrypt}(x, K_w) = \mathrm{DESDecrypt}(x, K_w)$ 等价于 $\{ k_i \} = \overline{\{ k_i \}}$,即轮密钥组成的序列构成回文。


由于 DES 的密钥编排仅涉及按位置换,可以较为平凡地使用 SMT 求解器实现,因此编写 Z3 脚本是不难的:

#!/usr/bin/env python3

# SPDX-License-Identifier: BSD-3-Clause

import functools as ft

import z3


def des_keysched_round(i: int, s: z3.BitVecRef) -> tuple[z3.BitVecRef, z3.BitVecRef]:
    """
    Args:
        i: 1-indexed round count.
        s: Ci ++ Di, 56 bits.

    Returns:
        * ki without permutation but with dropped bits
        * s' for next iteration
    """
    ROT_AMOUNT = (1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1)
    ci, di = z3.Extract(55, 28, s), z3.Extract(27, 0, s)
    ci1, di1 = z3.RotateLeft(ci, ROT_AMOUNT[i - 1]), z3.RotateLeft(
        di, ROT_AMOUNT[i - 1]
    )
    s1 = z3.Concat(ci1, di1)
    # Ignore bits (1-indexed) 9, 18, 22, 25, 35, 38, 43, 54
    #             (0-indexed) 8, 17, 21, 24, 34, 37, 42, 53
    ki = z3.Concat(
        z3.Extract(55, 54, s1),
        z3.Extract(52, 43, s1),
        z3.Extract(41, 38, s1),
        z3.Extract(36, 25, s1),
        z3.Extract(23, 22, s1),
        z3.Extract(20, 18, s1),
        z3.Extract(16, 9, s1),
        z3.Extract(7, 0, s1),
    )
    return (ki, s1)


def des_pc1_inv(x: int) -> int:
    # fmt: off
    TABLE = (
        57, 49, 41, 33, 25, 17, 9,
        1, 58, 50, 42, 34, 26, 18,
        10, 2, 59, 51, 43, 35, 27,
        19, 11, 3, 60, 52, 44, 36,
        63, 55, 47, 39, 31, 23, 15,
        7, 62, 54, 46, 38, 30, 22,
        14, 6, 61, 53, 45, 37, 29,
        21, 13, 5, 28, 20, 12, 4,
    )
    # fmt: on
    bits = [(x >> i) & 1 for i in range(55, -1, -1)]
    new_bits = [0] * 64
    for i, ti in enumerate(TABLE):
        new_bits[ti - 1] = bits[i]
    for i in range(0, 64, 8):
        parity = ft.reduce(lambda acc, b: acc ^ b, new_bits[i : i + 7], 1)
        new_bits[i + 7] = parity
    return ft.reduce(lambda acc, b: (acc << 1) | b, new_bits)


s0 = z3.BitVec("s0", 56)

k = []
si = s0
for i in range(1, 16 + 1):
    ki, si = des_keysched_round(i, si)
    k.append(ki)

solver = z3.Solver()

assert len(k) == 16
for i in range(len(k) // 2):
    solver.add(k[i] == k[len(k) - i - 1])

sol = []
while solver.check() == z3.sat:
    m = solver.model()
    s0v = None
    for d in m.decls():
        if d.name() == "s0":
            s0v = m[d].as_long()
    assert s0v is not None
    sol.append(s0v)
    solver.add(s0 != z3.BitVecVal(s0v, 56))

for i, s in enumerate(sol):
    print(f"Solution {i}: {hex(des_pc1_inv(s))}")

运行后即可求得唯一的四个密钥,其产生的轮密钥序列构成回文:

> py.exe -3 ./des-weak-key.py
Solution 0: 0x101010101010101
Solution 1: 0xe0e0e0e0f1f1f1f1
Solution 2: 0xfefefefefefefefe
Solution 3: 0x1f1f1f1f0e0e0e0e

之后借助引理即可推出 DES 弱密钥存在,且上述四个密钥即为全部弱密钥。