可验证的AES:使用零知识证明的加密

本文介绍了如何使用零知识证明(ZKP)来验证AES加密算法的正确执行。通过将AES加密过程转换为算术电路,并使用Marlin和KZG承诺方案生成加密证明,验证者可以在不泄露密钥或消息内容的前提下,验证密文是否由给定的明文和密钥正确加密生成。这在需要确保数据加密正确性但又不完全信任加密执行者的情况下非常有用。

download-1

范围

在 Lambdaclass,我们希望尝试和测试不同的密码学原语,我们可以使用它们来开发新的产品和应用程序,从而增强个人和组织的能力,更加关注安全性,并最大程度地减少各方之间的信任。在一系列文章中,我们将介绍强大的原语,例如零知识证明和全同态加密,以及它们的用法和用例。

加密是将消息转换为看起来随机的文本,以确保双方之间的机密性。

我们在这里的目标是什么?

我们想要生成一个证明,允许我们验证一种加密算法,确保它执行其设计目的。

为什么我们需要这个?

我们需要这个,以便用户不需要信任对方正确地执行了加密;我们用密码学证明代替信任。在某些用例中,接收者不希望解密消息,除非是紧急情况。但同时,接收者需要确保加密已正确完成,以便消息在那里等待他打开。

什么时候这很有用?

无论何时,当我们想要以安全的方式从不受信任的各方接收未知数据,并确保我们没有被欺骗时。

该项目的存储库可以在这里找到。

一般介绍

双方(我们称他们为 Alice 和 Bob)可以通过使用加密方案安全地通信。我们可以将加密方案大致分为以下几类:

  • 私钥(对称)加密。常用的方法是 AES 和 ChaCha20。
  • 公钥(非对称)加密。常用的方法是 RSA 和 ElGamal。

在对称加密中,Alice 和 Bob 必须在发送消息之前就一个共享密钥达成一致。问题是如果他们无法安全地相互发送消息,他们如何就某些事情达成一致?幸运的是,密钥协商方案(如 Diffie-Hellman)允许他们选择一个秘密密钥。我们将在此处关注椭圆曲线 Diffie-Hellman 协议。关键要素是有限域 $F_p$(其中 $p$ 是一个大素数)以及在 $F_p$ 上定义的椭圆曲线 $C$(其中包含一个具有生成元 $g$ 的素数阶 $r$ 的子群)。它包括以下步骤:

  1. Alice 在 $F_p$ 中选择一个元素 $s_A$,并计算她的公钥,$g_A = s_A g$。
  2. Alice 将她的公钥 $g_A$ 发送给 Bob。
  3. Bob 在 $F_p$ 中选择一个临时密钥 $s_B$,并计算他的公钥 $g_B = sB g$ 和共享密钥 $g{AB} = s_B g_A = s_A s_B g$。
  4. Bob 将 $gB$ 发送给 Alice;Alice 也可以通过执行 $g{AB} = s_A g_B$ 来导出共享密钥。
  5. 他们可以从相同的密钥派生函数计算对称密钥 $sk$,$sk = KDF(g_{AB})$。

给定一条消息 $m$,Bob 可以使用 AES 等方案和密钥对其进行加密并将其发送给 Alice,

$c = E(m, sk)$

任何加密方案都必须满足以下一致性检查:

$m = D(E(m, sk), sk) = D(c, sk)$

目标

Alice 需要 Bob 向她发送一些秘密 $sc$,她无法直接知道(否则,她就不需要与 Bob 通信)。她只知道 $sc$ 的哈希值(例如,Pedersen 承诺)。要发送秘密,他们首先需要就密钥达成一致。然后,Bob 必须使用该密钥加密 $sc$ 并将其发送给 Alice。最大的问题是 Alice 并不完全信任 Bob。他可以加密另一条消息或使用不同的密钥。我们想要开发一种方案,其中 Bob 可以向 Alice 证明他使用密钥 $sk$ 加密了消息 $sc$,而没有明显泄露有关密钥或消息的信息。

简而言之,我们可以说目标如下:Bob 必须证明密文 $c$ 是在方案 (AES) 下使用对称密钥 $sk$ 加密 $m$(其承诺为 $cm$)的结果。

以下列表显示了所有输入变量,指示它们是否敏感(应该是秘密的):

  • 秘密:Bob 的临时密钥 $s_B$,消息 $sc$。
  • 公开/非秘密:密文 $c$,Alice 和 Bob 的公钥 $g_A$ 和 $g_B$,对消息 $cm$ 的承诺。

