Add RSA wrappers
diff --git a/README.md b/README.md
index 760f798..d46afd8 100644
--- a/README.md
+++ b/README.md
@@ -132,6 +132,22 @@
 ## Python
 
 There is a Python wrapper in ./python.
+You can to specify the RSA levels to build in the wrappers using
+the cmake flag `PYTHON_RSA_LEVELS`. Supported levels are 2048 and 4096.
+E.g.
+
+```
+cmake -DPYTHON_RSA_LEVELS="2048,4096" ..
+```
+
+In order for the RSA wrappers to work, the appropriate dynamic
+libraries need to be generated and installed for AMCL. For instance, to
+install the dynamic libraries for RSA 2048 and 4069, modify the AMCL cmake
+build as follows.
+
+```
+cmake -D CMAKE_BUILD_TYPE=Release -D BUILD_SHARED_LIBS=ON -D AMCL_CHUNK=64 -D AMCL_CURVE="BLS381,SECP256K1" -D AMCL_RSA="2048,4096" -D BUILD_PAILLIER=ON -D BUILD_PYTHON=ON -D BUILD_BLS=ON -D BUILD_WCC=OFF -D BUILD_MPIN=ON -D BUILD_X509=OFF -D CMAKE_INSTALL_PREFIX=/usr/local ..
+```
 
 ## Virtual machine
 
diff --git a/cmake/PythonParameters.cmake b/cmake/PythonParameters.cmake
new file mode 100644
index 0000000..cccae1b
--- /dev/null
+++ b/cmake/PythonParameters.cmake
@@ -0,0 +1,45 @@
+# 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.
+
+set(PYTHON_RSA_FIELDS TB   TFF  BASE ML HML)
+set(PYTHON_RSA_2048   1024 2048 58   2  1  )
+set(PYTHON_RSA_4096   512  4096 60   8  4  )
+
+# Load RSA parameter in parent scope
+function(load_rsa_fields level)
+  if (NOT PYTHON_RSA_${level})
+    message(FATAL_ERROR "Invalid RSA level: ${level}")
+  endif()
+  
+  foreach(field ${PYTHON_RSA_FIELDS})
+    list(FIND PYTHON_RSA_FIELDS "${field}" index)
+    list(GET  PYTHON_RSA_${level} ${index} ${field})
+    set("${field}" "${${field}}" PARENT_SCOPE)
+  endforeach()
+
+  set(BD "${TB}_${BASE}" PARENT_SCOPE)
+endfunction()
+
+# Configure file
+macro(configure_rsa_file source target)
+  configure_file("${source}" "${target}" @ONLY)
+  file(READ "${target}" temp)
+  string(REPLACE WWW "${TFF}" temp "${temp}")
+  string(REPLACE XXX "${BD}"  temp "${temp}")
+
+  file(WRITE "${target}" "${temp}")
+endmacro()
\ No newline at end of file
diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt
index 5971f47..5c5401e 100644
--- a/python/CMakeLists.txt
+++ b/python/CMakeLists.txt
@@ -16,6 +16,13 @@
 cmake_minimum_required(VERSION 3.1 FATAL_ERROR)
 
 include(PythonSiteDirs)
+include(PythonParameters)
+
+if(NOT DEFINED PYTHON_RSA_LEVELS)
+  set(PYTHON_RSA_LEVELS "")
+endif()
+
+string(REPLACE "," ";" PYTHON_RSA_LEVELS "${PYTHON_RSA_LEVELS}")
 
 add_subdirectory(amcl)
 add_subdirectory(test)
diff --git a/python/amcl/CMakeLists.txt b/python/amcl/CMakeLists.txt
index be778cc..c5481d2 100644
--- a/python/amcl/CMakeLists.txt
+++ b/python/amcl/CMakeLists.txt
@@ -16,12 +16,17 @@
 file(GLOB SRCS *.py)
 file(COPY ${SRCS} DESTINATION "${PROJECT_BINARY_DIR}/python/amcl")
 
