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.
This commit is contained in:
Frank Denis 2022-07-01 13:18:08 +02:00 committed by GitHub
parent 48fd92365a
commit ee01dd4032
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 186 additions and 2 deletions

View File

@ -43,6 +43,7 @@ pub const auth = struct {
pub const core = struct {
pub const aes = @import("crypto/aes.zig");
pub const Gimli = @import("crypto/gimli.zig").State;
pub const Xoodoo = @import("crypto/xoodoo.zig").State;
/// Modes are generic compositions to construct encryption/decryption functions from block ciphers and permutations.
///

141
lib/std/crypto/xoodoo.zig Normal file
View File

@ -0,0 +1,141 @@
//! Xoodoo is a 384-bit permutation designed to achieve high security with high
//! performance across a broad range of platforms, including 64-bit Intel/AMD
//! server CPUs, 64-bit and 32-bit ARM smartphone CPUs, 32-bit ARM
//! microcontrollers, 8-bit AVR microcontrollers, FPGAs, ASICs without
//! side-channel protection, and ASICs with side-channel protection.
//!
//! Xoodoo is the core function of Xoodyak, a finalist of the NIST lightweight cryptography competition.
//! https://csrc.nist.gov/CSRC/media/Projects/Lightweight-Cryptography/documents/round-1/spec-doc/Xoodyak-spec.pdf
//!
//! It is not meant to be used directly, but as a building block for symmetric cryptography.
const std = @import("../std.zig");
const builtin = @import("builtin");
const mem = std.mem;
const math = std.math;
const testing = std.testing;
/// A Xoodoo state.
pub const State = struct {
/// Number of bytes in the state.
pub const block_bytes = 48;
const rcs = [12]u32{ 0x058, 0x038, 0x3c0, 0x0d0, 0x120, 0x014, 0x060, 0x02c, 0x380, 0x0f0, 0x1a0, 0x012 };
const Lane = @Vector(4, u32);
st: [3]Lane,
/// Initialize a state from a slice of bytes.
pub fn init(initial_state: [block_bytes]u8) State {
var state = State{ .st = undefined };
mem.copy(u8, state.asBytes(), &initial_state);
state.endianSwap();
return state;
}
// A representation of the state as 32-bit words.
fn asWords(self: *State) *[12]u32 {
return @ptrCast(*[12]u32, &self.st);
}
/// A representation of the state as bytes. The byte order is architecture-dependent.
pub fn asBytes(self: *State) *[block_bytes]u8 {
return mem.asBytes(&self.st);
}
/// Byte-swap words storing the bytes of a given range if the architecture is not little-endian.
pub fn endianSwapPartial(self: *State, from: usize, to: usize) void {
for (self.asWords()[from / 4 .. (to + 3) / 4]) |*w| {
w.* = mem.littleToNative(u32, w.*);
}
}
/// Byte-swap the entire state if the architecture is not little-endian.
pub fn endianSwap(self: *State) void {
for (self.asWords()) |*w| {
w.* = mem.littleToNative(u32, w.*);
}
}
/// XOR a byte into the state at a given offset.
pub fn addByte(self: *State, byte: u8, offset: usize) void {
self.endianSwapPartial(offset, offset);
self.asBytes()[offset] ^= byte;
self.endianSwapPartial(offset, offset);
}
/// XOR bytes into the beginning of the state.
pub fn addBytes(self: *State, bytes: []const u8) void {
self.endianSwap();
for (self.asBytes()[0..bytes.len]) |*byte, i| {
byte.* ^= bytes[i];
}
self.endianSwap();
}
/// Extract the first bytes of the state.
pub fn extract(self: *State, out: []u8) void {
self.endianSwap();
mem.copy(u8, out, self.asBytes()[0..out.len]);
self.endianSwap();
}
/// Set the words storing the bytes of a given range to zero.
pub fn clear(self: *State, from: usize, to: usize) void {
mem.set(u32, self.asWords()[from / 4 .. (to + 3) / 4], 0);
}
/// Apply the Xoodoo permutation.
pub fn permute(self: *State) void {
const rot8x32 = comptime if (builtin.target.cpu.arch.endian() == .Big)
[_]i32{ 9, 10, 11, 8, 13, 14, 15, 12, 1, 2, 3, 0, 5, 6, 7, 4 }
else
[_]i32{ 11, 8, 9, 10, 15, 12, 13, 14, 3, 0, 1, 2, 7, 4, 5, 6 };
var a = self.st[0];
var b = self.st[1];
var c = self.st[2];
inline for (rcs) |rc| {
var p = @shuffle(u32, a ^ b ^ c, undefined, [_]i32{ 3, 0, 1, 2 });
var e = math.rotl(Lane, p, 5);
p = math.rotl(Lane, p, 14);
e ^= p;
a ^= e;
b ^= e;
c ^= e;
b = @shuffle(u32, b, undefined, [_]i32{ 3, 0, 1, 2 });
c = math.rotl(Lane, c, 11);
a[0] ^= rc;
a ^= ~b & c;
b ^= ~c & a;
c ^= ~a & b;
b = math.rotl(Lane, b, 1);
c = @bitCast(Lane, @shuffle(u8, @bitCast(@Vector(16, u8), c), undefined, rot8x32));
}
self.st[0] = a;
self.st[1] = b;
self.st[2] = c;
}
};
test "xoodoo" {
const bytes = [_]u8{0x01} ** State.block_bytes;
var st = State.init(bytes);
var out: [State.block_bytes]u8 = undefined;
st.permute();
st.extract(&out);
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 };
try testing.expectEqualSlices(u8, &expected1, &out);
st.clear(0, 10);
st.extract(&out);
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 };
try testing.expectEqualSlices(u8, &expected2, &out);
st.addByte(1, 5);
st.addByte(2, 5);
st.extract(&out);
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 };
try testing.expectEqualSlices(u8, &expected3, &out);
st.addBytes(&bytes);
st.extract(&out);
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 };
try testing.expectEqualSlices(u8, &expected4, &out);
}

