Skip to content

Commit 506af7e

Browse files
authored
Merge pull request #7482 from ziglang/tlcsprng
std: introduce a thread-local CSPRNG for general use
2 parents ce65533 + f416535 commit 506af7e

33 files changed

+922
-656
lines changed

lib/std/c.zig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,11 @@ pub extern "c" fn pthread_attr_setguardsize(attr: *pthread_attr_t, guardsize: us
264264
pub extern "c" fn pthread_attr_destroy(attr: *pthread_attr_t) c_int;
265265
pub extern "c" fn pthread_self() pthread_t;
266266
pub extern "c" fn pthread_join(thread: pthread_t, arg_return: ?*?*c_void) c_int;
267+
pub extern "c" fn pthread_atfork(
268+
prepare: ?fn () callconv(.C) void,
269+
parent: ?fn () callconv(.C) void,
270+
child: ?fn () callconv(.C) void,
271+
) c_int;
267272

268273
pub extern "c" fn kqueue() c_int;
269274
pub extern "c" fn kevent(

lib/std/c/linux.zig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ pub extern "c" fn prlimit(pid: pid_t, resource: rlimit_resource, new_limit: *con
106106
pub extern "c" fn posix_memalign(memptr: *?*c_void, alignment: usize, size: usize) c_int;
107107
pub extern "c" fn malloc_usable_size(?*const c_void) usize;
108108

109+
pub extern "c" fn madvise(
110+
addr: *align(std.mem.page_size) c_void,
111+
length: usize,
112+
advice: c_uint,
113+
) c_int;
114+
109115
pub const pthread_attr_t = extern struct {
110116
__size: [56]u8,
111117
__align: c_long,

lib/std/crypto.zig

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,10 @@ pub const nacl = struct {
134134

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

137+
/// This is a thread-local, cryptographically secure pseudo random number generator.
138+
pub const random = &@import("crypto/tlcsprng.zig").interface;
139+
137140
const std = @import("std.zig");
138-
pub const randomBytes = std.os.getrandom;
139141

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

183+
test "CSPRNG" {
184+
const a = random.int(u64);
185+
const b = random.int(u64);
186+
const c = random.int(u64);
187+
std.testing.expect(a ^ b ^ c != 0);
188+
}
189+
181190
test "issue #4532: no index out of bounds" {
182191
const types = [_]type{
183192
hash.Md5,

lib/std/crypto/25519/ed25519.zig

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ pub const Ed25519 = struct {
4343
pub fn create(seed: ?[seed_length]u8) !KeyPair {
4444
const ss = seed orelse ss: {
4545
var random_seed: [seed_length]u8 = undefined;
46-
try crypto.randomBytes(&random_seed);
46+
crypto.random.bytes(&random_seed);
4747
break :ss random_seed;
4848
};
4949
var az: [Sha512.digest_length]u8 = undefined;
@@ -179,7 +179,7 @@ pub const Ed25519 = struct {
179179

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

@@ -232,8 +232,8 @@ test "ed25519 batch verification" {
232232
const key_pair = try Ed25519.KeyPair.create(null);
233233
var msg1: [32]u8 = undefined;
234234
var msg2: [32]u8 = undefined;
235-
try std.crypto.randomBytes(&msg1);
236-
try std.crypto.randomBytes(&msg2);
235+
std.crypto.random.bytes(&msg1);
236+
std.crypto.random.bytes(&msg2);
237237
const sig1 = try Ed25519.sign(&msg1, key_pair, null);
238238
const sig2 = try Ed25519.sign(&msg2, key_pair, null);
239239
var signature_batch = [_]Ed25519.BatchElement{

lib/std/crypto/25519/edwards25519.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -484,8 +484,8 @@ test "edwards25519 packing/unpacking" {
484484
test "edwards25519 point addition/substraction" {
485485
var s1: [32]u8 = undefined;
486486
var s2: [32]u8 = undefined;
487-
try std.crypto.randomBytes(&s1);
488-
try std.crypto.randomBytes(&s2);
487+
std.crypto.random.bytes(&s1);
488+
std.crypto.random.bytes(&s2);
489489
const p = try Edwards25519.basePoint.clampedMul(s1);
490490
const q = try Edwards25519.basePoint.clampedMul(s2);
491491
const r = p.add(q).add(q).sub(q).sub(q);

lib/std/crypto/25519/x25519.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ pub const X25519 = struct {
3434
pub fn create(seed: ?[seed_length]u8) !KeyPair {
3535
const sk = seed orelse sk: {
3636
var random_seed: [seed_length]u8 = undefined;
37-
try crypto.randomBytes(&random_seed);
37+
crypto.random.bytes(&random_seed);
3838
break :sk random_seed;
3939
};
4040
var kp: KeyPair = undefined;

lib/std/crypto/bcrypt.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ fn strHashInternal(password: []const u8, rounds_log: u6, salt: [salt_length]u8)
262262
/// and then use the resulting hash as the password parameter for bcrypt.
263263
pub fn strHash(password: []const u8, rounds_log: u6) ![hash_length]u8 {
264264
var salt: [salt_length]u8 = undefined;
265-
try crypto.randomBytes(&salt);
265+
crypto.random.bytes(&salt);
266266
return strHashInternal(password, rounds_log, salt);
267267
}
268268

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

284284
test "bcrypt codec" {
285285
var salt: [salt_length]u8 = undefined;
286-
try crypto.randomBytes(&salt);
286+
crypto.random.bytes(&salt);
287287
var salt_str: [salt_str_length]u8 = undefined;
288288
Codec.encode(salt_str[0..], salt[0..]);
289289
var salt2: [salt_length]u8 = undefined;

lib/std/crypto/salsa20.zig

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -571,9 +571,9 @@ test "xsalsa20poly1305" {
571571
var key: [XSalsa20Poly1305.key_length]u8 = undefined;
572572
var nonce: [XSalsa20Poly1305.nonce_length]u8 = undefined;
573573
var tag: [XSalsa20Poly1305.tag_length]u8 = undefined;
574-
try crypto.randomBytes(&msg);
575-
try crypto.randomBytes(&key);
576-
try crypto.randomBytes(&nonce);
574+
crypto.random.bytes(&msg);
575+
crypto.random.bytes(&key);
576+
crypto.random.bytes(&nonce);
577577

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

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

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

616616
var kp = try Box.KeyPair.create(null);
617617
try SealedBox.seal(boxed[0..], msg[0..], kp.public_key);

lib/std/crypto/tlcsprng.zig

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright (c) 2015-2020 Zig Contributors
3+
// This file is part of [zig](https://ziglang.org/), which is MIT licensed.
4+
// The MIT license requires this copyright notice to be included in all copies
5+
// and substantial portions of the software.
6+
7+
//! Thread-local cryptographically secure pseudo-random number generator.
8+
//! This file has public declarations that are intended to be used internally
9+
//! by the standard library; this namespace is not intended to be exposed
10+
//! directly to standard library users.
11+
12+
const std = @import("std");
13+
const root = @import("root");
14+
const mem = std.mem;
15+
16+
/// We use this as a layer of indirection because global const pointers cannot
17+
/// point to thread-local variables.
18+
pub var interface = std.rand.Random{ .fillFn = tlsCsprngFill };
19+
20+
const os_has_fork = switch (std.Target.current.os.tag) {
21+
.dragonfly,
22+
.freebsd,
23+
.ios,
24+
.kfreebsd,
25+
.linux,
26+
.macos,
27+
.netbsd,
28+
.openbsd,
29+
.solaris,
30+
.tvos,
31+
.watchos,
32+
=> true,
33+
34+
else => false,
35+
};
36+
const os_has_arc4random = std.builtin.link_libc and @hasDecl(std.c, "arc4random_buf");
37+
const want_fork_safety = os_has_fork and !os_has_arc4random and
38+
(std.meta.globalOption("crypto_fork_safety", bool) orelse true);
39+
const maybe_have_wipe_on_fork = std.Target.current.os.isAtLeast(.linux, .{
40+
.major = 4,
41+
.minor = 14,
42+
}) orelse true;
43+
44+
const WipeMe = struct {
45+
init_state: enum { uninitialized, initialized, failed },
46+
gimli: std.crypto.core.Gimli,
47+
};
48+
const wipe_align = if (maybe_have_wipe_on_fork) mem.page_size else @alignOf(WipeMe);
49+
50+
threadlocal var wipe_me: WipeMe align(wipe_align) = .{
51+
.gimli = undefined,
52+
.init_state = .uninitialized,
53+
};
54+
55+
fn tlsCsprngFill(_: *const std.rand.Random, buffer: []u8) void {
56+
if (std.builtin.link_libc and @hasDecl(std.c, "arc4random_buf")) {
57+
// arc4random is already a thread-local CSPRNG.
58+
return std.c.arc4random_buf(buffer.ptr, buffer.len);
59+
}
60+
// Allow applications to decide they would prefer to have every call to
61+
// std.crypto.random always make an OS syscall, rather than rely on an
62+
// application implementation of a CSPRNG.
63+
if (comptime std.meta.globalOption("crypto_always_getrandom", bool) orelse false) {
64+
return fillWithOsEntropy(buffer);
65+
}
66+
switch (wipe_me.init_state) {
67+
.uninitialized => {
68+
if (want_fork_safety) {
69+
if (maybe_have_wipe_on_fork) {
70+
if (std.os.madvise(
71+
@ptrCast([*]align(mem.page_size) u8, &wipe_me),
72+
@sizeOf(@TypeOf(wipe_me)),
73+
std.os.MADV_WIPEONFORK,
74+
)) |_| {
75+
return initAndFill(buffer);
76+
} else |_| if (std.Thread.use_pthreads) {
77+
return setupPthreadAtforkAndFill(buffer);
78+
} else {
79+
// Since we failed to set up fork safety, we fall back to always
80+
// calling getrandom every time.
81+
wipe_me.init_state = .failed;
82+
return fillWithOsEntropy(buffer);
83+
}
84+
} else if (std.Thread.use_pthreads) {
85+
return setupPthreadAtforkAndFill(buffer);
86+
} else {
87+
// We have no mechanism to provide fork safety, but we want fork safety,
88+
// so we fall back to calling getrandom every time.
89+
wipe_me.init_state = .failed;
90+
return fillWithOsEntropy(buffer);
91+
}
92+
} else {
93+
return initAndFill(buffer);
94+
}
95+
},
96+
.initialized => {
97+
return fillWithCsprng(buffer);
98+
},
99+
.failed => {
100+
if (want_fork_safety) {
101+
return fillWithOsEntropy(buffer);
102+
} else {
103+
unreachable;
104+
}
105+
},
106+
}
107+
}
108+
109+
fn setupPthreadAtforkAndFill(buffer: []u8) void {
110+
const failed = std.c.pthread_atfork(null, null, childAtForkHandler) != 0;
111+
if (failed) {
112+
wipe_me.init_state = .failed;
113+
return fillWithOsEntropy(buffer);
114+
} else {
115+
return initAndFill(buffer);
116+
}
117+
}
118+
119+
fn childAtForkHandler() callconv(.C) void {
120+
// TODO this is a workaround for https://github.com/ziglang/zig/issues/7495
121+
var wipe_slice: []u8 = undefined;
122+
wipe_slice = @ptrCast([*]u8, &wipe_me)[0..@sizeOf(@TypeOf(wipe_me))];
123+
std.crypto.utils.secureZero(u8, wipe_slice);
124+
}
125+
126+
fn fillWithCsprng(buffer: []u8) void {
127+
if (buffer.len != 0) {
128+
wipe_me.gimli.squeeze(buffer);
129+
} else {
130+
wipe_me.gimli.permute();
131+
}
132+
mem.set(u8, wipe_me.gimli.toSlice()[0..std.crypto.core.Gimli.RATE], 0);
133+
}
134+
135+
fn fillWithOsEntropy(buffer: []u8) void {
136+
std.os.getrandom(buffer) catch @panic("getrandom() failed to provide entropy");
137+
}
138+
139+
fn initAndFill(buffer: []u8) void {
140+
var seed: [std.crypto.core.Gimli.BLOCKBYTES]u8 = undefined;
141+
// Because we panic on getrandom() failing, we provide the opportunity
142+
// to override the default seed function. This also makes
143+
// `std.crypto.random` available on freestanding targets, provided that
144+
// the `cryptoRandomSeed` function is provided.
145+
if (@hasDecl(root, "cryptoRandomSeed")) {
146+
root.cryptoRandomSeed(&seed);
147+
} else {
148+
fillWithOsEntropy(&seed);
149+
}
150+
151+
wipe_me.gimli = std.crypto.core.Gimli.init(seed);
152+
153+
// This is at the end so that accidental recursive dependencies result
154+
// in stack overflows instead of invalid random data.
155+
wipe_me.init_state = .initialized;
156+
157+
return fillWithCsprng(buffer);
158+
}

lib/std/crypto/utils.zig

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ pub fn secureZero(comptime T: type, s: []T) void {
5151
test "crypto.utils.timingSafeEql" {
5252
var a: [100]u8 = undefined;
5353
var b: [100]u8 = undefined;
54-
try std.crypto.randomBytes(a[0..]);
55-
try std.crypto.randomBytes(b[0..]);
54+
std.crypto.random.bytes(a[0..]);
55+
std.crypto.random.bytes(b[0..]);
5656
testing.expect(!timingSafeEql([100]u8, a, b));
5757
mem.copy(u8, a[0..], b[0..]);
5858
testing.expect(timingSafeEql([100]u8, a, b));
@@ -61,8 +61,8 @@ test "crypto.utils.timingSafeEql" {
6161
test "crypto.utils.timingSafeEql (vectors)" {
6262
var a: [100]u8 = undefined;
6363
var b: [100]u8 = undefined;
64-
try std.crypto.randomBytes(a[0..]);
65-
try std.crypto.randomBytes(b[0..]);
64+
std.crypto.random.bytes(a[0..]);
65+
std.crypto.random.bytes(b[0..]);
6666
const v1: std.meta.Vector(100, u8) = a;
6767
const v2: std.meta.Vector(100, u8) = b;
6868
testing.expect(!timingSafeEql(std.meta.Vector(100, u8), v1, v2));

lib/std/fs.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ pub fn atomicSymLink(allocator: *Allocator, existing_path: []const u8, new_path:
8282
mem.copy(u8, tmp_path[0..], dirname);
8383
tmp_path[dirname.len] = path.sep;
8484
while (true) {
85-
try crypto.randomBytes(rand_buf[0..]);
85+
crypto.random.bytes(rand_buf[0..]);
8686
base64_encoder.encode(tmp_path[dirname.len + 1 ..], &rand_buf);
8787

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

159159
while (true) {
160-
try crypto.randomBytes(rand_buf[0..]);
160+
crypto.random.bytes(rand_buf[0..]);
161161
base64_encoder.encode(&tmp_path_buf, &rand_buf);
162162

163163
const file = dir.createFile(

lib/std/meta.zig

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const debug = std.debug;
99
const mem = std.mem;
1010
const math = std.math;
1111
const testing = std.testing;
12+
const root = @import("root");
1213

1314
pub const trait = @import("meta/trait.zig");
1415
pub const TrailerFlags = @import("meta/trailer_flags.zig").TrailerFlags;
@@ -1085,3 +1086,10 @@ test "Tuple" {
10851086
TupleTester.assertTuple(.{ u32, f16 }, Tuple(&[_]type{ u32, f16 }));
10861087
TupleTester.assertTuple(.{ u32, f16, []const u8, void }, Tuple(&[_]type{ u32, f16, []const u8, void }));
10871088
}
1089+
1090+
/// TODO: https://github.com/ziglang/zig/issues/425
1091+
pub fn globalOption(comptime name: []const u8, comptime T: type) ?T {
1092+
if (!@hasDecl(root, name))
1093+
return null;
1094+
return @as(T, @field(root, name));
1095+
}

0 commit comments

Comments
 (0)