+foreach(level ${PYTHON_RSA_LEVELS})
+  load_rsa_fields(${level})
+  configure_rsa_file("rsa.py.in" "${PROJECT_BINARY_DIR}/python/amcl/rsa_${TFF}.py")
+endforeach()
+
 install(DIRECTORY DESTINATION ${PYTHON_SITE_PACKAGES}/amcl DIRECTORY_PERMISSIONS
         OWNER_WRITE OWNER_READ OWNER_EXECUTE
         GROUP_READ GROUP_EXECUTE
         WORLD_READ WORLD_EXECUTE)
-      
+
 install(FILES ${SRCS} DESTINATION ${PYTHON_SITE_PACKAGES}/amcl PERMISSIONS
   OWNER_WRITE OWNER_READ OWNER_EXECUTE
-  GROUP_READ 
+  GROUP_READ
   WORLD_READ)
diff --git a/python/amcl/rsa.py.in b/python/amcl/rsa.py.in
new file mode 100644
index 0000000..b4f3f53
--- /dev/null
+++ b/python/amcl/rsa.py.in
@@ -0,0 +1,323 @@
+"""
+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 for the amcl RSA.
+
+"""
+
+from . import core_utils
+import platform
+
+_ffi = core_utils._ffi
+_ffi.cdef("""
+#define FFLEN_WWW @ML@
+#define HFLEN_WWW @HML@
+
+typedef signed int sign32;
+
+typedef struct
+{
+    sign32 e;
+    BIG_XXX n[FFLEN_WWW];
+} rsa_public_key_WWW;
+
+typedef struct
+{
+    BIG_XXX p[HFLEN_WWW];
+    BIG_XXX q[HFLEN_WWW];
+    BIG_XXX dp[HFLEN_WWW];
+    BIG_XXX dq[HFLEN_WWW];
+    BIG_XXX c[HFLEN_WWW];
+} rsa_private_key_WWW;
+
+extern void FF_WWW_toOctet(octet *X, BIG_XXX *x, int n);
+
+extern void RSA_WWW_KEY_PAIR(csprng *R,sign32 e,rsa_private_key_WWW* PRIV,rsa_public_key_WWW* PUB,octet *P, octet* Q);
+extern void RSA_WWW_ENCRYPT(rsa_public_key_WWW* PUB,octet *F,octet *G);
+extern void RSA_WWW_DECRYPT(rsa_private_key_WWW* PRIV,octet *G,octet *F);
+extern void RSA_WWW_PRIVATE_KEY_KILL(rsa_private_key_WWW *PRIV);
+extern void RSA_WWW_fromOctet(BIG_XXX *x,octet *S);
+
+extern int OAEP_ENCODE(int h,octet *M,csprng *R,octet *P,octet *F);
+extern int OAEP_DECODE(int h,octet *P,octet *F);
+""")
+
+if (platform.system() == 'Windows'):
+    _libamcl_rsa_WWW = _ffi.dlopen("libamcl_rsa_WWW.dll")
+elif (platform.system() == 'Darwin'):
+    _libamcl_rsa_WWW = _ffi.dlopen("libamcl_rsa_WWW.dylib")
+else:
+    _libamcl_rsa_WWW = _ffi.dlopen("libamcl_rsa_WWW.so")
+
+# Constants
+FFLEN  = @ML@        # FF size in BIGs
+FS     = @TFF@ // 8  # FF size in bytes
+SHA256 = 32
+SHA384 = 48
+SHA512 = 64
+
+OK   = 0
+FAIL = -1
+
+
+def generate_key_pair(rng, e, p = None, q = None):
+    """ Generate an RSA key pair
+
+    Generate an RSA key pair with encryption exponent e
+
+    Args::
+
+        rng: pointer to cryptographically secure prng
+        e:   decryption exponent. Integer
+        p:   Secret prime for the RSA modulus
+        q:   Secret prime for the RSA modulus
+
+    Returns::
+        public_key:  pointer to an RSA public key
+        private_key: pointer to an RSA private key
+
+    Raises::
+        Exception
+    """
+    if p and q:
+        p_oct, p_oct_val = core_utils.make_octet(None, p)
+        q_oct, q_oct_val = core_utils.make_octet(None, q)
+        _ = p_oct_val, q_oct_val
+        rng = _ffi.NULL
+    else:
+        p_oct = _ffi.NULL
+        q_oct = _ffi.NULL
+
+    public_key  = _ffi.new('rsa_public_key_WWW*')
+    private_key = _ffi.new('rsa_private_key_WWW*')
+
+    _libamcl_rsa_WWW.RSA_WWW_KEY_PAIR(rng, _ffi.cast("sign32", e), private_key, public_key, p_oct, q_oct)
+
+    if p_oct is not _ffi.NULL:
+        core_utils.clear_octet(p_oct)
+
+    if q_oct is not _ffi.NULL:
+        core_utils.clear_octet(q_oct)
+
+    return public_key, private_key
+
+
+def encrypt(public_key, pt):
+    """ RSA Encrypt
+
+    Encrypt a message pt to the given public key
+
+    Args::
+
+        public_key: RSA public key
+        pt:         input padded message. SHA bytes
+
+    Returns::
+        ct: output ciphertext
+
+    Raises::
+        Exception
+    """
+    pt_oct, pt_val = core_utils.make_octet(None, pt)
+    ct_oct, ct_val = core_utils.make_octet(FS)
+    _ = pt_val, ct_val
+
+    _libamcl_rsa_WWW.RSA_WWW_ENCRYPT(public_key, pt_oct, ct_oct)
+
+    core_utils.clear_octet(pt_oct)
+
+    return core_utils.to_str(ct_oct)
+
+
+def decrypt(private_key, ct):
+    """ RSA Decrypt
+
+    Decrypt a ciphertext ct using the given private key
+
+    Args::
+
+        private_key: RSA private key
+        ct:         input ciphertext
+
+    Returns::
+        pt: output plaintext. SHA bytes
+
+    Raises::
+        Exception
+    """
+    pt_oct, pt_val = core_utils.make_octet(FS)
+    ct_oct, ct_val = core_utils.make_octet(None, ct)
+    _ = pt_val, ct_val
+
+    _libamcl_rsa_WWW.RSA_WWW_DECRYPT(private_key, ct_oct, pt_oct)
+
+    pt = core_utils.to_str(pt_oct)
+
+    # Clear memory
+    core_utils.clear_octet(pt_oct)
+
+    return pt
+
+
+def kill_private_key(private_key):
+    """ Kill RSA Private Key
+
+    Clean secrets from an RSA private key
+
+    Args::
+
+        private_key: RSA private key to kill
+
+    Raises::
+        Exception
+    """
+    _libamcl_rsa_WWW.RSA_WWW_PRIVATE_KEY_KILL(private_key)
+
+def public_key_to_bytes(public_key):
+    """ Export public key to bytes
+
+    Export the public key modulus as bytes.
+    The public key exponent can be accessed as an integer
+
+    Args::
+
+        public_key: RSA private key to export
+
+    Returns::
+
+        n: public modulus of the public key
+
+    Raises::
+        Exception
+    """
+    n_oct, n_val = core_utils.make_octet(FS)
+    _ = n_val
+
+    _libamcl_rsa_WWW.FF_WWW_toOctet(n_oct, public_key.n, FFLEN)
+
+    return core_utils.to_str(n_oct)
+
+def public_key_from_bytes(n):
+    """ Import  public key from bytes
+
+    Import the public key modulus from bytes.
+    The public key exponent can be directly set as an integer
+
+    Args::
+
+        n: public modulus of the public key
+
+    Returns::
+
+        public_key: imported public key
+
+    Raises::
+        Exception
+    """
+    n_oct, n_val = core_utils.make_octet(None, n)
+    _ = n_val
+
+    public_key = _ffi.new('rsa_public_key_WWW*')
+
+    _libamcl_rsa_WWW.RSA_WWW_fromOctet(public_key.n, n_oct)
+
+    return public_key
+
+def oaep_encode(rng, sha, m, params=None):
+    """ Apply OAEP padding to the given message m
+
+    OAEP padding of the message m for RSA encryption.
+
+    Args::
+
+        rng:    pointer to cryptograpically secure PRNG
+        sha:    hash type. Supported types are SHA256, SHA384 and SHA512
+        m:      message to pad
+        params: optional parameter string for padding
+
+    Returns::
+
+        pt: padded message
+        rc: 0 if the message was succesfully padded or an error code
+
+    Raises::
+        Exception
+    """
+    if params is None:
+        p_oct = _ffi.NULL
+    else:
+        p_oct, p_val = core_utils.make_octet(None, params)
+        _ = p_val
+
+    m_oct, m_val   = core_utils.make_octet(None, m)
+    pt_oct, pt_val = core_utils.make_octet(FS)
+    _ = m_val, pt_val
+
+    rc = _libamcl_rsa_WWW.OAEP_ENCODE(sha, m_oct, rng, p_oct, pt_oct)
+    if rc != 0:
+        return None, FAIL
+
+    pt = core_utils.to_str(pt_oct)
+
+    # Clean memory
+    core_utils.clear_octet(pt_oct)
+    core_utils.clear_octet(m_oct)
+
+    return pt, OK
+
+
+def oaep_decode(sha, pt, params=None):
+    """ Remove OAEP padding from the given plaintext pt
+
+    OAEP unpadding of the plaintext pt to recover the message m
+
+    Args::
+
+        sha: hash type. Supported types are SHA256, SHA384 and SHA512
+        pt:  plaintext from RSA decryption
+
+    Returns::
+
+        m:  unpadded message
+        rc: 0 if the message was succesfully unpadded or an error code
+
+    Raises::
+        Exception
+    """
+    if params is None:
+        p_oct = _ffi.NULL
+    else:
+        p_oct, p_val = core_utils.make_octet(None, params)
+        _ = p_val
+
+    pt_oct, pt_val = core_utils.make_octet(None, pt)
+    _ = pt_val
+
+    rc = _libamcl_rsa_WWW.OAEP_DECODE(sha, p_oct, pt_oct)
+    if rc != 0:
+        return None, FAIL
+
+    m = core_utils.to_str(pt_oct)
+
+    # Clear memory
+    core_utils.clear_octet(pt_oct)
+
+    return m, OK
diff --git a/python/benchmark/CMakeLists.txt b/python/benchmark/CMakeLists.txt
index 8a39633..3dd91bd 100644
--- a/python/benchmark/CMakeLists.txt
+++ b/python/benchmark/CMakeLists.txt
@@ -15,3 +15,8 @@
 
 file(GLOB BENCH *.py)
 file(COPY ${BENCH} DESTINATION "${PROJECT_BINARY_DIR}/python/benchmark")
