Skip to content

Commit

Permalink
Merge 19dd264 into d067785
Browse files Browse the repository at this point in the history
  • Loading branch information
marcoonroad committed Sep 8, 2019
2 parents d067785 + 19dd264 commit 8739ffe
Show file tree
Hide file tree
Showing 18 changed files with 231 additions and 34 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -18,7 +18,7 @@ env:
- OCAML_VERSION=4.04 POST_INSTALL_HOOK="make report"
- OCAML_VERSION=4.05 POST_INSTALL_HOOK="make report"
- OCAML_VERSION=4.06 POST_INSTALL_HOOK="make lint-format; make report"
- OCAML_VERSION=4.07 POST_INSTALL_HOOK="make lint-format; make report"
- OCAML_VERSION=4.07 POST_INSTALL_HOOK="make lint-format; make bench; make report"
os:
- linux
# - osx
45 changes: 30 additions & 15 deletions Makefile
Expand Up @@ -8,7 +8,7 @@ default: build

test: build
@ opam lint
@ dune build @test/spec/runtest -f --no-buffer -j 1
@ dune build @test/spec/runtest -f --no-buffer

build:
@ dune build -j 1
Expand All @@ -23,6 +23,8 @@ clear:
@ rm -rfv bisect*.out
@ dune clean

clean: clear