所有其他元素(例如曲线和有限域)都已事先商定并且是公开已知的。

步骤

以下计算将允许 Bob 实现他的目标:

  1. 使用他的临时密钥,证明 $g_B == s_B g$。如果他成功了,他会得到一个布尔变量 $b_1 = 1$。
  2. 使用 $s_B$ 和 $g_A$,他派生出 $sk$,加密 $m$ 并证明 $c == E(sc, sk)$。如果此操作通过,他会得到 $b_2 = 1$。
  3. 使用 $sc$,他计算 $cm$ 并比较 $cm = commit(sc)$ 是否成立。如果这是正确的,他会得到 $b_3 = 1$。
  4. 如果 $b_1 \land b_2 \land b_3 = 1$。

最大的问题是他如何在不泄露敏感信息的情况下证明所有这些条件?这就是零知识证明发挥作用的地方。

什么是零知识证明?

零知识证明 (ZKP) 是一种强大的密码学原语,它允许我们证明一个陈述或计算的有效性,而无需泄露除陈述的真实性之外的任何信息。我们可以将任何有界计算表示为一个算术电路 $C$。ZKP 允许我们证明我们知道一些秘密 $w$ 和公开已知的值 $x$,使得 $C(z=(x,w))=0$。在我们的例子中,电路由执行检查 1-4 的计算给出。变量 $w$ 包含 $s_B$ 和 $sc$,$w=(s_B, sc)$。公共实例 $x$ 包含 $g_A, g_B, c, cm$ 和预期输出 $1$,$x=(g_A, g_B, c, cm, 1)$。

ZKP 使用多项式及其属性来证明陈述。Zk-SNARKs 是具有以下附加属性的 ZKP:简洁性(证明简短且验证速度比计算的原始重新执行更快)和非交互性(证明者和验证者不需要交换消息)。大多数 SNARK 有两个构建块:信息论设备(最常见的是,多项式交互式预言机证明,PIOP)和密码学承诺方案(特别是,多项式承诺方案,PCS)。在这种情况下,我们将使用 Marlin (PIOP) 和 Kate-Zaverucha-Goldberg (KZG) 承诺方案。

第一步是将我们的代码转换为算术电路,或者等效地,转换为(二次)秩一约束系统 (R1CS)。后者是以下形式的方程组:

$Az \cdot Bz = Cz$

其中 $A, B, C$ 是大小相同的矩阵,$\cdot$ 表示按分量乘积。

然后,我们将这些约束表示为多项式并生成证明。多项式承诺发挥作用,以确保证明者不会作弊并使协议实现零知识。我们现在将重点关注步骤 2(AES 加密)的证明生成和验证。

使用高级加密标准 (AES) 进行加密

AES 是一种分组密码:它取一个 128 位的消息(解释为 4×4 字节矩阵)和一个密钥 $sk$,并执行伪随机排列。AES 有一个轮函数,该函数被应用固定次数,每次使用不同的密钥来加密消息。我们使用密钥调度函数从主密钥 $sk$ 派生所有轮密钥。轮函数包括以下操作:

  1. 添加轮密钥。
  2. 替换字节(S 盒)。
  3. 移动行。
  4. 混合列。

这些操作中的每一个对于保证 AES 的安全性都是必要的。在多轮中重复这些操作保证了元素被充分地洗牌和混合,从而导致语义安全(也就是说,我们不能仅通过查看密文来了解有关明文的任何信息)。

AES 在 NIST 标准 中进行了描述。AES 需要使用一种模式来处理大于 128 位的消息。一些标准模式是 AES-CBC(密码块链接)和 AES-GCM(Galois 计数器模式,提供经过身份验证的加密)。

添加轮密钥

这是使加密依赖于密钥的步骤。对于每一轮,从主密钥 ($sk$) 派生一个轮密钥。该函数很简单:它包括轮密钥和状态(消息或其转换)之间的 XOR 运算。为了与代码保持一致,

$ret = inputtext \oplus key$

pub fn add_round_key(input_text: &[u8], key: &[u8; 16]) -> [u8; 16] {
    let mut ret = [0_u8; 16];

    let _ = zip(input_text, key)
        .map(|(cell_i, key_i)| cell_i ^ key_i)
        .collect_slice(&mut ret[..]);

    ret
}