+
+foreach(level ${PYTHON_RSA_LEVELS})
+  load_rsa_fields(${level})
+  configure_rsa_file("bench_rsa.py.in" "${PROJECT_BINARY_DIR}/python/benchmark/bench_rsa_${TFF}.py")
+endforeach()
diff --git a/python/benchmark/bench_rsa.py.in b/python/benchmark/bench_rsa.py.in
new file mode 100755
index 0000000..754928b
--- /dev/null
+++ b/python/benchmark/bench_rsa.py.in
@@ -0,0 +1,67 @@
+#!/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 core_utils, rsa_WWW
+
+pt_2048_hex = "53ea5dc08cd260fb3b858567287fa91552c30b2febfba213f0ae87702d068d19bab07fe574523dfb42139d68c3c5afeee0bfe4cb7969cbf382b804d6e61396144e2d0e60741f8993c3014b58b9b1957a8babcd23af854f4c356fb1662aa72bfcc7e586559dc4280d160c126785a723ebeebeff71f11594440aaef87d10793a8774a239d4a04c87fe1467b9daf85208ec6c7255794a96cc29142f9a8bd418e3c1fd67344b0cd0829df3b2bec60253196293c6b34d3f75d32f213dd45c6273d505adf4cced1057cb758fc26aeefa441255ed4e64c199ee075e7f16646182fdb464739b68ab5daff0e63e9552016824f054bf4d3c8c90a97bb6b6553284eb429fcc"
+pt_4096_hex = "53ea5dc08cd260fb3b858567287fa91552c30b2febfba213f0ae87702d068d19bab07fe574523dfb42139d68c3c5afeee0bfe4cb7969cbf382b804d6e61396144e2d0e60741f8993c3014b58b9b1957a8babcd23af854f4c356fb1662aa72bfcc7e586559dc4280d160c126785a723ebeebeff71f11594440aaef87d10793a8774a239d4a04c87fe1467b9daf85208ec6c7255794a96cc29142f9a8bd418e3c1fd67344b0cd0829df3b2bec60253196293c6b34d3f75d32f213dd45c6273d505adf4cced1057cb758fc26aeefa441255ed4e64c199ee075e7f16646182fdb464739b68ab5daff0e63e9552016824f054bf4d3c8c90a97bb6b6553284eb429fcc53ea5dc08cd260fb3b858567287fa91552c30b2febfba213f0ae87702d068d19bab07fe574523dfb42139d68c3c5afeee0bfe4cb7969cbf382b804d6e61396144e2d0e60741f8993c3014b58b9b1957a8babcd23af854f4c356fb1662aa72bfcc7e586559dc4280d160c126785a723ebeebeff71f11594440aaef87d10793a8774a239d4a04c87fe1467b9daf85208ec6c7255794a96cc29142f9a8bd418e3c1fd67344b0cd0829df3b2bec60253196293c6b34d3f75d32f213dd45c6273d505adf4cced1057cb758fc26aeefa441255ed4e64c199ee075e7f16646182fdb464739b68ab5daff0e63e9552016824f054bf4d3c8c90a97bb6b6553284eb429fcc"
+e = 0x10001
+
+seed_hex = "78d0fb6705ce77dee47d03eb5b9c5d30"
+
+if __name__ == "__main__":
+    pt = bytes.fromhex(pt_WWW_hex)
+    seed = bytes.fromhex(seed_hex)
+
+    rng = core_utils.create_csprng(seed)
+
+    m = b'test message'
+
+    # Generate quantities for bench run
+    public_key, private_key = rsa_WWW.generate_key_pair(rng, e)
+
+    enc, rc = rsa_WWW.oaep_encode(rng, rsa_WWW.SHA256, m)
+    assert rc == 0, 'OAEP encode failure'
+
+    _, rc = rsa_WWW.oaep_decode(rsa_WWW.SHA256, enc)
+    assert rc == 0, 'OAEP decode failure'
+
+    # Run benchmark
+    fncall = lambda: rsa_WWW.generate_key_pair(rng, e)
+    time_func("rsa_WWW.generate_key_pair", fncall)
+
+    fncall = lambda: rsa_WWW.encrypt(public_key, pt)
+    time_func("rsa_WWW.encrypt          ", fncall, unit = 'us')
+
+    fncall = lambda: rsa_WWW.decrypt(private_key, pt)
+    time_func("rsa_WWW.decrypt          ", fncall)
+
+    fncall = lambda: rsa_WWW.oaep_encode(rng, rsa_WWW.SHA256, m)
+    time_func("rsa_WWW.oaep_encode    ", fncall, unit = 'us')
+
+    fncall = lambda: rsa_WWW.oaep_decode(rsa_WWW.SHA256, enc)
+    time_func("rsa_WWW.oaep_decode     ", fncall, unit = 'us')
\ No newline at end of file
diff --git a/python/examples/CMakeLists.txt b/python/examples/CMakeLists.txt
index 1d768ba..763c69b 100644
--- a/python/examples/CMakeLists.txt
+++ b/python/examples/CMakeLists.txt
@@ -15,3 +15,8 @@
 
 file(GLOB EXAMPLES *.py)
 file(COPY ${EXAMPLES} DESTINATION "${PROJECT_BINARY_DIR}/python/examples")
