Skip to content

std: introduce a thread-local CSPRNG for general use #7482

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Dec 19, 2020
5 changes: 5 additions & 0 deletions lib/std/c.zig
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,11 @@ pub extern "c" fn pthread_attr_setguardsize(attr: *pthread_attr_t, guardsize: us
pub extern "c" fn pthread_attr_destroy(attr: *pthread_attr_t) c_int;
pub extern "c" fn pthread_self() pthread_t;
pub extern "c" fn pthread_join(thread: pthread_t, arg_return: ?*?*c_void) c_int;
pub extern "c" fn pthread_atfork(
prepare: ?fn () callconv(.C) void,
parent: ?fn () callconv(.C) void,
child: ?fn () callconv(.C) void,
) c_int;

pub extern "c" fn kqueue() c_int;
pub extern "c" fn kevent(
Expand Down
6 changes: 6 additions & 0 deletions lib/std/c/linux.zig
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ pub extern "c" fn prlimit(pid: pid_t, resource: rlimit_resource, new_limit: *con
pub extern "c" fn posix_memalign(memptr: *?*c_void, alignment: usize, size: usize) c_int;
pub extern "c" fn malloc_usable_size(?*const c_void) usize;

pub extern "c" fn madvise(
addr: *align(std.mem.page_size) c_void,
length: usize,
advice: c_uint,
) c_int;

pub const pthread_attr_t = extern struct {
__size: [56]u8,
__align: c_long,
Expand Down
11 changes: 10 additions & 1 deletion lib/std/crypto.zig
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,10 @@ pub const nacl = struct {

pub const utils = @import("crypto/utils.zig");

/// This is a thread-local, cryptographically secure pseudo random number generator.
pub const random = &@import("crypto/tlcsprng.zig").interface;

const std = @import("std.zig");
pub const randomBytes = std.os.getrandom;

test "crypto" {
inline for (std.meta.declarations(@This())) |decl| {
Expand Down Expand Up @@ -178,6 +180,13 @@ test "crypto" {
_ = @import("crypto/25519/ristretto255.zig");
}

test "CSPRNG" {
const a = random.int(u64);
const b = random.int(u64);
const c = random.int(u64);
std.testing.expect(a ^ b ^ c != 0);
}

test "issue #4532: no index out of bounds" {
const types = [_]type{
hash.Md5,
Expand Down
8 changes: 4 additions & 4 deletions lib/std/crypto/25519/ed25519.zig
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ pub const Ed25519 = struct {
pub fn create(seed: ?[seed_length]u8) !KeyPair {
const ss = seed orelse ss: {
var random_seed: [seed_length]u8 = undefined;
try crypto.randomBytes(&random_seed);
crypto.random.bytes(&random_seed);
break :ss random_seed;
};
var az: [Sha512.digest_length]u8 = undefined;
Expand Down Expand Up @@ -179,7 +179,7 @@ pub const Ed25519 = struct {

var z_batch: [count]Curve.scalar.CompressedScalar = undefined;
for (z_batch) |*z| {
try std.crypto.randomBytes(z[0..16]);
std.crypto.random.bytes(z[0..16]);
mem.set(u8, z[16..], 0);
}

Expand Down Expand Up @@ -232,8 +232,8 @@ test "ed25519 batch verification" {
const key_pair = try Ed25519.KeyPair.create(null);
var msg1: [32]u8 = undefined;
var msg2: [32]u8 = undefined;
try std.crypto.randomBytes(&msg1);
try std.crypto.randomBytes(&msg2);
std.crypto.random.bytes(&msg1);
std.crypto.random.bytes(&msg2);
const sig1 = try Ed25519.sign(&msg1, key_pair, null);
const sig2 = try Ed25519.sign(&msg2, key_pair, null);
var signature_batch = [_]Ed25519.BatchElement{
Expand Down
4 changes: 2 additions & 2 deletions lib/std/crypto/25519/edwards25519.zig
Original file line number Diff line number Diff line change
Expand Up @@ -484,8 +484,8 @@ test "edwards25519 packing/unpacking" {
test "edwards25519 point addition/substraction" {
var s1: [32]u8 = undefined;
var s2: [32]u8 = undefined;
try std.crypto.randomBytes(&s1);
try std.crypto.randomBytes(&s2);
std.crypto.random.bytes(&s1);
std.crypto.random.bytes(&s2);
const p = try Edwards25519.basePoint.clampedMul(s1);
const q = try Edwards25519.basePoint.clampedMul(s2);
const r = p.add(q).add(q).sub(q).sub(q);
Expand Down
2 changes: 1 addition & 1 deletion lib/std/crypto/25519/x25519.zig
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub const X25519 = struct {
pub fn create(seed: ?[seed_length]u8) !KeyPair {
const sk = seed orelse sk: {
var random_seed: [seed_length]u8 = undefined;
try crypto.randomBytes(&random_seed);
crypto.random.bytes(&random_seed);
break :sk random_seed;
};
var kp: KeyPair = undefined;
Expand Down
4 changes: 2 additions & 2 deletions lib/std/crypto/bcrypt.zig
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ fn strHashInternal(password: []const u8, rounds_log: u6, salt: [salt_length]u8)
/// and then use the resulting hash as the password parameter for bcrypt.
pub fn strHash(password: []const u8, rounds_log: u6) ![hash_length]u8 {
var salt: [salt_length]u8 = undefined;
try crypto.randomBytes(&salt);
crypto.random.bytes(&salt);
return strHashInternal(password, rounds_log, salt);
}

Expand All @@ -283,7 +283,7 @@ pub fn strVerify(h: [hash_length]u8, password: []const u8) BcryptError!void {

test "bcrypt codec" {
var salt: [salt_length]u8 = undefined;
try crypto.randomBytes(&salt);
crypto.random.bytes(&salt);
var salt_str: [salt_str_length]u8 = undefined;
Codec.encode(salt_str[0..], salt[0..]);
var salt2: [salt_length]u8 = undefined;
Expand Down
18 changes: 9 additions & 9 deletions lib/std/crypto/salsa20.zig
Original file line number Diff line number Diff line change
Expand Up @@ -571,9 +571,9 @@ test "xsalsa20poly1305" {
var key: [XSalsa20Poly1305.key_length]u8 = undefined;
var nonce: [XSalsa20Poly1305.nonce_length]u8 = undefined;
var tag: [XSalsa20Poly1305.tag_length]u8 = undefined;
try crypto.randomBytes(&msg);
try crypto.randomBytes(&key);
try crypto.randomBytes(&nonce);
crypto.random.bytes(&msg);
crypto.random.bytes(&key);
crypto.random.bytes(&nonce);

XSalsa20Poly1305.encrypt(c[0..], &tag, msg[0..], "ad", nonce, key);
try XSalsa20Poly1305.decrypt(msg2[0..], c[0..], tag, "ad", nonce, key);
Expand All @@ -585,9 +585,9 @@ test "xsalsa20poly1305 secretbox" {
var key: [XSalsa20Poly1305.key_length]u8 = undefined;
var nonce: [Box.nonce_length]u8 = undefined;
var boxed: [msg.len + Box.tag_length]u8 = undefined;
try crypto.randomBytes(&msg);
try crypto.randomBytes(&key);
try crypto.randomBytes(&nonce);
crypto.random.bytes(&msg);
crypto.random.bytes(&key);
crypto.random.bytes(&nonce);

SecretBox.seal(boxed[0..], msg[0..], nonce, key);
try SecretBox.open(msg2[0..], boxed[0..], nonce, key);
Expand All @@ -598,8 +598,8 @@ test "xsalsa20poly1305 box" {
var msg2: [msg.len]u8 = undefined;
var nonce: [Box.nonce_length]u8 = undefined;
var boxed: [msg.len + Box.tag_length]u8 = undefined;
try crypto.randomBytes(&msg);
try crypto.randomBytes(&nonce);
crypto.random.bytes(&msg);
crypto.random.bytes(&nonce);

var kp1 = try Box.KeyPair.create(null);
var kp2 = try Box.KeyPair.create(null);
Expand All @@ -611,7 +611,7 @@ test "xsalsa20poly1305 sealedbox" {
var msg: [100]u8 = undefined;
var msg2: [msg.len]u8 = undefined;
var boxed: [msg.len + SealedBox.seal_length]u8 = undefined;
try crypto.randomBytes(&msg);
crypto.random.bytes(&msg);

var kp = try Box.KeyPair.create(null);
try SealedBox.seal(boxed[0..], msg[0..], kp.public_key);
Expand Down
158 changes: 158 additions & 0 deletions lib/std/crypto/tlcsprng.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2015-2020 Zig Contributors
// This file is part of [zig](https://ziglang.org/), which is MIT licensed.
// The MIT license requires this copyright notice to be included in all copies
// and substantial portions of the software.

//! Thread-local cryptographically secure pseudo-random number generator.
//! This file has public declarations that are intended to be used internally
//! by the standard library; this namespace is not intended to be exposed
//! directly to standard library users.

const std = @import("std");
const root = @import("root");
const mem = std.mem;

/// We use this as a layer of indirection because global const pointers cannot
/// point to thread-local variables.
pub var interface = std.rand.Random{ .fillFn = tlsCsprngFill };

const os_has_fork = switch (std.Target.current.os.tag) {
.dragonfly,
.freebsd,
.ios,
.kfreebsd,
.linux,
.macos,
.netbsd,
.openbsd,
.solaris,
.tvos,
.watchos,
=> true,

else => false,
};
const os_has_arc4random = std.builtin.link_libc and @hasDecl(std.c, "arc4random_buf");
const want_fork_safety = os_has_fork and !os_has_arc4random and
(std.meta.globalOption("crypto_fork_safety", bool) orelse true);
const maybe_have_wipe_on_fork = std.Target.current.os.isAtLeast(.linux, .{
.major = 4,
.minor = 14,
}) orelse true;

const WipeMe = struct {
init_state: enum { uninitialized, initialized, failed },
gimli: std.crypto.core.Gimli,
};
const wipe_align = if (maybe_have_wipe_on_fork) mem.page_size else @alignOf(WipeMe);

threadlocal var wipe_me: WipeMe align(wipe_align) = .{
.gimli = undefined,
.init_state = .uninitialized,
};

fn tlsCsprngFill(_: *const std.rand.Random, buffer: []u8) void {
if (std.builtin.link_libc and @hasDecl(std.c, "arc4random_buf")) {
// arc4random is already a thread-local CSPRNG.
return std.c.arc4random_buf(buffer.ptr, buffer.len);
}
// Allow applications to decide they would prefer to have every call to
// std.crypto.random always make an OS syscall, rather than rely on an
// application implementation of a CSPRNG.
if (comptime std.meta.globalOption("crypto_always_getrandom", bool) orelse false) {
return fillWithOsEntropy(buffer);
}
switch (wipe_me.init_state) {
.uninitialized => {
if (want_fork_safety) {
if (maybe_have_wipe_on_fork) {
if (std.os.madvise(
@ptrCast([*]align(mem.page_size) u8, &wipe_me),
@sizeOf(@TypeOf(wipe_me)),
std.os.MADV_WIPEONFORK,
)) |_| {
return initAndFill(buffer);
} else |_| if (std.Thread.use_pthreads) {
return setupPthreadAtforkAndFill(buffer);
} else {
// Since we failed to set up fork safety, we fall back to always
// calling getrandom every time.
wipe_me.init_state = .failed;
return fillWithOsEntropy(buffer);
}
} else if (std.Thread.use_pthreads) {
return setupPthreadAtforkAndFill(buffer);
} else {
// We have no mechanism to provide fork safety, but we want fork safety,
// so we fall back to calling getrandom every time.
wipe_me.init_state = .failed;
return fillWithOsEntropy(buffer);
}
} else {
return initAndFill(buffer);
}
},
.initialized => {
return fillWithCsprng(buffer);
},
.failed => {
if (want_fork_safety) {
return fillWithOsEntropy(buffer);
} else {
unreachable;
}
},
}
}

fn setupPthreadAtforkAndFill(buffer: []u8) void {
const failed = std.c.pthread_atfork(null, null, childAtForkHandler) != 0;
if (failed) {
wipe_me.init_state = .failed;
return fillWithOsEntropy(buffer);
} else {
return initAndFill(buffer);
}
}

fn childAtForkHandler() callconv(.C) void {
// TODO this is a workaround for https://github.com/ziglang/zig/issues/7495
var wipe_slice: []u8 = undefined;
wipe_slice = @ptrCast([*]u8, &wipe_me)[0..@sizeOf(@TypeOf(wipe_me))];
std.crypto.utils.secureZero(u8, wipe_slice);
}

fn fillWithCsprng(buffer: []u8) void {
if (buffer.len != 0) {
wipe_me.gimli.squeeze(buffer);
} else {
wipe_me.gimli.permute();
}
mem.set(u8, wipe_me.gimli.toSlice()[0..std.crypto.core.Gimli.RATE], 0);
}

fn fillWithOsEntropy(buffer: []u8) void {
std.os.getrandom(buffer) catch @panic("getrandom() failed to provide entropy");
}

fn initAndFill(buffer: []u8) void {
var seed: [std.crypto.core.Gimli.BLOCKBYTES]u8 = undefined;
// Because we panic on getrandom() failing, we provide the opportunity
// to override the default seed function. This also makes
// `std.crypto.random` available on freestanding targets, provided that
// the `cryptoRandomSeed` function is provided.
if (@hasDecl(root, "cryptoRandomSeed")) {
root.cryptoRandomSeed(&seed);
} else {
fillWithOsEntropy(&seed);
}

wipe_me.gimli = std.crypto.core.Gimli.init(seed);

// This is at the end so that accidental recursive dependencies result
// in stack overflows instead of invalid random data.
wipe_me.init_state = .initialized;

return fillWithCsprng(buffer);
}
8 changes: 4 additions & 4 deletions lib/std/crypto/utils.zig
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ pub fn secureZero(comptime T: type, s: []T) void {
test "crypto.utils.timingSafeEql" {
var a: [100]u8 = undefined;
var b: [100]u8 = undefined;
try std.crypto.randomBytes(a[0..]);
try std.crypto.randomBytes(b[0..]);
std.crypto.random.bytes(a[0..]);
std.crypto.random.bytes(b[0..]);
testing.expect(!timingSafeEql([100]u8, a, b));
mem.copy(u8, a[0..], b[0..]);
testing.expect(timingSafeEql([100]u8, a, b));
Expand All @@ -61,8 +61,8 @@ test "crypto.utils.timingSafeEql" {
test "crypto.utils.timingSafeEql (vectors)" {
var a: [100]u8 = undefined;
var b: [100]u8 = undefined;
try std.crypto.randomBytes(a[0..]);
try std.crypto.randomBytes(b[0..]);
std.crypto.random.bytes(a[0..]);
std.crypto.random.bytes(b[0..]);
const v1: std.meta.Vector(100, u8) = a;
const v2: std.meta.Vector(100, u8) = b;
testing.expect(!timingSafeEql(std.meta.Vector(100, u8), v1, v2));
Expand Down
4 changes: 2 additions & 2 deletions lib/std/fs.zig
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ pub fn atomicSymLink(allocator: *Allocator, existing_path: []const u8, new_path:
mem.copy(u8, tmp_path[0..], dirname);
tmp_path[dirname.len] = path.sep;
while (true) {
try crypto.randomBytes(rand_buf[0..]);
crypto.random.bytes(rand_buf[0..]);
base64_encoder.encode(tmp_path[dirname.len + 1 ..], &rand_buf);

if (cwd().symLink(existing_path, tmp_path, .{})) {
Expand Down Expand Up @@ -157,7 +157,7 @@ pub const AtomicFile = struct {
tmp_path_buf[base64.Base64Encoder.calcSize(RANDOM_BYTES)] = 0;

while (true) {
try crypto.randomBytes(rand_buf[0..]);
crypto.random.bytes(rand_buf[0..]);
base64_encoder.encode(&tmp_path_buf, &rand_buf);

const file = dir.createFile(
Expand Down
8 changes: 8 additions & 0 deletions lib/std/meta.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const debug = std.debug;
const mem = std.mem;
const math = std.math;
const testing = std.testing;
const root = @import("root");

pub const trait = @import("meta/trait.zig");
pub const TrailerFlags = @import("meta/trailer_flags.zig").TrailerFlags;
Expand Down Expand Up @@ -1085,3 +1086,10 @@ test "Tuple" {
TupleTester.assertTuple(.{ u32, f16 }, Tuple(&[_]type{ u32, f16 }));
TupleTester.assertTuple(.{ u32, f16, []const u8, void }, Tuple(&[_]type{ u32, f16, []const u8, void }));
}

/// TODO: https://github.com/ziglang/zig/issues/425
pub fn globalOption(comptime name: []const u8, comptime T: type) ?T {
if (!@hasDecl(root, name))
return null;
return @as(T, @field(root, name));
}
Loading