Commit ea05223b63

Frank Denis <124872+jedisct1@users.noreply.github.com>
2022-11-22 18:16:04
std.crypto.auth: add AEGIS MAC (#13607)
* Update the AEGIS specification URL to the current draft * std.crypto.auth: add AEGIS MAC The Pelican-based authentication function of the AEGIS construction can be used independently from authenticated encryption, as a faster and more secure alternative to GHASH/POLYVAL/Poly1305. We already expose GHASH, POLYVAL and Poly1305 for use outside AES-GCM and ChaChaPoly, so there are no reasons not to expose the MAC from AEGIS as well. Like other 128-bit hash functions, finding a collision only requires ~2^64 attempts or inputs, which may still be acceptable for many practical applications. Benchmark (Apple M1): siphash128-1-3: 3222 MiB/s ghash: 8682 MiB/s aegis-128l mac: 12544 MiB/s Benchmark (Zen 2): siphash128-1-3: 4732 MiB/s ghash: 5563 MiB/s aegis-128l mac: 19270 MiB/s
1 parent 114244f
Changed files (3)
lib/std/crypto/aegis.zig
@@ -42,6 +42,12 @@ const State128L = struct {
         blocks[4] = blocks[4].xorBlocks(d2);
     }
 
+    fn absorb(state: *State128L, src: *const [32]u8) void {
+        const msg0 = AesBlock.fromBytes(src[0..16]);
+        const msg1 = AesBlock.fromBytes(src[16..32]);
+        state.update(msg0, msg1);
+    }
+
     fn enc(state: *State128L, dst: *[32]u8, src: *const [32]u8) void {
         const blocks = &state.blocks;
         const msg0 = AesBlock.fromBytes(src[0..16]);
@@ -86,11 +92,14 @@ const State128L = struct {
 /// The 128L variant of AEGIS has a 128 bit key, a 128 bit nonce, and processes 256 bit message blocks.
 /// It was designed to fully exploit the parallelism and built-in AES support of recent Intel and ARM CPUs.
 ///
-/// https://competitions.cr.yp.to/round3/aegisv11.pdf
+/// https://datatracker.ietf.org/doc/draft-irtf-cfrg-aegis-aead/
 pub const Aegis128L = struct {
     pub const tag_length = 16;
     pub const nonce_length = 16;
     pub const key_length = 16;
+    pub const block_length = 32;
+
+    const State = State128L;
 
     /// c: ciphertext: output buffer should be of size m.len
     /// tag: authentication tag: output MAC
@@ -105,12 +114,12 @@ pub const Aegis128L = struct {
         var dst: [32]u8 align(16) = undefined;
         var i: usize = 0;
         while (i + 32 <= ad.len) : (i += 32) {
-            state.enc(&dst, ad[i..][0..32]);
+            state.absorb(ad[i..][0..32]);
         }
         if (ad.len % 32 != 0) {
             mem.set(u8, src[0..], 0);
             mem.copy(u8, src[0 .. ad.len % 32], ad[i .. i + ad.len % 32]);
-            state.enc(&dst, &src);
+            state.absorb(&src);
         }
         i = 0;
         while (i + 32 <= m.len) : (i += 32) {
@@ -138,12 +147,12 @@ pub const Aegis128L = struct {
         var dst: [32]u8 align(16) = undefined;
         var i: usize = 0;
         while (i + 32 <= ad.len) : (i += 32) {
-            state.enc(&dst, ad[i..][0..32]);
+            state.absorb(ad[i..][0..32]);
         }
         if (ad.len % 32 != 0) {
             mem.set(u8, src[0..], 0);
             mem.copy(u8, src[0 .. ad.len % 32], ad[i .. i + ad.len % 32]);
-            state.enc(&dst, &src);
+            state.absorb(&src);
         }
         i = 0;
         while (i + 32 <= m.len) : (i += 32) {
@@ -212,6 +221,11 @@ const State256 = struct {
         blocks[0] = tmp.xorBlocks(d);
     }
 
+    fn absorb(state: *State256, src: *const [16]u8) void {
+        const msg = AesBlock.fromBytes(src);
+        state.update(msg);
+    }
+
     fn enc(state: *State256, dst: *[16]u8, src: *const [16]u8) void {
         const blocks = &state.blocks;
         const msg = AesBlock.fromBytes(src);
@@ -248,11 +262,14 @@ const State256 = struct {
 ///
 /// The 256 bit variant of AEGIS has a 256 bit key, a 256 bit nonce, and processes 128 bit message blocks.
 ///
-/// https://competitions.cr.yp.to/round3/aegisv11.pdf
+/// https://datatracker.ietf.org/doc/draft-irtf-cfrg-aegis-aead/
 pub const Aegis256 = struct {
     pub const tag_length = 16;
     pub const nonce_length = 32;
     pub const key_length = 32;
+    pub const block_length = 16;
+
+    const State = State256;
 
     /// c: ciphertext: output buffer should be of size m.len
     /// tag: authentication tag: output MAC
@@ -332,6 +349,101 @@ pub const Aegis256 = struct {
     }
 };
 
+/// The AEGIS-128L message authentication function outputs 128 bit tags.
+/// In addition to being extremely fast, its large state, non-linearity
+/// and non-invertibility provides the following properties:
+/// - 128 bit security, stronger than GHash/Polyval/Poly1305.
+/// - Recovering the secret key from the state would require ~2^128 attempts,
+///   which is infeasible for any practical adversary.
+/// - It has a large security margin against internal collisions.
+pub const Aegis128LMac = AegisMac(Aegis128L);
+
+/// The AEGIS-256 message authentication function has a 256-bit key size,
+/// but outputs 128 bit tags. Unless theoretical multi-target attacks are a
+/// concern, the AEGIS-128L variant should be preferred.
+/// AEGIS' large state, non-linearity and non-invertibility provides the
+/// following properties:
+/// - 128 bit security, stronger than GHash/Polyval/Poly1305.
+/// - Recovering the secret key from the state would require ~2^128 attempts,
+///   which is infeasible for any practical adversary.
+/// - It has a large security margin against internal collisions.
+pub const Aegis256Mac = AegisMac(Aegis256);
+
+fn AegisMac(comptime T: type) type {
+    return struct {
+        const Self = @This();
+
+        pub const mac_length = T.tag_length;
+        pub const key_length = T.key_length;
+        pub const block_length = T.block_length;
+
+        state: T.State,
+        buf: [block_length]u8 = undefined,
+        off: usize = 0,
+        msg_len: usize = 0,
+
+        /// Initialize a state for the MAC function
+        pub fn init(key: *const [key_length]u8) Self {
+            const nonce = [_]u8{0} ** T.nonce_length;
+            return Self{
+                .state = T.State.init(key.*, nonce),
+            };
+        }
+
+        /// Add data to the state
+        pub fn update(self: *Self, b: []const u8) void {
+            self.msg_len += b.len;
+
+            const len_partial = @min(b.len, block_length - self.off);
+            mem.copy(u8, self.buf[self.off..][0..len_partial], b[0..len_partial]);
+            self.off += len_partial;
+            if (self.off < block_length) {
+                return;
+            }
+            self.state.absorb(&self.buf);
+
+            var i = len_partial;
+            self.off = 0;
+            while (i + block_length <= b.len) : (i += block_length) {
+                self.state.absorb(b[i..][0..block_length]);
+            }
+            if (i != b.len) {
+                mem.copy(u8, self.buf[0..], b[i..]);
+                self.off = b.len - i;
+            }
+        }
+
+        /// Return an authentication tag for the current state
+        pub fn final(self: *Self, out: *[mac_length]u8) void {
+            if (self.off > 0) {
+                var pad = [_]u8{0} ** block_length;
+                mem.copy(u8, pad[0..], self.buf[0..self.off]);
+                self.state.absorb(&pad);
+            }
+            out.* = self.state.mac(self.msg_len, 0);
+        }
+
+        /// Return an authentication tag for a message and a key
+        pub fn create(out: *[mac_length]u8, msg: []const u8, key: *const [key_length]u8) void {
+            var ctx = Self.init(key);
+            ctx.update(msg);
+            ctx.final(out);
+        }
+
+        pub const Error = error{};
+        pub const Writer = std.io.Writer(*Self, Error, write);
+
+        fn write(self: *Self, bytes: []const u8) Error!usize {
+            self.update(bytes);
+            return bytes.len;
+        }
+
+        pub fn writer(self: *Self) Writer {
+            return .{ .context = self };
+        }
+    };
+}
+
 const htest = @import("test.zig");
 const testing = std.testing;
 
@@ -446,3 +558,37 @@ test "Aegis256 test vector 3" {
 
     try htest.assertEqual("f7a0878f68bd083e8065354071fc27c3", &tag);
 }
+
+test "Aegis MAC" {
+    const key = [_]u8{0x00} ** Aegis128LMac.key_length;
+    var msg: [64]u8 = undefined;
+    for (msg) |*m, i| {
+        m.* = @truncate(u8, i);
+    }
+    const st_init = Aegis128LMac.init(&key);
+    var st = st_init;
+    var tag: [Aegis128LMac.mac_length]u8 = undefined;
+
+    st.update(msg[0..32]);
+    st.update(msg[32..]);
+    st.final(&tag);
+    try htest.assertEqual("b4e8e46cee04a401ec67bad73df4aa60", &tag);
+
+    st = st_init;
+    st.update(msg[0..31]);
+    st.update(msg[31..]);
+    st.final(&tag);
+    try htest.assertEqual("b4e8e46cee04a401ec67bad73df4aa60", &tag);
+
+    st = st_init;
+    st.update(msg[0..14]);
+    st.update(msg[14..30]);
+    st.update(msg[30..]);
+    st.final(&tag);
+    try htest.assertEqual("b4e8e46cee04a401ec67bad73df4aa60", &tag);
+
+    var empty: [0]u8 = undefined;
+    const nonce = [_]u8{0x00} ** Aegis128L.nonce_length;
+    Aegis128L.encrypt(&empty, &tag, &empty, &msg, nonce, key);
+    try htest.assertEqual("b4e8e46cee04a401ec67bad73df4aa60", &tag);
+}
lib/std/crypto/benchmark.zig
@@ -64,6 +64,8 @@ const macs = [_]Crypto{
     Crypto{ .ty = crypto.auth.siphash.SipHash64(1, 3), .name = "siphash-1-3" },
     Crypto{ .ty = crypto.auth.siphash.SipHash128(2, 4), .name = "siphash128-2-4" },
     Crypto{ .ty = crypto.auth.siphash.SipHash128(1, 3), .name = "siphash128-1-3" },
+    Crypto{ .ty = crypto.auth.aegis.Aegis128LMac, .name = "aegis-128l mac" },
+    Crypto{ .ty = crypto.auth.aegis.Aegis256Mac, .name = "aegis-256 mac" },
 };
 
 pub fn benchmarkMac(comptime Mac: anytype, comptime bytes: comptime_int) !u64 {
lib/std/crypto.zig
@@ -37,6 +37,10 @@ pub const aead = struct {
 pub const auth = struct {
     pub const hmac = @import("crypto/hmac.zig");
     pub const siphash = @import("crypto/siphash.zig");
+    pub const aegis = struct {
+        pub const Aegis128LMac = @import("crypto/aegis.zig").Aegis128LMac;
+        pub const Aegis256Mac = @import("crypto/aegis.zig").Aegis256Mac;
+    };
 };
 
 /// Core functions, that should rarely be used directly by applications.