upstream: https://github.com/mirage/mirage-crypto

mirage-crypto-rng.unix: provide new generators: Urandom and Getentropy (#250)

* Add /dev/urandom and getentropy RNG generators

Provide guidance to use these by default, document that Fortuna is not
thread-safe. As suggested in #249

* require 4.14 (uses in_channel)
* bench/speed: use Urandom and Getentropy generators
* mirage-crypto-rng-unix: more documentation
* test_entropy: disable on arm64
* Mirage_crypto_rng.generate_into: check off and len being >= 0
* Mirage_crypto_rng.generate_into: adjust docstring
* Mirage_crypto_rng.Generator.generate_into: emit unsafe warning

Co-authored-by: Reynir Björnsson <reynir@reynir.dk>
Co-authored-by: Török Edwin <edwintorok@users.noreply.github.com>
Reviewed-by: Calascibetta Romain <romain.calascibetta@gmail.com>

authored by

Hannes Mehnert
Reynir Björnsson
Török Edwin
and committed by
GitHub
c52a56ac 9cdc4520

+171 -33
-1
.cirrus.yml
··· 4 freebsd_task: 5 env: 6 matrix: 7 - - OCAML_VERSION: 4.13.1 8 - OCAML_VERSION: 4.14.2 9 10 pkg_install_script: pkg install -y ocaml-opam gmp gmake pkgconf bash
··· 4 freebsd_task: 5 env: 6 matrix: 7 - OCAML_VERSION: 4.14.2 8 9 pkg_install_script: pkg install -y ocaml-opam gmp gmake pkgconf bash
+6 -6
.github/workflows/test.yml
··· 9 strategy: 10 fail-fast: false 11 matrix: 12 - ocaml-version: ["4.14.2", "4.13.1"] 13 operating-system: [macos-latest, ubuntu-latest] 14 15 runs-on: ${{ matrix.operating-system }} 16 17 steps: 18 - name: Checkout code 19 - uses: actions/checkout@v2 20 21 - name: Use OCaml ${{ matrix.ocaml-version }} 22 - uses: ocaml/setup-ocaml@v2 23 with: 24 opam-local-packages: | 25 *.opam ··· 42 strategy: 43 fail-fast: false 44 matrix: 45 - ocaml-version: ["5.0.0"] 46 operating-system: [macos-latest, ubuntu-latest] 47 48 runs-on: ${{ matrix.operating-system }} 49 50 steps: 51 - name: Checkout code 52 - uses: actions/checkout@v2 53 54 - name: Use OCaml ${{ matrix.ocaml-version }} 55 - uses: ocaml/setup-ocaml@v2 56 with: 57 opam-local-packages: | 58 mirage-crypto.opam
··· 9 strategy: 10 fail-fast: false 11 matrix: 12 + ocaml-version: ["4.14.2"] 13 operating-system: [macos-latest, ubuntu-latest] 14 15 runs-on: ${{ matrix.operating-system }} 16 17 steps: 18 - name: Checkout code 19 + uses: actions/checkout@v4 20 21 - name: Use OCaml ${{ matrix.ocaml-version }} 22 + uses: ocaml/setup-ocaml@v3 23 with: 24 opam-local-packages: | 25 *.opam ··· 42 strategy: 43 fail-fast: false 44 matrix: 45 + ocaml-version: ["5.2.1"] 46 operating-system: [macos-latest, ubuntu-latest] 47 48 runs-on: ${{ matrix.operating-system }} 49 50 steps: 51 - name: Checkout code 52 + uses: actions/checkout@v4 53 54 - name: Use OCaml ${{ matrix.ocaml-version }} 55 + uses: ocaml/setup-ocaml@v3 56 with: 57 opam-local-packages: | 58 mirage-crypto.opam
+3 -3
.github/workflows/windows.yml
··· 9 strategy: 10 fail-fast: false 11 matrix: 12 - ocaml-version: ["4.14.2", "4.13.1"] 13 operating-system: [windows-latest] 14 15 runs-on: ${{ matrix.operating-system }} 16 17 steps: 18 - name: Checkout code 19 - uses: actions/checkout@v2 20 21 - name: Use OCaml ${{ matrix.ocaml-compiler }} 22 - uses: ocaml/setup-ocaml@v2 23 with: 24 opam-repositories: | 25 opam-repository-mingw: https://github.com/ocaml-opam/opam-repository-mingw.git#sunset
··· 9 strategy: 10 fail-fast: false 11 matrix: 12 + ocaml-version: ["4.14.2"] 13 operating-system: [windows-latest] 14 15 runs-on: ${{ matrix.operating-system }} 16 17 steps: 18 - name: Checkout code 19 + uses: actions/checkout@v4 20 21 - name: Use OCaml ${{ matrix.ocaml-compiler }} 22 + uses: ocaml/setup-ocaml@v3 23 with: 24 opam-repositories: | 25 opam-repository-mingw: https://github.com/ocaml-opam/opam-repository-mingw.git#sunset
+14 -4
bench/speed.ml
··· 480 throughput_into name (fun dst cs -> DES.ECB.unsafe_encrypt_into ~key cs ~src_off:0 dst ~dst_off:0 (String.length cs))) ; 481 482 bm "fortuna" (fun name -> 483 - let open Mirage_crypto_rng.Fortuna in 484 - let g = create () in 485 - reseed ~g "abcd" ; 486 throughput name (fun buf -> 487 let buf = Bytes.unsafe_of_string buf in 488 - generate_into ~g buf ~off:0 (Bytes.length buf))) ; 489 ] 490 491 let help () =
··· 480 throughput_into name (fun dst cs -> DES.ECB.unsafe_encrypt_into ~key cs ~src_off:0 dst ~dst_off:0 (String.length cs))) ; 481 482 bm "fortuna" (fun name -> 483 + Mirage_crypto_rng_unix.initialize (module Mirage_crypto_rng.Fortuna); 484 throughput name (fun buf -> 485 let buf = Bytes.unsafe_of_string buf in 486 + Mirage_crypto_rng.generate_into buf ~off:0 (Bytes.length buf))) ; 487 + 488 + bm "getentropy" (fun name -> 489 + Mirage_crypto_rng_unix.use_getentropy (); 490 + throughput name (fun buf -> 491 + let buf = Bytes.unsafe_of_string buf in 492 + Mirage_crypto_rng.generate_into buf ~off:0 (Bytes.length buf))) ; 493 + 494 + bm "urandom" (fun name -> 495 + Mirage_crypto_rng_unix.use_dev_urandom (); 496 + throughput name (fun buf -> 497 + let buf = Bytes.unsafe_of_string buf in 498 + Mirage_crypto_rng.generate_into buf ~off:0 (Bytes.length buf))) ; 499 ] 500 501 let help () =
+1 -1
mirage-crypto-rng.opam
··· 13 ["dune" "runtest" "-p" name "-j" jobs] {with-test} ] 14 15 depends: [ 16 - "ocaml" {>= "4.13.0"} 17 "dune" {>= "2.7"} 18 "dune-configurator" {>= "2.0.0"} 19 "duration"
··· 13 ["dune" "runtest" "-p" name "-j" jobs] {with-test} ] 14 15 depends: [ 16 + "ocaml" {>= "4.14.0"} 17 "dune" {>= "2.7"} 18 "dune-configurator" {>= "2.0.0"} 19 "duration"
+22 -2
rng/mirage_crypto_rng.mli
··· 16 17 (** {b TL;DR} Don't forget to seed; don't maintain your own [g]. 18 19 The RNGs here are merely the deterministic part of a full random number 20 generation suite. For proper operation, they need to be seeded with a 21 high-quality entropy source. ··· 156 (** Create a new, unseeded {{!g}g}. *) 157 158 val generate_into : g:g -> bytes -> off:int -> int -> unit 159 (** [generate_into ~g buf ~off n] produces [n] uniformly distributed random 160 bytes into [buf] at offset [off], updating the state of [g]. 161 162 - @raise Invalid_argument if buffer is too small (it must be: Bytes.length buf - off >= n) 163 *) 164 165 val reseed : g:g -> string -> unit ··· 233 (** [generate_into ~g buf ~off len] invokes 234 {{!Generator.generate_into}generate_into} on [g] or 235 {{!generator}default generator}. The random data is put into [buf] starting 236 - at [off] (defaults to 0) with [len] bytes. *) 237 238 val generate : ?g:g -> int -> string 239 (** Invoke {!generate_into} on [g] or {{!generator}default generator} and a
··· 16 17 (** {b TL;DR} Don't forget to seed; don't maintain your own [g]. 18 19 + For common operations on Unix (independent of your asynchronous task 20 + library, you can use /dev/urandom or getentropy(3) (actually getrandom(3) on 21 + Linux, getentropy() on macOS and BSD systems, BCryptGenRandom on Windows). 22 + 23 + Please ensure to call [Mirage_crypto_rng_unix.use_default], or 24 + [Mirage_crypto_rng_unix.use_dev_urandom] (if you only want to use 25 + /dev/urandom), or [Mirage_crypto_rng_unix.use_getentropy] (if you only want 26 + to use getentropy). 27 + 28 + For fine-grained control (doing entropy harvesting, etc.), please continue 29 + reading the documentation below. {b Please be aware that the feeding of Fortuna 30 + and producing random numbers is not thread-safe} (it is on Miou_unix via Pfortuna). 31 + 32 The RNGs here are merely the deterministic part of a full random number 33 generation suite. For proper operation, they need to be seeded with a 34 high-quality entropy source. ··· 169 (** Create a new, unseeded {{!g}g}. *) 170 171 val generate_into : g:g -> bytes -> off:int -> int -> unit 172 + [@@alert unsafe "Does not do bounds checks. Use Mirage_crypto_rng.generate_into instead."] 173 (** [generate_into ~g buf ~off n] produces [n] uniformly distributed random 174 bytes into [buf] at offset [off], updating the state of [g]. 175 176 + Assumes that [buf] is at least [off + n] bytes long. Also assumes that 177 + [off] and [n] are positive integers. Caution: do not use in your 178 + application, use [Mirage_crypto_rng.generate_into] instead. 179 *) 180 181 val reseed : g:g -> string -> unit ··· 249 (** [generate_into ~g buf ~off len] invokes 250 {{!Generator.generate_into}generate_into} on [g] or 251 {{!generator}default generator}. The random data is put into [buf] starting 252 + at [off] (defaults to 0) with [len] bytes. 253 + 254 + @raise Invalid_argument if buffer is too small (it must be: [Bytes.length 255 + buf - off >= n]) or [off] or [n] are negative. 256 + *) 257 258 val generate : ?g:g -> int -> string 259 (** Invoke {!generate_into} on [g] or {{!generator}default generator} and a
+13 -2
rng/rng.ml
··· 5 exception No_default_generator 6 7 let setup_rng = 8 - "\nTo initialize the RNG with a default generator, and set up entropy \ 9 collection and periodic reseeding as a background task, do the \ 10 following:\ 11 \n If you are using MirageOS, use the random device in config.ml: \ ··· 37 val block : int 38 val create : ?time:(unit -> int64) -> unit -> g 39 val generate_into : g:g -> bytes -> off:int -> int -> unit 40 val reseed : g:g -> string -> unit 41 val accumulate : g:g -> source -> [`Acc of string -> unit] 42 val seeded : g:g -> bool ··· 68 let generate_into ?(g = default_generator ()) b ?(off = 0) n = 69 let Generator (g, _, m) = g in 70 let module M = (val m) in 71 if Bytes.length b - off < n then 72 invalid_arg "buffer too short"; 73 - M.generate_into ~g b ~off n 74 75 let generate ?g n = 76 let data = Bytes.create n in
··· 5 exception No_default_generator 6 7 let setup_rng = 8 + "\nPlease setup your default random number generator. On Unix, the best \ 9 + path is to call [Mirage_crypto_rng_unix.use_default ()].\ 10 + \nBut you can use Fortuna (or any other RNG) and setup the seeding \ 11 + (done by default in MirageOS): \ 12 + \n\ 13 + \nTo initialize the RNG with a default generator, and set up entropy \ 14 collection and periodic reseeding as a background task, do the \ 15 following:\ 16 \n If you are using MirageOS, use the random device in config.ml: \ ··· 42 val block : int 43 val create : ?time:(unit -> int64) -> unit -> g 44 val generate_into : g:g -> bytes -> off:int -> int -> unit 45 + [@@alert unsafe "Does not do bounds checks. Use Mirage_crypto_rng.generate_into instead."] 46 val reseed : g:g -> string -> unit 47 val accumulate : g:g -> source -> [`Acc of string -> unit] 48 val seeded : g:g -> bool ··· 74 let generate_into ?(g = default_generator ()) b ?(off = 0) n = 75 let Generator (g, _, m) = g in 76 let module M = (val m) in 77 + if off < 0 || n < 0 then 78 + invalid_arg ("negative offset " ^ string_of_int off ^ " or length " ^ 79 + string_of_int n); 80 if Bytes.length b - off < n then 81 invalid_arg "buffer too short"; 82 + begin[@alert "-unsafe"] 83 + M.generate_into ~g b ~off n 84 + end 85 86 let generate ?g n = 87 let data = Bytes.create n in
+3 -2
rng/unix/dune
··· 16 (library 17 (name mirage_crypto_rng_unix) 18 (public_name mirage-crypto-rng.unix) 19 - (modules mirage_crypto_rng_unix) 20 - (libraries mirage-crypto-rng unix logs) 21 (foreign_stubs 22 (language c) 23 (names mc_getrandom_stubs)) 24 (c_library_flags 25 (:include rng_c_flags.sexp)))
··· 16 (library 17 (name mirage_crypto_rng_unix) 18 (public_name mirage-crypto-rng.unix) 19 + (modules mirage_crypto_rng_unix urandom getentropy) 20 + (libraries mirage-crypto-rng unix logs threads.posix) 21 (foreign_stubs 22 (language c) 23 + (include_dirs ../../src/native) 24 (names mc_getrandom_stubs)) 25 (c_library_flags 26 (:include rng_c_flags.sexp)))
+25
rng/unix/getentropy.ml
···
··· 1 + 2 + external getrandom_buf : bytes -> int -> int -> unit = "mc_getrandom" [@@noalloc] 3 + 4 + type g = unit 5 + 6 + (* The maximum value for length is GETENTROPY_MAX for `getentropy`: https://pubs.opengroup.org/onlinepubs/9799919799/functions/getentropy.html 7 + The minimum acceptable value for GETENTROPY_MAX is 256 https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/limits.h.html 8 + 9 + The actual implementation may be one of `getrandom`, `getentropy`, or `BCryptGenRandom`, and will internally limit the maximum bytes read in one go and loop as needed if more bytes are requested and we get a short read. 10 + *) 11 + let block = 256 12 + 13 + let create ?time:_ () = () 14 + 15 + let generate_into ~g:_ buf ~off len = 16 + getrandom_buf buf off len 17 + 18 + let reseed ~g:_ _data = () 19 + 20 + let accumulate ~g:_ _source = 21 + `Acc (fun _data -> ()) 22 + 23 + let seeded ~g:_ = true 24 + 25 + let pools = 0
+8 -6
rng/unix/mc_getrandom_stubs.c
··· 2 # include <unistd.h> 3 #endif 4 5 #include <caml/mlvalues.h> 6 #include <caml/memory.h> 7 #include <caml/unixsupport.h> ··· 19 # define getrandom(buf, len, flags) getrandom((buf), (len), (flags)) 20 # endif 21 22 - void raw_getrandom (uint8_t *data, uint32_t len) { 23 size_t off = 0; 24 ssize_t r = 0; 25 while (off < len) { ··· 39 #endif 40 #include <sys/param.h> 41 42 - void raw_getrandom (uint8_t *data, uint32_t len) { 43 size_t rlen = 0; 44 - for (uint32_t i = 0; i <= len; i += 256) { 45 rlen = MIN(256, len - i); 46 if (getentropy(data + i, rlen) == -1) uerror("getentropy", Nothing); 47 } ··· 61 #include <ntstatus.h> 62 #include <bcrypt.h> 63 64 - void raw_getrandom(uint8_t *data, uint32_t len) { 65 NTSTATUS Status; 66 Status = BCryptGenRandom(NULL, data, len, BCRYPT_USE_SYSTEM_PREFERRED_RNG); 67 if (Status != STATUS_SUCCESS) ··· 72 #error "Retrieving random data not supported on this platform" 73 #endif 74 75 - CAMLprim value mc_getrandom (value buf, value len) { 76 - raw_getrandom(Bytes_val(buf), Int_val(len)); 77 return Val_unit; 78 }
··· 2 # include <unistd.h> 3 #endif 4 5 + #include "mirage_crypto.h" 6 + 7 #include <caml/mlvalues.h> 8 #include <caml/memory.h> 9 #include <caml/unixsupport.h> ··· 21 # define getrandom(buf, len, flags) getrandom((buf), (len), (flags)) 22 # endif 23 24 + void raw_getrandom (uint8_t *data, size_t len) { 25 size_t off = 0; 26 ssize_t r = 0; 27 while (off < len) { ··· 41 #endif 42 #include <sys/param.h> 43 44 + void raw_getrandom (uint8_t *data, size_t len) { 45 size_t rlen = 0; 46 + for (size_t i = 0; i <= len; i += 256) { 47 rlen = MIN(256, len - i); 48 if (getentropy(data + i, rlen) == -1) uerror("getentropy", Nothing); 49 } ··· 63 #include <ntstatus.h> 64 #include <bcrypt.h> 65 66 + void raw_getrandom(uint8_t *data, size_t len) { 67 NTSTATUS Status; 68 Status = BCryptGenRandom(NULL, data, len, BCRYPT_USE_SYSTEM_PREFERRED_RNG); 69 if (Status != STATUS_SUCCESS) ··· 74 #error "Retrieving random data not supported on this platform" 75 #endif 76 77 + CAMLprim value mc_getrandom (value buf, value off, value len) { 78 + raw_getrandom(_bp_uint8_off(buf, off), Long_val(len)); 79 return Val_unit; 80 }
+19 -2
rng/unix/mirage_crypto_rng_unix.ml
··· 1 open Mirage_crypto_rng 2 3 let src = Logs.Src.create "mirage-crypto-rng.unix" ~doc:"Mirage crypto RNG Unix" 4 module Log = (val Logs.src_log src : Logs.LOG) 5 6 - external getrandom_buf : bytes -> int -> unit = "mc_getrandom" [@@noalloc] 7 8 let getrandom size = 9 let buf = Bytes.create size in 10 - getrandom_buf buf size; 11 Bytes.unsafe_to_string buf 12 13 let getrandom_init i =
··· 1 open Mirage_crypto_rng 2 3 + module Urandom = Urandom 4 + 5 + module Getentropy = Getentropy 6 + 7 + let use_dev_urandom () = 8 + let g = create (module Urandom) in 9 + set_default_generator g 10 + 11 + let use_getentropy () = 12 + let g = create (module Getentropy) in 13 + set_default_generator g 14 + 15 + let use_default () = use_getentropy () 16 + 17 let src = Logs.Src.create "mirage-crypto-rng.unix" ~doc:"Mirage crypto RNG Unix" 18 module Log = (val Logs.src_log src : Logs.LOG) 19 20 + external getrandom_buf : bytes -> int -> int -> unit = "mc_getrandom" [@@noalloc] 21 + 22 + let getrandom_into buf ~off ~len = 23 + getrandom_buf buf off len 24 25 let getrandom size = 26 let buf = Bytes.create size in 27 + getrandom_into buf ~off:0 ~len:size; 28 Bytes.unsafe_to_string buf 29 30 let getrandom_init i =
+22
rng/unix/mirage_crypto_rng_unix.mli
··· 11 12 (** [getrandom size] returns a buffer of [size] filled with random bytes. *) 13 val getrandom : int -> string
··· 11 12 (** [getrandom size] returns a buffer of [size] filled with random bytes. *) 13 val getrandom : int -> string 14 + 15 + (** A generator that opens /dev/urandom and reads from that file descriptor 16 + data whenever random data is needed. The file descriptor is closed in 17 + [at_exit]. *) 18 + module Urandom : Mirage_crypto_rng.Generator 19 + 20 + (** A generator using [getrandom(3)] on Linux, [getentropy(3)] on BSD and macOS, 21 + and [BCryptGenRandom()] on Windows. *) 22 + module Getentropy : Mirage_crypto_rng.Generator 23 + 24 + (** [use_default ()] initializes the RNG [Mirage_crypto_rng.default_generator] 25 + with a sensible default, at the moment using [Getentropy]. *) 26 + val use_default : unit -> unit 27 + 28 + (** [use_dev_random ()] initializes the RNG 29 + [Mirage_crypto_rng.default_generator] with the [Urandom] generator. This 30 + raises an exception if "/dev/urandom" cannot be opened. *) 31 + val use_dev_urandom : unit -> unit 32 + 33 + (** [use_getentropy ()] initializes the RNG [Mirage_crypto_rng.default_generator] 34 + with the [Getentropy] generator. *) 35 + val use_getentropy : unit -> unit
+29
rng/unix/urandom.ml
···
··· 1 + 2 + type g = In_channel.t * Mutex.t 3 + 4 + (* The OCaml runtime always reads at least IO_BUFFER_SIZE from an input channel, which is currently 64 KiB *) 5 + let block = 65536 6 + 7 + let create ?time:_ () = 8 + let ic = In_channel.open_bin "/dev/urandom" 9 + and mutex = Mutex.create () 10 + in 11 + at_exit (fun () -> In_channel.close ic); 12 + (ic, mutex) 13 + 14 + let generate_into ~g:(ic, m) buf ~off len = 15 + let finally () = Mutex.unlock m in 16 + Mutex.lock m; 17 + Fun.protect ~finally (fun () -> 18 + match In_channel.really_input ic buf off len with 19 + | None -> failwith "couldn't read enough bytes from /dev/urandom" 20 + | Some () -> ()) 21 + 22 + let reseed ~g:_ _data = () 23 + 24 + let accumulate ~g:_ _source = 25 + `Acc (fun _data -> ()) 26 + 27 + let seeded ~g:_ = true 28 + 29 + let pools = 0
+3 -1
tests/dune
··· 41 (name test_entropy) 42 (modules test_entropy) 43 (package mirage-crypto-rng) 44 - (libraries mirage-crypto-rng ohex)) 45 46 (test 47 (name test_ec)
··· 41 (name test_entropy) 42 (modules test_entropy) 43 (package mirage-crypto-rng) 44 + (libraries mirage-crypto-rng ohex) 45 + (enabled_if (<> %{architecture} "arm64"))) 46 + ; see https://github.com/mirage/mirage-crypto/issues/216 47 48 (test 49 (name test_ec)
+1 -1
tests/test_ec.ml
··· 861 | Error _ -> Alcotest.fail "regression failed" 862 863 let () = 864 - Mirage_crypto_rng_unix.initialize (module Mirage_crypto_rng.Fortuna); 865 Alcotest.run "EC" 866 [ 867 ("P256 Key exchange", key_exchange);
··· 861 | Error _ -> Alcotest.fail "regression failed" 862 863 let () = 864 + Mirage_crypto_rng_unix.use_default (); 865 Alcotest.run "EC" 866 [ 867 ("P256 Key exchange", key_exchange);
+1 -1
tests/test_pk_runner.ml
··· 9 ] 10 11 let () = 12 - Mirage_crypto_rng_unix.initialize (module Mirage_crypto_rng.Fortuna); 13 run_test_tt_main suite
··· 9 ] 10 11 let () = 12 + Mirage_crypto_rng_unix.use_default (); 13 run_test_tt_main suite
+1 -1
tests/test_random_runner.ml
··· 105 ] 106 107 let () = 108 - Mirage_crypto_rng_unix.initialize (module Mirage_crypto_rng.Fortuna); 109 run_test_tt_main suite
··· 105 ] 106 107 let () = 108 + Mirage_crypto_rng_unix.use_default (); 109 run_test_tt_main suite