coverage: clear
@ mkdir -p docs/
@ rm -rf docs/apicov
Expand Down Expand Up @@ -71,11 +73,14 @@ docs: build
@ mv ./_build/default/_doc/_html/* ./docs/apiref/

pin:
@ opam pin add nocoiner . -n --yes
@ opam pin add nocoiner . -n --yes --working-dir

unpin:
@ opam pin remove nocoiner --yes

deps:
@ opam install . --deps-only --yes
@ opam install alcotest core --yes # force such test dependences
@ opam install . --deps-only --yes --working-dir
@ opam install alcotest --yes # force such test dependences

dev-deps:
@ opam install \
Expand All @@ -87,18 +92,20 @@ dev-deps:
merlin \
bisect_ppx \
utop \
core_bench \
--yes
@ opam update --yes
@ opam upgrade \
odoc \
ocveralls \
alcotest \
ocp-indent \
ocamlformat \
merlin \
bisect_ppx \
utop \
--yes
# @ opam update --yes
# @ opam upgrade \
# odoc \
# ocveralls \
# alcotest \
# ocp-indent \
# ocamlformat \
# merlin \
# bisect_ppx \
# utop \
# core_bench \
# --yes

lint-format:
@ opam install ocamlformat --yes
Expand All @@ -117,6 +124,14 @@ local-site-setup:
local-site-start:
@ cd docs && bundle exec jekyll serve && cd ..

bench: clean build
@ opam install core_bench --yes
@ NOCOINER_KDF_COST=2 \
NOCOINER_KDF_WORKERS=1 \
dune build @test/bench/runtest -f --no-buffer --auto-promote \
--diff-command="git diff --unified=10 --break-rewrites --no-index --exit-code --histogram --word-diff=none --color --no-prefix" || echo \
"\n\n=== Differences detected! ===\n\n"

# to run inside docker alpine context
binary: clear
@ dune build --profile deploy
Expand Down
64 changes: 63 additions & 1 deletion README.md
Expand Up @@ -191,6 +191,67 @@ The complete API reference is available [here][7]. Coverage reports are
generated too, please refer to the respective [page][8].


### Cryptoanalysis

We have performed some benchmarks on valid inputs and on invalid inputs as well.
This is just to discover and prove exploitable loopholes. The kind of side-channel
vulnerabilities shown on version `1.0.0` are related to _timing attacks_. The used
_Key Derivation Function_ on both `commit` and `reveal` phases conceals a lot the
response time if this library is used as an _oracle_ (that is, an external server).
On the other hand, this algorithm is open and then the attacker can pre-compute the
derivation keys, and just perform her own cryptoanalysis on the next steps of the
algorithm.

Assuming that our _Nocoiner_ algorithm is just a black-box (oracle) where all the
steps are called "atomically", there are still some exploitable information if the
attacker gains access on the host machine for the oracle service. The benchmarks
provided with the `core_bench` library only work well for functions halting under
milliseconds, the KDF imposes a computation around few seconds. Due that issue, we
execute the benchmarks with a lower KDF cost (just to cover the possibility of
pre-computed derived keys, and also to remove timing noise imposed by a KDF with
stronger cycles). To run the benchmarks, just type `$ make bench` on this project's
root directory.

We will only take the relevant information (with major differences). The version
`1.0.0` is vulnerable during the opening phase, mostly 'cause:

- We compare the tags for the authenticated ciphertext in non-constant /
non-linear time. This is the most famous kind of exploitable timing attack.
- We don't decypher the AES ciphertext even if the opening key is wrong (don't
pass the MAC tag test). The result plaintext will be ignored 'cause the
authentication failed, but decryption must be performed to not leak side
information for the attacker.

The benchmarks results stored on this repository were performed on an Intel(R)
Dual-Core Celeron(R) of 1GHz each (both vulnerable to Meltdown, Spectre and MDS
CPU bugs, and possibly some NSA hardware backdoors too, you know). The first test
is the one with valid inputs, and the rest are evaluated with invalid inputs:

<p></p>

| Name | Time/Run | Cycls/Run | mWd/Run | mWd Overhd | mjWd/Run | mjWd Overhd | mGC/Run | Percentage |
|:-------------------|---------:|----------:|--------:|-----------:|---------:|------------:|---------:|-----------:|
| bound opening | 830.05us | 863.66kc | 19.77kw | 24.18w | 17.87w | 116.97w | 70.61e-3 | 100.00% |
| unbound commitment | 809.76us | 842.54kc | 19.56kw | 28.59w | 20.29w | -219.19w | 69.79e-3 | 97.56% |
| unbound opening | 807.82us | 840.52kc | 19.56kw | 28.59w | 20.29w | -219.19w | 69.79e-3 | 97.32% |

<div align="center" style="text-align: center;"><center>
<i>This table shows informations about the GC, minor heap & major heap. All cases
were executed with major heap compaction disabled to not mask execution time.</i>
</center></div>

<p></p>

As you can see, there's much more computations performed on valid/bound inputs than on unbound inputs. Inputs
are bound (the opening key and the commitment box) if they were previously computed during commitment phase.
Otherwise, the inputs are unbound _even if they were computed over the same secret during commitment_. This
is a huge important thing when we want a group of commitments (performed by many parties) to be independent
of each other. The security patch introduced on version `1.0.1` uses the [eqaf][10] library to compare in
constant time the MAC tags, and we also force decryption step even if a MAC tag mismatch occurs (obviously
the decrypted plain-text is ignored in this case and the whole opening phase fails).



### Disclaimer

This library was not fully tested against side-channel attacks. Keep in mind
Expand All @@ -217,4 +278,5 @@ process context).
[6]: https://en.wikipedia.org/wiki/Authenticated_encryption
[7]: https://marcoonroad.dev/nocoiner/apiref/nocoiner/Nocoiner/index.html
[8]: https://marcoonroad.dev/nocoiner/apicov/index.html
[9]: https://github.com/marcoonroad/nocoiner/issues/1
[9]: https://github.com/marcoonroad/nocoiner/issues/1
[10]: https://github.com/mirage/eqaf
2 changes: 1 addition & 1 deletion VERSION
@@ -1 +1 @@
1.0.0
1.0.1
2 changes: 1 addition & 1 deletion docs/dune
@@ -1 +1 @@
(dirs :standard \ vendor)
(dirs :standard \ vendor _site .bundle)
2 changes: 1 addition & 1 deletion dune-project
@@ -1,4 +1,4 @@
(lang dune 1.9)
(name nocoiner)
(version 1.0.0)
(version 1.0.1)
(using fmt 1.1)
12 changes: 12 additions & 0 deletions lib/constants.ml
@@ -0,0 +1,12 @@
module Sys = Core.Sys
module Option = Core.Option
module Int = Core.Int

let get variable default =
let optional = Sys.getenv variable in
Option.value optional ~default


let _KDF_COST = get "NOCOINER_KDF_COST" "8192" |> Int.of_string

let _KDF_WORKERS = get "NOCOINER_KDF_WORKERS" "2" |> Int.of_string
3 changes: 3 additions & 0 deletions lib/constants.mli
@@ -0,0 +1,3 @@
val _KDF_COST : int

val _KDF_WORKERS : int
3 changes: 2 additions & 1 deletion lib/dune
Expand Up @@ -2,7 +2,8 @@
(name nocoiner)
(public_name nocoiner)
(wrapped true)
(libraries core nocrypto.unix nocrypto digestif digestif.c scrypt-kdf)
(libraries core nocrypto.unix nocrypto digestif digestif.c eqaf eqaf.cstruct
scrypt-kdf)
(synopsis "The Nocoiner module for nocoiner library.")
(preprocess
(pps bisect_ppx -conditional -no-comment-parsing)))
25 changes: 20 additions & 5 deletions lib/encryption.ml
Expand Up @@ -29,14 +29,29 @@ let encrypt ~key ~iv ~metadata ~message:msg =
(ciphertext, tag)


exception DecryptedPlaintext of Cstruct.t

let decrypt ~reason ~key ~iv ~metadata ~cipher ~tag =
let aes_key, mac_key = __kdf key in
let secret = hash mac_key in
let payload = Cstruct.concat [ metadata; iv; cipher ] in
let tag' = mac ~key:secret payload in
if Cstruct.equal tag tag'
then
let aes_key' = AES.of_secret aes_key in
let plaintext = AES.decrypt ~iv ~key:aes_key' cipher in
(* we decypher before the tag verification to avoid
exploitable side-channels vulnerabilities such as
timing attacks. we also check the tags in linear
time regarding the tag size in bytes *)
let aes_key' = AES.of_secret aes_key in
let plaintext = AES.decrypt ~iv ~key:aes_key' cipher in
let decrypted =
Cstruct.of_string @@ Helpers.unpad @@ Cstruct.to_string plaintext
else raise reason
in
(* forces both bound and unbound flows to pass through the exception triggering
pipeline. this is just to approximate both execution timings to reduce the
vector attacks for side-channel attacks *)
try
if Eqaf_cstruct.equal tag tag'
then raise (DecryptedPlaintext decrypted)
else raise reason
with
| DecryptedPlaintext result ->
result
10 changes: 9 additions & 1 deletion lib/hardening.ml
@@ -1,2 +1,10 @@
open Constants

let kdf ~size ~salt password =
Scrypt_kdf.scrypt_kdf ~password ~salt ~dk_len:size ~r:8 ~p:2 ~n:8192
Scrypt_kdf.scrypt_kdf
~password
~salt
~dk_len:size
~r:8
~p:_KDF_WORKERS
~n:_KDF_COST
5 changes: 4 additions & 1 deletion lib/helpers.ml
Expand Up @@ -14,4 +14,7 @@ let pad ~basis msg =

let __nonzero char = char != __nullchar

let unpad msg = Encoding.decode @@ String.filter ~f:__nonzero msg
(* ignores input if it can't be base64-decoded after dropping null-padding data *)
let unpad msg =
let filtered = String.filter ~f:__nonzero msg in
try Encoding.decode @@ filtered with Failure _ -> msg
13 changes: 7 additions & 6 deletions nocoiner.opam
@@ -1,6 +1,6 @@
opam-version: "2.0"
name: "nocoiner"
version: "1.0.0"
version: "1.0.1"
synopsis: "A Commitment Scheme library for Coin Flipping/Tossing algorithms and sort"
description: """
This project implements Commitment Schemes using the
Expand All @@ -20,16 +20,17 @@ build: [
["dune" "build" "-p" name "-j" jobs]
]

run-test: ["dune" "runtest" "-p" name "-j" jobs]
run-test: ["dune" "build" "@test/spec/runtest" "-p" name "-j" jobs]

depends: [
"ocaml" {>= "4.03.0"}
"dune" {>= "1.9"}
"cmdliner" {>= "1.0.0"}
"alcotest" {with-test}
"nocrypto" {>= "0.5.4-1"}
"alcotest" {>= "0.8.0" & with-test}
"nocrypto" {>= "0.5.0"}
"scrypt-kdf" {>= "1.0.0"}
"digestif" {>= "0.7.0"}
"core" {>= "v0.9.1"}
"bisect_ppx" {>= "1.4.1"}
"core" {>= "v0.9.0"}
"eqaf" {>= "0.5"}
"bisect_ppx" {>= "1.4.0"}
]
8 changes: 8 additions & 0 deletions test/bench/dune
@@ -0,0 +1,8 @@
(test
(name timing)
(modules timing)
(action
(run %{test} time cycles alloc gc percentage speedup samples -all-values
-ascii -fork -no-compactions -overheads -quota 15 -stabilize-gc -width
300 -v -display tall))
(libraries nocoiner.bench))
11 changes: 11 additions & 0 deletions test/bench/timing.expected
@@ -0,0 +1,11 @@
Estimated testing time 45s (3 benchmarks x 15s). Change using -quota SECS.
bound opening: Total time taken 15.14s (156 samples, max runs 156).
unbound commitment: Total time taken 15.1478s (158 samples, max runs 158).
unbound opening: Total time taken 15.0146s (157 samples, max runs 157).

Name Runs @ Samples Time/Run Cycls/Run mWd/Run mWd Overhd mjWd/Run mjWd Overhd Prom/Run Prom Overhd mGC/Run mjGC/Run Comp/Run Percentage Speedup
-------------------- ---------------- ---------- ----------- --------- ------------ ---------- ------------- ---------- ------------- ---------- ---------- ---------- ------------ ---------
bound opening 156 @ 156 841.47us 874.71kc 19.78kw 25.82w 18.59w 35.39w 18.59w 35.39w 70.62e-3 0.00e-9 0.00e-9 100.00% 1.02
unbound commitment 158 @ 158 823.70us 856.24kc 19.69kw 23.02w 22.75w -132.15w 22.75w -132.15w 70.32e-3 0.00e-9 0.00e-9 97.89% 1.00
unbound opening 157 @ 157 826.47us 859.12kc 19.69kw 23.02w 22.81w -135.02w 22.81w -135.02w 70.33e-3 0.00e-9 0.00e-9 98.22% 1.00

42 changes: 42 additions & 0 deletions test/bench/timing.ml
@@ -0,0 +1,42 @@
open Nocoiner_bench
open Core_bench.Bench
module Command = Core.Command

let reveals c o =
try
ignore @@ Nocoiner.reveal ~commitment:c ~opening:o ;
true
with
| Nocoiner.Reasons.BindingFailure ->
false


let _RIGHT_SECRET = "P = NP would prove God's existence."

let _WRONG_SECRET = "The Quantum Nature is just Godel..."

let _RIGHT_C, _RIGHT_O = Nocoiner.commit _RIGHT_SECRET

let _WRONG_C, _WRONG_O = Nocoiner.commit _WRONG_SECRET

let __test_case_01 () = assert (reveals _RIGHT_C _RIGHT_O)

let __test_case_02 () = assert (not (reveals _WRONG_C _RIGHT_O))

let __test_case_03 () = assert (not (reveals _RIGHT_C _WRONG_O))

let _TEST_NAME_01 = "bound opening"

let _TEST_NAME_02 = "unbound commitment"

let _TEST_NAME_03 = "unbound opening"

let __test_01 = Test.create ~name:_TEST_NAME_01 __test_case_01

let __test_02 = Test.create ~name:_TEST_NAME_02 __test_case_02

let __test_03 = Test.create ~name:_TEST_NAME_03 __test_case_03

let suite = [ __test_01; __test_02; __test_03 ]

let _ = Command.run @@ make_command suite
9 changes: 9 additions & 0 deletions test/support/dune
@@ -0,0 +1,9 @@
;; wrapper library just to enable optional core_bench
;; library installation on ocaml versions >= 4.04.1

(library
(name nocoiner_bench)
(optional)
(public_name nocoiner.bench)
(modules nocoiner_bench)
(libraries core_bench nocoiner))
7 changes: 7 additions & 0 deletions test/support/nocoiner_bench.ml
@@ -0,0 +1,7 @@
module Nocoiner = struct
include Nocoiner
end

module Core_bench = struct
include Core_bench
end

0 comments on commit 8739ffe

Please sign in to comment.