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

mirage-crypto-rng-eio: Eio backend of mirage-crypto-rng #155

Merged
merged 14 commits into from
Sep 13, 2022
Merged
Show file tree
Hide file tree
Changes from 9 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
38 changes: 33 additions & 5 deletions .cirrus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,37 @@ freebsd_task:
matrix:
- OCAML_VERSION: 4.11.1
- OCAML_VERSION: 4.12.0

pkg_install_script: pkg install -y ocaml-opam gmp gmake pkgconf bash

ocaml_script:
hannesm marked this conversation as resolved.
Show resolved Hide resolved
- opam init -a --comp=$OCAML_VERSION
- opam env

pin_packages_script:
hannesm marked this conversation as resolved.
Show resolved Hide resolved
- opam install -y solo5-bindings-hvt zarith-freestanding opam-depext
- opam pin add mirage-crypto . -y --with-version=0.10.6 --with-test
- opam pin add mirage-crypto-rng . -y --with-version=0.10.6 --with-test
- opam pin add mirage-crypto-rng-mirage . -y --with-version=0.10.6 --with-test
- opam pin add mirage-crypto-pk . -y --with-version=0.10.6 --with-test
- opam pin add mirage-crypto-ec . -y --with-version=0.10.6 --with-test
- opam pin add mirage-crypto-rng-async . -y --with-version=0.10.6 --with-test
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the changes above. Could you minimize the difference in this file, please?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opam 2.1 command opam install -y --deps-only . didn't work - possibly because currently the repo contains an unpublished package(mirage-crypto-rng-eio). This is a way to get around that issue.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, thanks for your reply. now that it'll soon be merged into opam-repository, would you mind to PR something that removes the mentions of specific versions and packages?


test_script: opam exec -- dune runtest tests
test_mirage_script: eval `opam env` && ./.test-mirage.sh

freebsd_eio_task:
hannesm marked this conversation as resolved.
Show resolved Hide resolved
pkg_install_script: pkg install -y ocaml-opam gmp gmake pkgconf bash
ocaml_script: opam init -a --comp=$OCAML_VERSION
dependencies_script: eval `opam env` && opam install -y --deps-only .
build_script: eval `opam env` && dune build @install
test_script: eval `opam env` && opam install -y -t --deps-only . && dune build @runtest
test_mirage_script: eval `opam env` && opam install -y solo5-bindings-hvt zarith-freestanding opam-depext && opam pin add -y . && ./.test-mirage.sh

ocaml_script:
hannesm marked this conversation as resolved.
Show resolved Hide resolved
- opam init -a --bare
- opam update
- opam switch create 5.0.0~alpha0 --repositories=default,alpha=git+https://github.com/kit-ty-kate/opam-alpha-repository.git
- opam env

pin_packages_script:
hannesm marked this conversation as resolved.
Show resolved Hide resolved
- opam pin add mirage-crypto . -y --with-version=dev
- opam pin add mirage-crypto-rng . -y --with-version=dev
- opam pin add mirage-crypto-rng-eio . -y --with-version=dev --with-test

test_script: opam exec -- dune runtest eio
44 changes: 41 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,51 @@ jobs:
- name: Use OCaml ${{ matrix.ocaml-version }}
uses: ocaml/setup-ocaml@v2
with:
opam-local-packages: |
*.opam
!mirage-crypto-rng-eio.opam
ocaml-compiler: ${{ matrix.ocaml-version }}

- name: Install dependencies
run: opam install --deps-only -t .
run: opam install --deps-only -t mirage-crypto mirage-crypto-rng mirage-crypto-rng-mirage mirage-crypto-pk mirage-crypto-ec mirage-crypto-rng-async
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've no idea why here there are explicit names for all the packages, when above opam-local-packages already should contain exactly this list?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is so that it doesn't pick up mirage-crypto-rng-eio.opam dependencies. I think the workflow failed if this line wasn't there then. Perhaps it has changed now, but not sure.


- name: Build
run: opam exec -- dune build
run: opam exec -- dune build -p mirage-crypto,mirage-crypto-rng,mirage-crypto-rng-mirage,mirage-crypto-pk,mirage-crypto-ec,mirage-crypto-rng-async

- name: Test
run: opam exec -- dune runtest
run: opam exec -- dune runtest tests

build-test-unix-eio:
name : Unix (eio)
strategy:
fail-fast: false
matrix:
ocaml-version: [ocaml-variants.5.0.0+trunk]
operating-system: [macos-latest, ubuntu-latest]

