diff --git a/lib/std/crypto/timing_safe.zig b/lib/std/crypto/timing_safe.zig index 72543340c3..64b37d5b49 100644 --- a/lib/std/crypto/timing_safe.zig +++ b/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)); +}