diff --git a/lib/std/crypto.zig b/lib/std/crypto.zig index 20522c175d..4dd7ccbcf8 100644 --- a/lib/std/crypto.zig +++ b/lib/std/crypto.zig @@ -46,6 +46,7 @@ pub const auth = struct { /// Core functions, that should rarely be used directly by applications. pub const core = struct { pub const aes = @import("crypto/aes.zig"); + pub const Ascon = @import("crypto/ascon.zig").State; pub const Gimli = @import("crypto/gimli.zig").State; pub const Xoodoo = @import("crypto/xoodoo.zig").State; @@ -205,7 +206,9 @@ test { _ = auth.siphash; _ = core.aes; + _ = core.Ascon; _ = core.Gimli; + _ = core.Xoodoo; _ = core.modes; _ = dh.X25519; diff --git a/lib/std/crypto/ascon.zig b/lib/std/crypto/ascon.zig new file mode 100644 index 0000000000..f692bdbe71 --- /dev/null +++ b/lib/std/crypto/ascon.zig @@ -0,0 +1,227 @@ +//! Ascon is a 320-bit permutation, selected as new standard for lightweight cryptography +//! in the NIST Lightweight Cryptography competition (2019–2023). +//! https://csrc.nist.gov/News/2023/lightweight-cryptography-nist-selects-ascon +//! +//! The permutation is compact, and optimized for timing and side channel resistance, +//! making it a good choice for embedded applications. +//! +//! It is not meant to be used directly, but as a building block for symmetric cryptography. + +const std = @import("std"); +const builtin = std.builtin; +const debug = std.debug; +const mem = std.mem; +const testing = std.testing; +const rotr = std.math.rotr; + +/// An Ascon state. +/// +/// The state is represented as 5 64-bit words. +/// +/// The NIST submission (v1.2) serializes these words as big-endian, +/// but software implementations are free to use native endianness. +pub fn State(comptime endian: builtin.Endian) type { + return struct { + const Self = @This(); + + /// Number of bytes in the state. + pub const block_bytes = 40; + + const Block = [5]u64; + + st: Block, + + /// Initialize the state from a slice of bytes. + pub fn init(initial_state: [block_bytes]u8) Self { + var state = Self{ .st = undefined }; + mem.copy(u8, state.asBytes(), &initial_state); + state.endianSwap(); + return state; + } + + /// Initialize the state from u64 words in native endianness. + pub fn initFromWords(initial_state: [5]u64) Self { + var state = Self{ .st = initial_state }; + return state; + } + + /// Initialize the state for Ascon XOF + pub fn initXof() Self { + return Self{ .st = Block{ + 0xb57e273b814cd416, + 0x2b51042562ae2420, + 0x66a3a7768ddf2218, + 0x5aad0a7a8153650c, + 0x4f3e0e32539493b6, + } }; + } + + /// Initialize the state for Ascon XOFa + pub fn initXofA() Self { + return Self{ .st = Block{ + 0x44906568b77b9832, + 0xcd8d6cae53455532, + 0xf7b5212756422129, + 0x246885e1de0d225b, + 0xa8cb5ce33449973f, + } }; + } + + /// A representation of the state as bytes. The byte order is architecture-dependent. + pub fn asBytes(self: *Self) *[block_bytes]u8 { + return mem.asBytes(&self.st); + } + + /// Byte-swap the entire state if the architecture doesn't match the required endianness. + pub fn endianSwap(self: *Self) void { + for (self.st) |*w| { + w.* = mem.toNative(u64, w.*, endian); + } + } + + /// Set bytes starting at the beginning of the state. + pub fn setBytes(self: *Self, bytes: []const u8) void { + var i: usize = 0; + while (i + 8 <= bytes.len) : (i += 8) { + self.st[i / 8] = mem.readInt(u64, bytes[i..][0..8], endian); + } + if (i < bytes.len) { + var padded = [_]u8{0} ** 8; + mem.copy(u8, padded[0 .. bytes.len - i], bytes[i..]); + self.st[i / 8] = mem.readInt(u64, padded[0..], endian); + } + } + + /// XOR a byte into the state at a given offset. + pub fn addByte(self: *Self, byte: u8, offset: usize) void { + const z = switch (endian) { + .Big => 64 - 8 - 8 * @truncate(u6, offset % 8), + .Little => 8 * @truncate(u6, offset % 8), + }; + self.st[offset / 8] ^= @as(u64, byte) << z; + } + + /// XOR bytes into the beginning of the state. + pub fn addBytes(self: *Self, bytes: []const u8) void { + var i: usize = 0; + while (i + 8 <= bytes.len) : (i += 8) { + self.st[i / 8] ^= mem.readInt(u64, bytes[i..][0..8], endian); + } + if (i < bytes.len) { + var padded = [_]u8{0} ** 8; + mem.copy(u8, padded[0 .. bytes.len - i], bytes[i..]); + self.st[i / 8] ^= mem.readInt(u64, padded[0..], endian); + } + } + + /// Extract the first bytes of the state. + pub fn extractBytes(self: *Self, out: []u8) void { + var i: usize = 0; + while (i + 8 <= out.len) : (i += 8) { + mem.writeInt(u64, out[i..][0..8], self.st[i / 8], endian); + } + if (i < out.len) { + var padded = [_]u8{0} ** 8; + mem.writeInt(u64, padded[0..], self.st[i / 8], endian); + mem.copy(u8, out[i..], padded[0 .. out.len - i]); + } + } + + /// XOR the first bytes of the state into a slice of bytes. + pub fn xorBytes(self: *Self, out: []u8, in: []const u8) void { + debug.assert(out.len == in.len); + + var i: usize = 0; + while (i + 8 <= in.len) : (i += 8) { + const x = mem.readIntNative(u64, in[i..][0..8]) ^ mem.nativeTo(u64, self.st[i / 8], endian); + mem.writeIntNative(u64, out[i..][0..8], x); + } + if (i < in.len) { + var padded = [_]u8{0} ** 8; + mem.copy(u8, padded[0 .. in.len - i], in[i..]); + const x = mem.readIntNative(u64, &padded) ^ mem.nativeTo(u64, self.st[i / 8], endian); + mem.writeIntNative(u64, &padded, x); + mem.copy(u8, out[i..], padded[0 .. in.len - i]); + } + } + + /// Set the words storing the bytes of a given range to zero. + pub fn clear(self: *Self, from: usize, to: usize) void { + mem.set(u64, self.st[from / 8 .. (to + 7) / 8], 0); + } + + /// Clear the entire state, disabling compiler optimizations. + pub fn secureZero(self: *Self) void { + std.crypto.utils.secureZero(u64, &self.st); + } + + /// Apply a reduced-round permutation to the state. + pub inline fn permuteR(state: *Self, comptime rounds: u4) void { + const rks = [12]u64{ 0xf0, 0xe1, 0xd2, 0xc3, 0xb4, 0xa5, 0x96, 0x87, 0x78, 0x69, 0x5a, 0x4b }; + inline for (rks[rks.len - rounds ..]) |rk| { + state.round(rk); + } + } + + /// Apply a full-round permutation to the state. + pub inline fn permute(state: *Self) void { + state.permuteR(12); + } + + // Core Ascon permutation. + inline fn round(state: *Self, rk: u64) void { + const x = &state.st; + x[2] ^= rk; + + x[0] ^= x[4]; + x[4] ^= x[3]; + x[2] ^= x[1]; + var t: Block = .{ + x[0] ^ (~x[1] & x[2]), + x[1] ^ (~x[2] & x[3]), + x[2] ^ (~x[3] & x[4]), + x[3] ^ (~x[4] & x[0]), + x[4] ^ (~x[0] & x[1]), + }; + t[1] ^= t[0]; + t[3] ^= t[2]; + t[0] ^= t[4]; + + x[2] = t[2] ^ rotr(u64, t[2], 6 - 1); + x[3] = t[3] ^ rotr(u64, t[3], 17 - 10); + x[4] = t[4] ^ rotr(u64, t[4], 41 - 7); + x[0] = t[0] ^ rotr(u64, t[0], 28 - 19); + x[1] = t[1] ^ rotr(u64, t[1], 61 - 39); + x[2] = t[2] ^ rotr(u64, x[2], 1); + x[3] = t[3] ^ rotr(u64, x[3], 10); + x[4] = t[4] ^ rotr(u64, x[4], 7); + x[0] = t[0] ^ rotr(u64, x[0], 19); + x[1] = t[1] ^ rotr(u64, x[1], 39); + x[2] = ~x[2]; + } + }; +} + +test "ascon" { + const Ascon = State(.Big); + const bytes = [_]u8{0x01} ** Ascon.block_bytes; + var st = Ascon.init(bytes); + var out: [Ascon.block_bytes]u8 = undefined; + st.permute(); + st.extractBytes(&out); + const expected1 = [_]u8{ 148, 147, 49, 226, 218, 221, 208, 113, 186, 94, 96, 10, 183, 219, 119, 150, 169, 206, 65, 18, 215, 97, 78, 106, 118, 81, 211, 150, 52, 17, 117, 64, 216, 45, 148, 240, 65, 181, 90, 180 }; + try testing.expectEqualSlices(u8, &expected1, &out); + st.clear(0, 10); + st.extractBytes(&out); + const expected2 = [_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 169, 206, 65, 18, 215, 97, 78, 106, 118, 81, 211, 150, 52, 17, 117, 64, 216, 45, 148, 240, 65, 181, 90, 180 }; + try testing.expectEqualSlices(u8, &expected2, &out); + st.addByte(1, 5); + st.addByte(2, 5); + st.extractBytes(&out); + const expected3 = [_]u8{ 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 169, 206, 65, 18, 215, 97, 78, 106, 118, 81, 211, 150, 52, 17, 117, 64, 216, 45, 148, 240, 65, 181, 90, 180 }; + try testing.expectEqualSlices(u8, &expected3, &out); + st.addBytes(&bytes); + st.extractBytes(&out); + const expected4 = [_]u8{ 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 168, 207, 64, 19, 214, 96, 79, 107, 119, 80, 210, 151, 53, 16, 116, 65, 217, 44, 149, 241, 64, 180, 91, 181 }; + try testing.expectEqualSlices(u8, &expected4, &out); +} diff --git a/lib/std/crypto/isap.zig b/lib/std/crypto/isap.zig index c7706df1df..0888cfa4dd 100644 --- a/lib/std/crypto/isap.zig +++ b/lib/std/crypto/isap.zig @@ -1,9 +1,11 @@ const std = @import("std"); +const crypto = std.crypto; const debug = std.debug; const mem = std.mem; const math = std.math; const testing = std.testing; -const AuthenticationError = std.crypto.errors.AuthenticationError; +const Ascon = crypto.core.Ascon(.Big); +const AuthenticationError = crypto.errors.AuthenticationError; /// ISAPv2 is an authenticated encryption system hardened against side channels and fault attacks. /// https://csrc.nist.gov/CSRC/media/Projects/lightweight-cryptography/documents/round-2/spec-doc-rnd2/isap-spec-round2.pdf @@ -25,90 +27,26 @@ pub const IsapA128A = struct { const iv2 = [_]u8{ 0x02, 0x80, 0x40, 0x01, 0x0c, 0x01, 0x06, 0x0c }; const iv3 = [_]u8{ 0x03, 0x80, 0x40, 0x01, 0x0c, 0x01, 0x06, 0x0c }; - const Block = [5]u64; - - block: Block, - - fn round(isap: *IsapA128A, rk: u64) void { - var x = &isap.block; - x[2] ^= rk; - x[0] ^= x[4]; - x[4] ^= x[3]; - x[2] ^= x[1]; - var t = x.*; - x[0] = t[0] ^ ((~t[1]) & t[2]); - x[2] = t[2] ^ ((~t[3]) & t[4]); - x[4] = t[4] ^ ((~t[0]) & t[1]); - x[1] = t[1] ^ ((~t[2]) & t[3]); - x[3] = t[3] ^ ((~t[4]) & t[0]); - x[1] ^= x[0]; - t[1] = x[1]; - x[1] = math.rotr(u64, x[1], 39); - x[3] ^= x[2]; - t[2] = x[2]; - x[2] = math.rotr(u64, x[2], 1); - t[4] = x[4]; - t[2] ^= x[2]; - x[2] = math.rotr(u64, x[2], 5); - t[3] = x[3]; - t[1] ^= x[1]; - x[3] = math.rotr(u64, x[3], 10); - x[0] ^= x[4]; - x[4] = math.rotr(u64, x[4], 7); - t[3] ^= x[3]; - x[2] ^= t[2]; - x[1] = math.rotr(u64, x[1], 22); - t[0] = x[0]; - x[2] = ~x[2]; - x[3] = math.rotr(u64, x[3], 7); - t[4] ^= x[4]; - x[4] = math.rotr(u64, x[4], 34); - x[3] ^= t[3]; - x[1] ^= t[1]; - x[0] = math.rotr(u64, x[0], 19); - x[4] ^= t[4]; - t[0] ^= x[0]; - x[0] = math.rotr(u64, x[0], 9); - x[0] ^= t[0]; - } - - fn p12(isap: *IsapA128A) void { - const rks = [12]u64{ 0xf0, 0xe1, 0xd2, 0xc3, 0xb4, 0xa5, 0x96, 0x87, 0x78, 0x69, 0x5a, 0x4b }; - inline for (rks) |rk| { - isap.round(rk); - } - } - - fn p6(isap: *IsapA128A) void { - const rks = [6]u64{ 0x96, 0x87, 0x78, 0x69, 0x5a, 0x4b }; - inline for (rks) |rk| { - isap.round(rk); - } - } - - fn p1(isap: *IsapA128A) void { - isap.round(0x4b); - } + st: Ascon, fn absorb(isap: *IsapA128A, m: []const u8) void { - var block = &isap.block; var i: usize = 0; while (true) : (i += 8) { const left = m.len - i; if (left >= 8) { - block[0] ^= mem.readIntBig(u64, m[i..][0..8]); - isap.p12(); + isap.st.addBytes(m[i..][0..8]); + isap.st.permute(); if (left == 8) { - block[0] ^= 0x8000000000000000; - isap.p12(); + isap.st.addByte(0x80, 0); + isap.st.permute(); break; } } else { var padded = [_]u8{0} ** 8; mem.copy(u8, padded[0..left], m[i..]); padded[left] = 0x80; - block[0] ^= mem.readIntBig(u64, padded[0..]); - isap.p12(); + isap.st.addBytes(&padded); + isap.st.permute(); break; } } @@ -116,65 +54,59 @@ pub const IsapA128A = struct { fn trickle(k: [16]u8, iv: [8]u8, y: []const u8, comptime out_len: usize) [out_len]u8 { var isap = IsapA128A{ - .block = Block{ + .st = Ascon.initFromWords(.{ mem.readIntBig(u64, k[0..8]), mem.readIntBig(u64, k[8..16]), mem.readIntBig(u64, iv[0..8]), 0, 0, - }, + }), }; - isap.p12(); + isap.st.permute(); var i: usize = 0; while (i < y.len * 8 - 1) : (i += 1) { const cur_byte_pos = i / 8; const cur_bit_pos = @truncate(u3, 7 - (i % 8)); - const cur_bit = @as(u64, ((y[cur_byte_pos] >> cur_bit_pos) & 1) << 7); - isap.block[0] ^= cur_bit << 56; - isap.p1(); + const cur_bit = ((y[cur_byte_pos] >> cur_bit_pos) & 1) << 7; + isap.st.addByte(cur_bit, 0); + isap.st.permuteR(1); } - const cur_bit = @as(u64, (y[y.len - 1] & 1) << 7); - isap.block[0] ^= cur_bit << 56; - isap.p12(); + const cur_bit = (y[y.len - 1] & 1) << 7; + isap.st.addByte(cur_bit, 0); + isap.st.permute(); var out: [out_len]u8 = undefined; - var j: usize = 0; - while (j < out_len) : (j += 8) { - mem.writeIntBig(u64, out[j..][0..8], isap.block[j / 8]); - } - std.crypto.utils.secureZero(u64, &isap.block); + isap.st.extractBytes(&out); + isap.st.secureZero(); return out; } fn mac(c: []const u8, ad: []const u8, npub: [16]u8, key: [16]u8) [16]u8 { var isap = IsapA128A{ - .block = Block{ + .st = Ascon.initFromWords(.{ mem.readIntBig(u64, npub[0..8]), mem.readIntBig(u64, npub[8..16]), mem.readIntBig(u64, iv1[0..]), 0, 0, - }, + }), }; - isap.p12(); + isap.st.permute(); isap.absorb(ad); - isap.block[4] ^= 1; + isap.st.addByte(1, Ascon.block_bytes - 1); isap.absorb(c); var y: [16]u8 = undefined; - mem.writeIntBig(u64, y[0..8], isap.block[0]); - mem.writeIntBig(u64, y[8..16], isap.block[1]); + isap.st.extractBytes(&y); const nb = trickle(key, iv2, y[0..], 16); - isap.block[0] = mem.readIntBig(u64, nb[0..8]); - isap.block[1] = mem.readIntBig(u64, nb[8..16]); - isap.p12(); + isap.st.setBytes(&nb); + isap.st.permute(); var tag: [16]u8 = undefined; - mem.writeIntBig(u64, tag[0..8], isap.block[0]); - mem.writeIntBig(u64, tag[8..16], isap.block[1]); - std.crypto.utils.secureZero(u64, &isap.block); + isap.st.extractBytes(&tag); + isap.st.secureZero(); return tag; } @@ -183,34 +115,31 @@ pub const IsapA128A = struct { const nb = trickle(key, iv3, npub[0..], 24); var isap = IsapA128A{ - .block = Block{ + .st = Ascon.initFromWords(.{ mem.readIntBig(u64, nb[0..8]), mem.readIntBig(u64, nb[8..16]), mem.readIntBig(u64, nb[16..24]), mem.readIntBig(u64, npub[0..8]), mem.readIntBig(u64, npub[8..16]), - }, + }), }; - isap.p6(); + isap.st.permuteR(6); var i: usize = 0; while (true) : (i += 8) { const left = in.len - i; if (left >= 8) { - mem.writeIntNative(u64, out[i..][0..8], mem.bigToNative(u64, isap.block[0]) ^ mem.readIntNative(u64, in[i..][0..8])); + isap.st.xorBytes(out[i..][0..8], in[i..][0..8]); if (left == 8) { break; } - isap.p6(); + isap.st.permuteR(6); } else { - var pad = [_]u8{0} ** 8; - mem.copy(u8, pad[0..left], in[i..][0..left]); - mem.writeIntNative(u64, pad[i..][0..8], mem.bigToNative(u64, isap.block[0]) ^ mem.readIntNative(u64, pad[i..][0..8])); - mem.copy(u8, out[i..][0..left], pad[0..left]); + isap.st.xorBytes(out[i..], in[i..]); break; } } - std.crypto.utils.secureZero(u64, &isap.block); + isap.st.secureZero(); } pub fn encrypt(c: []u8, tag: *[tag_length]u8, m: []const u8, ad: []const u8, npub: [nonce_length]u8, key: [key_length]u8) void { @@ -220,12 +149,9 @@ pub const IsapA128A = struct { pub fn decrypt(m: []u8, c: []const u8, tag: [tag_length]u8, ad: []const u8, npub: [nonce_length]u8, key: [key_length]u8) AuthenticationError!void { var computed_tag = mac(c, ad, npub, key); - var acc: u8 = 0; - for (computed_tag) |_, j| { - acc |= (computed_tag[j] ^ tag[j]); - } - std.crypto.utils.secureZero(u8, &computed_tag); - if (acc != 0) { + const res = crypto.utils.timingSafeEql([tag_length]u8, computed_tag, tag); + crypto.utils.secureZero(u8, &computed_tag); + if (!res) { return error.AuthenticationFailed; } xor(m, c, npub, key); diff --git a/lib/std/rand.zig b/lib/std/rand.zig index 914419b863..f5a1ffe57e 100644 --- a/lib/std/rand.zig +++ b/lib/std/rand.zig @@ -18,8 +18,9 @@ const maxInt = std.math.maxInt; pub const DefaultPrng = Xoshiro256; /// Cryptographically secure random numbers. -pub const DefaultCsprng = Xoodoo; +pub const DefaultCsprng = Ascon; +pub const Ascon = @import("rand/Ascon.zig"); pub const Isaac64 = @import("rand/Isaac64.zig"); pub const Xoodoo = @import("rand/Xoodoo.zig"); pub const Pcg = @import("rand/Pcg.zig"); diff --git a/lib/std/rand/Ascon.zig b/lib/std/rand/Ascon.zig new file mode 100644 index 0000000000..b6e8ce4899 --- /dev/null +++ b/lib/std/rand/Ascon.zig @@ -0,0 +1,45 @@ +//! CSPRNG based on the Ascon XOFa construction + +const std = @import("std"); +const min = std.math.min; +const mem = std.mem; +const Random = std.rand.Random; +const Self = @This(); + +state: std.crypto.core.Ascon(.Little), + +const rate = 8; +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) Self { + var state = std.crypto.core.Ascon(.Little).initXofA(); + var i: usize = 0; + while (i + rate <= secret_seed.len) : (i += rate) { + state.addBytes(secret_seed[i..][0..rate]); + state.permuteR(8); + } + const left = secret_seed.len - i; + if (left > 0) state.addBytes(secret_seed[i..]); + state.addByte(0x80, left); + state.permute(); + return Self{ .state = state }; +} + +pub fn random(self: *Self) Random { + return Random.init(self, fill); +} + +pub fn fill(self: *Self, buf: []u8) void { + var i: usize = 0; + while (true) { + const left = buf.len - i; + const n = min(left, rate); + self.state.extractBytes(buf[i..][0..n]); + if (left == 0) break; + self.state.permuteR(8); + i += n; + } + self.state.clear(0, rate); + self.state.permuteR(8); +}