authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-04-20 12:05:16+02:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-04-20 18:29:46+02:00
log525aff6048adae9b1a17db53bfeb1b9a02c3571f
tree722c87727286ceb19472cd4fa056e7bd4b443d3c
parent3a07f50dabbda0532b9e87d76662d81a4740fe40

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).

1 files changed, 75 insertions(+), 33 deletions(-)

lib/std/crypto/ascon.zig+75-33
...@@ -681,6 +681,8 @@ pub const AsconXof128 = struct {...@@ -681,6 +681,8 @@ pub const AsconXof128 = struct {
681681
682 st: AsconState,682 st: AsconState,
683 squeezed: bool,683 squeezed: bool,
684 buf: [block_length]u8,
685 buf_len: usize,
684686
685 pub const Options = struct {};687 pub const Options = struct {};
686688
...@@ -698,7 +700,7 @@ pub const AsconXof128 = struct {...@@ -698,7 +700,7 @@ pub const AsconXof128 = struct {
698 const words: [5]u64 = .{ iv, 0, 0, 0, 0 };700 const words: [5]u64 = .{ iv, 0, 0, 0, 0 };
699 var st = AsconState.initFromWords(words);701 var st = AsconState.initFromWords(words);
700 st.permuteR(12);702 st.permuteR(12);
701 return AsconXof128{ .st = st, .squeezed = false };703 return AsconXof128{ .st = st, .squeezed = false, .buf = @splat(0), .buf_len = 0 };
702 }704 }
703705
704 /// Hash a slice of bytes with variable-length output.706 /// Hash a slice of bytes with variable-length output.
...@@ -726,24 +728,26 @@ pub const AsconXof128 = struct {...@@ -726,24 +728,26 @@ pub const AsconXof128 = struct {
726728
727 var i: usize = 0;729 var i: usize = 0;
728730
729 // Process full 64-bit blocks731 if (self.buf_len > 0) {
730 while (i + 8 <= b.len) : (i += 8) {732 const to_fill = @min(block_length - self.buf_len, b.len);
731 self.st.addBytes(b[i..][0..8]);733 @memcpy(self.buf[self.buf_len..][0..to_fill], b[0..to_fill]);
734 self.buf_len += to_fill;
735 i += to_fill;
736 if (self.buf_len == block_length) {
737 self.st.addBytes(&self.buf);
738 self.st.permuteR(12);
739 self.buf_len = 0;
740 }
741 }
742
743 while (i + block_length <= b.len) : (i += block_length) {
744 self.st.addBytes(b[i..][0..block_length]);
732 self.st.permuteR(12);745 self.st.permuteR(12);
733 }746 }
734747
735 // Store partial block for finalization
736 if (i < b.len) {748 if (i < b.len) {
737 var padded: [8]u8 = @splat(0);749 self.buf_len = b.len - i;
738 const remaining = b.len - i;750 @memcpy(self.buf[0..self.buf_len], b[i..]);
739 @memcpy(padded[0..remaining], b[i..]);
740 padded[remaining] = 0x01;
741 self.st.addBytes(&padded);
742 } else {
743 // Add padding block
744 var padded: [8]u8 = @splat(0);
745 padded[0] = 0x01;
746 self.st.addBytes(&padded);
747 }751 }
748 }752 }
749753
...@@ -756,7 +760,10 @@ pub const AsconXof128 = struct {...@@ -756,7 +760,10 @@ pub const AsconXof128 = struct {
756 /// After first call, no more data can be absorbed with update().760 /// After first call, no more data can be absorbed with update().
757 pub fn squeeze(self: *AsconXof128, out: []u8) void {761 pub fn squeeze(self: *AsconXof128, out: []u8) void {
758 if (!self.squeezed) {762 if (!self.squeezed) {
759 // First squeeze - apply final permutation763 var padded: [block_length]u8 = @splat(0);
764 @memcpy(padded[0..self.buf_len], self.buf[0..self.buf_len]);
765 padded[self.buf_len] = 0x01;
766 self.st.addBytes(&padded);
760 self.st.permuteR(12);767 self.st.permuteR(12);
761 self.squeezed = true;768 self.squeezed = true;
762 }769 }
...@@ -783,6 +790,8 @@ pub const AsconCxof128 = struct {...@@ -783,6 +790,8 @@ pub const AsconCxof128 = struct {
783790
784 st: AsconState,791 st: AsconState,
785 squeezed: bool,792 squeezed: bool,
793 buf: [block_length]u8,
794 buf_len: usize,
786795
787 pub const Options = struct { custom: []const u8 = "" };796 pub const Options = struct { custom: []const u8 = "" };
788797
...@@ -804,7 +813,7 @@ pub const AsconCxof128 = struct {...@@ -804,7 +813,7 @@ pub const AsconCxof128 = struct {
804 var st = AsconState.initFromWords(words);813 var st = AsconState.initFromWords(words);
805 st.permuteR(12);814 st.permuteR(12);
806815
807 var self = AsconCxof128{ .st = st, .squeezed = false };816 var self = AsconCxof128{ .st = st, .squeezed = false, .buf = @splat(0), .buf_len = 0 };
808817
809 // Process customization string - always process length and padding818 // Process customization string - always process length and padding
810 // First block: length of customization string819 // First block: length of customization string
...@@ -867,28 +876,30 @@ pub const AsconCxof128 = struct {...@@ -867,28 +876,30 @@ pub const AsconCxof128 = struct {
867 ///876 ///
868 /// Note: Cannot be called after squeeze() has been called877 /// Note: Cannot be called after squeeze() has been called
869 pub fn update(self: *AsconCxof128, b: []const u8) void {878 pub fn update(self: *AsconCxof128, b: []const u8) void {
870 debug.assert(!self.squeezed);879 debug.assert(!self.squeezed); // Cannot update after squeezing
871880
872 var i: usize = 0;881 var i: usize = 0;
873882
874 // Process full 64-bit blocks883 if (self.buf_len > 0) {
875 while (i + 8 <= b.len) : (i += 8) {884 const to_fill = @min(block_length - self.buf_len, b.len);
876 self.st.addBytes(b[i..][0..8]);885 @memcpy(self.buf[self.buf_len..][0..to_fill], b[0..to_fill]);
886 self.buf_len += to_fill;
887 i += to_fill;
888 if (self.buf_len == block_length) {
889 self.st.addBytes(&self.buf);
890 self.st.permuteR(12);
891 self.buf_len = 0;
892 }
893 }
894
895 while (i + block_length <= b.len) : (i += block_length) {
896 self.st.addBytes(b[i..][0..block_length]);
877 self.st.permuteR(12);897 self.st.permuteR(12);
878 }898 }
879899
880 // Store partial block for finalization
881 if (i < b.len) {900 if (i < b.len) {
882 var padded: [8]u8 = @splat(0);901 self.buf_len = b.len - i;
883 const remaining = b.len - i;902 @memcpy(self.buf[0..self.buf_len], b[i..]);
884 @memcpy(padded[0..remaining], b[i..]);
885 padded[remaining] = 0x01;
886 self.st.addBytes(&padded);
887 } else {
888 // Add padding block
889 var padded: [8]u8 = @splat(0);
890 padded[0] = 0x01;
891 self.st.addBytes(&padded);
892 }903 }
893 }904 }
894905
...@@ -901,7 +912,10 @@ pub const AsconCxof128 = struct {...@@ -901,7 +912,10 @@ pub const AsconCxof128 = struct {
901 /// After first call, no more data can be absorbed with update().912 /// After first call, no more data can be absorbed with update().
902 pub fn squeeze(self: *AsconCxof128, out: []u8) void {913 pub fn squeeze(self: *AsconCxof128, out: []u8) void {
903 if (!self.squeezed) {914 if (!self.squeezed) {
904 // First squeeze - apply final permutation915 var padded: [block_length]u8 = @splat(0);
916 @memcpy(padded[0..self.buf_len], self.buf[0..self.buf_len]);
917 padded[self.buf_len] = 0x01;
918 self.st.addBytes(&padded);
905 self.st.permuteR(12);919 self.st.permuteR(12);
906 self.squeezed = true;920 self.squeezed = true;
907 }921 }
...@@ -1267,6 +1281,34 @@ test "Ascon-XOF128 official test vectors" {...@@ -1267,6 +1281,34 @@ test "Ascon-XOF128 official test vectors" {
1267 }1281 }
1268}1282}
12691283
1284test "Ascon-XOF128/CXOF128 streaming chunking invariance" {
1285 const msg = "Hello, World!";
1286
1287 // XOF128: one-shot vs split must match
1288 var out1: [32]u8 = undefined;
1289 var out2: [32]u8 = undefined;
1290 var xof1 = AsconXof128.init(.{});
1291 xof1.update(msg);
1292 xof1.squeeze(&out1);
1293 var xof2 = AsconXof128.init(.{});
1294 xof2.update("Hello, ");
1295 xof2.update("World!");
1296 xof2.squeeze(&out2);
1297 try testing.expectEqualSlices(u8, &out1, &out2);
1298
1299 // CXOF128: one-shot vs split must match
1300 var cout1: [32]u8 = undefined;
1301 var cout2: [32]u8 = undefined;
1302 var cxof1 = AsconCxof128.init(.{ .custom = "cust" });
1303 cxof1.update(msg);
1304 cxof1.squeeze(&cout1);
1305 var cxof2 = AsconCxof128.init(.{ .custom = "cust" });
1306 cxof2.update("Hello, ");
1307 cxof2.update("World!");
1308 cxof2.squeeze(&cout2);
1309 try testing.expectEqualSlices(u8, &cout1, &cout2);
1310}
1311
1270test "Ascon-CXOF128 official test vectors" {1312test "Ascon-CXOF128 official test vectors" {
12711313
1272 // Test vector 1: Empty message, empty customization, 64-byte output1314 // Test vector 1: Empty message, empty customization, 64-byte output