XOR 运算在密码学中经常出现。除非我们知道密钥,否则我们有 50% 的机会猜测到每个位的正确值,这与抛掷一枚均匀的Coin并进行猜测一样好。

替换字节 / S 盒

S 盒将非线性分量添加到分组密码中。在这里,矩阵的每个字节都一对一地映射到另一个字节。在这里,我们展示了 S 盒的完整代码,但在实践中,这是通过查找表完成的。这将证明在生成证明时很有用,因为查看表比整个操作需要更少的约束。

在 AES 中,我们将字节解释为最多 7 次的多项式,系数为 0,1。例如,字节 10010110 被解释为多项式 $x^7 + x^4 + x^2 + x$,而 00100001 是 $x^5 + 1$。我们可以将多项式相乘,但如果次数大于 7,我们必须取乘积及其不可约多项式的余数。我们可以将多项式相乘,但如果次数大于 7,我们必须取乘积及其不可约多项式 $m(x) = x^8 + x^4 + x^3 + x + 1$ 的余数。

fn rotate_left(byte: u8, n: u8) -> u8 {
    (byte << n) | (byte >> (8 - n))
}

pub fn substitute_byte(byte: u8) -> Result<u8> {
    if byte == 0x00 {
        return Ok(0x63);
    }

    let mut p = 1_u8;
    let mut q = 1_u8;
    let mut sbox = [0_u8; 256];

    /* loop invariant: p * q == 1 in the Galois field */
    // 循环不变式:p * q == 1 在伽罗瓦域中
    loop {
        /* multiply p by 3 */
        // 将 p 乘以 3
        p = p ^ (p << 1_u8) ^ (((p >> 7_u8) & 1) * 0x1B);

        /* divide q by 3 (equals multiplication by 0xf6) */
        // 将 q 除以 3(等于乘以 0xf6)
        q ^= q << 1_u8;
        q ^= q << 2_u8;
        q ^= q << 4_u8;
        q ^= ((q >> 7_u8) & 1) * 0x09;

        /* compute the affine transformation */
        // 计算仿射变换
        let xformed =
            q ^ rotate_left(q, 1) ^ rotate_left(q, 2) ^ rotate_left(q, 3) ^ rotate_left(q, 4);

        let p_as_usize: usize = p.try_into()?;
        *sbox
            .get_mut(p_as_usize)
            .to_anyhow("Error saving substitution box value")? = xformed ^ 0x63;
        if p == 1 {
            break;
        }
    }

    let byte_index: usize = byte.try_into()?;

    Ok(*sbox
        .get(byte_index)
        .to_anyhow("Error getting substitution box value")?)
}

在第一步中,每个字节都映射到它的乘法逆元。如果 $p(x)$ 是与字节关联的多项式,则存在另一个多项式 $q(x)$,使得 $p(x)q(x) \equiv 1 \pmod{m(x)}$(存在 $q(x)$ 使得 $m(x)$ 除 $p(x)q(x) - 1$)。唯一的边缘情况是 0 字节,它没有逆元并映射到自身(这是函数开头的 if)。

以下步骤给出了逆的计算:

let mut p = 1_u8;
let mut q = 1_u8;
p = p ^ (p << 1_u8) ^ (((p >> 7_u8) & 1) * 0x1B);
q ^= q << 1_u8;
q ^= q << 2_u8;
q ^= q << 4_u8;
q ^= ((q >> 7_u8) & 1) * 0x09;

接下来,我们对逆执行仿射变换,该变换组合了不同位置的位:

let xformed =
            q ^ rotate_left(q, 1) ^ rotate_left(q, 2) ^ rotate_left(q, 3) ^ rotate_left(q, 4);

最后一个操作包括四次左旋转和四次 XOR 运算。

ShiftRows

此函数通过执行循环移位来更改每行中元素的顺序。第二行将每个元素向左移动一位,第三行移动两位,第四行移动三位。这种变换是线性的;与之相关的约束也将是线性的。

