git@vger.kernel.org mailing list mirror (one of many)
 help / color / mirror / code / Atom feed
From: "brian m. carlson" <sandals@crustytoothpaste.net>
To: <git@vger.kernel.org>
Subject: [PATCH 1/2] wrapper: add a helper to generate numbers from a CSPRNG
Date: Tue, 16 Nov 2021 03:35:41 +0000	[thread overview]
Message-ID: <20211116033542.3247094-2-sandals@crustytoothpaste.net> (raw)
In-Reply-To: <20211116033542.3247094-1-sandals@crustytoothpaste.net>

There are many situations in which having access to a cryptographically
secure pseudorandom number generator (CSPRNG) is helpful.  In the
future, we'll encounter one of these when dealing with temporary files.
To make this possible, let's add a function which reads from a system
CSPRNG and returns some bytes.

Because this is a security sensitive interface, we take some
precautions.  We either succeed by filling the buffer completely as we
requested, or we fail.  We don't return partial data because the caller
will almost never find that to be a useful behavior.

The order of options is also important here.  On systems with
arc4random, which is most of the BSDs, we use that, since, except on
MirBSD, it uses ChaCha20, which is extremely fast, and sits entirely in
userspace, avoiding a system call.  We then prefer getrandom over
getentropy, because the former has been available longer on Linux, and
finally, if none of those are available, we use /dev/urandom, because
most Unix-like operating systems provide that API.  We prefer options
that don't involve device files when possible because those work in some
restricted environments where device files may not be available.

macOS appears to have arc4random but not the arc4random_buf function we
want to use, so we let it use the fallback of /dev/urandom.  Set the
configuration variables appropriately for Linux and the other BSDs.  We
specifically only consider versions which receive publicly available
security support; for example, getrandom(2) and getentropy(3) are only
available in FreeBSD 12, which is the oldest version with current
security support.  For the same reason, we don't specify getrandom(2) on
Linux, because CentOS 7 doesn't support it in glibc (although its kernel
does) and we don't want to resort to making syscalls.

Finally, add a self-test option here to make sure that our buffer
handling is correct and we aren't truncating data.  We simply read 64
KiB and then make sure we've seen each byte.  The probability of this
test failing spuriously is less than 10^-100.

Signed-off-by: brian m. carlson <sandals@crustytoothpaste.net>
---
 Makefile                            | 25 ++++++++++++
 compat/winansi.c                    |  6 +++
 config.mak.uname                    |  9 +++++
 contrib/buildsystems/CMakeLists.txt |  2 +-
 git-compat-util.h                   | 16 ++++++++
 t/helper/test-csprng.c              | 63 +++++++++++++++++++++++++++++
 t/helper/test-tool.c                |  1 +
 t/helper/test-tool.h                |  1 +
 t/t0000-basic.sh                    |  4 ++
 wrapper.c                           | 56 +++++++++++++++++++++++++
 10 files changed, 182 insertions(+), 1 deletion(-)
 create mode 100644 t/helper/test-csprng.c

diff --git a/Makefile b/Makefile
index 12be39ac49..1d17021f59 100644
--- a/Makefile
+++ b/Makefile
@@ -234,6 +234,14 @@ all::
 # Define NO_TRUSTABLE_FILEMODE if your filesystem may claim to support
 # the executable mode bit, but doesn't really do so.
 #
+# Define HAVE_ARC4RANDOM if your system has arc4random and arc4random_buf.
+#
+# Define HAVE_GETRANDOM if your system has getrandom.
+#
+# Define HAVE_GETENTROPY if your system has getentropy.
+#
+# Define HAVE_RTLGENRANDOM if your system has RtlGenRandom (Windows only).
+#
 # Define NEEDS_MODE_TRANSLATION if your OS strays from the typical file type
 # bits in mode values (e.g. z/OS defines I_SFMT to 0xFF000000 as opposed to the
 # usual 0xF000).
