Rust AEAD benchmark与Const generics

作者: 王江桐

本篇将会简要介绍什么是《This week in Rust》,第416篇推文。 中有关Rust密码学相关的两个库,RustCrypto和Ring,中AEAD算法的性能测试,以及Const generics相关的内容。

AEAD Benchmark

带有关联数据的认证加密(Authenticated Encryption with Associated Data)是一种能够同时保证数据的保密性、 完整性和真实性的一种加密模式。"A Conventional Authenticated-Encryption Mode"中提到,当人们将对称加密与消息认证码手动组合到一起时非常容易出错。很多实际攻击会通过这类错误导致的身份验证(包括SSL与TLS)的不正确实现或缺失,从而攻击安全协议和应用程序。因此,通常来说,比起分组加密,AEAD能提供更高的安全性。

AEAD通常有三种模式:

  • Encrypt-then-MAC (EtM),最后发送消息 = 明文加密 + 密文MAC,当MAC算法强不可伪造时是安全的。是ISO/IEC 19772:2009规定的六种认证加密方法中的一种,并且在RFC 7366中作为TLS与DTLS拓展发布。
  • Encrypt-and-MAC (E&M),最后发送消息 = 明文加密 + 明文MAC,但是E&M方法本身并未被证明是强不可伪造的。
  • MAC-then-Encrypt (MtE),最后发送消息 = 明文与明文MAC拼接之后计算MAC。尽管理论安全,但是SSL/TLS中将其实现为MAC-then-pad-then-encrypt,也就是说,明文会先填充到加密函数的块大小,之后再进行相应计算,而填充错误通常会导致接收方发现可检测到的错误,从而造成Padding oracle attack,例如Lucky Thirteen attack。

在Rust生态中,通常使用的已认证AEAD加密如下:

测试源码可见作者github repo。总体性能表统计如下:

100B1kB100kB1MB10MB100MB
RustCrypto’s XChaCha20-Poly1305 v0.8.2928.91 ns (102.67 MiB/s)1.9851 us (480.41 MiB/s)116.50 us (818.58 MiB/s)1.1579 ms (823.59 MiB/s)11.571 ms (824.17 MiB/s)117.74 ms (809.99 MiB/s)
RustCrypto’s ChaCha20-Poly1305 v0.8.2805.40 ns (118.41 MiB/s)1.8660 us (511.08 MiB/s)116.02 us (821.96 MiB/s)1.1522 ms (827.68 MiB/s)11.517 ms (828.02 MiB/s)117.87 ms (809.11 MiB/s)
RustCrypto’s AES-256-GCM v0.9.4154.27 ns (618.20 MiB/s)910.31 ns (1.0231 GiB/s)84.677 us (1.0999 GiB/s)844.85 us (1.1023 GiB/s)8.4719 ms (1.0993 GiB/s)88.666 ms (1.0504 GiB/s)
ring’s ChaCha20-Poly1305 v0.16.20195.90 ns (486.81 MiB/s)701.99 ns (1.3267 GiB/s)51.594 us (1.8051 GiB/s)563.75 us (1.6520 GiB/s)5.1991 ms (1.7913 GiB/s)54.879 ms (1.6971 GiB/s)
ring’s AES-256-GCM v0.16.20214.48 ns (444.64 MiB/s)455.70 ns (2.0437 GiB/s)26.476 us (3.5177 GiB/s)264.13 us (3.5260 GiB/s)2.6474 ms (3.5179 GiB/s)30.450 ms (3.0585 GiB/s)

Const generics

在版本1.51之后,Rust支持const generics使用。对于Rust 1.56(Rust 2021)而言,此后并没有对于Const generics进行更新,因此这个特性仍与1.51时相同。

Const generics使得用户定义struct时,可以使用常量变量定义一些类型,例如:

struct ArrayPair<T, const N: usize> {
    left: [T; N],
    right: [T; N],
}

impl<T: Debug, const N: usize> Debug for ArrayPair<T, N> {
    // ...
}

T是一个普通的类型参数,N则是常量泛型。不过,当N不同时,实例并不属于同一个类型,例如ArrayPair<_, 16>并不与ArrayPair<_, 32>同属于同一个类型。

