Add NM commit wrappers with test, benchmark and example
diff --git a/python/amcl/commitments.py b/python/amcl/commitments.py
new file mode 100644
index 0000000..153addd
--- /dev/null
+++ b/python/amcl/commitments.py
@@ -0,0 +1,222 @@
+#!/usr/bin/env python3
+
+"""
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+"""
+
+"""
+
+This module use cffi to access the c functions in the amcl_mpc library.
+
+"""
+import platform
+from amcl import core_utils
+
+_ffi = core_utils._ffi
+_ffi.cdef("""
+extern void COMMITMENTS_NM_commit(csprng *RNG, octet *X, octet *R, octet *C);
+extern int COMMITMENTS_NM_decommit(octet* X, octet* R, octet* C);
+""")
+
+if (platform.system() == 'Windows'):
+    libamcl_mpc = _ffi.dlopen("libamcl_mpc.dll")
+    libamcl_core = _ffi.dlopen("libamcl_core.dll")
+elif (platform.system() == 'Darwin'):
+    libamcl_mpc = _ffi.dlopen("libamcl_mpc.dylib")
+    libamcl_core = _ffi.dlopen("libamcl_core.dylib")
+else:
+    libamcl_mpc = _ffi.dlopen("libamcl_mpc.so")
+    libamcl_core = _ffi.dlopen("libamcl_core.so")
+
+# Constants
+SHA256 = 32
+
+OK   = 0
+FAIL = 81
+
+
+def to_str(octet_value):
+    """Converts an octet type into a string
+
+    Add all the values in an octet into an array.
+
+    Args::
+
+        octet_value. An octet pointer type
+
+    Returns::
+
+        String
+
+    Raises:
+        Exception
+    """
+    i = 0
+    val = []
+    while i < octet_value.len:
+        val.append(octet_value.val[i])
+        i = i + 1
+    out = b''
+    for x in val:
+        out = out + x
+    return out
+
+
+def make_octet(length, value=None):
+    """Generates an octet pointer
+
+    Generates an empty octet or one filled with the input value
+
+    Args::
+
+        length: Length of empty octet
+        value:  Data to assign to octet
+
+    Returns::
+
+        oct_ptr: octet pointer
+        val: data associated with octet to prevent garbage collection
+
+    Raises:
+
+    """
+    oct_ptr = _ffi.new("octet*")
+    if value:
+        val = _ffi.new("char [%s]" % len(value), value)
+        oct_ptr.val = val
+        oct_ptr.max = len(value)
+        oct_ptr.len = len(value)
+    else:
+        val = _ffi.new("char []", length)
+        oct_ptr.val = val
+        oct_ptr.max = length
+        oct_ptr.len = 0
+    return oct_ptr, val
+
+
+def create_csprng(seed):
+    """Make a Cryptographically secure pseudo-random number generator instance
+
+    Make a Cryptographically secure pseudo-random number generator instance
+
+    Args::
+
+        seed:   random seed value
+
+    Returns::
+
+        rng: Pointer to cryptographically secure pseudo-random number generator instance
+
+    Raises:
+
+    """
+    seed_val = _ffi.new("char [%s]" % len(seed), seed)
+    seed_len = len(seed)
+
+    # random number generator
+    rng = _ffi.new('csprng*')
+    libamcl_core.RAND_seed(rng, seed_len, seed_val)
+
+    return rng
+
+
+def kill_csprng(rng):
+    """Kill a random number generator
+
+    Deletes all internal state
+
+    Args::
+
+        rng: Pointer to cryptographically secure pseudo-random number generator instance
+
+    Returns::
+
+    Raises:
+
+    """
+    libamcl_core.RAND_clean(rng)
+
+    return 0
+
+
+def nm_commit(rng, x, r=None):
+    """ Commit to the value x
+
+    Generate a commitment c to the value x, using the value r.
+    If r is empty it is randomly generated
+
+    Args::
+
+        rng : Pointer to cryptographically secure pseudo-random generator instance
+        x   : value to commit
+        r   : random value for the commitment. If empty it is randomly generated
+              If not empty it must be 256 bit long
+
+    Returns::
+
+    Raises::
+
+    """
+
+    if r is None:
+        r_oct, r_val = make_octet(SHA256)
+    else:
+        r_oct, r_val = make_octet(None, r)
+        rng = _ffi.NULL
+
+    _ = r_val # Suppress warning
+
+    x_oct, x_val = make_octet(None, x)
+    c_oct, c_val = make_octet(SHA256)
+    _ = x_val, c_val # Suppress warning
+
+    libamcl_mpc.COMMITMENTS_NM_commit(rng, x_oct, r_oct, c_oct)
+
+    r = to_str(r_oct)
+
+    # Clean memory
+    libamcl_core.OCT_clear(x_oct)
+    libamcl_core.OCT_clear(r_oct)
+
+    return r, to_str(c_oct)
+
+def nm_decommit(x, r, c):
+    """ Decommit commitment c
+
+    Decommit a commitment c to the value x, using the value r.
+
+    Args::
+
+        x : value to commit
+        r : random value for the commitment. It must be 256 bit
+        c : commitment value
+
+    Returns::
+
+    Raises::
+
+    """
+
+    x_oct, x_val = make_octet(None, x)
+    r_oct, r_val = make_octet(None, r)
+    c_oct, c_val = make_octet(None, c)
+    _ = x_val, r_val, c_val # Suppress warning
+
+    ec = libamcl_mpc.COMMITMENTS_NM_decommit(x_oct, r_oct, c_oct)
+
+    return ec
diff --git a/python/benchmark/bench_nm_commit.py b/python/benchmark/bench_nm_commit.py
new file mode 100755
index 0000000..a10012e
--- /dev/null
+++ b/python/benchmark/bench_nm_commit.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+
+"""
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+"""
+
+import os
+import sys
+from bench import time_func
+
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+
+from amcl import commitments
+
+x_hex = "40576370e36018f6bfaffc4c66780303a361f0c5f4a18a86a74fb179ca0fcf22"
+r_hex = "296f910bde4530efe3533ed3b74475d6022364db2e57773207734b6daf547ac8"
+
+if __name__ == "__main__":
+    x = bytes.fromhex(x_hex)
+    r = bytes.fromhex(r_hex)
+
+    # Generate quantities for benchmark
+    r, c = commitments.nm_commit(None, x, r)
+
+    assert commitments.nm_decommit(x, r, c) == commitments.OK
+
+    # Run benchmark
+    fncall = lambda: commitments.nm_commit(None, x, r)
+    time_func("nm_commit  ", fncall, unit="us")
+
+    fncall = lambda: commitments.nm_decommit(x, r, c)
+    time_func("nm_decommit", fncall, unit="us")
diff --git a/python/examples/example_nm_commit.py b/python/examples/example_nm_commit.py
new file mode 100755
index 0000000..024e60c
--- /dev/null
+++ b/python/examples/example_nm_commit.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+
+"""
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+"""
+
+import os
+import sys
+
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+
+from amcl import core_utils, commitments
+
+seed_hex = "78d0fb6705ce77dee47d03eb5b9c5d30"
+
+if __name__ == "__main__":
+    seed = bytes.fromhex(seed_hex)
+    rng = core_utils.create_csprng(seed)
+
+    print("Example Non Malleable Commitment")
+    print("Message: BANANA")
+
+    x = b'BANANA'
+
+    # Commitment Phase
+    r, c = commitments.nm_commit(rng, x)
+
+    print("\nCommitment")
+    print(f"\tr = {r.hex()}")
+    print(f"\tc = {c.hex()}")
+
+    # Decommitment Phase. After both c, r and x have been revealed
+    rc = commitments.nm_decommit(x, r, c)
+
+    print("\nDecommitment")
+    if rc == commitments.OK:
+        print("\tSuccess")
+    else:
+        print("\tFailure")
diff --git a/python/test/CMakeLists.txt b/python/test/CMakeLists.txt
index a01ddf4..b2fdbc7 100644
--- a/python/test/CMakeLists.txt
+++ b/python/test/CMakeLists.txt
@@ -44,10 +44,16 @@
 file(GLOB SCHNORR_TV "${PROJECT_SOURCE_DIR}/testVectors/schnorr/*.json")
 file(COPY ${SCHNORR_TV} DESTINATION "${PROJECT_BINARY_DIR}/python/test/schnorr/")
 