@@ -694,6 +702,7 @@ TEST_BUILTINS_OBJS += test-bloom.o
 TEST_BUILTINS_OBJS += test-chmtime.o
 TEST_BUILTINS_OBJS += test-config.o
 TEST_BUILTINS_OBJS += test-crontab.o
+TEST_BUILTINS_OBJS += test-csprng.o
 TEST_BUILTINS_OBJS += test-ctype.o
 TEST_BUILTINS_OBJS += test-date.o
 TEST_BUILTINS_OBJS += test-delta.o
@@ -1900,6 +1909,22 @@ ifdef HAVE_GETDELIM
 	BASIC_CFLAGS += -DHAVE_GETDELIM
 endif
 
+ifdef HAVE_ARC4RANDOM
+	BASIC_CFLAGS += -DHAVE_ARC4RANDOM
+endif
+
+ifdef HAVE_GETRANDOM
+	BASIC_CFLAGS += -DHAVE_GETRANDOM
+endif
+
+ifdef HAVE_GETENTROPY
+	BASIC_CFLAGS += -DHAVE_GETENTROPY
+endif
+
+ifdef HAVE_RTLGENRANDOM
+	BASIC_CFLAGS += -DHAVE_RTLGENRANDOM
+endif
+
 ifneq ($(PROCFS_EXECUTABLE_PATH),)
 	procfs_executable_path_SQ = $(subst ','\'',$(PROCFS_EXECUTABLE_PATH))
 	BASIC_CFLAGS += '-DPROCFS_EXECUTABLE_PATH="$(procfs_executable_path_SQ)"'
diff --git a/compat/winansi.c b/compat/winansi.c
index c27b20a79d..0e5a9cc82e 100644
--- a/compat/winansi.c
+++ b/compat/winansi.c
@@ -3,6 +3,12 @@
  */
 
 #undef NOGDI
+
+/*
+ * Including the appropriate header file for RtlGenRandom causes MSVC to see a
+ * redefinition of types in an incompatible way when including headers below.
+ */
+#undef HAVE_RTLGENRANDOM
 #include "../git-compat-util.h"
 #include <wingdi.h>
 #include <winreg.h>
diff --git a/config.mak.uname b/config.mak.uname
index 3236a4918a..5030d3c70b 100644
--- a/config.mak.uname
+++ b/config.mak.uname
@@ -257,6 +257,9 @@ ifeq ($(uname_S),FreeBSD)
 	HAVE_PATHS_H = YesPlease
 	HAVE_BSD_SYSCTL = YesPlease
 	HAVE_BSD_KERN_PROC_SYSCTL = YesPlease
+	HAVE_ARC4RANDOM = YesPlease
+	HAVE_GETRANDOM = YesPlease
+	HAVE_GETENTROPY = YesPlease
 	PAGER_ENV = LESS=FRX LV=-c MORE=FRX
 	FREAD_READS_DIRECTORIES = UnfortunatelyYes
 	FILENO_IS_A_MACRO = UnfortunatelyYes
@@ -271,6 +274,8 @@ ifeq ($(uname_S),OpenBSD)
 	HAVE_PATHS_H = YesPlease
 	HAVE_BSD_SYSCTL = YesPlease
 	HAVE_BSD_KERN_PROC_SYSCTL = YesPlease
+	HAVE_ARC4RANDOM = YesPlease
+	HAVE_GETENTROPY = YesPlease
 	PROCFS_EXECUTABLE_PATH = /proc/curproc/file
 	FREAD_READS_DIRECTORIES = UnfortunatelyYes
 	FILENO_IS_A_MACRO = UnfortunatelyYes
@@ -282,6 +287,7 @@ ifeq ($(uname_S),MirBSD)
 	NEEDS_LIBICONV = YesPlease
 	HAVE_PATHS_H = YesPlease
 	HAVE_BSD_SYSCTL = YesPlease
+	HAVE_ARC4RANDOM = YesPlease
 endif
 ifeq ($(uname_S),NetBSD)
 	ifeq ($(shell expr "$(uname_R)" : '[01]\.'),2)