这样做的好处之一是,当用户想在列表上实现trait,当不能使用常量泛型时,用户必须对于每一个可能大小的列表都实现这个trait。标准库中的一些trait甚至也受此限制,在Rust 1.47之前很多trait只能在长度小于等于32的列表上实现。

同时这样做带来了另一个便利:限制trait的实现条件。通常情况下,限制可以这样达成:

struct Assert<const COND: bool> {}

trait IsTrue {}

impl IsTrue for Assert<true> {}

不过同样,如果是对于列表等不定长度的类型实现trait,那么列举这些类型将会非常麻烦。常量变量可以非常快速地解决这个问题:

/// The struct we're going to conditionally implement for
#[derive(Clone, Debug)]
struct Foo<const N: usize>{ inner: [usize; N] }

impl<const N: usize> Copy for Foo<N> where Assert::<{N < 128}>: IsTrue {}

这个例子限定了只有包含长度为128以下的列表的Foo实例才支持Copy。当长度过大时,编译器将会检测到这个错误,并给出相应告警。

另一个好处是,可以避免运行时检查。例如:

/// A region of memory containing at least `N` `T`s.
pub struct MinSlice<T, const N: usize> {
    /// The bounded region of memory. Exactly `N` `T`s.
    pub head: [T; N],
    /// Zero or more remaining `T`s after the `N` in the bounded region.
    pub tail: [T],
}

let slice: &[u8] = b"Hello, world";
let reference: Option<&u8> = slice.get(6);
// We know this value is `Some(b' ')`,
// but the compiler can't know that.
assert!(reference.is_some())

let slice: &[u8] = b"Hello, world";
// Length check is performed when we construct a MinSlice,
// and it's known at compile time to be of length 12.
// If the `unwrap()` succeeds, no more checks are needed
// throughout the `MinSlice`'s lifetime.
let minslice = MinSlice::<u8, 12>::from_slice(slice).unwrap();
let value: u8 = minslice.head[6];
assert_eq!(value, b' ')

虽然Const generics提供了很多便利,目前的版本只支持数字类型,包括且不限于有符号/无符号数字、char、bool等,并且不支持非常复杂的泛型表达式。常量泛型只能由常量参数初始化,例如单一的常量参数,常量定值(例如有符号/无符号数字、char、bool),或是常量定值的表达计算式,但是不能由涉及到常量泛型的表达计算式。例如:

fn foo<const N: usize>() {}

fn bar<T, const M: usize>() {
    foo::<M>(); // ok: `M` is a const parameter
    foo::<2021>(); // ok: `2021` is a literal
    foo::<{20 * 100 + 20 * 10 + 1}>(); // ok: const expression contains no generic parameters
    
    foo::<{ M + 1 }>(); // error: const expression contains the generic parameter `M`
    foo::<{ std::mem::size_of::<T>() }>(); // error: const expression contains the generic parameter `T`
    
    let _: [u8; M]; // ok: `M` is a const parameter
    let _: [u8; std::mem::size_of::<T>()]; // error: const expression contains the generic parameter `T`
}

在未来这些限制或许会被取消,Rust版本会支持新的功能,不过由于这些限制涉及到很多的设计问题,或许这个功能的拓展还需要很长的一段时间。不过由于便利性,很多三方库也实现了Rust标准库不支持的一些常量泛型类似的功能,例如typenum。尽管它的编译性能以及编译器集成并不是特别良好,但是它仅依赖于libcore,并且支持在编译时检查的类型级数字,也就是将数字作为类型而不是值使用。

引用

Benchmarking symmetric encryption (AEAD) in Rust,https://kerkour.com/rust-symmetric-encryption-aead-benchmark/

Authenticated encryption,https://en.wikipedia.org/wiki/Authenticated_encryption

A Conventional Authenticated-Encryption Mode,https://csrc.nist.gov/csrc/media/projects/block-cipher-techniques/documents/bcm/proposed-modes/eax/eax-spec.pdf

Const generics MVP hits beta!,https://blog.rust-lang.org/2021/02/26/const-generics-mvp-beta.html

It's Time to Get Hyped About Const Generics in Rust,https://nora.codes/post/its-time-to-get-hyped-about-const-generics-in-rust/