authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-09-07 21:02:48+02:00
committergravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-09-22 13:32:43+02:00
log53b508142ee9d53fd503541b7e4583b36529106c
tree6094c0f6ab5cddde080160aa6590b188ca589a78
parent8a353860179a1663987dae856ba08ccbbd037400

crypto.ff: fix byte encoding and decoding with padding

We should accept zero padding and encodings that use the full rounded storage capacity, while rejecting nonzero bits that don't fit. And allow shorter output buffers when the limbs we skip are all zero.

1 files changed, 134 insertions(+), 45 deletions(-)

lib/std/crypto/ff.zig+134-45
...@@ -55,14 +55,18 @@ pub const RepresentationError = error{UnexpectedRepresentation};...@@ -55,14 +55,18 @@ pub const RepresentationError = error{UnexpectedRepresentation};
55pub const Error = OverflowError || InvalidModulusError || NullExponentError || FieldElementError || RepresentationError;55pub const Error = OverflowError || InvalidModulusError || NullExponentError || FieldElementError || RepresentationError;
5656
57/// An unsigned big integer with a fixed maximum size (`max_bits`), suitable for cryptographic operations.57/// An unsigned big integer with a fixed maximum size (`max_bits`), suitable for cryptographic operations.
58/// Storage rounds up to whole limbs and can hold up to `capacity_bits` bits.
58/// Unless side-channels mitigations are explicitly disabled, operations are designed to be constant-time.59/// Unless side-channels mitigations are explicitly disabled, operations are designed to be constant-time.
59pub fn Uint(comptime max_bits: comptime_int) type {60pub fn Uint(comptime max_bits: comptime_int) type {
60 comptime assert(@bitSizeOf(Limb) % 8 == 0); // Limb size must be a multiple of 861 comptime assert(@bitSizeOf(Limb) % 8 == 0); // Limb size must be a multiple of 8
62 comptime assert(max_bits > 0);
6163
62 return struct {64 return struct {
63 const Self = @This();65 const Self = @This();
64 const max_limbs_count = @divCeil(max_bits, t_bits);66 const max_limbs_count = @divCeil(max_bits, t_bits);
6567
68 pub const capacity_bits = max_limbs_count * t_bits;
69
66 limbs_buffer: [max_limbs_count]Limb,70 limbs_buffer: [max_limbs_count]Limb,
67 /// The number of active limbs.71 /// The number of active limbs.
68 limbs_len: usize,72 limbs_len: usize,
...@@ -154,15 +158,15 @@ pub fn Uint(comptime max_bits: comptime_int) type {...@@ -154,15 +158,15 @@ pub fn Uint(comptime max_bits: comptime_int) type {
154 var remaining_bits = t_bits;158 var remaining_bits = t_bits;
155 var limb = self.limbsConst()[i];159 var limb = self.limbsConst()[i];
156 while (remaining_bits >= 8) {160 while (remaining_bits >= 8) {
157 bytes[out_i] |= math.shl(u8, @as(u8, @truncate(limb)), shift);161 bytes[out_i] |= math.shl(u8, @truncate(limb), shift);
158 const consumed = 8 - shift;162 const consumed = 8 - shift;
159 limb >>= @as(u4, @truncate(consumed));163 limb >>= @truncate(consumed);
160 remaining_bits -= consumed;164 remaining_bits -= consumed;
161 shift = 0;165 shift = 0;
162 switch (endian) {166 switch (endian) {
163 .big => {167 .big => {
164 if (out_i == 0) {168 if (out_i == 0) {
165 if (i != self.limbs_len - 1 or limb != 0) {169 if (limb | orLimbs(self.limbsConst()[i + 1 ..]) != 0) {
166 return error.Overflow;170 return error.Overflow;
167 }171 }
168 return;172 return;
...@@ -172,7 +176,7 @@ pub fn Uint(comptime max_bits: comptime_int) type {...@@ -172,7 +176,7 @@ pub fn Uint(comptime max_bits: comptime_int) type {
172 .little => {176 .little => {
173 out_i += 1;177 out_i += 1;
174 if (out_i == bytes.len) {178 if (out_i == bytes.len) {
175 if (i != self.limbs_len - 1 or limb != 0) {179 if (limb | orLimbs(self.limbsConst()[i + 1 ..]) != 0) {
176 return error.Overflow;180 return error.Overflow;
177 }181 }
178 return;182 return;
...@@ -180,48 +184,16 @@ pub fn Uint(comptime max_bits: comptime_int) type {...@@ -180,48 +184,16 @@ pub fn Uint(comptime max_bits: comptime_int) type {
180 },184 },
181 }185 }
182 }186 }
183 bytes[out_i] |= @as(u8, @truncate(limb));187 bytes[out_i] |= @truncate(limb);
184 shift = remaining_bits;188 shift = remaining_bits;
185 }189 }
186 }190 }
187191
188 /// Creates a new big integer from a byte array.192 /// Creates a new big integer from a byte array.
189 pub fn fromBytes(bytes: []const u8, comptime endian: Endian) OverflowError!Self {193 pub fn fromBytes(bytes: []const u8, comptime endian: Endian) OverflowError!Self {
190 if (bytes.len == 0) return Self.zero;
191 var shift: usize = 0;
192 var out = Self.zero;194 var out = Self.zero;
193 var out_i: usize = 0;195 if (decodeBytes(out.limbs(), bytes, endian) != 0) {
194 var i: usize = switch (endian) {196 return error.Overflow;
195 .big => bytes.len - 1,
196 .little => 0,
197 };
198 while (true) {
199 const bi = bytes[i];
200 out.limbs()[out_i] |= math.shl(Limb, bi, shift);
201 shift += 8;
202 if (shift >= t_bits) {
203 shift -= t_bits;
204 out.limbs()[out_i] = @as(TLimb, @truncate(out.limbs()[out_i]));
205 const overflow = math.shr(Limb, bi, 8 - shift);
206 out_i += 1;
207 if (out_i >= out.limbs_len) {
208 if (overflow != 0 or i != 0) {
209 return error.Overflow;
210 }
211 break;
212 }
213 out.limbs()[out_i] = overflow;
214 }
215 switch (endian) {
216 .big => {
217 if (i == 0) break;
218 i -= 1;
219 },
220 .little => {
221 i += 1;
222 if (i == bytes.len) break;
223 },
224 }
225 }197 }
226 return out;198 return out;
227 }199 }
...@@ -243,11 +215,7 @@ pub fn Uint(comptime max_bits: comptime_int) type {...@@ -243,11 +215,7 @@ pub fn Uint(comptime max_bits: comptime_int) type {
243215
244 /// Returns `true` if the integer is zero.216 /// Returns `true` if the integer is zero.
245 pub fn isZero(x: Self) bool {217 pub fn isZero(x: Self) bool {
246 var t: Limb = 0;218 return ct.eql(orLimbs(x.limbsConst()), 0);
247 for (x.limbsConst()) |elem| {
248 t |= elem;
249 }
250 return ct.eql(t, 0);
251 }219 }
252220
253 /// Returns `true` if the integer is odd.221 /// Returns `true` if the integer is odd.
...@@ -380,6 +348,47 @@ fn Fe_(comptime bits: comptime_int) type {...@@ -380,6 +348,47 @@ fn Fe_(comptime bits: comptime_int) type {
380 };348 };
381}349}
382350
351// Decodes into zeroed limbs and returns the OR of the bits that didn't fit.
352fn decodeBytes(limbs: []Limb, bytes: []const u8, comptime endian: Endian) Limb {
353 var acc: Limb = 0;
354 var shift: usize = 0;
355 var out_i: usize = 0;
356 for (0..bytes.len) |k| {
357 const bi = bytes[
358 switch (endian) {
359 .big => bytes.len - 1 - k,
360 .little => k,
361 }
362 ];
363 if (out_i >= limbs.len) {
364 acc |= bi;
365 continue;
366 }
367 limbs[out_i] |= math.shl(Limb, bi, shift);
368 shift += 8;
369 if (shift >= t_bits) {
370 shift -= t_bits;
371 limbs[out_i] = @as(TLimb, @truncate(limbs[out_i]));
372 const spill = math.shr(Limb, bi, 8 - shift);
373 out_i += 1;
374 if (out_i < limbs.len) {
375 limbs[out_i] = spill;
376 } else {
377 acc |= spill;
378 }
379 }
380 }
381 return acc;
382}
383
384fn orLimbs(limbs: []const Limb) Limb {
385 var t: Limb = 0;
386 for (limbs) |limb| {
387 t |= limb;
388 }
389 return t;
390}
391
383/// A modulus, defining a finite field.392/// A modulus, defining a finite field.
384/// All operations within the field are performed modulo this modulus, without heap allocations.393/// All operations within the field are performed modulo this modulus, without heap allocations.
385/// `max_bits` represents the number of bits in the maximum value the modulus can be set to.394/// `max_bits` represents the number of bits in the maximum value the modulus can be set to.
...@@ -1090,7 +1099,40 @@ test ct {...@@ -1090,7 +1099,40 @@ test ct {
1090 try testCt(ct_unprotected);1099 try testCt(ct_unprotected);
1091}1100}
10921101
1093test "Uint serialization and predicates" {1102fn expectWellFormedLimbs(x: anytype) !void {
1103 for (x.limbsConst()) |limb| {
1104 try testing.expect(limb <= math.maxInt(TLimb));
1105 }
1106 for (x.limbs_buffer[x.limbs_len..]) |limb| {
1107 try testing.expectEqual(0, limb);
1108 }
1109}
1110
1111// Big-endian encoding of the largest value a `Uint` can store.
1112fn maxUintBytes(comptime U: type) [@divCeil(U.capacity_bits, 8)]u8 {
1113 var buf: [@divCeil(U.capacity_bits, 8)]u8 = @splat(0xff);
1114 buf[0] = 0xff >> (8 * buf.len - U.capacity_bits);
1115 return buf;
1116}
1117
1118test "modulus creation" {
1119 if (builtin.zig_backend == .stage2_c) return error.SkipZigTest;
1120
1121 const M = Modulus(256);
1122 try testing.expectError(error.EvenModulus, M.fromPrimitive(u8, 0));
1123 try testing.expectError(error.ModulusTooSmall, M.fromPrimitive(u8, 1));
1124 try testing.expectError(error.EvenModulus, M.fromPrimitive(u8, 2));
1125 for ([_]u65{ 3, 255, (1 << 64) + 1 }) |v| {
1126 const m = try M.fromPrimitive(u65, v);
1127 try testing.expectEqual(v, try m.toUint().toPrimitive(u65));
1128 try testing.expectEqual(@divCeil(65 - @clz(v), 8), m.encodedLen());
1129 try testing.expect((try M.fromUint(m.toUint())).v.eql(m.v));
1130 }
1131 const cap = maxUintBytes(Uint(256));
1132 try testing.expectEqual(cap.len, (try M.fromBytes(&cap, .big)).encodedLen());
1133}
1134
1135test "Uint serialization" {
1094 if (builtin.zig_backend == .stage2_c) return error.SkipZigTest;1136 if (builtin.zig_backend == .stage2_c) return error.SkipZigTest;
10951137
1096 const U = Uint(256);1138 const U = Uint(256);
...@@ -1105,4 +1147,51 @@ test "Uint serialization and predicates" {...@@ -1105,4 +1147,51 @@ test "Uint serialization and predicates" {
1105 const signed_max = try U.fromPrimitive(u128, math.maxInt(i128));1147 const signed_max = try U.fromPrimitive(u128, math.maxInt(i128));
1106 try testing.expectEqual(math.maxInt(i128), try signed_max.toPrimitive(i128));1148 try testing.expectEqual(math.maxInt(i128), try signed_max.toPrimitive(i128));
1107 try testing.expectError(error.Overflow, (try U.fromPrimitive(u128, 1 << 127)).toPrimitive(i128));1149 try testing.expectError(error.Overflow, (try U.fromPrimitive(u128, 1 << 127)).toPrimitive(i128));
1150
1151 inline for (.{ .big, .little }) |endian| {
1152 const zero = try U.fromBytes(&.{}, endian);
1153 var empty: [0]u8 = .{};
1154 try zero.toBytes(&empty, endian);
1155 try testing.expectError(error.Overflow, x.toBytes(&empty, endian));
1156 var tight: [16]u8 = undefined;
1157 try x.toBytes(&tight, endian);
1158 try testing.expectEqual((1 << t_bits) + 5, mem.readInt(u128, &tight, endian));
1159 try testing.expect(x.eql(try U.fromBytes(&tight, endian)));
1160 var normalized: [16]u8 = undefined;
1161 try x.normalize().toBytes(&normalized, endian);
1162 try testing.expectEqualSlices(u8, &tight, &normalized);
1163
1164 var cap = maxUintBytes(U);
1165 if (endian == .little) mem.reverse(u8, &cap);
1166 const full = try U.fromBytes(&cap, endian);
1167 try expectWellFormedLimbs(full);
1168 var out: [cap.len]u8 = undefined;
1169 try full.toBytes(&out, endian);
1170 try testing.expectEqualSlices(u8, &cap, &out);
1171 out[if (endian == .big) 0 else out.len - 1] |= 1 << (U.capacity_bits % 8);
1172 try testing.expectError(error.Overflow, U.fromBytes(&out, endian));
1173 var padded = if (endian == .big) [_]u8{0} ++ cap else cap ++ [_]u8{0};
1174 try testing.expect(full.eql(try U.fromBytes(&padded, endian)));
1175 padded[if (endian == .big) 0 else padded.len - 1] = 1;
1176 try testing.expectError(error.Overflow, U.fromBytes(&padded, endian));
1177 }
1178}
1179
1180test "field element decoding" {
1181 if (builtin.zig_backend == .stage2_c) return error.SkipZigTest;
1182
1183 const M = Modulus(256);
1184 const mv: u256 = (1 << 190) + 33;
1185 const m = try M.fromPrimitive(u256, mv);
1186 try testing.expectError(error.NonCanonical, M.Fe.fromPrimitive(u256, m, mv));
1187 try testing.expectError(error.Overflow, M.Fe.fromPrimitive(u256, m, 1 << 255));
1188
1189 inline for (.{ .big, .little }) |endian| {
1190 var buf: [32]u8 = undefined;
1191 try m.toBytes(&buf, endian);
1192 try testing.expectError(error.NonCanonical, M.Fe.fromBytes(m, &buf, endian));
1193 const x = try M.Fe.fromPrimitive(u256, m, mv - 1);
1194 try x.toBytes(&buf, endian);
1195 try testing.expect(x.eql(try M.Fe.fromBytes(m, &buf, endian)));
1196 }
1108}1197}