Expose OS-provided cryptographically secure RNG (#257)

* Expose OS-provided cryptographically secure RNG

* small fixes

* some more csprngs fixes
This commit is contained in:
Mamy Ratsimbazafy 2023-08-27 20:50:09 +02:00 committed by GitHub
parent 8b43b55345
commit ad04e6ea57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 183 additions and 3 deletions

View File

@ -274,9 +274,13 @@ const buildParallel = "test_parallel.txt"
# Basic primitives should stay on to catch compiler regressions.
const testDesc: seq[tuple[path: string, useGMP: bool]] = @[
# CSPRNG
# ----------------------------------------------------------
("tests/t_csprngs.nim", false),
# Hashing vs OpenSSL
# ----------------------------------------------------------
("tests/t_hash_sha256_vs_openssl.nim", true), # skip OpenSSL tests on Windows
("tests/t_hash_sha256_vs_openssl.nim", false), # skip OpenSSL tests on Windows
# Ciphers
# ----------------------------------------------------------

View File

@ -0,0 +1,142 @@
# Constantine
# Copyright (c) 2018-2019 Status Research & Development GmbH
# Copyright (c) 2020-Present Mamy André-Ratsimbazafy
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.
# ############################################################
#
# Operating System provided
# Cryptographically Secure Pseudo-Random Number Generator
#
# ############################################################
# We use Nim effect system to track RNG subroutines
type
CSPRNG = object
when defined(windows):
# There are several Windows CSPRNG APIs:
# - CryptGenRandom
# - RtlGenRandom
# - BCryptGenRandom
#
# CryptGenRandom is Intel CPU only.
# RtlGenRandom is deprecated, in particular it doesn't work for Windows UWP
# (Universal Windows Platform, single source for PC, mobile, Xbox, ...)
# It is the API used by Chromium, Firefox, libsodium, Rust, Go, ...
# BCryptGenRandom is supposedly the recommended API,
# however it has sandbox issues (it tries to read the user config in registry)
# and random crashes when trying to force an algorithm to avoid reading user config.
#
# So we pick RtlGenRandom.
#
# - https://github.com/rust-random/getrandom/issues/65#issuecomment-753634074
# - https://stackoverflow.com/questions/48875929/rtlgenrandom-cryptgenrandom-or-other-winapi-to-generate-cryptographically-secure
# - https://github.com/rust-random/getrandom/issues/314
# - https://learn.microsoft.com/en-us/archive/blogs/michael_howard/cryptographically-secure-random-number-on-windows-without-using-cryptoapi
proc RtlGenRandom(pbuffer: pointer, len: culong): bool {.importc: "SystemFunction036", stdcall, dynlib: "advapi32.dll", sideeffect, tags: [CSPRNG].}
#https://learn.microsoft.com/en-us/archive/blogs/michael_howard/cryptographically-secure-random-number-on-windows-without-using-cryptoapi
# https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-rtlgenrandom
#
# BOOLEAN RtlGenRandom(
# [out] PVOID RandomBuffer,
# [in] ULONG RandomBufferLength
# );
#
# https://learn.microsoft.com/en-US/windows/win32/winprog/windows-data-types
# BOOLEAN (to not be confused with winapi BOOL)
# is `typedef BYTE BOOLEAN;` and so has the same representation as Nim bools.
proc sysrand*[T](buffer: var T): bool {.inline.} =
## Fills the buffer with cryptographically secure random data
return RtlGenRandom(buffer.addr, culong sizeof(T))
elif defined(linux):
proc syscall(sysno: clong): cint {.importc, header:"<unistd.h>", varargs.}
let
SYS_getrandom {.importc, header: "<sys/syscall.h>".}: clong
EAGAIN {.importc, header: "<errno.h>".}: cint
EINTR {.importc, header: "<errno.h>".}: cint
var errno {.importc, header: "<errno.h>".}: cint
# https://man7.org/linux/man-pages/man2/getrandom.2.html
#
# ssize_t getrandom(void buf[.buflen], size_t buflen, unsigned int flags);
#
# For buffer <= 256 bytes, getrandom is uninterruptible
# otherwise it can be interrupted by signals.
# So either we read by chunks of 256 or we handle partial buffer fills after signals interruption
#
# We choose to handle partial buffer fills to limit the number of syscalls
proc urandom(pbuffer: pointer, len: int): bool {.sideeffect, tags: [CSPRNG].} =
var cur = 0
while cur < len:
let bytesRead = syscall(SYS_getrandom, pbuffer, len-cur, 0)
if bytesRead > 0:
cur += bytesRead
elif bytesRead == 0:
# According to documentation this should never happen,
# either we read a positive number of bytes, or we have a negative error code
return false
elif errno == EAGAIN or errno == EINTR:
# No entropy yet or interrupted by signal => retry
discard
else:
# EFAULT The address referred to by buf is outside the accessible address space.
# EINVAL An invalid flag was specified in flags.
return false
return true
proc sysrand*[T](buffer: var T): bool {.inline.} =
## Fills the buffer with cryptographically secure random data
return urandom(buffer.addr, sizeof(T))
elif defined(ios) or defined(macosx):
# There are 4 APIs we can use
# - The getentropy(2) system call (similar to OpenBSD)
# - The random device (/dev/random)
# - SecRandomCopyBytes
# - CCRandomGenerateBytes
#
# SecRandomCopyBytes (https://opensource.apple.com/source/Security/Security-55471/sec/Security/SecFramework.c.auto.html)
# requires linking with the Security framework,
# uses pthread_once (so initializes Grand Central Dispatch)
# and opens /dev/random
# This is heavy https://github.com/rust-random/getrandom/issues/38#issuecomment-505629378
# - It makes linking more complex
# - It incurs a notable startup cost
#
# getentropy is private on IOS and can lead to appstore rejection: https://github.com/openssl/openssl/pull/15924
# the random device can be subject to file descriptor exhaustion
#
# CCRandomGenerateBytes adds a DRBG on top of the raw system RNG, but it's fast
# - https://github.com/dotnet/runtime/pull/51526
# - https://github.com/aws/aws-lc/pull/300
type CCRNGStatus {.importc, header: "<CommonCrypto/CommonRandom.h>".} = distinct int32
let kCCSuccess {.importc, header: "<CommonCrypto/CommonCryptoError.h>".}: CCRNGStatus
# https://opensource.apple.com/source/CommonCrypto/CommonCrypto-60061.30.1/include/CommonCryptoError.h.auto.html
func `==`(x, y: CCRNGStatus): bool {.borrow.}
proc CCRandomGenerateBytes(pbuffer: pointer, len: int): CCRNGStatus {.sideeffect, tags: [CSPRNG], importc, header: "<CommonCrypto/CommonRandom.h>".}
# https://opensource.apple.com/source/CommonCrypto/CommonCrypto-60178.40.2/include/CommonRandom.h.auto.html
proc sysrand*[T](buffer: var T): bool {.inline.} =
## Fills the buffer with cryptographically secure random data
if kCCSuccess == CCRandomGenerateBytes(buffer.addr, sizeof(T)):
return true
return false
else:
{.error: "The OS '" & $hostOS & "' has no CSPRNG configured.".}

View File

@ -23,7 +23,7 @@ const
FUTEX_WAIT_PRIVATE = 128
FUTEX_WAKE_PRIVATE = 129
proc syscall(sysno: clong): cint {.header:"<unistd.h>", varargs.}
proc syscall(sysno: clong): cint {.importc, header:"<unistd.h>", varargs.}
proc sysFutex(
futexAddr: pointer, operation: uint32, expected: uint32 or int32,

View File

@ -39,6 +39,9 @@ import
#
# We use 2^512 to cover the range the base field elements
# We use Nim effect system to track RNG subroutines
type UnsafePRNG* = object
type RngState* = object
## This is the state of a Xoshiro512** PRNG
## Unsafe: for testing and benchmarking purposes only
@ -69,7 +72,7 @@ func rotl(x: uint64, k: static int): uint64 {.inline.} =
template `^=`(x: var uint64, y: uint64) =
x = x xor y
func next*(rng: var RngState): uint64 =
func next*(rng: var RngState): uint64 {.tags: [UnsafePRNG].} =
## Compute a random uint64 from the input state
## using xoshiro512** algorithm by Vigna et al
## State is updated.

31
tests/t_csprngs.nim Normal file
View File

@ -0,0 +1,31 @@
# Constantine
# Copyright (c) 2018-2019 Status Research & Development GmbH
# Copyright (c) 2020-Present Mamy André-Ratsimbazafy
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.
import
std/unittest,
../constantine/csprngs/sysrand
suite "[CSPRNG] sysrand":
test "Non-nil initialization":
# Initializing to full 0 has a chance of 2^-256
proc checkNonNil() =
var buf: array[32, byte] # zero-init
doAssert sysrand(buf)
var nonNil = false
for b in buf:
nonNil = nonNil or (b != byte 0)
doAssert nonNil
checkNonNil()
# TODO:
# - Hamming weight average 50%
# - statistics/hypothesis tests