authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2022-07-01 13:18:08+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2022-07-01 13:18:08+02:00
logee01dd40322db39dcbb0e791d0885d6da9f4150a
tree584c0a9868755bfe5415afb9c62e8b2a513f290d
parent48fd92365a93a06a57ae0270be5f2614804e5749
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

crypto: add the Xoodoo permutation, prepare for Gimli deprecation (#11866)

Gimli was a game changer. A permutation that is large enough to be used in sponge-like constructions, yet small enough to be compact to implement and fast on a wide range of platforms. And Gimli being part of the Zig standard library was awesome. But since then, Gimli entered the NIST Lightweight Cryptography Competition, competing againt other candidates sharing a similar set of properties. Unfortunately, Gimli didn't pass the 3rd round. There are no practical attacks against Gimli when used correctly, but NIST's decision means that Gimli is unlikely to ever get any traction. So, maybe the time has come to move Gimli from the standard library to another repository. We shouldn't do it without providing an alternative, though. And the best candidate for this is probably Xoodoo. Xoodoo is the core function of Xoodyak, one of the finalists of the NIST LWC competition, and the most direct competitor to Gimli. It is also a 384-bit permutation, so it can easily be used everywhere Gimli was used with no parameter changes. It is the building block of Xoodyak (for actual encryption and hashing) as well as Charm, that some Zig applications are already using. Like Gimli that it was heavily inspired from, it is compact and suitable for constrained environments. This change adds the Xoodoo permutation to std.crypto.core. The set of public functions includes everything required to later implement existing Xoodoo-based constructions. In order to prepare for the Gimli deprecation, the default CSPRNG was changed to a Xoodoo-based that works exactly the same way.

4 files changed, 186 insertions(+), 2 deletions(-)

lib/std/crypto.zig+1
......@@ -43,6 +43,7 @@ pub const auth = struct {
4343pub const core = struct {
4444 pub const aes = @import("crypto/aes.zig");
4545 pub const Gimli = @import("crypto/gimli.zig").State;
46 pub const Xoodoo = @import("crypto/xoodoo.zig").State;
4647
4748 /// Modes are generic compositions to construct encryption/decryption functions from block ciphers and permutations.
4849 ///
lib/std/crypto/xoodoo.zig created+141
......@@ -0,0 +1,141 @@
1//! Xoodoo is a 384-bit permutation designed to achieve high security with high
2//! performance across a broad range of platforms, including 64-bit Intel/AMD
3//! server CPUs, 64-bit and 32-bit ARM smartphone CPUs, 32-bit ARM
4//! microcontrollers, 8-bit AVR microcontrollers, FPGAs, ASICs without
5//! side-channel protection, and ASICs with side-channel protection.
6//!
7//! Xoodoo is the core function of Xoodyak, a finalist of the NIST lightweight cryptography competition.
8//! https://csrc.nist.gov/CSRC/media/Projects/Lightweight-Cryptography/documents/round-1/spec-doc/Xoodyak-spec.pdf
9//!
10//! It is not meant to be used directly, but as a building block for symmetric cryptography.
11
12const std = @import("../std.zig");
13const builtin = @import("builtin");
14const mem = std.mem;
15const math = std.math;
16const testing = std.testing;
17
18/// A Xoodoo state.
19pub const State = struct {
20 /// Number of bytes in the state.
21 pub const block_bytes = 48;
22
23 const rcs = [12]u32{ 0x058, 0x038, 0x3c0, 0x0d0, 0x120, 0x014, 0x060, 0x02c, 0x380, 0x0f0, 0x1a0, 0x012 };
24 const Lane = @Vector(4, u32);
25 st: [3]Lane,
26
27 /// Initialize a state from a slice of bytes.
28 pub fn init(initial_state: [block_bytes]u8) State {
29 var state = State{ .st = undefined };
30 mem.copy(u8, state.asBytes(), &initial_state);
31 state.endianSwap();
32 return state;
33 }
34
35 // A representation of the state as 32-bit words.
36 fn asWords(self: *State) *[12]u32 {
37 return @ptrCast(*[12]u32, &self.st);
38 }
39
40 /// A representation of the state as bytes. The byte order is architecture-dependent.
41 pub fn asBytes(self: *State) *[block_bytes]u8 {
42 return mem.asBytes(&self.st);
43 }
44
45 /// Byte-swap words storing the bytes of a given range if the architecture is not little-endian.
46 pub fn endianSwapPartial(self: *State, from: usize, to: usize) void {
47 for (self.asWords()[from / 4 .. (to + 3) / 4]) |*w| {
48 w.* = mem.littleToNative(u32, w.*);
49 }
50 }
51
52 /// Byte-swap the entire state if the architecture is not little-endian.
53 pub fn endianSwap(self: *State) void {
54 for (self.asWords()) |*w| {
55 w.* = mem.littleToNative(u32, w.*);
56 }
57 }
58
59 /// XOR a byte into the state at a given offset.
60 pub fn addByte(self: *State, byte: u8, offset: usize) void {
61 self.endianSwapPartial(offset, offset);
62 self.asBytes()[offset] ^= byte;
63 self.endianSwapPartial(offset, offset);
64 }
65
66 /// XOR bytes into the beginning of the state.
67 pub fn addBytes(self: *State, bytes: []const u8) void {
68 self.endianSwap();
69 for (self.asBytes()[0..bytes.len]) |*byte, i| {
70 byte.* ^= bytes[i];
71 }
72 self.endianSwap();
73 }
74
75 /// Extract the first bytes of the state.
76 pub fn extract(self: *State, out: []u8) void {
77 self.endianSwap();
78 mem.copy(u8, out, self.asBytes()[0..out.len]);
79 self.endianSwap();
80 }
81
82 /// Set the words storing the bytes of a given range to zero.
83 pub fn clear(self: *State, from: usize, to: usize) void {
84 mem.set(u32, self.asWords()[from / 4 .. (to + 3) / 4], 0);
85 }
86
87 /// Apply the Xoodoo permutation.
88 pub fn permute(self: *State) void {
89 const rot8x32 = comptime if (builtin.target.cpu.arch.endian() == .Big)
90 [_]i32{ 9, 10, 11, 8, 13, 14, 15, 12, 1, 2, 3, 0, 5, 6, 7, 4 }
91 else
92 [_]i32{ 11, 8, 9, 10, 15, 12, 13, 14, 3, 0, 1, 2, 7, 4, 5, 6 };
93
94 var a = self.st[0];
95 var b = self.st[1];
96 var c = self.st[2];
97 inline for (rcs) |rc| {
98 var p = @shuffle(u32, a ^ b ^ c, undefined, [_]i32{ 3, 0, 1, 2 });
99 var e = math.rotl(Lane, p, 5);
100 p = math.rotl(Lane, p, 14);
101 e ^= p;
102 a ^= e;
103 b ^= e;
104 c ^= e;
105 b = @shuffle(u32, b, undefined, [_]i32{ 3, 0, 1, 2 });
106 c = math.rotl(Lane, c, 11);
107 a[0] ^= rc;
108 a ^= ~b & c;
109 b ^= ~c & a;
110 c ^= ~a & b;
111 b = math.rotl(Lane, b, 1);
112 c = @bitCast(Lane, @shuffle(u8, @bitCast(@Vector(16, u8), c), undefined, rot8x32));
113 }
114 self.st[0] = a;
115 self.st[1] = b;
116 self.st[2] = c;
117 }
118};
119
120test "xoodoo" {
121 const bytes = [_]u8{0x01} ** State.block_bytes;
122 var st = State.init(bytes);
123 var out: [State.block_bytes]u8 = undefined;
124 st.permute();
125 st.extract(&out);
126 const expected1 = [_]u8{ 51, 240, 163, 117, 43, 238, 62, 200, 114, 52, 79, 41, 48, 108, 150, 181, 24, 5, 252, 185, 235, 179, 28, 3, 116, 170, 36, 15, 232, 35, 116, 61, 110, 4, 109, 227, 91, 205, 0, 180, 179, 146, 112, 235, 96, 212, 206, 205 };
127 try testing.expectEqualSlices(u8, &expected1, &out);
128 st.clear(0, 10);
129 st.extract(&out);
130 const expected2 = [_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 108, 150, 181, 24, 5, 252, 185, 235, 179, 28, 3, 116, 170, 36, 15, 232, 35, 116, 61, 110, 4, 109, 227, 91, 205, 0, 180, 179, 146, 112, 235, 96, 212, 206, 205 };
131 try testing.expectEqualSlices(u8, &expected2, &out);
132 st.addByte(1, 5);
133 st.addByte(2, 5);
134 st.extract(&out);
135 const expected3 = [_]u8{ 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 48, 108, 150, 181, 24, 5, 252, 185, 235, 179, 28, 3, 116, 170, 36, 15, 232, 35, 116, 61, 110, 4, 109, 227, 91, 205, 0, 180, 179, 146, 112, 235, 96, 212, 206, 205 };
136 try testing.expectEqualSlices(u8, &expected3, &out);
137 st.addBytes(&bytes);
138 st.extract(&out);
139 const expected4 = [_]u8{ 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 49, 109, 151, 180, 25, 4, 253, 184, 234, 178, 29, 2, 117, 171, 37, 14, 233, 34, 117, 60, 111, 5, 108, 226, 90, 204, 1, 181, 178, 147, 113, 234, 97, 213, 207, 204 };
140 try testing.expectEqualSlices(u8, &expected4, &out);
141}
lib/std/rand.zig+2-2
......@@ -18,10 +18,10 @@ const maxInt = std.math.maxInt;
1818pub const DefaultPrng = Xoshiro256;
1919
2020/// Cryptographically secure random numbers.
21pub const DefaultCsprng = Gimli;
21pub const DefaultCsprng = Xoodoo;
2222
2323pub const Isaac64 = @import("rand/Isaac64.zig");
24pub const Gimli = @import("rand/Gimli.zig");
24pub const Xoodoo = @import("rand/Xoodoo.zig");
2525pub const Pcg = @import("rand/Pcg.zig");
2626pub const Xoroshiro128 = @import("rand/Xoroshiro128.zig");
2727pub const Xoshiro256 = @import("rand/Xoshiro256.zig");
lib/std/rand/Xoodoo.zig created+42
......@@ -0,0 +1,42 @@
1//! CSPRNG
2
3const std = @import("std");
4const Random = std.rand.Random;
5const min = std.math.min;
6const mem = std.mem;
7const Xoodoo = @This();
8
9const State = std.crypto.core.Xoodoo;
10
11state: State,
12
13const rate = 16;
14pub const secret_seed_length = 32;
15
16/// The seed must be uniform, secret and `secret_seed_length` bytes long.
17pub fn init(secret_seed: [secret_seed_length]u8) Xoodoo {
18 var initial_state: [State.block_bytes]u8 = undefined;
19 mem.copy(u8, initial_state[0..secret_seed_length], &secret_seed);
20 mem.set(u8, initial_state[secret_seed_length..], 0);
21 var state = State.init(initial_state);
22 state.permute();
23 return Xoodoo{ .state = state };
24}
25
26pub fn random(self: *Xoodoo) Random {
27 return Random.init(self, fill);
28}
29
30pub fn fill(self: *Xoodoo, buf: []u8) void {
31 var i: usize = 0;
32 while (true) {
33 const left = buf.len - i;
34 const n = min(left, rate);
35 self.state.extract(buf[i..][0..n]);
36 if (left == 0) break;
37 self.state.permute();
38 i += n;
39 }
40 self.state.clear(0, rate);
41 self.state.permute();
42}