pub fn shift_rows(bytes: &[u8; 16], cs: &ConstraintSystemRef<ConstraintF>) -> Result<[u8; 16]> {
    // Add each number to the constraint system.
    // 将每个数字添加到约束系统中
    for byte in bytes {
        UInt8::new_witness(ark_relations::ns!(cs, "shift_rows_witness"), || Ok(byte))?;
    }

    // Turn the bytes into the 4x4 AES state matrix.
    // 将字节转换为 4x4 AES 状态矩阵
    // The matrix is represented by a 2D array,
    // 该矩阵由一个二维数组表示,
    // where each array is a row.
    // 其中每个数组都是一行
    // That is, let's suppose that the flattened_bytes variable
    // 也就是说,假设 flattened_bytes 变量
    // is formed by the bytes
    // 由字节组成
    // [b0, ..., b15]
    // Then the AES state matrix will look like this:
    // 那么 AES 状态矩阵将如下所示:
    // b0, b4, b8, b12,
    // b1, b5, b9, b13,
    // b2, b6, b10, b14,
    // b3, b7, b11, b15
    // And our array will look like this:
    // 我们的数组将如下所示:
    //[\
    //  [b0, b4, b8, b12],\
    //  [b1, b5, b9, b13],\
    //  [b2, b6, b10,b14],\
    //  [b3, b7, b11,b15]\
    //]
    let mut state_matrix = [[0_u8; 4]; 4];
    for (i, state) in state_matrix.iter_mut().enumerate() {
        *state = [\
            *(bytes.get(i).context("Out of bounds"))?,\
            *(bytes.get(i + 4).context("Out of bounds")?),\
            *(bytes.get(i + 8).context("Out of bounds")?),\
            *(bytes.get(i + 12).context("Out of bounds")?),\
        ];
    }

    // Rotate every state matrix row (u8 array) as specified by
    // 按照 AES 密码算法的规定,旋转每个状态矩阵行(u8 数组)
    // the AES cipher algorithm.
    //
    for (rotations, bytes) in state_matrix.iter_mut().enumerate() {
        // For the moment, this operation does not generate constraints in the
        // 目前,此操作不会在约束系统中生成约束,
        // circuit, but it should in the future.
        // 但将来应该会。
        bytes.rotate_left(rotations);
    }

    // Turn the rotated arrays into a flattened
    // 将旋转后的数组转换为扁平的
    //16-byte array, ordered by column.
    //16 字节数组,按列排序
    let mut flattened_matrix = [0_u8; 16];
    for i in 0..4 {
        for j in 0..4 {
            *flattened_matrix
                .get_mut((i * 4) + j)
                .to_anyhow("Error getting element of flattened_matrix slice")? = *state_matrix
                .get(j)
                .to_anyhow("Error getting element of state_matrix")?
                .get(i)
                .to_anyhow("Error getting element of state_matrix")?;
        }
    }
    Ok(flattened_matrix)
}

MixColumns

MixColumn 函数对状态矩阵的每一列进行操作。每个四字节列都被解释为一个多项式次数为四的多项式,模为 $x^4 + 1$。我们可以将每一列视为线性组合的结果。每个字节可以乘以 1、2 或 3。如果结果超过模数,我们必须减少结果,类似于我们在替换字节中所做的。

fn gmix_column(input: [u8; 4]) -> Option<[u8; 4]> {
    let mut b: [u8; 4] = [0; 4];
    /* The array 'a' is simply a copy of the input array 'r'
     * 数组 'a' 只是输入数组 'r' 的一个副本
     * The array 'b' is each element of the array 'a' multiplied by 2
     * 数组 'b' 是数组 'a' 的每个元素乘以 2
     * in Rijndael's Galois field
     * 在 Rijndael 的伽罗瓦域中
     * a[n] ^ b[n] is element n multiplied by 3 in Rijndael's Galois field */
     * a[n] ^ b[n] 是元素 n 乘以 3 在 Rijndael 的伽罗瓦域中*/

    for (i, c) in input.iter().enumerate() {
        let h = (c >> 7_usize) & 1; /* arithmetic right shift, thus shifting in either zeros or ones */
        // 算术右移,因此移入零或一
        *b.get_mut(i)? = (c << 1_usize) ^ (h * 0x1B); /* implicitly removes high bit because b[c] is an 8-bit char, so we xor by 0x1b and not 0x11b in the next line */
        // 隐式删除高位,因为 b[c] 是一个 8 位字符,所以在下一行中我们 xor 0x1b 而不是 0x11b
        /* Rijndael's Galois field */
        // Rijndael 的伽罗瓦域
    }

    Some([\
        b.first()? ^ input.get(3)? ^ input.get(2)? ^ b.get(1)? ^ input.get(1)?,\
        b.get(1)? ^ input.first()? ^ input.get(3)? ^ b.get(2)? ^ input.get(2)?,\
        b.get(2)? ^ input.get(1)? ^ input.first()? ^ b.get(3)? ^ input.get(3)?,\
        b.get(3)? ^ input.get(2)? ^ input.get(1)? ^ b.first()? ^ input.first()?,\
    ])
}

