master
1//! Please see this accepted proposal for the long-term plans regarding
2//! constant-time operations in Zig: https://github.com/ziglang/zig/issues/1776
3
4const std = @import("../std.zig");
5const assert = std.debug.assert;
6const Endian = std.builtin.Endian;
7const Order = std.math.Order;
8
9/// Compares two arrays in constant time (for a given length) and returns whether they are equal.
10/// This function was designed to compare short cryptographic secrets (MACs, signatures).
11/// For all other applications, use mem.eql() instead.
12pub fn eql(comptime T: type, a: T, b: T) bool {
13 switch (@typeInfo(T)) {
14 .array => |info| {
15 const C = info.child;
16 if (@typeInfo(C) != .int) {
17 @compileError("Elements to be compared must be integers");
18 }
19 var acc = @as(C, 0);
20 for (a, 0..) |x, i| {
21 acc |= x ^ b[i];
22 }
23 const s = @typeInfo(C).int.bits;
24 const Cu = std.meta.Int(.unsigned, s);
25 const Cext = std.meta.Int(.unsigned, s + 1);
26 return @as(bool, @bitCast(@as(u1, @truncate((@as(Cext, @as(Cu, @bitCast(acc))) -% 1) >> s))));
27 },
28 .vector => |info| {
29 const C = info.child;
30 if (@typeInfo(C) != .int) {
31 @compileError("Elements to be compared must be integers");
32 }
33 const acc = @reduce(.Or, a ^ b);
34 const s = @typeInfo(C).int.bits;
35 const Cu = std.meta.Int(.unsigned, s);
36 const Cext = std.meta.Int(.unsigned, s + 1);
37 return @as(bool, @bitCast(@as(u1, @truncate((@as(Cext, @as(Cu, @bitCast(acc))) -% 1) >> s))));
38 },
39 else => {
40 @compileError("Only arrays and vectors can be compared");
41 },
42 }
43}
44
45/// Compare two integers serialized as arrays of the same size, in constant time.
46/// Returns .lt if a<b, .gt if a>b and .eq if a=b
47pub fn compare(comptime T: type, a: []const T, b: []const T, endian: Endian) Order {
48 assert(a.len == b.len);
49 const bits = switch (@typeInfo(T)) {
50 .int => |cinfo| if (cinfo.signedness != .unsigned) @compileError("Elements to be compared must be unsigned") else cinfo.bits,
51 else => @compileError("Elements to be compared must be integers"),
52 };
53 const Cext = std.meta.Int(.unsigned, bits + 1);
54 var gt: T = 0;
55 var eq: T = 1;
56 if (endian == .little) {
57 var i = a.len;
58 while (i != 0) {
59 i -= 1;
60 const x1 = a[i];
61 const x2 = b[i];
62 gt |= @as(T, @truncate((@as(Cext, x2) -% @as(Cext, x1)) >> bits)) & eq;
63 eq &= @as(T, @truncate((@as(Cext, (x2 ^ x1)) -% 1) >> bits));
64 }
65 } else {
66 for (a, 0..) |x1, i| {
67 const x2 = b[i];
68 gt |= @as(T, @truncate((@as(Cext, x2) -% @as(Cext, x1)) >> bits)) & eq;
69 eq &= @as(T, @truncate((@as(Cext, (x2 ^ x1)) -% 1) >> bits));
70 }
71 }
72 if (gt != 0) {
73 return Order.gt;
74 } else if (eq != 0) {
75 return Order.eq;
76 }
77 return Order.lt;
78}
79
80/// Add two integers serialized as arrays of the same size, in constant time.
81/// The result is stored into `result`, and `true` is returned if an overflow occurred.
82pub fn add(comptime T: type, a: []const T, b: []const T, result: []T, endian: Endian) bool {
83 const len = a.len;
84 assert(len == b.len and len == result.len);
85 var carry: u1 = 0;
86 if (endian == .little) {
87 var i: usize = 0;
88 while (i < len) : (i += 1) {
89 const ov1 = @addWithOverflow(a[i], b[i]);
90 const ov2 = @addWithOverflow(ov1[0], carry);
91 result[i] = ov2[0];
92 carry = ov1[1] | ov2[1];
93 }
94 } else {
95 var i: usize = len;
96 while (i != 0) {
97 i -= 1;
98 const ov1 = @addWithOverflow(a[i], b[i]);
99 const ov2 = @addWithOverflow(ov1[0], carry);
100 result[i] = ov2[0];
101 carry = ov1[1] | ov2[1];
102 }
103 }
104 return @as(bool, @bitCast(carry));
105}
106
107/// Subtract two integers serialized as arrays of the same size, in constant time.
108/// The result is stored into `result`, and `true` is returned if an underflow occurred.
109pub fn sub(comptime T: type, a: []const T, b: []const T, result: []T, endian: Endian) bool {
110 const len = a.len;
111 assert(len == b.len and len == result.len);
112 var borrow: u1 = 0;
113 if (endian == .little) {
114 var i: usize = 0;
115 while (i < len) : (i += 1) {
116 const ov1 = @subWithOverflow(a[i], b[i]);
117 const ov2 = @subWithOverflow(ov1[0], borrow);
118 result[i] = ov2[0];
119 borrow = ov1[1] | ov2[1];
120 }
121 } else {
122 var i: usize = len;
123 while (i != 0) {
124 i -= 1;
125 const ov1 = @subWithOverflow(a[i], b[i]);
126 const ov2 = @subWithOverflow(ov1[0], borrow);
127 result[i] = ov2[0];
128 borrow = ov1[1] | ov2[1];
129 }
130 }
131 return @as(bool, @bitCast(borrow));
132}
133
134fn markSecret(ptr: anytype, comptime action: enum { classify, declassify }) void {
135 const t = @typeInfo(@TypeOf(ptr));
136 if (t != .pointer) @compileError("Pointer expected - Found: " ++ @typeName(@TypeOf(ptr)));
137 const p = t.pointer;
138 if (p.is_allowzero) @compileError("A nullable pointer is always assumed to leak information via side channels");
139 const child = @typeInfo(p.child);
140
141 switch (child) {
142 .void, .null, .comptime_int, .comptime_float => return,
143 .pointer => {
144 if (child.pointer.size == .Slice) {
145 @compileError("Found pointer to pointer. If the intent was to pass a slice, maybe remove the leading & in the function call");
146 }
147 @compileError("A pointer value is always assumed leak information via side channels");
148 },
149 else => {
150 const mem8: *const [@sizeOf(@TypeOf(ptr.*))]u8 = @ptrCast(@constCast(ptr));
151 if (action == .classify) {
152 std.valgrind.memcheck.makeMemUndefined(mem8);
153 } else {
154 std.valgrind.memcheck.makeMemDefined(mem8);
155 }
156 },
157 }
158}
159
160/// Mark a value as sensitive or secret, helping to detect potential side-channel vulnerabilities.
161///
162/// When Valgrind is enabled, this function allows for the detection of conditional jumps or lookups
163/// that depend on secrets or secret-derived data. Violations are reported by Valgrind as operations
164/// relying on uninitialized values.
165///
166/// If Valgrind is disabled, it has no effect.
167///
168/// Use this function to verify that cryptographic operations perform constant-time arithmetic on sensitive data,
169/// ensuring the confidentiality of secrets and preventing information leakage through side channels.
170pub fn classify(ptr: anytype) void {
171 markSecret(ptr, .classify);
172}
173
174/// Mark a value as non-sensitive or public, indicating it's safe from side-channel attacks.
175///
176/// Signals that a value has been securely processed and is no longer confidential, allowing for
177/// relaxed handling without fear of information leakage through conditional jumps or lookups.
178pub fn declassify(ptr: anytype) void {
179 markSecret(ptr, .declassify);
180}
181
182test eql {
183 const random = std.crypto.random;
184 const expect = std.testing.expect;
185 var a: [100]u8 = undefined;
186 var b: [100]u8 = undefined;
187 random.bytes(a[0..]);
188 random.bytes(b[0..]);
189 try expect(!eql([100]u8, a, b));
190 a = b;
191 try expect(eql([100]u8, a, b));
192}
193
194test "eql (vectors)" {
195 const random = std.crypto.random;
196 const expect = std.testing.expect;
197 var a: [100]u8 = undefined;
198 var b: [100]u8 = undefined;
199 random.bytes(a[0..]);
200 random.bytes(b[0..]);
201 const v1: @Vector(100, u8) = a;
202 const v2: @Vector(100, u8) = b;
203 try expect(!eql(@Vector(100, u8), v1, v2));
204 const v3: @Vector(100, u8) = a;
205 try expect(eql(@Vector(100, u8), v1, v3));
206}
207
208test compare {
209 const expectEqual = std.testing.expectEqual;
210 var a = [_]u8{10} ** 32;
211 var b = [_]u8{10} ** 32;
212 try expectEqual(compare(u8, &a, &b, .big), .eq);
213 try expectEqual(compare(u8, &a, &b, .little), .eq);
214 a[31] = 1;
215 try expectEqual(compare(u8, &a, &b, .big), .lt);
216 try expectEqual(compare(u8, &a, &b, .little), .lt);
217 a[0] = 20;
218 try expectEqual(compare(u8, &a, &b, .big), .gt);
219 try expectEqual(compare(u8, &a, &b, .little), .lt);
220}
221
222test "add and sub" {
223 const expectEqual = std.testing.expectEqual;
224 const expectEqualSlices = std.testing.expectEqualSlices;
225 const random = std.crypto.random;
226 const len = 32;
227 var a: [len]u8 = undefined;
228 var b: [len]u8 = undefined;
229 var c: [len]u8 = undefined;
230 const zero = [_]u8{0} ** len;
231 var iterations: usize = 100;
232 while (iterations != 0) : (iterations -= 1) {
233 random.bytes(&a);
234 random.bytes(&b);
235 const endian = if (iterations % 2 == 0) Endian.big else Endian.little;
236 _ = sub(u8, &a, &b, &c, endian); // a-b
237 _ = add(u8, &c, &b, &c, endian); // (a-b)+b
238 try expectEqualSlices(u8, &c, &a);
239 const borrow = sub(u8, &c, &a, &c, endian); // ((a-b)+b)-a
240 try expectEqualSlices(u8, &c, &zero);
241 try expectEqual(borrow, false);
242 }
243}
244
245test classify {
246 const random = std.crypto.random;
247 const expect = std.testing.expect;
248
249 var secret: [32]u8 = undefined;
250 random.bytes(&secret);
251
252 // Input of the hash function is marked as secret
253 classify(&secret);
254
255 var out: [32]u8 = undefined;
256 std.crypto.hash.sha3.TurboShake128(null).hash(&secret, &out, .{});
257
258 // Output of the hash function is derived from secret data, so
259 // it will automatically be considered secret as well. But it can be
260 // declassified; the input itself will still be considered secret.
261 declassify(&out);
262
263 // Comparing public data in non-constant time is acceptable.
264 try expect(!std.mem.eql(u8, &out, &[_]u8{0} ** out.len));
265
266 // Comparing secret data must be done in constant time. The result
267 // is going to be considered as secret as well.
268 var res = std.crypto.timing_safe.eql([32]u8, out, secret);
269
270 // If we want to make a conditional jump based on a secret,
271 // it has to be declassified.
272 declassify(&res);
273 try expect(!res);
274
275 // Once a secret has been declassified, a comparison in
276 // non-constant time is fine.
277 declassify(&secret);
278 try expect(!std.mem.eql(u8, &out, &secret));
279}