+# NM Commitment test vector
+file(
+  COPY ${PROJECT_SOURCE_DIR}/testVectors/commitments/nm_commit.json
+  DESTINATION "${PROJECT_BINARY_DIR}/python/test/commitments/")
+
 if(NOT CMAKE_BUILD_TYPE STREQUAL "ASan")
-  add_python_test(test_python_mpc_mta     test_mta.py)
-  add_python_test(test_python_mpc_r       test_r.py)
-  add_python_test(test_python_mpc_s       test_s.py)
-  add_python_test(test_python_mpc_ecdsa   test_ecdsa.py)
-  add_python_test(test_python_mpc_schnorr test_schnorr.py)
+  add_python_test(test_python_mpc_mta       test_mta.py)
+  add_python_test(test_python_mpc_r         test_r.py)
+  add_python_test(test_python_mpc_s         test_s.py)
+  add_python_test(test_python_mpc_ecdsa     test_ecdsa.py)
+  add_python_test(test_python_mpc_schnorr   test_schnorr.py)
+  add_python_test(test_python_mpc_nm_commit test_nm_commit.py)
 endif(NOT CMAKE_BUILD_TYPE STREQUAL "ASan")
diff --git a/python/test/test_nm_commit.py b/python/test/test_nm_commit.py
new file mode 100755
index 0000000..a317a79
--- /dev/null
+++ b/python/test/test_nm_commit.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python3
+
+"""
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+"""
+
+import os
+import sys
+import json
+from unittest import TestCase
+
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+
+from amcl import commitments, core_utils
+
+# Load and preprocess test vectors
+with open("commitments/nm_commit.json", "r") as f:
+    vectors = json.load(f)
+
+for vector in vectors:
+    for key, val in vector.items():
+        if key != "TEST":
+            vector[key] = bytes.fromhex(val)
+
+
+class TestNMCommit(TestCase):
+    """ Test NM Commitment Commit """
+
+    def setUp(self):
+        # Deterministic PRNG for testing purposes
+        seed_hex = "78d0fb6705ce77dee47d03eb5b9c5d30"
+        seed = bytes.fromhex(seed_hex)
+        self.rng = core_utils.create_csprng(seed)
+
+        self.msg = b'BANANA'
+
+        r_hex = "296f910bde4530efe3533ed3b74475d6022364db2e57773207734b6daf547ac8"
+        c_hex = "b60ebd5193252d22c771a7702724e9922662aae5f634494225cdd3a9e22f9826"
+        self.r_golden = bytes.fromhex(r_hex)
+        self.c_golden = bytes.fromhex(c_hex)
+
+    def test_tv(self):
+        """ Test using test vectors """
+
+        for vector in vectors:
+            r, c = commitments.nm_commit(None, vector['X'], vector['R'])
+
+            self.assertEqual(vector['R'], r)
+            self.assertEqual(vector['C'], c)
+
+    def test_random(self):
+        """ Test using rng """
+        r, c = commitments.nm_commit(self.rng, self.msg)
+
+        self.assertEqual(r, self.r_golden)
+        self.assertEqual(c, self.c_golden)
+
+
+class TestNMDecommit(TestCase):
+    """ Test NM Commitment Decommit """
+
+    def test_tv(self):
+        """ Test using test vectors """
+
+        for vector in vectors:
+            rc = commitments.nm_decommit(vector['X'], vector['R'], vector['C'])
+
+            self.assertEqual(rc, commitments.OK)
+
+    def test_failure(self):
+        """ Test error codes are propagated correctly """
+
+        rc = commitments.nm_decommit(vector['X'], vector['X'], vector['C'])
+
+        self.assertEqual(rc, commitments.FAIL)
diff --git a/testVectors/commitments/nm_commit.json b/testVectors/commitments/nm_commit.json
new file mode 100644
index 0000000..aa506a3
--- /dev/null
+++ b/testVectors/commitments/nm_commit.json
@@ -0,0 +1,62 @@
+[
+    {
+        "TEST":"0",
+        "X": "3f3cc85d2a99534d",
+        "R": "bba1e28251a39a13c037b180ed41cb76c23ca5fd639725706ead86f40d7f7b1e",
+        "C": "d49772a2af3c0904199b5620014f34ee2152cb427a4f241b2e45c62ce1a11f69"
+    },
+    {
+        "TEST": "1",
+        "X": "e8687750094198b455",
+        "R": "3ec7e47abe1345061e4c54808779c43493e4b6d15749d33becd1efb418ec0d34",
+        "C": "4eb9edfe9b66dcdb1882a9ea862794b2308d9a768155e96810502cc361f2afcc"
+    },
+    {
+        "TEST": "2",
+        "X": "ed45810ce81e4bf27980",
+        "R": "18a8edb9f5211df1f0f876e012a1404973cd173acea497d6fb93b5b39b910a89",
+        "C": "52bf67e6e9f4fd68382d2c94dc4044191edf5005cbbfa2cea6a6c7b69846e043"
+    },
+    {
+        "TEST": "3",
+        "X": "965f61c651654622880bcc",
+        "R": "88a038a78dba707e72b192eef24aab965335c51c3e15e39b132229807c828c23",
+        "C": "7e1c0cbcc37b311cabe367bdd85dcacc1fc5e5dcbb374bf12ea224832a6ad91c"
+    },
+    {
+        "TEST": "4",
+        "X": "311b616d5e92ea7e7d6bb8e2",
+        "R": "ec3712c94f66098933c57abbc46c4298bb276eceac3597654e1fd72ba434191d",
+        "C": "a6b6083665669c6de43d0dcd15f29964ffcd9075ecd46ca8e58711ab359002c3"
+    },
+    {
+        "TEST": "5",
+        "X": "1ce58293a6ab70f62b4c724e60",
+        "R": "1c4c20f3d10a5af166e45afbc88c98008fff529b6cc8ce008d375848712a0778",
+        "C": "90986555e5c9bbace9048913ef0eb44a06ed8b5dd76ae3090b4911c6fa2b4d7b"
+    },
+    {
+        "TEST": "6",
+        "X": "e1c508ee1862474e1080bbaefe89",
+        "R": "4d812c8519837c696dc1967adb2452972119bea337174182768fd0f781d41b6d",
+        "C": "83594768dbd29e0fc7dedc0dd999e573a478292305d6764ed3b981d26cc55666"
+    },
+    {
+        "TEST": "7",
+        "X": "df5b2f3e4a2f35e81dc93f9d33b540",
+        "R": "4d9112b5f3b34a3e6db75078e48006b8d86459cfb9ea9093272c416a56a4794b",
+        "C": "e8d031761ed9ab072dec45457710ce7a7d88695b854b8fbd42a06387972cbb79"
+    },
+    {
+        "TEST": "8",
+        "X": "966244192989663e85ee431f90182539",
+        "R": "e330c3a33e2657eca224e778952e1abfde0ca62f8da0417ca64d34b2bb16de3f",
+        "C": "1ef1b10077de18cda783dbbeeefaacc93051d2456e6fc53958506ff1cfda064c"
+    },
+    {
+        "TEST": "9",
+        "X": "76cba6658a3730513c7b7cd2135e3e1f16",
+        "R": "40576370e36018f6bfaffc4c66780303a361f0c5f4a18a86a74fb179ca0fcf22",
+        "C": "88c318a79e481cc7ce7041bb3e66e50cbae6b88efaaa649a4b6b06fb6351b952"
+    }
+]