pub fn mix_columns(input: &[u8; 16]) -> Option<[u8; 16]> {
    let mut ret = [0_u8; 16];

    for (pos, column) in input.chunks(4).enumerate() {
        let column_aux = [\
            *column.first()?,\
            *column.get(1)?,\
            *column.get(2)?,\
            *column.get(3)?,\
        ];
        let column_ret = gmix_column(column_aux)?;

        // put column_ret in ret:
        // 将 column_ret 放入 ret 中:
        *ret.get_mut(pos * 4)? = *column_ret.first()?;
        *ret.get_mut(pos * 4 + 1)? = *column_ret.get(1)?;
        *ret.get_mut(pos * 4 + 2)? = *column_ret.get(2)?;
        *ret.get_mut(pos * 4 + 3)? = *column_ret.get(3)?;
    }

    Some(ret)
}

密钥调度

此函数从主密钥派生轮密钥。

pub fn derive_keys(secret_key: &[u8; 16]) -> Result<[[u8; 16]; 11]> {
    const ROUND_CONSTANTS: [u32; 10] = [\
        u32::from_be_bytes([0x01, 0x00, 0x00, 0x00]),\
        u32::from_be_bytes([0x02, 0x00, 0x00, 0x00]),\
        u32::from_be_bytes([0x04, 0x00, 0x00, 0x00]),\
        u32::from_be_bytes([0x08, 0x00, 0x00, 0x00]),\
        u32::from_be_bytes([0x10, 0x00, 0x00, 0x00]),\
        u32::from_be_bytes([0x20, 0x00, 0x00, 0x00]),\
        u32::from_be_bytes([0x40, 0x00, 0x00, 0x00]),\
        u32::from_be_bytes([0x80, 0x00, 0x00, 0x00]),\
        u32::from_be_bytes([0x1B, 0x00, 0x00, 0x00]),\
        u32::from_be_bytes([0x36, 0x00, 0x00, 0x00]),\
    ];

    let mut result = [0_u32; 44];

    result[0] = to_u32(&secret_key[..4]).to_anyhow("Error converting to u32")?;
    result[1] = to_u32(&secret_key[4..8]).to_anyhow("Error converting to u32")?;
    result[2] = to_u32(&secret_key[8..12]).to_anyhow("Error converting to u32")?;
    result[3] = to_u32(&secret_key[12..16]).to_anyhow("Error converting to u32")?;

    for i in 4..44 {
        if i % 4 == 0 {
            let substituted_and_rotated = to_u32(&substitute_word(rotate_word(
                *result.get(i - 1).to_anyhow("Error converting to u32")?,
            ))?)
            .to_anyhow("Error converting to u32")?;

            *result.get_mut(i).to_anyhow("Error getting elem")? =
                (result.get(i - 4).to_anyhow("Error getting elem")? ^ (substituted_and_rotated))
                    ^ ROUND_CONSTANTS
                        .get(i / 4 - 1)
                        .to_anyhow("Error getting elem")?;
        } else {
            *result.get_mut(i).to_anyhow("Error getting elem")? =
                result.get(i - 4).to_anyhow("Error getting elem")?
                    ^ result.get(i - 1).to_anyhow("Error getting elem")?;
        }
    }

    let mut ret = [[0_u8; 16]; 11];

    for (i, elem) in result.chunks(4).enumerate() {
        elem.iter()
            .flat_map(|e| e.to_be_bytes())
            .collect_slice(&mut ret.get_mut(i).to_anyhow("Error getting elem")?[..]);
    }

    Ok(ret)
}

fn to_u32(value: &[u8]) -> Option<u32> {
    let array_aux: [u8; 4] = [\
        *value.first()?,\
        *value.get(1)?,\
        *value.get(2)?,\
        *value.get(3)?,\
    ];
    Some(u32::from_be_bytes(array_aux))
}

fn rotate_word(input: u32) -> [u8; 4] {
    let bytes: [u8; 4] = input.to_be_bytes();
    [\
        *bytes.get(1).unwrap_or(&0),\
        *bytes.get(2).unwrap_or(&0),\
        *bytes.get(3).unwrap_or(&0),\
        *bytes.first().unwrap_or(&0),\
    ]
}