View File

@ -18,10 +18,10 @@ const maxInt = std.math.maxInt;
pub const DefaultPrng = Xoshiro256;
/// Cryptographically secure random numbers.
pub const DefaultCsprng = Gimli;
pub const DefaultCsprng = Xoodoo;
pub const Isaac64 = @import("rand/Isaac64.zig");
pub const Gimli = @import("rand/Gimli.zig");
pub const Xoodoo = @import("rand/Xoodoo.zig");
pub const Pcg = @import("rand/Pcg.zig");
pub const Xoroshiro128 = @import("rand/Xoroshiro128.zig");
pub const Xoshiro256 = @import("rand/Xoshiro256.zig");

42
lib/std/rand/Xoodoo.zig Normal file
View File

@ -0,0 +1,42 @@
//! CSPRNG
const std = @import("std");
const Random = std.rand.Random;
const min = std.math.min;
const mem = std.mem;
const Xoodoo = @This();
const State = std.crypto.core.Xoodoo;
state: State,
const rate = 16;
pub const secret_seed_length = 32;
/// The seed must be uniform, secret and `secret_seed_length` bytes long.
pub fn init(secret_seed: [secret_seed_length]u8) Xoodoo {
var initial_state: [State.block_bytes]u8 = undefined;
mem.copy(u8, initial_state[0..secret_seed_length], &secret_seed);
mem.set(u8, initial_state[secret_seed_length..], 0);
var state = State.init(initial_state);
state.permute();
return Xoodoo{ .state = state };
}
pub fn random(self: *Xoodoo) Random {
return Random.init(self, fill);
}
pub fn fill(self: *Xoodoo, buf: []u8) void {
var i: usize = 0;
while (true) {
const left = buf.len - i;
const n = min(left, rate);
self.state.extract(buf[i..][0..n]);
if (left == 0) break;
self.state.permute();
i += n;
}
self.state.clear(0, rate);
self.state.permute();
}