std.crypto.ascon: fix streaming XOF/CXOF

AsconXof128 and AsconCxof128 were applying the padding in update()
calls. That was totally fine for one-shot hashing, but not for
streaming (multiple update() calls before finalization).
This commit is contained in:
Frank Denis
2026-04-20 12:05:16 +02:00
committed by Andrew Kelley
parent 3a07f50dab
commit 525aff6048
+75 -33
View File
@@ -681,6 +681,8 @@ pub const AsconXof128 = struct {
st: AsconState,
squeezed: bool,
buf: [block_length]u8,
buf_len: usize,
pub const Options = struct {};
@@ -698,7 +700,7 @@ pub const AsconXof128 = struct {
const words: [5]u64 = .{ iv, 0, 0, 0, 0 };
var st = AsconState.initFromWords(words);
st.permuteR(12);
return AsconXof128{ .st = st, .squeezed = false };
return AsconXof128{ .st = st, .squeezed = false, .buf = @splat(0), .buf_len = 0 };
}
/// Hash a slice of bytes with variable-length output.
@@ -726,24 +728,26 @@ pub const AsconXof128 = struct {
var i: usize = 0;
// Process full 64-bit blocks
while (i + 8 <= b.len) : (i += 8) {
self.st.addBytes(b[i..][0..8]);
if (self.buf_len > 0) {
const to_fill = @min(block_length - self.buf_len, b.len);
@memcpy(self.buf[self.buf_len..][0..to_fill], b[0..to_fill]);
self.buf_len += to_fill;
i += to_fill;
if (self.buf_len == block_length) {
self.st.addBytes(&self.buf);
self.st.permuteR(12);
self.buf_len = 0;
}
}
while (i + block_length <= b.len) : (i += block_length) {
self.st.addBytes(b[i..][0..block_length]);
self.st.permuteR(12);
}
// Store partial block for finalization
if (i < b.len) {
var padded: [8]u8 = @splat(0);
const remaining = b.len - i;
@memcpy(padded[0..remaining], b[i..]);
padded[remaining] = 0x01;
self.st.addBytes(&padded);
} else {
// Add padding block
var padded: [8]u8 = @splat(0);
padded[0] = 0x01;
self.st.addBytes(&padded);
self.buf_len = b.len - i;
@memcpy(self.buf[0..self.buf_len], b[i..]);
}
}
@@ -756,7 +760,10 @@ pub const AsconXof128 = struct {
/// After first call, no more data can be absorbed with update().
pub fn squeeze(self: *AsconXof128, out: []u8) void {
if (!self.squeezed) {
// First squeeze - apply final permutation
var padded: [block_length]u8 = @splat(0);
@memcpy(padded[0..self.buf_len], self.buf[0..self.buf_len]);
padded[self.buf_len] = 0x01;
self.st.addBytes(&padded);
self.st.permuteR(12);
self.squeezed = true;
}
@@ -783,6 +790,8 @@ pub const AsconCxof128 = struct {
st: AsconState,
squeezed: bool,
buf: [block_length]u8,
buf_len: usize,
pub const Options = struct { custom: []const u8 = "" };
@@ -804,7 +813,7 @@ pub const AsconCxof128 = struct {
var st = AsconState.initFromWords(words);
st.permuteR(12);
var self = AsconCxof128{ .st = st, .squeezed = false };
var self = AsconCxof128{ .st = st, .squeezed = false, .buf = @splat(0), .buf_len = 0 };
// Process customization string - always process length and padding
// First block: length of customization string
@@ -867,28 +876,30 @@ pub const AsconCxof128 = struct {
///
/// Note: Cannot be called after squeeze() has been called
pub fn update(self: *AsconCxof128, b: []const u8) void {
debug.assert(!self.squeezed);
debug.assert(!self.squeezed); // Cannot update after squeezing
var i: usize = 0;
// Process full 64-bit blocks
while (i + 8 <= b.len) : (i += 8) {
self.st.addBytes(b[i..][0..8]);
if (self.buf_len > 0) {
const to_fill = @min(block_length - self.buf_len, b.len);
@memcpy(self.buf[self.buf_len..][0..to_fill], b[0..to_fill]);
self.buf_len += to_fill;
i += to_fill;
if (self.buf_len == block_length) {
self.st.addBytes(&self.buf);
self.st.permuteR(12);
self.buf_len = 0;
}
}
while (i + block_length <= b.len) : (i += block_length) {
self.st.addBytes(b[i..][0..block_length]);
self.st.permuteR(12);
}
// Store partial block for finalization
if (i < b.len) {
var padded: [8]u8 = @splat(0);
const remaining = b.len - i;
@memcpy(padded[0..remaining], b[i..]);
padded[remaining] = 0x01;
self.st.addBytes(&padded);
} else {
// Add padding block
var padded: [8]u8 = @splat(0);
padded[0] = 0x01;
self.st.addBytes(&padded);
self.buf_len = b.len - i;
@memcpy(self.buf[0..self.buf_len], b[i..]);
}
}
@@ -901,7 +912,10 @@ pub const AsconCxof128 = struct {
/// After first call, no more data can be absorbed with update().
pub fn squeeze(self: *AsconCxof128, out: []u8) void {
if (!self.squeezed) {
// First squeeze - apply final permutation
var padded: [block_length]u8 = @splat(0);
@memcpy(padded[0..self.buf_len], self.buf[0..self.buf_len]);
padded[self.buf_len] = 0x01;
self.st.addBytes(&padded);
self.st.permuteR(12);
self.squeezed = true;
}
@@ -1267,6 +1281,34 @@ test "Ascon-XOF128 official test vectors" {
}
}
test "Ascon-XOF128/CXOF128 streaming chunking invariance" {
const msg = "Hello, World!";
// XOF128: one-shot vs split must match
var out1: [32]u8 = undefined;
var out2: [32]u8 = undefined;
var xof1 = AsconXof128.init(.{});
xof1.update(msg);
xof1.squeeze(&out1);
var xof2 = AsconXof128.init(.{});
xof2.update("Hello, ");
xof2.update("World!");
xof2.squeeze(&out2);
try testing.expectEqualSlices(u8, &out1, &out2);
// CXOF128: one-shot vs split must match
var cout1: [32]u8 = undefined;
var cout2: [32]u8 = undefined;
var cxof1 = AsconCxof128.init(.{ .custom = "cust" });
cxof1.update(msg);
cxof1.squeeze(&cout1);
var cxof2 = AsconCxof128.init(.{ .custom = "cust" });
cxof2.update("Hello, ");
cxof2.update("World!");
cxof2.squeeze(&cout2);
try testing.expectEqualSlices(u8, &cout1, &cout2);
}
test "Ascon-CXOF128 official test vectors" {
// Test vector 1: Empty message, empty customization, 64-byte output