电路和小工具

如果我们尝试硬编码 AES 的电路,这将是一项不可能完成的任务,因为我们要执行的操作种类和数量。例如,假设我们想要在消息的一个字节和轮密钥之间执行 XOR 运算,$st[i] \oplus rk[i] = st'[i]$。首先,我们需要将每个字节分解为其组成位,并检查每个位是否确实为 0 或 1:

$st[i,j] \times st[i,j] = st[i,j]$

$rk[i,j] \times rk[i,j] = rk[i,j]$

$st'[i,j] \times st'[i,j] = st'[i,j]$ 接下来,我们需要计算位之间的 XOR 运算, $2st[i,j] \times rk[i,j] = st[i,j] + rk[i,j] - st'[i,j]$ 每个字节有八个方程,因此每个字节 XOR 有 32 个约束(我们可以删除对 $st'$ 的检查,因为 XOR 保证它们是 0 或 1,从而将计数减少到 24)。每个添加轮密钥函数采用 16 个字节,因此我们每轮采用 512(或 384)个约束!

我们可以实现一个小工具,只要我们定义一个新的字节变量,该小工具就会添加与其二进制分解相对应的约束。我们还可以在字节之间实现一个 XOR 小工具,为该操作添加约束。以下代码使用了 u8 的小工具:

use ark_r1cs_std::bits::uint8::UInt8;

let a = UInt8::new_input(cs, || Ok(1))?;

let result = a.xor(&a)?;
let zero = UInt8::constant(0);
result.enforce_equal(&zero)?;

替换盒会发生什么?我们可以为整个操作实现一个小工具。问题是约束的数量扩展得非常快!每步有超过 10 个 XOR 操作,这很耗时。s 盒通常从查找表中获得,该表具有预先计算的所有可能的输出值。

fn substitute_byte(byte: &UInt8Gadget, lookup_table: &[UInt8Gadget]) -> Result<UInt8Gadget> {
    Ok(UInt8Gadget::conditionally_select_power_of_two_vector(
        &byte.to_bits_be()?,
        lookup_table,
    )?)
}

pub fn substitute_bytes(
    bytes: &[UInt8Gadget],
    lookup_table: &[UInt8Gadget],
) -> Result<Vec<UInt8Gadget>> {
    ensure!(
        bytes.len() == 16,
        "Input must be 16 bytes length when substituting bytes"
    );

    let mut substituted_bytes = vec![];
    for byte in bytes {
        substituted_bytes.push(substitute_byte(byte, lookup_table)?);
    }

    ensure!(substituted_bytes.len() == 16, "Error substituting bytes");
    Ok(substituted_bytes)
}

证明生成

生成证明的第一步是获得证明和验证密钥。这些密钥是从通过安全多方计算获得的结构化引用字符串 (SRS) 中派生的。

let (proving_key, verifying_key) = synthesize_keys(message_length)?;

这是 synthesize keys 函数的定义:

pub fn synthesize_keys(plaintext_length: usize) -> Result<(ProvingKey, VerifyingKey)> {
    let rng = &mut simpleworks::marlin::generate_rand();
    let universal_srs =
        simpleworks::marlin::generate_universal_srs(1_000_000, 250_000, 3_000_000, rng)?;
    let constraint_system = ConstraintSystem::<ConstraintF>::new_ref();

    let default_message_input = vec![0_u8; plaintext_length];
    let default_secret_key_input = [0_u8; 16];
    let default_ciphertext_input = vec![0_u8; plaintext_length];

    let mut message_circuit: Vec<UInt8Gadget> = Vec::with_capacity(default_message_input.len());
    for byte in default_message_input {
        message_circuit.push(UInt8Gadget::new_witness(constraint_system.clone(), || {
            Ok(byte)
        })?);
    }

    let mut secret_key_circuit: Vec<UInt8Gadget> =
        Vec::with_capacity(default_secret_key_input.len());
    for byte in default_secret_key_input {
        secret_key_circuit.push(UInt8Gadget::new_witness(constraint_system.clone(), || {
            Ok(byte)
        })?);
    }

    let mut ciphertext_circuit: Vec<UInt8Gadget> =
        Vec::with_capacity(default_ciphertext_input.len());
    for byte in default_ciphertext_input {
        ciphertext_circuit.push(UInt8Gadget::new_input(constraint_system.clone(), || {
            Ok(byte)
        })?);
    }

    let _ciphertext = encrypt_and_generate_constraints(
        &message_circuit,
        &secret_key_circuit,
        &ciphertext_circuit,
        constraint_system.clone(),
    );

    simpleworks::marlin::generate_proving_and_verifying_keys(&universal_srs, constraint_system)
}

由于这只是一个测试,我们从一个函数生成 SRS,而不是从多方计算的结果中读取它。

我们现在定义一个包含生成证明的所有步骤的函数:

pub fn encrypt(
    message: &[u8],
    secret_key: &[u8; 16],
    ciphertext: &[u8],
    proving_key: ProvingKey,
) -> Result<MarlinProof> {
    let rng = &mut simpleworks::marlin::generate_rand();
    let constraint_system = ConstraintSystem::<ConstraintF>::new_ref();

    let mut message_circuit: Vec<UInt8Gadget> = Vec::with_capacity(message.len());
    for byte in message {
        message_circuit.push(UInt8Gadget::new_witness(constraint_system.clone(), || {
            Ok(byte)
        })?);
    }

    let mut secret_key_circuit: Vec<UInt8Gadget> = Vec::with_capacity(secret_key.len());
    for byte in secret_key {
        secret_key_circuit.push(UInt8Gadget::new_witness(constraint_system.clone(), || {
            Ok(byte)
        })?);
    }

    let mut ciphertext_circuit: Vec<UInt8Gadget> = Vec::with_capacity(ciphertext.len());
    for byte in ciphertext {
        ciphertext_circuit.push(UInt8Gadget::new_input(constraint_system.clone(), || {
            Ok(byte)
        })?);
    }

    encrypt_and_generate_constraints(
        &message_circuit,
        &secret_key_circuit,
        &ciphertext_circuit,
        constraint_system.clone(),
    )?;

    // Here we clone the constraint system because deep down when generating
    // 在这里,我们克隆约束系统,因为在深层生成时
    // the proof the constraint system is consumed and it has to have one
    // 证明约束系统被消耗,并且必须有一个
    // reference for it to be consumed.
    // 引用才能被消耗
    let cs_clone = (*constraint_system
        .borrow()
        .ok_or("Error borrowing")
        .map_err(|e| anyhow!("{}", e))?)
    .clone();
    let cs_ref_clone = ConstraintSystemRef::CS(Rc::new(RefCell::new(cs_clone)));

    let proof = simpleworks::marlin::generate_proof(cs_ref_clone, proving_key, rng)?;

    Ok(proof)
}

最后,我们运行以下几行来获得证明:

let message = [1_u8; 16]; \\示例消息
let secret_key = [0_u8; 16]; \\示例密钥

let proof = encrypt(&message, &secret_key, &primitive_ciphertext, proving_key)?;

验证

为了验证 proof,我们首先将所有步骤封装在这个函数中,读取验证密钥、proof 和密文:

pub fn verify_encryption(
    verifying_key: VerifyingKey,
    proof: &MarlinProof,
    ciphertext: &[u8],
) -> Result<bool> {
    let mut ciphertext_as_field_array = vec![];

    for byte in ciphertext {
        let field_array = byte_to_field_array(*byte);
        for field_element in field_array {
            ciphertext_as_field_array.push(field_element);
        }
    }

    simpleworks::marlin::verify_proof(
        verifying_key,
        &ciphertext_as_field_array,
        proof,
        &mut simpleworks::marlin::generate_rand(),
    )
}

然后,我们运行并检查结果

let result = verify_encryption(
    verifying_key,
    &proof,
    &primitive_ciphertext
)?;

assert!(result);

总结

AES是最广泛使用的加密方法。在这篇文章中,我们解决了为给定明文-密钥对的AES加密函数的正确执行提供密码学 proof 的问题。使用Arkworks库,我们实现了AES并获得了它作为R1CS的表示。之后,使用Marlin和Kate-Zaverucha-Goldberg多项式承诺方案,我们生成了一个密码学 proof。验证者使用密文作为输入,可以验证 proof 以断言该函数的正确执行。

  • 原文链接: blog.lambdaclass.com/ver...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
lambdaclass
lambdaclass
LambdaClass是一家风险投资工作室,致力于解决与分布式系统、机器学习、编译器和密码学相关的难题。