Skip to content
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

Expose OS-provided cryptographically secure RNG #257

Merged
merged 3 commits into from
Aug 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion constantine.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ----------------------------------------------------------
Expand Down
142 changes: 142 additions & 0 deletions constantine/csprngs/sysrand.nim
Original file line number Diff line number Diff line change
@@ -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.".}
2 changes: 1 addition & 1 deletion constantine/threadpool/primitives/futexes_linux.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion helpers/prng_unsafe.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
31 changes: 31 additions & 0 deletions tests/t_csprngs.nim
Original file line number Diff line number Diff line change
@@ -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
Loading