From 00e826a3ae33f5cb50476f68df971fbb06434a39 Mon Sep 17 00:00:00 2001 From: Dmitriy Musatkin <63878209+DmitriyMusatkin@users.noreply.github.com> Date: Wed, 18 Oct 2023 17:26:06 -0700 Subject: [PATCH] rsa bindings (#511) Co-authored-by: Michael Graeb --- .gitignore | 223 ++++++++++++++++++++++++++++----- awscrt/crypto.py | 89 +++++++++++++ crt/aws-c-cal | 2 +- docsrc/source/api/crypto.rst | 5 + docsrc/source/index.rst | 1 + source/crypto.c | 233 +++++++++++++++++++++++++++++++++++ source/crypto.h | 10 ++ source/io.h | 2 +- source/module.c | 8 ++ test/test_crypto.py | 100 ++++++++++++++- 10 files changed, 639 insertions(+), 34 deletions(-) create mode 100644 docsrc/source/api/crypto.rst diff --git a/.gitignore b/.gitignore index 58824d991..e81f6b675 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ - -# Created by https://www.gitignore.io/api/git,c++,cmake,python,visualstudio,visualstudiocode +# Created by https://www.toptal.com/developers/gitignore/api/git,c++,cmake,python,visualstudio,visualstudiocode,macos +# Edit at https://www.toptal.com/developers/gitignore?templates=git,c++,cmake,python,visualstudio,visualstudiocode,macos ### C++ ### # Prerequisites @@ -36,6 +36,7 @@ *.app ### CMake ### +CMakeLists.txt.user CMakeCache.txt CMakeFiles CMakeScripts @@ -45,6 +46,11 @@ cmake_install.cmake install_manifest.txt compile_commands.json CTestTestfile.cmake +_deps + +### CMake Patch ### +# External projects +*-prefix/ ### Git ### # Created by git for backups. To disable backups in Git: @@ -61,6 +67,39 @@ CTestTestfile.cmake *_LOCAL_*.txt *_REMOTE_*.txt +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + ### Python ### # Byte-compiled / optimized / DLL files __pycache__/ @@ -72,7 +111,6 @@ __pycache__/ # Distribution / packaging .Python build/ -deps_build/ develop-eggs/ dist/ downloads/ @@ -83,8 +121,8 @@ lib64/ parts/ sdist/ var/ -wheelhouse/ wheels/ +share/python-wheels/ *.egg-info/ .installed.cfg *.egg @@ -110,8 +148,10 @@ htmlcov/ nosetests.xml coverage.xml *.cover +*.py,cover .hypothesis/ .pytest_cache/ +cover/ # Translations *.mo @@ -121,6 +161,7 @@ coverage.xml *.log local_settings.py db.sqlite3 +db.sqlite3-journal # Flask stuff: instance/ @@ -129,7 +170,11 @@ instance/ # Scrapy stuff: .scrapy +# Sphinx documentation +docs/_build/ + # PyBuilder +.pybuilder/ target/ # Jupyter Notebook @@ -140,10 +185,38 @@ profile_default/ ipython_config.py # pyenv -.python-version - -# celery beat schedule file +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff celerybeat-schedule +celerybeat.pid # SageMath parsed files *.sage.py @@ -172,29 +245,59 @@ venv.bak/ .dmypy.json dmypy.json +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + ### Python Patch ### -.venv/ - -### Python.VirtualEnv Stack ### -# Virtualenv -# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ -[Ii]nclude -[Ll]ib -[Ll]ib64 -[Ll]ocal -pyvenv.cfg -pip-selfcheck.json +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json ### VisualStudioCode ### .vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide ### VisualStudio ### ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore # User-specific files +*.rsuser *.suo *.user *.userosscache @@ -203,6 +306,9 @@ pip-selfcheck.json # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs +# Mono auto generated files +mono_crash.* + # Build results [Dd]ebug/ [Dd]ebugPublic/ @@ -210,9 +316,14 @@ pip-selfcheck.json [Rr]eleases/ x64/ x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ bld/ +[Bb]in/ [Oo]bj/ [Ll]og/ +[Ll]ogs/ # Visual Studio 2015/2017 cache/options directory .vs/ @@ -226,9 +337,10 @@ Generated\ Files/ [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* -# NUNIT +# NUnit *.VisualState.xml TestResult.xml +nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ @@ -243,6 +355,9 @@ project.lock.json project.fragment.lock.json artifacts/ +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + # StyleCop StyleCopReport.xml @@ -265,6 +380,7 @@ StyleCopReport.xml *.tmp *.tmp_proj *_wpftmp.csproj +*.tlog *.vspscc *.vssscc .builds @@ -306,9 +422,6 @@ _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user -# JustCode is a .NET coding add-in -.JustCode - # TeamCity is a build add-in _TeamCity* @@ -319,6 +432,11 @@ _TeamCity* .axoCover/* !.axoCover/settings.json +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + # Visual Studio code coverage results *.coverage *.coveragexml @@ -366,6 +484,8 @@ PublishScripts/ # NuGet Packages *.nupkg +# NuGet Symbol Packages +*.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. @@ -390,12 +510,14 @@ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx +*.appxbundle +*.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache -!*.[Cc]ache/ +!?*.[Cc]ache/ # Others ClientBin/ @@ -439,6 +561,9 @@ ServiceFabricBackup/ *.bim.layout *.bim_*.settings *.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ @@ -459,6 +584,15 @@ node_modules/ # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files + # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts @@ -474,10 +608,6 @@ paket-files/ # FAKE - F# Make .fake/ -# JetBrains Rider -.idea/ -*.sln.iml - # CodeRush personal settings .cr/personal @@ -518,8 +648,41 @@ ASALocalRun/ # Local History for Visual Studio .localhistory/ +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +*.code-workspace + +# Local History for Visual Studio Code + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +### VisualStudio Patch ### +# Additional files built by Visual Studio -# End of https://www.gitignore.io/api/git,c++,cmake,python,visualstudio,visualstudiocode +# End of +# https://www.toptal.com/developers/gitignore/api/git,c++,cmake,python,visualstudio,visualstudiocode,macos # credentials .key @@ -530,4 +693,4 @@ ASALocalRun/ deps/ # API docs are updated automatically by .github/workflows/docs.yml -docs/ +docs/ \ No newline at end of file diff --git a/awscrt/crypto.py b/awscrt/crypto.py index 6663052c5..35b7027fa 100644 --- a/awscrt/crypto.py +++ b/awscrt/crypto.py @@ -2,6 +2,9 @@ # SPDX-License-Identifier: Apache-2.0. import _awscrt +from awscrt import NativeResource +from typing import Union +from enum import IntEnum class Hash: @@ -59,3 +62,89 @@ def update(self, to_hmac): def digest(self, truncate_to=0): return _awscrt.hmac_digest(self._hmac, truncate_to) + + +class RSAEncryptionAlgorithm(IntEnum): + """RSA Encryption Algorithm""" + + PKCS1_5 = 0 + """ + PKCSv1.5 padding + """ + + OAEP_SHA256 = 1 + """ + OAEP padding with sha256 hash function + """ + + OAEP_SHA512 = 2 + """ + OAEP padding with sha512 hash function + """ + + +class RSASignatureAlgorithm(IntEnum): + """RSA Encryption Algorithm""" + + PKCS1_5_SHA256 = 0 + """ + PKCSv1.5 padding with sha256 hash function + """ + + PSS_SHA256 = 1 + """ + PSS padding with sha256 hash function + """ + + +class RSA(NativeResource): + def __init__(self, binding): + super().__init__() + self._binding = binding + + @staticmethod + def new_private_key_from_pem_data(pem_data: Union[str, bytes, bytearray, memoryview]) -> 'RSA': + """ + Creates a new instance of private RSA key pair from pem data. + Raises ValueError if pem does not have private key object. + """ + return RSA(binding=_awscrt.rsa_private_key_from_pem_data(pem_data)) + + @staticmethod + def new_public_key_from_pem_data(pem_data: Union[str, bytes, bytearray, memoryview]) -> 'RSA': + """ + Creates a new instance of public RSA key pair from pem data. + Raises ValueError if pem does not have public key object. + """ + return RSA(binding=_awscrt.rsa_public_key_from_pem_data(pem_data)) + + def encrypt(self, encryption_algorithm: RSAEncryptionAlgorithm, + plaintext: Union[bytes, bytearray, memoryview]) -> bytes: + """ + Encrypts data using a given algorithm. + """ + return _awscrt.rsa_encrypt(self._binding, encryption_algorithm, plaintext) + + def decrypt(self, encryption_algorithm: RSAEncryptionAlgorithm, + ciphertext: Union[bytes, bytearray, memoryview]) -> bytes: + """ + Decrypts data using a given algorithm. + """ + return _awscrt.rsa_decrypt(self._binding, encryption_algorithm, ciphertext) + + def sign(self, signature_algorithm: RSASignatureAlgorithm, + digest: Union[bytes, bytearray, memoryview]) -> bytes: + """ + Signs data using a given algorithm. + Note: function expects digest of the message, ex sha256 + """ + return _awscrt.rsa_sign(self._binding, signature_algorithm, digest) + + def verify(self, signature_algorithm: RSASignatureAlgorithm, + digest: Union[bytes, bytearray, memoryview], + signature: Union[bytes, bytearray, memoryview]) -> bool: + """ + Verifies signature against digest. + Returns True if signature matches and False if not. + """ + return _awscrt.rsa_verify(self._binding, signature_algorithm, digest, signature) diff --git a/crt/aws-c-cal b/crt/aws-c-cal index a916a84ec..033a132df 160000 --- a/crt/aws-c-cal +++ b/crt/aws-c-cal @@ -1 +1 @@ -Subproject commit a916a84ec07d028fa7d8c09d4aecaa81df7e8a23 +Subproject commit 033a132dfbcd1822ac65969a1feee31d6e1a81f9 diff --git a/docsrc/source/api/crypto.rst b/docsrc/source/api/crypto.rst new file mode 100644 index 000000000..806d09b95 --- /dev/null +++ b/docsrc/source/api/crypto.rst @@ -0,0 +1,5 @@ +awscrt.crypto +============= + +.. automodule:: awscrt.crypto + :members: diff --git a/docsrc/source/index.rst b/docsrc/source/index.rst index 58349df2e..0558f3f54 100644 --- a/docsrc/source/index.rst +++ b/docsrc/source/index.rst @@ -13,6 +13,7 @@ API Reference api/auth api/common + api/crypto api/exceptions api/eventstream api/http diff --git a/source/crypto.c b/source/crypto.c index 4238d00d2..249e9276f 100644 --- a/source/crypto.c +++ b/source/crypto.c @@ -7,9 +7,12 @@ #include "aws/cal/hash.h" #include "aws/cal/hmac.h" +#include "aws/cal/rsa.h" +#include "aws/io/pem.h" const char *s_capsule_name_hash = "aws_hash"; const char *s_capsule_name_hmac = "aws_hmac"; +const char *s_capsule_name_rsa = "aws_rsa"; static void s_hash_destructor(PyObject *hash_capsule) { assert(PyCapsule_CheckExact(hash_capsule)); @@ -238,3 +241,233 @@ PyObject *aws_py_hmac_digest(PyObject *self, PyObject *args) { return PyBytes_FromStringAndSize((const char *)output, digest_buf.len); } + +static void s_rsa_destructor(PyObject *rsa_capsule) { + struct aws_rsa_key_pair *key_pair = PyCapsule_GetPointer(rsa_capsule, s_capsule_name_rsa); + assert(key_pair); + + aws_rsa_key_pair_release(key_pair); +} + +struct aws_pem_object *s_find_pem_object(struct aws_array_list *pem_list, enum aws_pem_object_type pem_type) { + for (size_t i = 0; i < aws_array_list_length(pem_list); ++i) { + struct aws_pem_object *pem_object = NULL; + if (aws_array_list_get_at_ptr(pem_list, (void **)&pem_object, 0)) { + return NULL; + } + + if (pem_object->type == pem_type) { + return pem_object; + } + } + + return NULL; +} + +PyObject *aws_py_rsa_private_key_from_pem_data(PyObject *self, PyObject *args) { + (void)self; + + struct aws_byte_cursor pem_data_cur; + if (!PyArg_ParseTuple(args, "s#", &pem_data_cur.ptr, &pem_data_cur.len)) { + return NULL; + } + + PyObject *capsule = NULL; + struct aws_allocator *allocator = aws_py_get_allocator(); + struct aws_array_list pem_list; + if (aws_pem_objects_init_from_file_contents(&pem_list, allocator, pem_data_cur)) { + return PyErr_AwsLastError(); + } + + /* From hereon, we need to clean up if errors occur */ + + struct aws_pem_object *found_pem_object = s_find_pem_object(&pem_list, AWS_PEM_TYPE_PRIVATE_RSA_PKCS1); + + if (found_pem_object == NULL) { + PyErr_SetString(PyExc_ValueError, "RSA private key not found in PEM."); + goto on_done; + } + + struct aws_rsa_key_pair *key_pair = + aws_rsa_key_pair_new_from_private_key_pkcs1(allocator, aws_byte_cursor_from_buf(&found_pem_object->data)); + + if (key_pair == NULL) { + PyErr_AwsLastError(); + goto on_done; + } + + capsule = PyCapsule_New(key_pair, s_capsule_name_rsa, s_rsa_destructor); + + if (capsule == NULL) { + aws_rsa_key_pair_release(key_pair); + } + +on_done: + aws_pem_objects_clean_up(&pem_list); + return capsule; +} + +PyObject *aws_py_rsa_public_key_from_pem_data(PyObject *self, PyObject *args) { + (void)self; + + struct aws_byte_cursor pem_data_cur; + if (!PyArg_ParseTuple(args, "s#", &pem_data_cur.ptr, &pem_data_cur.len)) { + return NULL; + } + + PyObject *capsule = NULL; + struct aws_allocator *allocator = aws_py_get_allocator(); + struct aws_array_list pem_list; + if (aws_pem_objects_init_from_file_contents(&pem_list, allocator, pem_data_cur)) { + return PyErr_AwsLastError(); + } + + /* From hereon, we need to clean up if errors occur */ + + struct aws_pem_object *found_pem_object = s_find_pem_object(&pem_list, AWS_PEM_TYPE_PUBLIC_RSA_PKCS1); + + if (found_pem_object == NULL) { + PyErr_SetString(PyExc_ValueError, "RSA public key not found in PEM."); + goto on_done; + } + + struct aws_rsa_key_pair *key_pair = + aws_rsa_key_pair_new_from_public_key_pkcs1(allocator, aws_byte_cursor_from_buf(&found_pem_object->data)); + + if (key_pair == NULL) { + PyErr_AwsLastError(); + goto on_done; + } + + capsule = PyCapsule_New(key_pair, s_capsule_name_rsa, s_rsa_destructor); + + if (capsule == NULL) { + aws_rsa_key_pair_release(key_pair); + } + +on_done: + aws_pem_objects_clean_up(&pem_list); + return capsule; +} + +PyObject *aws_py_rsa_encrypt(PyObject *self, PyObject *args) { + (void)self; + + struct aws_allocator *allocator = aws_py_get_allocator(); + PyObject *rsa_capsule = NULL; + int encrypt_algo = 0; + struct aws_byte_cursor plaintext_cur; + if (!PyArg_ParseTuple(args, "Ois#", &rsa_capsule, &encrypt_algo, &plaintext_cur.ptr, &plaintext_cur.len)) { + return NULL; + } + + struct aws_rsa_key_pair *rsa = PyCapsule_GetPointer(rsa_capsule, s_capsule_name_rsa); + if (rsa == NULL) { + return NULL; + } + + struct aws_byte_buf result_buf; + aws_byte_buf_init(&result_buf, allocator, aws_rsa_key_pair_block_length(rsa)); + + if (aws_rsa_key_pair_encrypt(rsa, encrypt_algo, plaintext_cur, &result_buf)) { + aws_byte_buf_clean_up_secure(&result_buf); + return PyErr_AwsLastError(); + } + + PyObject *ret = PyBytes_FromStringAndSize((const char *)result_buf.buffer, result_buf.len); + aws_byte_buf_clean_up_secure(&result_buf); + return ret; +} + +PyObject *aws_py_rsa_decrypt(PyObject *self, PyObject *args) { + (void)self; + + struct aws_allocator *allocator = aws_py_get_allocator(); + PyObject *rsa_capsule = NULL; + int encrypt_algo = 0; + struct aws_byte_cursor ciphertext_cur; + if (!PyArg_ParseTuple(args, "Oiy#", &rsa_capsule, &encrypt_algo, &ciphertext_cur.ptr, &ciphertext_cur.len)) { + return NULL; + } + + struct aws_rsa_key_pair *rsa = PyCapsule_GetPointer(rsa_capsule, s_capsule_name_rsa); + if (rsa == NULL) { + return NULL; + } + + struct aws_byte_buf result_buf; + aws_byte_buf_init(&result_buf, allocator, aws_rsa_key_pair_block_length(rsa)); + + if (aws_rsa_key_pair_decrypt(rsa, encrypt_algo, ciphertext_cur, &result_buf)) { + aws_byte_buf_clean_up_secure(&result_buf); + return PyErr_AwsLastError(); + } + + PyObject *ret = PyBytes_FromStringAndSize((const char *)result_buf.buffer, result_buf.len); + aws_byte_buf_clean_up_secure(&result_buf); + return ret; +} + +PyObject *aws_py_rsa_sign(PyObject *self, PyObject *args) { + (void)self; + + struct aws_allocator *allocator = aws_py_get_allocator(); + PyObject *rsa_capsule = NULL; + int sign_algo = 0; + struct aws_byte_cursor digest_cur; + if (!PyArg_ParseTuple(args, "Oiy#", &rsa_capsule, &sign_algo, &digest_cur.ptr, &digest_cur.len)) { + return NULL; + } + + struct aws_rsa_key_pair *rsa = PyCapsule_GetPointer(rsa_capsule, s_capsule_name_rsa); + if (rsa == NULL) { + return NULL; + } + + struct aws_byte_buf result_buf; + aws_byte_buf_init(&result_buf, allocator, aws_rsa_key_pair_signature_length(rsa)); + + if (aws_rsa_key_pair_sign_message(rsa, sign_algo, digest_cur, &result_buf)) { + aws_byte_buf_clean_up_secure(&result_buf); + return PyErr_AwsLastError(); + } + + PyObject *ret = PyBytes_FromStringAndSize((const char *)result_buf.buffer, result_buf.len); + aws_byte_buf_clean_up_secure(&result_buf); + return ret; +} + +PyObject *aws_py_rsa_verify(PyObject *self, PyObject *args) { + (void)self; + + PyObject *rsa_capsule = NULL; + int sign_algo = 0; + struct aws_byte_cursor digest_cur; + struct aws_byte_cursor signature_cur; + if (!PyArg_ParseTuple( + args, + "Oiy#y#", + &rsa_capsule, + &sign_algo, + &digest_cur.ptr, + &digest_cur.len, + &signature_cur.ptr, + &signature_cur.len)) { + return NULL; + } + + struct aws_rsa_key_pair *rsa = PyCapsule_GetPointer(rsa_capsule, s_capsule_name_rsa); + if (rsa == NULL) { + return NULL; + } + + if (aws_rsa_key_pair_verify_signature(rsa, sign_algo, digest_cur, signature_cur)) { + if (aws_last_error() == AWS_ERROR_CAL_SIGNATURE_VALIDATION_FAILED) { + aws_reset_error(); + Py_RETURN_FALSE; + } + return PyErr_AwsLastError(); + } + + Py_RETURN_TRUE; +} diff --git a/source/crypto.h b/source/crypto.h index 374aaa26e..4c03e65a4 100644 --- a/source/crypto.h +++ b/source/crypto.h @@ -10,6 +10,8 @@ extern const char *s_capsule_name_hash; /** Name string for hmac capsule. */ extern const char *s_capsule_name_hmac; +/** Name string for rsa capsule. */ +extern const char *s_capsule_name_rsa; PyObject *aws_py_sha1_new(PyObject *self, PyObject *args); PyObject *aws_py_sha256_new(PyObject *self, PyObject *args); @@ -27,4 +29,12 @@ PyObject *aws_py_sha256_compute(PyObject *self, PyObject *args); PyObject *aws_py_md5_compute(PyObject *self, PyObject *args); PyObject *aws_py_sha256_hmac_compute(PyObject *self, PyObject *args); +PyObject *aws_py_rsa_private_key_from_pem_data(PyObject *self, PyObject *args); +PyObject *aws_py_rsa_public_key_from_pem_data(PyObject *self, PyObject *args); + +PyObject *aws_py_rsa_encrypt(PyObject *self, PyObject *args); +PyObject *aws_py_rsa_decrypt(PyObject *self, PyObject *args); +PyObject *aws_py_rsa_sign(PyObject *self, PyObject *args); +PyObject *aws_py_rsa_verify(PyObject *self, PyObject *args); + #endif /* AWS_CRT_PYTHON_CRYPTO_H */ diff --git a/source/io.h b/source/io.h index c1d1cbc70..4fbb1064d 100644 --- a/source/io.h +++ b/source/io.h @@ -29,7 +29,7 @@ PyObject *aws_py_init_logging(PyObject *self, PyObject *args); PyObject *aws_py_is_alpn_available(PyObject *self, PyObject *args); /** - * Returns True if the input TLS Cipher Preference Enum is suupported on the current platform. False otherwise. + * Returns True if the input TLS Cipher Preference Enum is supported on the current platform. False otherwise. */ PyObject *aws_py_is_tls_cipher_supported(PyObject *self, PyObject *args); diff --git a/source/module.c b/source/module.c index a971bb6d0..132f1353d 100644 --- a/source/module.c +++ b/source/module.c @@ -719,6 +719,14 @@ static PyMethodDef s_module_methods[] = { AWS_PY_METHOD_DEF(hash_update, METH_VARARGS), AWS_PY_METHOD_DEF(hash_digest, METH_VARARGS), + /* RSA crypto primitives */ + AWS_PY_METHOD_DEF(rsa_private_key_from_pem_data, METH_VARARGS), + AWS_PY_METHOD_DEF(rsa_public_key_from_pem_data, METH_VARARGS), + AWS_PY_METHOD_DEF(rsa_encrypt, METH_VARARGS), + AWS_PY_METHOD_DEF(rsa_decrypt, METH_VARARGS), + AWS_PY_METHOD_DEF(rsa_sign, METH_VARARGS), + AWS_PY_METHOD_DEF(rsa_verify, METH_VARARGS), + /* Checksum primitives */ AWS_PY_METHOD_DEF(checksums_crc32, METH_VARARGS), AWS_PY_METHOD_DEF(checksums_crc32c, METH_VARARGS), diff --git a/test/test_crypto.py b/test/test_crypto.py index 06d01db10..7c74a6335 100644 --- a/test/test_crypto.py +++ b/test/test_crypto.py @@ -3,10 +3,50 @@ from test import NativeResourceTest -from awscrt.crypto import Hash -from awscrt.io import TlsCipherPref +from awscrt.crypto import Hash, RSA, RSAEncryptionAlgorithm, RSASignatureAlgorithm import unittest +RSA_PRIVATE_KEY_PEM = """ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAxaEsLWE2t3kJqsF1sFHYk7rSCGfGTSDa+3r5typT0cb/TtJ9 +89C8dLcfInx4Dxq0ewo6NOxQ/TD8JevUda86jSh1UKEQUOl7qy+QwOhFMpwHq/uO +gMy5khDDLlkxD5U32RrDfqLK+4WUDapHlQ6g+E6wS1j1yDRoTZJk3WnTpR0sJHst +tLWV+mb2wPC7TkhGMbFMzbt6v0ahF7abVOOGiHVZ77uhS66hgP9nfgMHug8EN/xm +Vc/TxgMJci1Irh66xVZQ9aT2OZwb0TXglULm+b8HM+GKHgoTMwr9gAGpFDoYi22P +vxC/cqKHKIaYw7KNOPwImzQ6cp5oQJTAPQKRUwIDAQABAoIBACcuUfTZPiDX1UvO +OQfw4hA/zJ4v/MeTyPZspg9jS+TeIAW/g4sQChzVpU2QAbl04O031NxjMZdQ29yk +yaVfTStpJwEKPZLdB1CkCH3GTtm+x2KYZ+MvM2c6/Yc11Z0yRzU6siFsIvQEwpqG +9NQfZ1hzOU5m36uGgFtIt8iRz4z/RxpZUOXpaEosb0uMK3VPBuZBu8uVQBFdyAA7 +xAGtJphxQ5u0Ct9aidPjD7MhCVzcb2XbgCgxb2hbCmDMOgeNVYrTo2fdBzNxLcXv +j4sUNmO+mLbUMFOePuP8JZaGNTTmznZkavskozfdbubuS3/4/0HH1goytFheVt1B +vfxzpgkCgYEA9QgEMKny0knDHV7BC2uAd7Vvd+5iikA3WdJ9i11zas9AbMMmf9cX +E3xNt6DO42hnVCNN4uAWH5uGWltWZ8pmGKk6mesqZfYPsyTz1cK6fP6KyQrkWRNT +V3nRMEMbziAWxFD5hxP9p1KlqI2Py+W4fJ0LGZ4Mwvn3dKYOilxK+50CgYEAznny +ZxQiJGt8/FtH9f/GDIY24Cz53Cuj+BWG2EH4kLo24ET2QTVvohFJVCm3Hf8Qe4cA +ASabRUg1vS4Tr2FmIqD2Iw/ogSmDcJdYuwhdtWKa8fDbehCN5hmXjn2WKYvjvZNv +Gcx6gfqULD9SaQv+N7lL8eJxKiLLBeVYD7qoha8CgYA8udnf/Z5yQ1mZw8vv+pqC +EHMps+iz/qo5FpOKoIRkKiz7R3oZIMNVTu8r3Syo600Aayd4XLTe7HplllFZs62N +2xLs5n1Be7P0X+oWRgZVx/e5T3u8H6/98/DGFzui4A0EZlURBwFMII1xsnO6wpnw +ODNyC9t5zt1nCWh9HdZveQKBgAm4+E8eRZVNcm83pSXSS3Mfhsn7lDBn5aqy6Mya +HqhB/H+G/8mGSKFrCvbpl/PTpOUMMFXdiYYzpkQoPUkO3w5WYgC4qQwb9lKA7e6w +sCjwYbduzgbrbKMfJWHSTBXcvnaY0Kx4UnR4Zi3HNYw4wlnBYfAb55RCWykF6aWj +9neFAoGBAMqQA2YWCHhnRtjn4iGMrTk8iOHBd8AGBBzX9rPKXDqWlOr/iQq90qX0 +59309stR/bAhMzxOx31777XEPO1md854iXXr0XDMQlwCYkWyWb6hp4JlsqFBPMjn +nGXWA0Gp6UWgpg4Hvjdsu+0FQ3AhDMBKZZ8fBFb4EW+HRQIHPnbH +-----END RSA PRIVATE KEY----- +""" + +RSA_PUBLIC_KEY_PEM = """ +-----BEGIN RSA PUBLIC KEY----- +MIIBCgKCAQEAxaEsLWE2t3kJqsF1sFHYk7rSCGfGTSDa+3r5typT0cb/TtJ989C8 +dLcfInx4Dxq0ewo6NOxQ/TD8JevUda86jSh1UKEQUOl7qy+QwOhFMpwHq/uOgMy5 +khDDLlkxD5U32RrDfqLK+4WUDapHlQ6g+E6wS1j1yDRoTZJk3WnTpR0sJHsttLWV ++mb2wPC7TkhGMbFMzbt6v0ahF7abVOOGiHVZ77uhS66hgP9nfgMHug8EN/xmVc/T +xgMJci1Irh66xVZQ9aT2OZwb0TXglULm+b8HM+GKHgoTMwr9gAGpFDoYi22PvxC/ +cqKHKIaYw7KNOPwImzQ6cp5oQJTAPQKRUwIDAQAB +-----END RSA PUBLIC KEY----- +""" + class TestCredentials(NativeResourceTest): @@ -76,6 +116,62 @@ def test_md5_iterated(self): expected = b'\x90\x01\x50\x98\x3c\xd2\x4f\xb0\xd6\x96\x3f\x7d\x28\xe1\x7f\x72' self.assertEqual(expected, digest) + def test_rsa_encryption_roundtrip(self): + param_list = [RSAEncryptionAlgorithm.PKCS1_5, + RSAEncryptionAlgorithm.OAEP_SHA256, + RSAEncryptionAlgorithm.OAEP_SHA512] + + for p in param_list: + with self.subTest(msg="RSA Encryption Roundtrip using algo p", p=p): + test_pt = b'totally original test string' + rsa = RSA.new_private_key_from_pem_data(RSA_PRIVATE_KEY_PEM) + ct = rsa.encrypt(p, test_pt) + pt = rsa.decrypt(p, ct) + self.assertEqual(test_pt, pt) + + rsa_pub = RSA.new_public_key_from_pem_data(RSA_PUBLIC_KEY_PEM) + ct_pub = rsa_pub.encrypt(p, test_pt) + pt_pub = rsa.decrypt(p, ct_pub) + self.assertEqual(test_pt, pt_pub) + + def test_rsa_signing_roundtrip(self): + h = Hash.sha256_new() + h.update(b'totally original test string') + digest = h.digest() + + param_list = [RSASignatureAlgorithm.PKCS1_5_SHA256, + RSASignatureAlgorithm.PSS_SHA256] + + for p in param_list: + with self.subTest(msg="RSA Signing Roundtrip using algo p", p=p): + rsa = RSA.new_private_key_from_pem_data(RSA_PRIVATE_KEY_PEM) + signature = rsa.sign(p, digest) + self.assertTrue(rsa.verify(p, digest, signature)) + + rsa_pub = RSA.new_private_key_from_pem_data(RSA_PRIVATE_KEY_PEM) + self.assertTrue(rsa_pub.verify(p, digest, signature)) + + def test_rsa_load_error(self): + with self.assertRaises(ValueError): + RSA.new_private_key_from_pem_data(RSA_PUBLIC_KEY_PEM) + + with self.assertRaises(ValueError): + RSA.new_public_key_from_pem_data(RSA_PRIVATE_KEY_PEM) + + def test_rsa_signing_verify_fail(self): + h = Hash.sha256_new() + h.update(b'totally original test string') + digest = h.digest() + + h2 = Hash.sha256_new() + h2.update(b'another totally original test string') + digest2 = h2.digest() + + rsa = RSA.new_private_key_from_pem_data(RSA_PRIVATE_KEY_PEM) + signature = rsa.sign(RSASignatureAlgorithm.PKCS1_5_SHA256, digest) + self.assertFalse(rsa.verify(RSASignatureAlgorithm.PKCS1_5_SHA256, digest2, signature)) + self.assertFalse(rsa.verify(RSASignatureAlgorithm.PKCS1_5_SHA256, digest, b'bad signature')) + if __name__ == '__main__': unittest.main()