+
+foreach(level ${PYTHON_RSA_LEVELS})
+  load_rsa_fields(${level})
+  configure_rsa_file("example_rsa.py.in" "${PROJECT_BINARY_DIR}/python/examples/example_rsa_${TFF}.py")
+endforeach()
diff --git a/python/examples/example_rsa.py.in b/python/examples/example_rsa.py.in
new file mode 100755
index 0000000..66025d9
--- /dev/null
+++ b/python/examples/example_rsa.py.in
@@ -0,0 +1,56 @@
+#!/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, rsa_WWW
+
+seed_hex = "78d0fb6705ce77dee47d03eb5b9c5d30"
+
+e = 0x10001
+
+if __name__ == "__main__":
+    seed = bytes.fromhex(seed_hex)
+    rng = core_utils.create_csprng(seed)
+
+    m = b'test message'
+
+    print('Generate key pair')
+    public_key, private_key = rsa_WWW.generate_key_pair(rng, e)
+
+    print(f"\nEncode message '{m.decode('utf-8')}'")
+    pt, rc = rsa_WWW.oaep_encode(rng, rsa_WWW.SHA256, m)
+    assert rc == 0, 'Failure OAEP padding message'
+
+    print(f"\nEncrypt plaintext '{pt.hex()}'")
+    ct = rsa_WWW.encrypt(public_key, pt)
+
+    print(f"\nDecrypt cyphertext {ct.hex()}")
+    dec_pt = rsa_WWW.decrypt(private_key, ct)
+
+    print(f"\nDecode plaintext '{dec_pt.hex()}'")
+    dec_m, rc = rsa_WWW.oaep_decode(rsa_WWW.SHA256, dec_pt)
+    assert rc == 0, 'Failure OAEP unpadding message'
+
+    print(f"Recovered message '{dec_m.decode('utf-8')}'")
diff --git a/python/test/CMakeLists.txt b/python/test/CMakeLists.txt
index bdc05c3..ced24cd 100644
--- a/python/test/CMakeLists.txt
+++ b/python/test/CMakeLists.txt
@@ -67,3 +67,10 @@
   add_python_test(test_python_mpc_nm_commit    test_nm_commit.py)
   add_python_test(test_python_mpc_zk_factoring test_zk_factoring.py)
 endif(NOT CMAKE_BUILD_TYPE STREQUAL "ASan")