runs-on: ${{ matrix.operating-system }}

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Use OCaml ${{ matrix.ocaml-version }}
uses: ocaml/setup-ocaml@v2
with:
opam-local-packages: |
mirage-crypto.opam
mirage-crypto-rng.opam
mirage-crypto-rng-eio.opam
ocaml-compiler: ${{ matrix.ocaml-version }}
opam-repositories: |
default: https://github.com/ocaml/opam-repository.git
alpha: https://github.com/kit-ty-kate/opam-alpha-repository.git

- name: Install dependencies
run: opam install --deps-only -t mirage-crypto mirage-crypto-rng mirage-crypto-rng-eio

- name: Build
run: opam exec -- dune build -p mirage-crypto,mirage-crypto-rng,mirage-crypto-rng-eio

- name: Test
run: opam exec -- dune runtest eio
3 changes: 2 additions & 1 deletion .github/workflows/windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ jobs:
- name: Checkout code
uses: actions/checkout@v2

- name: Use OCaml ${{ matrix.ocaml-version }}
- name: Use OCaml ${{ matrix.ocaml-compiler }}
uses: ocaml/setup-ocaml@v2
with:
opam-local-packages: |
*.opam
!mirage-crypto-rng-async.opam
!mirage-crypto-rng-eio.opam
ocaml-compiler: ${{ matrix.ocaml-version }}

- name: Install dependencies
Expand Down
10 changes: 10 additions & 0 deletions eio/src/dune
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
(library
(name mirage_crypto_rng_eio)
(public_name mirage-crypto-rng-eio)
(libraries
eio
cstruct
logs
mirage-crypto-rng
duration
hannesm marked this conversation as resolved.
Show resolved Hide resolved
mtime.clock.os))
81 changes: 81 additions & 0 deletions eio/src/mirage_crypto_rng_eio.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
open Mirage_crypto_rng
hannesm marked this conversation as resolved.
Show resolved Hide resolved

type env = <
clock: Eio.Time.clock;
secure_random: Eio.Flow.source;
>

let src = Logs.Src.create "mirage-crypto-rng-eio" ~doc:"Mirage crypto RNG Eio"
module Log = (val Logs.src_log src: Logs.LOG)

let getrandom env i =
hannesm marked this conversation as resolved.
Show resolved Hide resolved
let buf = Cstruct.create i in
Eio.Flow.read_exact env#secure_random buf;
buf

let getrandom_init env i =
hannesm marked this conversation as resolved.
Show resolved Hide resolved
let data = getrandom env 128 in
Entropy.header i data

let rec periodic env f delta =
f ();
Eio.Time.sleep env#clock (Duration.to_f delta);
periodic env f delta
hannesm marked this conversation as resolved.
Show resolved Hide resolved

let periodically_feed_entropy env delta source =
hannesm marked this conversation as resolved.
Show resolved Hide resolved
let task () =
hannesm marked this conversation as resolved.
Show resolved Hide resolved
let per_pool = 8 in
let size = per_pool * pools None in
let random = getrandom env size in
let idx = ref 0 in
let f () =
hannesm marked this conversation as resolved.
Show resolved Hide resolved
incr idx;
Cstruct.sub random (per_pool * (pred !idx)) per_pool
in
Entropy.feed_pools None source f
in
periodic env task delta

