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}