Skip to content

Commit

Permalink
Expose OS-provided cryptographically secure RNG
Browse files Browse the repository at this point in the history
  • Loading branch information
mratsim committed Aug 26, 2023
1 parent f57d071 commit 435fac1
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 3 deletions.
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
139 changes: 139 additions & 0 deletions constantine/csprngs/sysrand.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# 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, stdcall, dynlib: "advapi32.dll", sideeffect, tags: [CSPRNG].}
# 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, sizeof(T))

Check failure on line 55 in constantine/csprngs/sysrand.nim

View workflow job for this annotation

GitHub Actions / windows-amd64-c-NO_ASM (version-1-6)

type mismatch: got <ptr array[0..31, byte], int>

Check failure on line 55 in constantine/csprngs/sysrand.nim

View workflow job for this annotation

GitHub Actions / windows-amd64-c-ASM (version-1-6)

type mismatch: got <ptr array[0..31, byte], int>

Check failure on line 55 in constantine/csprngs/sysrand.nim

View workflow job for this annotation

GitHub Actions / windows-amd64-c-NO_ASM (version-1-6)

type mismatch: got <ptr array[0..31, byte], int>

Check failure on line 55 in constantine/csprngs/sysrand.nim

View workflow job for this annotation

GitHub Actions / windows-amd64-c-ASM (version-1-6)

type mismatch: got <ptr array[0..31, byte], int>

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

Check failure on line 126 in constantine/csprngs/sysrand.nim

View workflow job for this annotation

GitHub Actions / macos-amd64-c-NO_ASM (version-1-6)

invalid type: 'typedesc[CCRNGStatus]' for let

Check failure on line 126 in constantine/csprngs/sysrand.nim

View workflow job for this annotation

GitHub Actions / macos-amd64-c-ASM (version-1-6)

invalid type: 'typedesc[CCRNGStatus]' for let

Check failure on line 126 in constantine/csprngs/sysrand.nim

View workflow job for this annotation

GitHub Actions / macos-amd64-c-NO_ASM (version-1-6)

invalid type: 'typedesc[CCRNGStatus]' for let

Check failure on line 126 in constantine/csprngs/sysrand.nim

View workflow job for this annotation

GitHub Actions / macos-amd64-c-ASM (version-1-6)

invalid type: 'typedesc[CCRNGStatus]' for let
# https://opensource.apple.com/source/CommonCrypto/CommonCrypto-60061.30.1/include/CommonCryptoError.h.auto.html

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

0 comments on commit 435fac1

Please sign in to comment.