let rdrand_task env delta =
hannesm marked this conversation as resolved.
Show resolved Hide resolved
match Entropy.cpu_rng with
| Error `Not_supported -> []
| Ok cpu_rng -> [ fun () -> periodic env (cpu_rng None) delta ]

let running = ref false
hannesm marked this conversation as resolved.
Show resolved Hide resolved

let run
?g
?(sleep = Duration.of_sec 1)
generator
env
fn
=
if !running then begin
Log.debug
(fun m -> m "Mirage_crypto_rng_eio.initialize has already been called, \
ignoring this call.");
fn ()
end
else begin
running := true;
Fun.protect
~finally:( fun () -> running := false )
(fun () ->
(try
let _ = default_generator () in
Log.warn (fun m -> m "Mirage_crypto_rng.default_generator has already \
been set, check that this call is intentional");
with
No_default_generator -> ());
let seed =
let init =
Entropy.[ bootstrap ; whirlwind_bootstrap ; bootstrap ; getrandom_init env ] in
List.mapi (fun i f -> f i) init |> Cstruct.concat
in
let rng = create ?g ~seed ~time:Mtime_clock.elapsed_ns generator in
set_default_generator rng;
let source = Entropy.register_source "getrandom" in
let feed_entropy () = periodically_feed_entropy env (Int64.mul sleep 10L) source in
Eio.Fiber.any (rdrand_task env sleep @ [feed_entropy ; fn])
)
end
32 changes: 32 additions & 0 deletions eio/src/mirage_crypto_rng_eio.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
(** {b RNG} seeding on {b Eio backends}.

This module initializes a Fortuna RNG with [getrandom()], and CPU RNG.
[Eio.Stdenv.secure_random] is used as the [getrandom()] implementation.
*)

type env = <
clock: Eio.Time.clock;
secure_random: Eio.Flow.source;
>

(** [run ~sleep env fn] will bring the RNG into a working state. The argument
bikallem marked this conversation as resolved.
Show resolved Hide resolved
[sleep] is measured in ns (default 1s), and used to sleep between collection
of entropy from the CPU RNG, every [10 * sleep] getrandom is used to collect
entropy.

[fn] is the main function that will have access to a running RNG.

[[
let () =
Eio_main.run @@ fun env ->
Mirage_crypto_rng_eio.run (module Mirage_crypto_rng.Fortuna) env @@ fun () ->
let random_num = Mirage_crypto_rng.generate 32 in
Printf.printf "Random number: %S%!" (Cstruct.to_string random_num)
]]
*)
val run
hannesm marked this conversation as resolved.
Show resolved Hide resolved
: ?g:'a
-> ?sleep:int64
hannesm marked this conversation as resolved.
Show resolved Hide resolved
-> 'a Mirage_crypto_rng.generator
-> <env; ..>
hannesm marked this conversation as resolved.
Show resolved Hide resolved
-> (unit -> 'b) -> 'b
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After thinking again about 3871e3b, I wonder what the semantics of run is (and why it is as it is). According to the documentation, it accepts as last argument a function fn "that will have access to a running RNG".

So is it expected that run is executed multiple times (on different domains)?

While it sets and resets the module-local mutable state running, a second execution will print a warning message. Also, if I understand the implementation correctly (please help me since I'm a bit lost in respect to cancellation and eio), after run has been executed (and finished), any Mirage_crypto_rng.generate will produce random numbers. But once the fn in run finishes, the periodic entropy feeding is stopped (and thus the rng keeps to produce random numbers, but that opens attacks if some bits of the rng state can be predicted -- this is the reason why periodic entropy feeding is necessary).

The other implementations (lwt, mirage, async) expect initialize (and why is it called run here?) to be called exactly once, and this initialize registers several tasks, and finishes immediately -- but the entropy feeding is never stopped.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reading the discussion, I noticed @talex5 is in favour of the run semantics "so that periodic task quits once the main task finishes". I wonder whether that should the imply that the defaiult_rng is reset to None (at the moment there's no API for this)? It is also a bit tricky, since certainly fn may contain the set to a different default generator, which run would once the execution finishes, be reset to None.

I'm a bit puzzled: what is the advantage of cancelling the periodic tasks? Is there an example application which only needs for a startup phase the rng? E.g. the lwt implementation registers (via Lwt.async) some tasks, so execution will be done whenever Lwt_main.run is executed -- does eio have similar functionality?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So is it expected that run is executed multiple times (on different domains)?

I was assuming that only one domain would be collecting entropy, and mirage-crypto would be able to use it from all domains. The documentation should certainly say something about that.

The idea is that run runs at the entry point of your application and everything else happens inside it (including spawning more domains). Because of the structured concurrency, any domains spawned within run will finish before run does, so as long as you follow the run env @@ fun () -> pattern it should be hard to go wrong.

Structured concurrency prevents leaking fibers, which is what an initialize function with the same API as the others would do here. You could pass a switch argument (as the original version of this PR did), and have fibers added to that, but that causes the application to wait for the RNG thread to finish before exiting, which isn't what we want. I recently added a Fiber.fork_daemon function in the Git version of eio, which solves that problem, but the run API would still be easiest for most uses, I think.

I wonder whether that should the imply that the defaiult_rng is reset to None (at the moment there's no API for this)?

That would be ideal.

E.g. the lwt implementation registers (via Lwt.async) some tasks, so execution will be done whenever Lwt_main.run is executed -- does eio have similar functionality?

The run API requires you to pass env, which you get from Eio_main.run, so you can't call this before entering the main loop anyway.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 96d7ce6, does that make sense in that way?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that eio and multicore is different in the semantics, just to be clear:

  • entropy collection is expected to run on one CPU, the rng is used on any CPU (this is fine I guess)
  • in the experience of RNG initialization, having Mirage_crypto_rng_lwt.initialize outside the Lwt monad allowed us to embed the call in Tls_lwt at toplevel / conduit-lwt as well, which means common users of these libraries do not need to think about RNG. I suspect the run API is then different (and adds some burden e.g. for those only interested in establishing a TLS client connection -- since now they've to think about initializing the RNG somewhere in their application)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need running at all now, then? We can just look at whether the default generator is set.

in the experience of RNG initialization, having Mirage_crypto_rng_lwt.initialize outside the Lwt monad allowed us to embed the call in Tls_lwt at toplevel / conduit-lwt as well, which means common users of these libraries do not need to think about RNG.

Doesn't that mean that the user's choice of sleep duration or of which generator to use would get overridden by the arguments passed by Tls?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need running at all now, then? We can just look at whether the default generator is set.

That's right. Assuming nobody else sets the default generator.

Doesn't that mean that the user's choice of sleep duration or of which generator to use would get overridden by the arguments passed by Tls?

Since there's the mutable running, only the first call to Mirage_crypto_rng_lwt.initialize will setup tasks etc. So indeed that may be that Tls_lwt sets the sleep parameter already.

I do think that this API could be improved, but I don't have the headspace at the moment to figure out what would be sensible. Putting the burden to the application to call Mirage_crypto_rng_....initialize / run (and not calling it in library top-level code) is definitively one option (but that has been a source of misunderstandings in the recent decade (issue reports to nocrypto etc. were mostly about that). That is how the current API came to existance, application developers are not bothered with executing initialization code. For MirageOS, the mirage utility executes the initialization function call.

4 changes: 4 additions & 0 deletions eio/tests/dune
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(tests
(names test_eio_rng test_entropy_collection)
(libraries mirage-crypto-rng-eio duration eio_main)
(package mirage-crypto-rng-eio))
11 changes: 11 additions & 0 deletions eio/tests/test_eio_rng.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
open Mirage_crypto_rng

let () =
Eio_main.run @@ fun env ->
Mirage_crypto_rng_eio.run (module Fortuna) env @@ fun () ->
let random_num = Mirage_crypto_rng.generate 32 in
assert (Cstruct.length random_num = 32);
Printf.printf "32 bit random number: %S\n%!" (Cstruct.to_string random_num);
let random_num = Mirage_crypto_rng.generate 16 in
assert (Cstruct.length random_num = 16);
Printf.printf "16 bit random number: %S\n%!" (Cstruct.to_string random_num);
36 changes: 36 additions & 0 deletions eio/tests/test_entropy_collection.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module Printing_rng = struct
type g = unit

let block = 16
let create ?time:_ () = ()
let generate ~g:_ _n = assert false
let seeded ~g:_ = true
let pools = 1

let reseed ~g:_ data =
Format.printf "reseeding: %a@.%!" Cstruct.hexdump_pp data

let accumulate ~g:_ source =
let print data =
Format.printf "accumulate: (src: %a) %a@.%!"
Mirage_crypto_rng.Entropy.pp_source source Cstruct.hexdump_pp data
in
`Acc print
end