+
+foreach(level ${PYTHON_RSA_LEVELS})
+  load_rsa_fields(${level})
+  configure_rsa_file("test_rsa.py.in" "${PROJECT_BINARY_DIR}/python/test/test_rsa_${TFF}.py")
+
+  add_python_test(test_python_rsa_${TFF} test_rsa_${TFF}.py)
+endforeach()
diff --git a/python/test/test_rsa.py.in b/python/test/test_rsa.py.in
new file mode 100755
index 0000000..0e53533
--- /dev/null
+++ b/python/test/test_rsa.py.in
@@ -0,0 +1,143 @@
+#!/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
+import unittest
+
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+
+from amcl import core_utils, rsa_WWW
+
+p_2048_hex  = "94f689d07ba20cf7c7ca7ccbed22ae6b40c426db74eaee4ce0ced2b6f52a5e136663f5f1ef379cdbb0c4fdd6e4074d6cff21082d4803d43d89e42fd8dfa82b135aa31a8844ffea25f255f956cbc1b9d8631d01baf1010d028a190b94ce40f3b72897e8196df19edf1ff62e6556f2701d52cef1442e3301db7608ecbdcca703db"
+q_2048_hex  = "9a9ad73f246df853e129c589925fdad9df05606a61081e62e72be4fb33f6e5ec492cc734f28bfb71fbe2ba9a11e4c02e2c0d103a5cbb0a9d6402c07de63b1b995dd72ac8f29825d66923a088b421fb4d52b0b855d2f5dde2be9b0ca0cee6f7a94e5566735fe6cff1fcad3199602f88528d19aa8d0263adff8f5053c38254a2a3"
+pt_2048_hex = "53ea5dc08cd260fb3b858567287fa91552c30b2febfba213f0ae87702d068d19bab07fe574523dfb42139d68c3c5afeee0bfe4cb7969cbf382b804d6e61396144e2d0e60741f8993c3014b58b9b1957a8babcd23af854f4c356fb1662aa72bfcc7e586559dc4280d160c126785a723ebeebeff71f11594440aaef87d10793a8774a239d4a04c87fe1467b9daf85208ec6c7255794a96cc29142f9a8bd418e3c1fd67344b0cd0829df3b2bec60253196293c6b34d3f75d32f213dd45c6273d505adf4cced1057cb758fc26aeefa441255ed4e64c199ee075e7f16646182fdb464739b68ab5daff0e63e9552016824f054bf4d3c8c90a97bb6b6553284eb429fcc"
+
+p_4096_hex  = "b18f69bd4e52677d48d846055988877ce9e97b962f01e3f425f3101a6a589f020c858b1ee5ae8f79e4c63ce2356d8a9a4ef144d3a55e05badfbebdb0e97594cdb4ebebd6177b2eb04149aa463ede7ba2216657e3b4de42f496c0d493b4d734131e63edcde042d951b9bf285622b9d69e9ee170156deeb173725032a952068e685aa31a8844ffea25f255f956cbc1b9d8631d01baf1010d028a190b94ce40f3b72897e8196df19edf1ff62e6556f2701d52cef1442e3301db7608ecbdcca6ef9994f689d07ba20cf7c7ca7ccbed22ae6b40c426db74eaee4ce0ced2b6f52a5e136663f5f1ef379cdbb0c4fdd6e4074d6cff21082d4803d43d89e42fd8dfa82c0f"
+q_4096_hex  = "e87190e478b1132e3c05ade06a196858b4d24a4c8350ce9ecda7f0a1c4e3e75c136c250dd8b67e377670021e4810e0f19f3ecdc780b836febc939fc7ad7c70300323bf4b24f03e8656bb49614fcbfe0687fef150ce34e646806a2b4369259ecc2c01c796be2a2317f4a9974f4ee101a63ac1383091fde717dac1fe529abb6a276559c8185776c332b98f51d55c85311af1138e9a8858693142d0109383929143d17ed7645d22afcad045d85eba7c5df02ed0bd4d9a8f22d30865d538ba933a1579377f979390894ab558922352acaa05d94aa8fa9d273f35912d5efabaaf647ebdb03e55db04941df0409bc2a124a2732ac989186a4987bcbcbc1dfb4e91f79f"
+pt_4096_hex = "53ea5dc08cd260fb3b858567287fa91552c30b2febfba213f0ae87702d068d19bab07fe574523dfb42139d68c3c5afeee0bfe4cb7969cbf382b804d6e61396144e2d0e60741f8993c3014b58b9b1957a8babcd23af854f4c356fb1662aa72bfcc7e586559dc4280d160c126785a723ebeebeff71f11594440aaef87d10793a8774a239d4a04c87fe1467b9daf85208ec6c7255794a96cc29142f9a8bd418e3c1fd67344b0cd0829df3b2bec60253196293c6b34d3f75d32f213dd45c6273d505adf4cced1057cb758fc26aeefa441255ed4e64c199ee075e7f16646182fdb464739b68ab5daff0e63e9552016824f054bf4d3c8c90a97bb6b6553284eb429fcc53ea5dc08cd260fb3b858567287fa91552c30b2febfba213f0ae87702d068d19bab07fe574523dfb42139d68c3c5afeee0bfe4cb7969cbf382b804d6e61396144e2d0e60741f8993c3014b58b9b1957a8babcd23af854f4c356fb1662aa72bfcc7e586559dc4280d160c126785a723ebeebeff71f11594440aaef87d10793a8774a239d4a04c87fe1467b9daf85208ec6c7255794a96cc29142f9a8bd418e3c1fd67344b0cd0829df3b2bec60253196293c6b34d3f75d32f213dd45c6273d505adf4cced1057cb758fc26aeefa441255ed4e64c199ee075e7f16646182fdb464739b68ab5daff0e63e9552016824f054bf4d3c8c90a97bb6b6553284eb429fcc"
+
+e = 0x10001
+
+
+class TestBareRSA(unittest.TestCase):
+    """ Test RSA2048 API """
+
+    def setUp(self):
+        # Deterministic PRNG for testing purposes
+        seed_hex = "78d0fb6705ce77dee47d03eb5b9c5d30"
+        seed = bytes.fromhex(seed_hex)
+        self.rng = core_utils.create_csprng(seed)
+
+        self.p =  bytes.fromhex(p_WWW_hex)
+        self.q =  bytes.fromhex(q_WWW_hex)
+        self.pt = bytes.fromhex(pt_WWW_hex)
+
+    def test_pq(self):
+        public_key, private_key = rsa_WWW.generate_key_pair(None, e, p=self.p, q=self.q)
+
+        ct = rsa_WWW.encrypt(public_key, self.pt)
+        pt = rsa_WWW.decrypt(private_key, ct)
+        self.assertEqual(pt, self.pt)
+
+    def test_rng(self):
+        public_key, private_key = rsa_WWW.generate_key_pair(self.rng, e)
+
+        ct = rsa_WWW.encrypt(public_key, self.pt)
+        pt = rsa_WWW.decrypt(private_key, ct)
+        self.assertEqual(pt, self.pt)
+
+
+class TestOAEP(unittest.TestCase):
+    """ Test RSA2048 OAEP encryption/decryption """
+
+    def setUp(self):
+        # Deterministic PRNG for testing purposes
+        seed_hex = "78d0fb6705ce77dee47d03eb5b9c5d30"
+        seed = bytes.fromhex(seed_hex)
+        self.rng = core_utils.create_csprng(seed)
+
+        self.p =  bytes.fromhex(p_WWW_hex)
+        self.q =  bytes.fromhex(q_WWW_hex)
+        self.pt = bytes.fromhex(pt_WWW_hex)
+
+        self.long_bytes = ('a'*(rsa_WWW.FS + 1)).encode('utf-8')
+
+    def test_consistency(self):
+        m = b'test_message'
+        pt, rc = rsa_WWW.oaep_encode(self.rng, rsa_WWW.SHA256, m)
+        self.assertEqual(rc, 0)
+
+        m_dec, rc = rsa_WWW.oaep_decode(rsa_WWW.SHA256, pt)
+        self.assertEqual(rc, 0)
+        self.assertEqual(m, m_dec)
+
+    def test_encryption_decryption(self):
+        public_key, private_key = rsa_WWW.generate_key_pair(None, e, self.p, self.q)
+
+        m = b'test_encryption_decryption'
+        pt, rc = rsa_WWW.oaep_encode(self.rng, rsa_WWW.SHA256, m)
+        self.assertEqual(rc, 0)
+
+        ct = rsa_WWW.encrypt(public_key, pt)
+        pt_dec = rsa_WWW.decrypt(private_key, ct)
+        self.assertEqual(pt_dec, pt)
+
+        m_dec, rc = rsa_WWW.oaep_decode(rsa_WWW.SHA256, pt_dec)
+        self.assertEqual(rc, 0)
+        self.assertEqual(m_dec, m)
+
+    def test_encoding_error(self):
+        enc, rc = rsa_WWW.oaep_encode(self.rng, rsa_WWW.SHA256, self.long_bytes)
+        self.assertEqual(rc, -1)
+        self.assertEqual(enc, None)
+
+    def test_decoding_error(self):
+        dec, rc = rsa_WWW.oaep_decode(rsa_WWW.SHA256, self.long_bytes)
+        self.assertEqual(rc, -1)
+        self.assertEqual(dec, None)
+
+
+class TestIO(unittest.TestCase):
+    """ Test RSA2048 I/O for public key """
+
+    def setUp(self):
+        p =  bytes.fromhex(p_WWW_hex)
+        q =  bytes.fromhex(q_WWW_hex)
+        self.public_key, self.private_key = rsa_WWW.generate_key_pair(None, e, p, q)
+
+        self.pt = bytes.fromhex(pt_WWW_hex)
+
+    def test_consistency(self):
+        n = rsa_WWW.public_key_to_bytes(self.public_key)
+        e = self.public_key.e
+
+        public_key = rsa_WWW.public_key_from_bytes(n)
+        public_key.e = e
+
+        ct = rsa_WWW.encrypt(public_key, self.pt)
+        pt = rsa_WWW.decrypt(self.private_key, ct)
+        self.assertEqual(pt, self.pt)
+
+if __name__ == '__main__':
+    # Run tests
+    unittest.main()