@@ -293,6 +299,7 @@ ifeq ($(uname_S),NetBSD)
 	HAVE_PATHS_H = YesPlease
 	HAVE_BSD_SYSCTL = YesPlease
 	HAVE_BSD_KERN_PROC_SYSCTL = YesPlease
+	HAVE_ARC4RANDOM = YesPlease
 	PROCFS_EXECUTABLE_PATH = /proc/curproc/exe
 endif
 ifeq ($(uname_S),AIX)
@@ -422,6 +429,7 @@ ifeq ($(uname_S),Windows)
 	NO_STRTOUMAX = YesPlease
 	NO_MKDTEMP = YesPlease
 	NO_INTTYPES_H = YesPlease
+	HAVE_RTLGENRANDOM = YesPlease
 	# VS2015 with UCRT claims that snprintf and friends are C99 compliant,
 	# so we don't need this:
 	#
@@ -624,6 +632,7 @@ ifeq ($(uname_S),MINGW)
 	NO_POSIX_GOODIES = UnfortunatelyYes
 	DEFAULT_HELP_FORMAT = html
 	HAVE_PLATFORM_PROCINFO = YesPlease
+	HAVE_RTLGENRANDOM = YesPlease
 	BASIC_LDFLAGS += -municode
 	COMPAT_CFLAGS += -DNOGDI -Icompat -Icompat/win32
 	COMPAT_CFLAGS += -DSTRIP_EXTENSION=\".exe\"
diff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt
index fd1399c440..134e00bde3 100644
--- a/contrib/buildsystems/CMakeLists.txt
+++ b/contrib/buildsystems/CMakeLists.txt
@@ -260,7 +260,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
 				_CONSOLE DETECT_MSYS_TTY STRIP_EXTENSION=".exe"  NO_SYMLINK_HEAD UNRELIABLE_FSTAT
 				NOGDI OBJECT_CREATION_MODE=1 __USE_MINGW_ANSI_STDIO=0
 				USE_NED_ALLOCATOR OVERRIDE_STRDUP MMAP_PREVENTS_DELETE USE_WIN32_MMAP
