authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-10-04 12:35:04+02:00
committergravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-10-15 10:04:30+02:00
log5d5711e6fac29faac0731acdc58286c0be5c50b7
tree8cc0d385b1d2a90b160b31bc9540383725b651d5
parent692a774deb45a7326e4783f8ce828865c3559f4b

crypto.ascon: fix incremental processing

In Ascon-AEAD128, we forgot a padding-only block after the AD. And for the hash/XOF moes, we should keep track of the offset between update() and squeeze() calls to make them equivalent to a single-shot call, as in other hash functions/XOFs. Otherwise the construction is not insecure, but the output doesn't match the specification either.

1 files changed, 44 insertions(+), 25 deletions(-)

lib/std/crypto/ascon.zig+44-25
......@@ -386,6 +386,9 @@ pub const AsconAead128 = struct {
386386 self.st.st[0] ^= mem.readInt(u64, &buf, .little);
387387 }
388388 self.st.permuteR(8);
389 } else {
390 self.st.st[0] ^= 0x01;
391 self.st.permuteR(8);
389392 }
390393 }
391394
......@@ -589,6 +592,7 @@ pub const AsconHash256 = struct {
589592 pub const block_length = 8;
590593
591594 st: AsconState,
595 offset: usize,
592596
593597 pub const Options = struct {};
594598
......@@ -606,7 +610,7 @@ pub const AsconHash256 = struct {
606610 const words: [5]u64 = .{ iv, 0, 0, 0, 0 };
607611 var st = AsconState.initFromWords(words);
608612 st.permuteR(12);
609 return AsconHash256{ .st = st };
613 return AsconHash256{ .st = st, .offset = 0 };
610614 }
611615
612616 /// Compute Ascon-Hash256 hash of input data in one call.
......@@ -630,6 +634,13 @@ pub const AsconHash256 = struct {
630634 pub fn update(self: *AsconHash256, b: []const u8) void {
631635 var i: usize = 0;
632636
637 // Finish the block left partially absorbed by a previous call
638 while (self.offset > 0 and i < b.len) : (i += 1) {
639 self.st.addByte(b[i], self.offset);
640 self.offset = (self.offset + 1) % block_length;
641 if (self.offset == 0) self.st.permuteR(12);
642 }
643
633644 // Process full 64-bit blocks
634645 while (i + 8 <= b.len) : (i += 8) {
635646 self.st.addBytes(b[i..][0..8]);
......@@ -638,16 +649,8 @@ pub const AsconHash256 = struct {
638649
639650 // Store partial block for finalization
640651 if (i < b.len) {
641 var padded: [8]u8 = @splat(0);
642 const remaining = b.len - i;
643 @memcpy(padded[0..remaining], b[i..]);
644 padded[remaining] = 0x01;
645 self.st.addBytes(&padded);
646 } else {
647 // Add padding block
648 var padded: [8]u8 = @splat(0);
649 padded[0] = 0x01;
650 self.st.addBytes(&padded);
652 self.st.addBytes(b[i..]);
653 self.offset = b.len - i;
651654 }
652655 }
653656
......@@ -658,6 +661,7 @@ pub const AsconHash256 = struct {
658661 ///
659662 /// Note: After calling final(), the hasher should not be used again
660663 pub fn final(self: *AsconHash256, out: *[digest_length]u8) void {
664 self.st.addByte(0x01, self.offset);
661665 // Final permutation after padding
662666 self.st.permuteR(12);
663667
......@@ -772,19 +776,21 @@ pub const AsconXof128 = struct {
772776 self.st.addBytes(&padded);
773777 self.st.permuteR(12);
774778 self.squeezed = true;
779 self.buf_len = 0;
775780 }
776781
777782 var i: usize = 0;
778783 while (i < out.len) {
779 const to_copy = @min(8, out.len - i);
784 if (self.buf_len == block_length) {
785 self.st.permuteR(12);
786 self.buf_len = 0;
787 }
788 const to_copy = @min(block_length - self.buf_len, out.len - i);
780789 var block: [8]u8 = undefined;
781790 mem.writeInt(u64, &block, self.st.st[0], .little);
782 @memcpy(out[i..][0..to_copy], block[0..to_copy]);
791 @memcpy(out[i..][0..to_copy], block[self.buf_len..][0..to_copy]);
783792 i += to_copy;
784
785 if (i < out.len) {
786 self.st.permuteR(12);
787 }
793 self.buf_len += to_copy;
788794 }
789795 }
790796};
......@@ -924,19 +930,21 @@ pub const AsconCxof128 = struct {
924930 self.st.addBytes(&padded);
925931 self.st.permuteR(12);
926932 self.squeezed = true;
933 self.buf_len = 0;
927934 }
928935
929936 var i: usize = 0;
930937 while (i < out.len) {
931 const to_copy = @min(8, out.len - i);
938 if (self.buf_len == block_length) {
939 self.st.permuteR(12);
940 self.buf_len = 0;
941 }
942 const to_copy = @min(block_length - self.buf_len, out.len - i);
932943 var block: [8]u8 = undefined;
933944 mem.writeInt(u64, &block, self.st.st[0], .little);
934 @memcpy(out[i..][0..to_copy], block[0..to_copy]);
945 @memcpy(out[i..][0..to_copy], block[self.buf_len..][0..to_copy]);
935946 i += to_copy;
936
937 if (i < out.len) {
938 self.st.permuteR(12);
939 }
947 self.buf_len += to_copy;
940948 }
941949 }
942950};
......@@ -949,6 +957,11 @@ test "Ascon-Hash256 basic test" {
949957
950958 // Verify hash is generated (exact value depends on test vectors)
951959 try testing.expect(hash.len == 32);
960
961 var h = AsconHash256.init(.{});
962 h.update(message[0..5]);
963 h.update(message[5..]);
964 try testing.expectEqualSlices(u8, &hash, &h.finalResult());
952965}
953966
954967test "Ascon-XOF128 basic test" {
......@@ -1122,6 +1135,10 @@ test "Ascon-AEAD128 official test vectors" {
11221135 var expected_tag: [16]u8 = undefined;
11231136 _ = std.fmt.hexToBytes(&expected_tag, "CCCB674FE18A09A285D6AB11B35675C0") catch unreachable;
11241137 try testing.expectEqualSlices(u8, &expected_tag, &tag);
1138
1139 AsconAead128.encrypt(&ciphertext, &tag, plaintext, "0123456789:;<=>?", nonce, key);
1140 _ = std.fmt.hexToBytes(&expected_tag, "E4230CDB8330EE9DC0CFD7C7B346E6DC") catch unreachable;
1141 try testing.expectEqualSlices(u8, &expected_tag, &tag);
11251142 }
11261143
11271144 // Test vector 34: Single byte plaintext 0x20
......@@ -1297,7 +1314,8 @@ test "Ascon-XOF128/CXOF128 streaming chunking invariance" {
12971314 var xof2 = AsconXof128.init(.{});
12981315 xof2.update("Hello, ");
12991316 xof2.update("World!");
1300 xof2.squeeze(&out2);
1317 xof2.squeeze(out2[0..3]);
1318 xof2.squeeze(out2[3..]);
13011319 try testing.expectEqualSlices(u8, &out1, &out2);
13021320
13031321 // CXOF128: one-shot vs split must match
......@@ -1309,7 +1327,8 @@ test "Ascon-XOF128/CXOF128 streaming chunking invariance" {
13091327 var cxof2 = AsconCxof128.init(.{ .custom = "cust" });
13101328 cxof2.update("Hello, ");
13111329 cxof2.update("World!");
1312 cxof2.squeeze(&cout2);
1330 cxof2.squeeze(cout2[0..8]);
1331 cxof2.squeeze(cout2[8..]);
13131332 try testing.expectEqualSlices(u8, &cout1, &cout2);
13141333}
13151334