Commit 84cdb62159

Frank Denis <124872+jedisct1@users.noreply.github.com>
2025-02-20 12:32:37
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 parent c41bc20
Changed files (1)
lib
std
lib/std/crypto/timing_safe.zig
@@ -131,6 +131,54 @@ pub fn sub(comptime T: type, a: []const T, b: []const T, result: []T, endian: En
     return @as(bool, @bitCast(borrow));
 }
 
+fn markSecret(ptr: anytype, comptime action: enum { classify, declassify }) void {
+    const t = @typeInfo(@TypeOf(ptr));
+    if (t != .pointer) @compileError("Pointer expected - Found: " ++ @typeName(@TypeOf(ptr)));
+    const p = t.pointer;
+    if (p.is_allowzero) @compileError("A nullable pointer is always assumed to leak information via side channels");
+    const child = @typeInfo(p.child);
+
+    switch (child) {
+        .void, .null, .comptime_int, .comptime_float => return,
+        .pointer => {
+            if (child.pointer.size == .Slice) {
+                @compileError("Found pointer to pointer. If the intent was to pass a slice, maybe remove the leading & in the function call");
+            }
+            @compileError("A pointer value is always assumed leak information via side channels");
+        },
+        else => {
+            const mem8: *const [@sizeOf(@TypeOf(ptr.*))]u8 = @constCast(@ptrCast(ptr));
+            if (action == .classify) {
+                std.valgrind.memcheck.makeMemUndefined(mem8);
+            } else {
+                std.valgrind.memcheck.makeMemDefined(mem8);
+            }
+        },
+    }
+}
+
+/// Mark a value as sensitive or secret, helping to detect potential side-channel vulnerabilities.
+///
+/// When Valgrind is enabled, this function allows for the detection of conditional jumps or lookups
+/// that depend on secrets or secret-derived data. Violations are reported by Valgrind as operations
+/// relying on uninitialized values.
+///
+/// If Valgrind is disabled, it has no effect.
+///
+/// Use this function to verify that cryptographic operations perform constant-time arithmetic on sensitive data,
+/// ensuring the confidentiality of secrets and preventing information leakage through side channels.
+pub fn classify(ptr: anytype) void {
+    markSecret(ptr, .classify);
+}
+
+/// Mark a value as non-sensitive or public, indicating it's safe from side-channel attacks.
+///
+/// Signals that a value has been securely processed and is no longer confidential, allowing for
+/// relaxed handling without fear of information leakage through conditional jumps or lookups.
+pub fn declassify(ptr: anytype) void {
+    markSecret(ptr, .declassify);
+}
+
 test eql {
     const random = std.crypto.random;
     const expect = std.testing.expect;
@@ -195,3 +243,39 @@ test "add and sub" {
         try expectEqual(borrow, false);
     }
 }
+
+test classify {
+    const random = std.crypto.random;
+    const expect = std.testing.expect;
+
+    var secret: [32]u8 = undefined;
+    random.bytes(&secret);
+
+    // Input of the hash function is marked as secret
+    classify(&secret);
+
+    var out: [32]u8 = undefined;
+    std.crypto.hash.sha3.TurboShake128(null).hash(&secret, &out, .{});
+
+    // Output of the hash function is derived from secret data, so
+    // it will automatically be considered secret as well. But it can be
+    // declassified; the input itself will still be considered secret.
+    declassify(&out);
+
+    // Comparing public data in non-constant time is acceptable.
+    try expect(!std.mem.eql(u8, &out, &[_]u8{0} ** out.len));
+
+    // Comparing secret data must be done in constant time. The result
+    // is going to be considered as secret as well.
+    var res = std.crypto.utils.timingSafeEql([32]u8, out, secret);
+
+    // If we want to make a conditional jump based on a secret,
+    // it has to be declassified.
+    declassify(&res);
+    try expect(!res);
+
+    // Once a secret has been declassified, a comparison in
+    // non-constant time is fine.
+    declassify(&secret);
+    try expect(!std.mem.eql(u8, &out, &secret));
+}