-				UNICODE _UNICODE HAVE_WPGMPTR ENSURE_MSYSTEM_IS_SET)
+				UNICODE _UNICODE HAVE_WPGMPTR ENSURE_MSYSTEM_IS_SET HAVE_RTLGENRANDOM)
 	list(APPEND compat_SOURCES compat/mingw.c compat/winansi.c compat/win32/path-utils.c
 		compat/win32/pthread.c compat/win32mmap.c compat/win32/syslog.c
 		compat/win32/trace2_win32_process_info.c compat/win32/dirent.c
diff --git a/git-compat-util.h b/git-compat-util.h
index d70ce14286..f2cff656e7 100644
--- a/git-compat-util.h
+++ b/git-compat-util.h
@@ -165,6 +165,12 @@
 #endif
 #include <windows.h>
 #define GIT_WINDOWS_NATIVE
+#ifdef HAVE_RTLGENRANDOM
+/* This is required to get access to RtlGenRandom. */
+#define SystemFunction036 NTAPI SystemFunction036
+#include <NTSecAPI.h>
+#undef SystemFunction036
+#endif
 #endif
 
 #include <unistd.h>
@@ -235,6 +241,9 @@
 #else
 #include <stdint.h>
 #endif
+#ifdef HAVE_GETRANDOM
+#include <sys/random.h>
+#endif
 #ifdef NO_INTPTR_T
 /*
  * On I16LP32, ILP32 and LP64 "long" is the safe bet, however
@@ -1381,4 +1390,11 @@ static inline void *container_of_or_null_offset(void *ptr, size_t offset)
 
 void sleep_millisec(int millisec);
 
+/*
+ * Generate len bytes from the system cryptographically secure PRNG.
+ * Returns 0 on success and -1 on error, setting errno.  The inability to
+ * satisfy the full request is an error.
+ */
+int csprng_bytes(void *buf, size_t len);
+
 #endif
diff --git a/t/helper/test-csprng.c b/t/helper/test-csprng.c
new file mode 100644
index 0000000000..196c14e44f
--- /dev/null
+++ b/t/helper/test-csprng.c
@@ -0,0 +1,63 @@
+#include "test-tool.h"
+#include "git-compat-util.h"
+
+/*
+ * Check that we read each byte value at least once when reading 64 KiB from the
+ * CSPRNG.  This is not to test the quality of the CSPRNG, but to test our
+ * buffer handling of it.
+ *
+ * The probability of this failing by random is less than 10^-100.
+ */
+static int selftest(void)
+{
+	int buckets[256] = { 0 };
+	unsigned char buf[1024];
+	unsigned long count = 64 * 1024;
+	int i;
+
+	while (count) {
+		if (csprng_bytes(buf, sizeof(buf)) < 0) {
+			perror("failed to read");
+			return 3;
+		}
+		for (i = 0; i < sizeof(buf); i++)
+			buckets[buf[i]]++;
+		count -= sizeof(buf);
+	}
+	for (i = 0; i < ARRAY_SIZE(buckets); i++)
+		if (!buckets[i]) {
+			fprintf(stderr, "failed to find any bytes with value %02x\n", i);
+			return 4;
+		}
+	return 0;
+}
+
+int cmd__csprng(int argc, const char **argv)
+{
+	unsigned long count;
+	unsigned char buf[1024];
+
+	if (argc > 2) {
+		fprintf(stderr, "usage: %s [--selftest | <size>]\n", argv[0]);
+		return 2;
+	}
+
+	if (!strcmp(argv[1], "--selftest")) {
+		return selftest();
+	}
+
+	count = (argc == 2) ? strtoul(argv[1], NULL, 0) : -1L;
+
+	while (count) {
+		unsigned long chunk = count < sizeof(buf) ? count : sizeof(buf);
+		if (csprng_bytes(buf, chunk) < 0) {
+			perror("failed to read");
+			return 5;
+		}
+		if (fwrite(buf, chunk, 1, stdout) != chunk)
+			return 1;
+		count -= chunk;
+	}
+
+	return 0;
+}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 3ce5585e53..fc0fb86c1b 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -20,6 +20,7 @@ static struct test_cmd cmds[] = {
 	{ "chmtime", cmd__chmtime },
 	{ "config", cmd__config },
 	{ "crontab", cmd__crontab },
+	{ "csprng", cmd__csprng },
 	{ "ctype", cmd__ctype },
 	{ "date", cmd__date },
 	{ "delta", cmd__delta },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index 9f0f522850..077d9bfcca 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -10,6 +10,7 @@ int cmd__bloom(int argc, const char **argv);
 int cmd__chmtime(int argc, const char **argv);
 int cmd__config(int argc, const char **argv);
 int cmd__crontab(int argc, const char **argv);
+int cmd__csprng(int argc, const char **argv);
 int cmd__ctype(int argc, const char **argv);
 int cmd__date(int argc, const char **argv);
 int cmd__delta(int argc, const char **argv);
diff --git a/t/t0000-basic.sh b/t/t0000-basic.sh
index b007f0efef..9647ec9629 100755
--- a/t/t0000-basic.sh
+++ b/t/t0000-basic.sh
@@ -1131,4 +1131,8 @@ test_expect_success 'test_must_fail rejects a non-git command with env' '
 	grep -F "test_must_fail: only '"'"'git'"'"' is allowed" err
 '
 
+test_expect_success 'CSPRNG handling functions correctly' '
+	test-tool csprng --selftest
+'
+
 test_done
diff --git a/wrapper.c b/wrapper.c
index 36e12119d7..0046f32e46 100644
--- a/wrapper.c
+++ b/wrapper.c
@@ -702,3 +702,59 @@ int open_nofollow(const char *path, int flags)
 	return open(path, flags);
 #endif
 }
+
+int csprng_bytes(void *buf, size_t len)
+{
+#if defined(HAVE_ARC4RANDOM)
+	arc4random_buf(buf, len);
+	return 0;
+#elif defined(HAVE_GETRANDOM)
+	ssize_t res;
+	char *p = buf;
+	while (len) {
+		res = getrandom(p, len, 0);
+		if (res < 0)
+			return -1;
+		len -= res;
+		p += res;
+	}
+	return 0;
+#elif defined(HAVE_GETENTROPY)
+	int res;
+	char *p = buf;
+	while (len) {
+		/* getentropy has a maximum size of 256 bytes. */
+		size_t chunk = len < 256 ? len : 256;
+		res = getentropy(p, chunk);
+		if (res < 0)
+			return -1;
+		len -= chunk;
+		p += chunk;
+	}
+	return 0;
+#elif defined(HAVE_RTLGENRANDOM)
+	if (!RtlGenRandom(buf, len))
+		return -1;
+	return 0;
+#else
+	ssize_t res;
+	char *p = buf;
+	int fd, err;
+	fd = open("/dev/urandom", O_RDONLY);
+	if (fd < 0)
+		return -1;
+	while (len) {
+		res = xread(fd, p, len);
+		if (res < 0) {
+			err = errno;
+			close(fd);
+			errno = err;
+			return -1;
+		}
+		len -= res;
+		p += res;
+	}
+	close(fd);
+	return 0;
+#endif
+}

  reply	other threads:[~2021-11-16  3:36 UTC|newest]

Thread overview: 37+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-11-16  3:35 [PATCH 0/2] Generate temporary files using a CSPRNG brian m. carlson
2021-11-16  3:35 ` brian m. carlson [this message]
2021-11-16 15:31   ` [PATCH 1/2] wrapper: add a helper to generate numbers from " Jeff King
2021-11-16 16:01     ` rsbecker
2021-11-16 18:22       ` Taylor Blau
2021-11-16 19:58         ` rsbecker
2021-11-16 22:41       ` brian m. carlson
2021-11-16 23:20         ` rsbecker
2021-11-17  0:47           ` Carlo Arenas
2021-11-17  3:05             ` rsbecker
2021-11-17  1:03           ` brian m. carlson
2021-11-17  1:50             ` Carlo Arenas
2021-11-17  3:04               ` Jeff King
2021-11-17  3:12                 ` rsbecker
2021-11-17  3:36                 ` Carlo Arenas
2021-11-17 20:01                   ` Jeff King
2021-11-17 20:19                     ` rsbecker
2021-11-17 23:30                       ` brian m. carlson
2021-11-17 23:34                         ` rsbecker
2021-11-17  3:03             ` rsbecker
2021-11-17  7:39   ` Junio C Hamano
2021-11-17 23:01     ` brian m. carlson
2021-11-18  7:19       ` Junio C Hamano
2021-11-18 22:16         ` brian m. carlson
2021-11-22  9:10           ` Junio C Hamano
2021-11-16  3:35 ` [PATCH 2/2] wrapper: use a CSPRNG to generate random file names brian m. carlson
2021-11-16 15:36   ` Jeff King
2021-11-16 18:28     ` Taylor Blau
2021-11-16 18:57       ` Junio C Hamano
2021-11-16 19:21         ` Jeff King
2021-11-16 19:33           ` Taylor Blau
2021-11-16 15:44 ` [PATCH 0/2] Generate temporary files using a CSPRNG Jeff King
2021-11-16 22:17   ` brian m. carlson
2021-11-16 22:29     ` rsbecker
2021-11-16 20:35 ` Ævar Arnfjörð Bjarmason
2021-11-16 21:06   ` Jeff King
2021-11-17  8:36     ` Ævar Arnfjörð Bjarmason

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: http://vger.kernel.org/majordomo-info.html

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20211116033542.3247094-2-sandals@crustytoothpaste.net \
    --to=sandals@crustytoothpaste.net \
    --cc=git@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://80x24.org/mirrors/git.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).