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};
5555pub const Error = OverflowError || InvalidModulusError || NullExponentError || FieldElementError || RepresentationError;
5656
5757/// 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.
5859/// Unless side-channels mitigations are explicitly disabled, operations are designed to be constant-time.
5960pub fn Uint(comptime max_bits: comptime_int) type {
6061 comptime assert(@bitSizeOf(Limb) % 8 == 0); // Limb size must be a multiple of 8
62 comptime assert(max_bits > 0);
6163
6264 return struct {
6365 const Self = @This();
6466 const max_limbs_count = @divCeil(max_bits, t_bits);
6567
68 pub const capacity_bits = max_limbs_count * t_bits;
69
6670 limbs_buffer: [max_limbs_count]Limb,
6771 /// The number of active limbs.
6872 limbs_len: usize,
......@@ -154,15 +158,15 @@ pub fn Uint(comptime max_bits: comptime_int) type {
154158 var remaining_bits = t_bits;
155159 var limb = self.limbsConst()[i];
156160 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);
158162 const consumed = 8 - shift;
159 limb >>= @as(u4, @truncate(consumed));
163 limb >>= @truncate(consumed);
160164 remaining_bits -= consumed;
161165 shift = 0;
162166 switch (endian) {
163167 .big => {
164168 if (out_i == 0) {
165 if (i != self.limbs_len - 1 or limb != 0) {
169 if (limb | orLimbs(self.limbsConst()[i + 1 ..]) != 0) {
166170 return error.Overflow;
167171 }
168172 return;
......@@ -172,7 +176,7 @@ pub fn Uint(comptime max_bits: comptime_int) type {
172176 .little => {
173177 out_i += 1;
174178 if (out_i == bytes.len) {
175 if (i != self.limbs_len - 1 or limb != 0) {
179 if (limb | orLimbs(self.limbsConst()[i + 1 ..]) != 0) {
176180 return error.Overflow;
177181 }
178182 return;
......@@ -180,48 +184,16 @@ pub fn Uint(comptime max_bits: comptime_int) type {
180184 },
181185 }
182186 }
183 bytes[out_i] |= @as(u8, @truncate(limb));
187 bytes[out_i] |= @truncate(limb);
184188 shift = remaining_bits;
185189 }
186190 }
187191
188192 /// Creates a new big integer from a byte array.
189193 pub fn fromBytes(bytes: []const u8, comptime endian: Endian) OverflowError!Self {
190 if (bytes.len == 0) return Self.zero;
191 var shift: usize = 0;
192194 var out = Self.zero;
193 var out_i: usize = 0;
194 var i: usize = switch (endian) {
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 }
195 if (decodeBytes(out.limbs(), bytes, endian) != 0) {
196 return error.Overflow;
225197 }
226198 return out;
227199 }
......@@ -243,11 +215,7 @@ pub fn Uint(comptime max_bits: comptime_int) type {
243215
244216 /// Returns `true` if the integer is zero.
245217 pub fn isZero(x: Self) bool {
246 var t: Limb = 0;
247 for (x.limbsConst()) |elem| {
248 t |= elem;
249 }
250 return ct.eql(t, 0);
218 return ct.eql(orLimbs(x.limbsConst()), 0);
251219 }
252220
253221 /// Returns `true` if the integer is odd.
......@@ -380,6 +348,47 @@ fn Fe_(comptime bits: comptime_int) type {
380348 };
381349}
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
383392/// A modulus, defining a finite field.
384393/// All operations within the field are performed modulo this modulus, without heap allocations.
385394/// `max_bits` represents the number of bits in the maximum value the modulus can be set to.
......@@ -1090,7 +1099,40 @@ test ct {
10901099 try testCt(ct_unprotected);
10911100}
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" {
10941136 if (builtin.zig_backend == .stage2_c) return error.SkipZigTest;
10951137
10961138 const U = Uint(256);
......@@ -1105,4 +1147,51 @@ test "Uint serialization and predicates" {
11051147 const signed_max = try U.fromPrimitive(u128, math.maxInt(i128));
11061148 try testing.expectEqual(math.maxInt(i128), try signed_max.toPrimitive(i128));
11071149 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 }
11081197}