let () =
Eio_main.run @@ fun env ->
Mirage_crypto_rng_eio.run (module Printing_rng) env @@ fun () ->
Eio.Fiber.both
begin fun () ->
let sleep = Duration.(of_sec 2 |> to_f) in
Eio.Time.sleep env#clock sleep
end
begin fun () ->
Format.printf "entropy sources: %a@,%!"
(fun ppf -> List.iter (fun x ->
Mirage_crypto_rng.Entropy.pp_source ppf x;
Format.pp_print_space ppf ()))
(Mirage_crypto_rng.Entropy.sources ())
end

29 changes: 29 additions & 0 deletions mirage-crypto-rng-eio.opam
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
opam-version: "2.0"
homepage: "https://github.com/mirage/mirage-crypto"
dev-repo: "git+https://github.com/mirage/mirage-crypto.git"
bug-reports: "https://github.com/mirage/mirage-crypto/issues"
doc: "https://mirage.github.io/mirage-crypto/doc"
authors: ["Bikal Gurung <gbikal@gmail.com>" ]
maintainer: "Bikal Gurung <gbikal@gmail.com>"
license: "ISC"
synopsis: "Feed the entropy source in an eio-friendly way"

build: [ ["dune" "subst"] {dev}
["dune" "build" "-p" name "-j" jobs ]
["dune" "runtest" "-p" name "-j" jobs] {with-test} ]

depends: [
"base-domains"
"dune" {>= "3.0"}
"eio" {>= "0.3"}
"cstruct" {>= "6.0.0"}
"logs"
"mirage-crypto-rng" {=version}
"duration"
"mtime"
"eio_main" {with-test}
]
description: """
Mirage-crypto-rng-eio feeds the entropy source for Mirage_crypto_rng-based
random number genreator implementations, in an eio-friendly way.
"""