authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2025-02-20 12:32:37+01:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2025-02-20 12:32:37+01:00
log84cdb621599373b044cef88e6b921f86f2f6fa6c
tree9e427dc49cb6bed46cd9ce35f318a2a782b26ee7
parentc41bc20ec7a3dd113d1eb4349d11082c6fc4242a
signaturebadge-check Signed by PGP key B5690EEEBB952194

std.crypto: add the ability to explicitly tag a value as secret (#19907)

* std.crypto: add the ability to explicitly tag a value as secret It turns out that Valgrind can be a very useful tool to check that secrets are not leaked via side channels involving lookups or conditional jumps. Valgrind tracks uninitialized data, and memcheck reports operations involving uninitialized values. By permanently or temporarily telling Valgrind that a memory region containing secrets is uninitialized, we can detect common side-channel vulnerabilities. For example, the following code snippets would immediately report that the result is not computed in constant time: ```zig classify(&key); const len = std.mem.indexOfScalar(u8, &key, 0); ``` ```zig classify(&key); const idx = key[0]; x += idx; ``` ```zig var x: [4]u8 = undefined; std.crypto.random.bytes(&x); classify(&x); if (std.mem.eql(u8, "test", &x)) return; ``` This is not fool-proof, but it can help a lot to detect unwanted compiler optimizations. Also, right now, this is relying on Valgrind primitives, but these annotations can be used to do more interesting things later, especially with our own code generation backends. * Update for Zig 0.14 * Remove checks for Valgrind enablement

1 files changed, 84 insertions(+), 0 deletions(-)

lib/std/crypto/timing_safe.zig+84
......@@ -131,6 +131,54 @@ pub fn sub(comptime T: type, a: []const T, b: []const T, result: []T, endian: En
131131 return @as(bool, @bitCast(borrow));
132132}
133133
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 = @constCast(@ptrCast(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
134182test eql {
135183 const random = std.crypto.random;
136184 const expect = std.testing.expect;
......@@ -195,3 +243,39 @@ test "add and sub" {
195243 try expectEqual(borrow, false);
196244 }
197245}
246
247test classify {
248 const random = std.crypto.random;
249 const expect = std.testing.expect;
250
251 var secret: [32]u8 = undefined;
252 random.bytes(&secret);
253
254 // Input of the hash function is marked as secret
255 classify(&secret);
256
257 var out: [32]u8 = undefined;
258 std.crypto.hash.sha3.TurboShake128(null).hash(&secret, &out, .{});
259
260 // Output of the hash function is derived from secret data, so
261 // it will automatically be considered secret as well. But it can be
262 // declassified; the input itself will still be considered secret.
263 declassify(&out);
264
265 // Comparing public data in non-constant time is acceptable.
266 try expect(!std.mem.eql(u8, &out, &[_]u8{0} ** out.len));
267
268 // Comparing secret data must be done in constant time. The result
269 // is going to be considered as secret as well.
270 var res = std.crypto.utils.timingSafeEql([32]u8, out, secret);
271
272 // If we want to make a conditional jump based on a secret,
273 // it has to be declassified.
274 declassify(&res);
275 try expect(!res);
276
277 // Once a secret has been declassified, a comparison in
278 // non-constant time is fine.
279 declassify(&secret);
280 try expect(!std.mem.eql(u8, &out, &secret));
281}