Skip to content

Commit

Permalink
OpenSSL 3 support (#996)
Browse files Browse the repository at this point in the history
* Update API usage to OpenSSL 3 (#989)

These changes are supposed to make Themis more compatible with OpenSSL 3
by dropping usage of deprecated functions and using new slternatives instead.
There are also places where non-deprecated functions were used, but they turned
out to be incompatible with EVP_PKEY* created using newer API. Such places are
affected as well, using `#if` macro to conditionally compile code based on target
OpenSSL version.

Update CMakeLists.txt, add flags
* to control building for OpenSSL 3 using
  WITH_EXPERIMENTAL_OPENSSL_3_SUPPORT
* to disable NIST STS tests

Create copy of `soter_ec_key.c` that contains newer implementation and uses
OpenSSL 3 APIs for key serialization/deserialization routines.
Difference from OpenSSL 1.1 includes:
* Get rid of `EC_KEY*`
* Get rid of `EC_GROUP*`, use string curve identifier instead, extracted
  from `EVP_PKEY*` using `EVP_PKEY_get_utf8_string_param()`
* Get rid of `EC_POINT*`, use `EVP_PKEY_get_octet_string_param()` to
  extract curve public key from `EVP_PKEY*` directly. Deserialize public EC key
  directly from provided buffer using `EVP_PKEY_fromdata()`, this same
  function yields recreated `EVP_PKEY*` in case of success

Also, in a different file:
* Replace `EVP_MD_CTX_md()` with `EVP_MD_CTX_get0_md()`

Update CHANGELOG.md

* Update API usage to OpenSSL 3 (RSA) (#993)

* Move common RSA util functinos to separate file

* Create copy of soter_rsa_key.c for OpenSSL 3

Just a copy for now, OpenSSL 3 specific changes will follow

* Rewrite RSA keys serialization

* Get rid of `RSA*` struct usage and its depracated methods, extract
  bigints directly from `EVP_PKEY*` using EVP_PKEY_get_bn_param()
* Extract following params for public key:
  - OSSL_PKEY_PARAM_RSA_N
  - OSSL_PKEY_PARAM_RSA_E
* And a couple of additional ones for private key:
  - OSSL_PKEY_PARAM_RSA_FACTOR1 (also known as P)
  - OSSL_PKEY_PARAM_RSA_FACTOR2 (also known as Q)
  - OSSL_PKEY_PARAM_RSA_EXPONENT1
  - OSSL_PKEY_PARAM_RSA_EXPONENT2
  - OSSL_PKEY_PARAM_RSA_COEFFICIENT1

* Implement RSA key deserialization, few related fixes

Implement public and private key deserialization using new
EVP_PKEY_fromdata() function. Get rid of RSA*.

Few other places were affected as well because things are quite bound to
each other, EVP_PKEY_size() on newer key won't work correctly.

* Fix build, minor updates

* Fix build

* Fix build

* Fix build

* Fix build

* Updates after review

* Use EVP_PKEY_private_check() to check private key
* Make rsa_mod_size round value to nearest whole byte
* Remove few functions completely unused in OpenSSL 3 implementation of
  RSA routines

* Simplify bigint serialization, remove redundant functions

Just serialize BIGINTs directly with BN_bn2binpad() instead of using
functions that 1) check size 2) serialize number 3) add zeroes padding
because BN_bn2binpad() could do all that and return -1 in case
destination buffer is too small

* Make RSA key serialization functions reuse bigint

Use the fact that EVP_PKEY_get_bn_param() could write into existing
BIGINT instead of allocating a new one, reuse that single BIGINT for
multiple values

* Zeroize partially serialized RSA private key on fail

* Updates after review

* Updates after review

* Enable building on Ubuntu 22.04, remove experimental flag

* Remove unneeded CI job

* Enable build with warnings as errors

* Minor updates after review

* Fix build

* Add few more checks, move common function to separate file

* Update CHANGELOG.md

* Don't use EVP_PKEY_get_bn_param() for private keys

This function uses temporary buffer inside, asks EVP_PKEY_get_params()
to put bigint into it, makes BIGINT, doesn't clean the buffer afterwards.
Decided to instead call EVP_PKEY_get_params() manually, and free the
buffer after usage. Also have to reverse byte order in this case because
of the fact that EVP_PKEY_get_params() puts bigints in native-endian.

* Attempt to fix implementation on macOS

Explicitly zeroize buffers for bigints in case EVP_PKEY_get_params() doesn't add padding

* Attempt to fix implementation on macOS

Revert back to using BIGNUM* during key serialization, but create it
with custom get_bn_param() function.

* Fix build

Replace EVP_PKEY_get_bn_param -> get_bn_param in one more place,
remove memcpy_big_endian()

* Updates after review

* up version of macos runner and sdk

---------

Co-authored-by: Lagovas <[email protected]>
  • Loading branch information
iamnotacake and Lagovas committed Jun 9, 2023
1 parent 068a178 commit 542fd9a
Show file tree
Hide file tree
Showing 22 changed files with 1,531 additions and 268 deletions.
38 changes: 5 additions & 33 deletions .github/workflows/test-core.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ jobs:
with:
submodules: true
- name: Build Themis Core (OpenSSL)
if: ${{ matrix.os != 'ubuntu-22.04' }}
run: make prepare_tests_basic ENGINE=openssl BUILD_PATH=build-openssl
- name: Build Themis Core (OpenSSL 3.0)
if: ${{ matrix.os != 'ubuntu-20.04' }}
Expand All @@ -70,21 +69,15 @@ jobs:
export ENGINE_INCLUDE_PATH="$openssl3/include"
export ENGINE_LIB_PATH="$openssl3/lib"
fi
# TODO: stop using deprecated API so that warnings can be errors again
export WITH_EXPERIMENTAL_OPENSSL_3_SUPPORT=yes
export WITH_FATAL_WARNINGS=no
make prepare_tests_basic BUILD_PATH=build-openssl-3.0
- name: Build Themis Core (BoringSSL)
if: always()
run: make prepare_tests_basic ENGINE=boringssl BUILD_PATH=build-boringssl
- name: Build Themis Core (WITH_SCELL_COMPAT)
if: ${{ matrix.os != 'ubuntu-22.04' }}
run: make prepare_tests_basic WITH_SCELL_COMPAT=yes BUILD_PATH=build-compat
- name: Run test suite (OpenSSL)
if: ${{ matrix.os != 'ubuntu-22.04' }}
run: make test ENGINE=openssl BUILD_PATH=build-openssl
- name: Run test suite (OpenSSL, uncompressed keys)
if: ${{ matrix.os != 'ubuntu-22.04' }}
env:
THEMIS_GEN_EC_KEY_PAIR_UNCOMPRESSED: "1"
run: make test ENGINE=openssl BUILD_PATH=build-openssl
Expand All @@ -98,8 +91,6 @@ jobs:
export ENGINE_INCLUDE_PATH="$openssl3/include"
export ENGINE_LIB_PATH="$openssl3/lib"
fi
export WITH_EXPERIMENTAL_OPENSSL_3_SUPPORT=yes
export WITH_FATAL_WARNINGS=no
make test BUILD_PATH=build-openssl-3.0
- name: Run test suite (OpenSSL 3.0, uncompressed keys)
if: ${{ matrix.os != 'ubuntu-20.04' }}
Expand All @@ -113,8 +104,6 @@ jobs:
export ENGINE_INCLUDE_PATH="$openssl3/include"
export ENGINE_LIB_PATH="$openssl3/lib"
fi
export WITH_EXPERIMENTAL_OPENSSL_3_SUPPORT=yes
export WITH_FATAL_WARNINGS=no
make test BUILD_PATH=build-openssl-3.0
- name: Run test suite (BoringSSL)
if: always()
Expand All @@ -125,26 +114,7 @@ jobs:
THEMIS_GEN_EC_KEY_PAIR_UNCOMPRESSED: "1"
run: make test ENGINE=boringssl BUILD_PATH=build-boringssl
- name: Run test suite (WITH_SCELL_COMPAT)
if: ${{ matrix.os != 'ubuntu-22.04' }}
run: make test WITH_SCELL_COMPAT=yes BUILD_PATH=build-compat
- name: Ensure OpenSSL 3.0 fails
if: ${{ matrix.os != 'ubuntu-20.04' }}
run: |
export ENGINE=openssl
# Themis uses OpenSSL 1.1 by default if installed.
# Explicitly request OpenSSL 3.0 by pointing the build into OpenSSL 3.0's paths.
if [[ "$MATRIX_OS" = "macos-12" ]]; then
openssl3=$(brew --prefix openssl@3)
export ENGINE_INCLUDE_PATH="$openssl3/include"
export ENGINE_LIB_PATH="$openssl3/lib"
fi
if ! make BUILD_PATH=build-openssl-3.0-without-magic-word
then
true
else
echo "Build with OpenSSL 3.0 did not fail when it should have"
exit 1
fi

examples:
name: Code examples
Expand Down Expand Up @@ -432,7 +402,9 @@ jobs:
runs-on: windows-2022
env:
SOTER_KDF_RUN_LONG_TESTS: yes
# TODO: stop using deprecated API so that warnings can be errors again
# TODO: remove these two options when MSYS2 build issues are solved,
# because now PKGBUILD.MSYS2 just downloads latest release archive
# from GitHub instead of using checked out files
WITH_EXPERIMENTAL_OPENSSL_3_SUPPORT: yes
WITH_FATAL_WARNINGS: "no" # YAML :trollface:
defaults:
Expand Down Expand Up @@ -482,7 +454,7 @@ jobs:
macos-cross-compile:
name: macOS cross toolchain
runs-on: macos-10.15
runs-on: macos-12
steps:
- name: Install system dependencies
run: |
Expand All @@ -498,7 +470,7 @@ jobs:
# and that changes the available SDKs. Check the combinations here:
# https://github.com/actions/virtual-environments/blob/main/images/macos/macos-10.15-Readme.md#xcode
- name: Build Themis Core (BoringSSL, arm64)
run: make SDK=macosx11.1 ARCH=arm64 ENGINE=boringssl
run: make SDK=macosx13.1 ARCH=arm64 ENGINE=boringssl
# Of course we can't run unit tests either because there is no emulator.
# So I will be satisifed to see the build succeed and produce binaries
# with expected architecture.
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ _Code:_
- **Soter** (low-level security core used by Themis)

- `soter_sign_export_key()` is now deprecated, superseded by `soter_sign_export_private_key()` and `soter_sign_export_public_key()` ([#959](https://github.com/cossacklabs/themis/pull/959))
- better OpenSSL 3 support, with many EC- and RSA-related deprecated functions being replaced with newer alternatives, and OpenSSL 1.X is still supported
- removed build option THEMIS_EXPERIMENTAL_OPENSSL_3_SUPPORT since building/linking with OpenSSL 3 now works out of the box

- **Android**

Expand Down
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ project(themis)
cmake_minimum_required(VERSION 3.8)
include_directories(src)

if (NO_NIST_STS)
add_definitions(-DNO_NIST_STS=1)
endif()

file(GLOB SOTER_SOURCE_FILES src/soter/*.c src/soter/openssl/*.c src/soter/ed25519/*)
add_library(soter ${SOTER_SOURCE_FILES})
Expand Down
4 changes: 0 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,6 @@ ifeq ($(RSA_KEY_LENGTH),8192)
CFLAGS += -DTHEMIS_RSA_KEY_LENGTH=RSA_KEY_LENGTH_8192
endif

ifeq ($(WITH_EXPERIMENTAL_OPENSSL_3_SUPPORT),yes)
CFLAGS += -DTHEMIS_EXPERIMENTAL_OPENSSL_3_SUPPORT=1
endif

########################################################################
#
# Compilation flags for C/C++ code
Expand Down
80 changes: 64 additions & 16 deletions src/soter/openssl/soter_asym_cipher.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
#include "soter/soter_asym_cipher.h"

#include <openssl/evp.h>
#include <openssl/opensslv.h>
#include <openssl/rsa.h>
#if OPENSSL_VERSION_NUMBER >= 0x30000000
#include <openssl/core_names.h>
#endif

#include "soter/openssl/soter_engine.h"
#include "soter/soter_api.h"
Expand All @@ -30,6 +34,63 @@
#define SOTER_RSA_KEY_LENGTH 2048
#endif

#if OPENSSL_VERSION_NUMBER >= 0x30000000
static bool get_rsa_mod_size(EVP_PKEY* pkey, int* rsa_mod_size)
{
if (!EVP_PKEY_get_int_param(pkey, OSSL_PKEY_PARAM_BITS, rsa_mod_size)) {
return false;
}
*rsa_mod_size = (*rsa_mod_size + 7) / 8;
return true;
}
#else
static bool get_rsa_mod_size(EVP_PKEY* pkey, int* rsa_mod_size)
{
RSA* rsa = EVP_PKEY_get0(pkey);
if (!rsa) {
return false;
}
*rsa_mod_size = RSA_size(rsa);
return true;
}
#endif

#if OPENSSL_VERSION_NUMBER < 0x10100000L
static bool check_rsa_private_key(EVP_PKEY* pkey, EVP_PKEY_CTX* pkey_ctx)
{
UNUSED(pkey_ctx);
RSA* rsa = EVP_PKEY_get0(pkey);

if (rsa->d == NULL) {
return false;
}

return true;
}
#elif OPENSSL_VERSION_NUMBER < 0x30000000
static bool check_rsa_private_key(EVP_PKEY* pkey, EVP_PKEY_CTX* pkey_ctx)
{
UNUSED(pkey_ctx);
RSA* rsa = EVP_PKEY_get0(pkey);
const BIGNUM* d = NULL;

RSA_get0_key(rsa, NULL, NULL, &d);

if (NULL == d) {
return false;
}

return true;
}
#else /* OPENSSL_VERSION_NUMBER >= 0x30000000 */
static bool check_rsa_private_key(EVP_PKEY* pkey, EVP_PKEY_CTX* pkey_ctx)
{
UNUSED(pkey);
// EVP_PKEY_private_check was added in 3.0
return EVP_PKEY_private_check(pkey_ctx) == 1;
}
#endif

soter_status_t soter_asym_cipher_import_key(soter_asym_cipher_t* asym_cipher_ctx,
const void* key,
size_t key_length)
Expand Down Expand Up @@ -139,7 +200,6 @@ soter_status_t soter_asym_cipher_encrypt(soter_asym_cipher_t* asym_cipher,
size_t* cipher_data_length)
{
EVP_PKEY* pkey;
RSA* rsa;
int rsa_mod_size;
size_t output_length;

Expand All @@ -158,12 +218,10 @@ soter_status_t soter_asym_cipher_encrypt(soter_asym_cipher_t* asym_cipher,
return SOTER_INVALID_PARAMETER;
}

rsa = EVP_PKEY_get0(pkey);
if (NULL == rsa) {
if (!get_rsa_mod_size(pkey, &rsa_mod_size)) {
return SOTER_FAIL;
}

rsa_mod_size = RSA_size(rsa);
int oaep_max_payload_length = (rsa_mod_size - 2 - (2 * OAEP_HASH_SIZE));
if (oaep_max_payload_length < 0 || plain_data_length > (size_t)oaep_max_payload_length) {
/* The plaindata is too large for this key size */
Expand Down Expand Up @@ -223,8 +281,6 @@ soter_status_t soter_asym_cipher_decrypt(soter_asym_cipher_t* asym_cipher,
size_t* plain_data_length)
{
EVP_PKEY* pkey;
RSA* rsa;
const BIGNUM* d = NULL;
int rsa_mod_size;
size_t output_length;

Expand All @@ -243,13 +299,10 @@ soter_status_t soter_asym_cipher_decrypt(soter_asym_cipher_t* asym_cipher,
return SOTER_INVALID_PARAMETER;
}

rsa = EVP_PKEY_get0(pkey);
if (NULL == rsa) {
if (!get_rsa_mod_size(pkey, &rsa_mod_size)) {
return SOTER_FAIL;
}

rsa_mod_size = RSA_size(rsa);

if (rsa_mod_size < 0 || cipher_data_length < (size_t)rsa_mod_size) {
/* The cipherdata is too small for this key size */
return SOTER_INVALID_PARAMETER;
Expand All @@ -258,12 +311,7 @@ soter_status_t soter_asym_cipher_decrypt(soter_asym_cipher_t* asym_cipher,
/* we can only decrypt, if we have the private key */
/* some versions of OpenSSL just crash, if you send RSA public key to EVP_PKEY_decrypt, so we do
* checks here */
#if OPENSSL_VERSION_NUMBER < 0x10100000L
d = rsa->d;
#else
RSA_get0_key(rsa, NULL, NULL, &d);
#endif
if (NULL == d) {
if (!check_rsa_private_key(pkey, asym_cipher->pkey_ctx)) {
return SOTER_INVALID_PARAMETER;
}

Expand Down
70 changes: 70 additions & 0 deletions src/soter/openssl/soter_bignum_utils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright (c) 2023 Cossack Labs Limited
*
* Licensed 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.
*/

#ifndef THEMIS_SOTER_BIGNUM_UTILS_H
#define THEMIS_SOTER_BIGNUM_UTILS_H

#include <openssl/bn.h>
#include <openssl/opensslv.h>
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
#include <openssl/params.h>
#endif

#if OPENSSL_VERSION_NUMBER < 0x10100000L
/* Simple implementation for OpenSSL <1.1.0 where this function is missing */
static int BN_bn2binpad(const BIGNUM* a, unsigned char* to, int tolen)
{
int bn_size = BN_num_bytes(a);
int bytes_copied;

if (a == NULL || to == NULL) {
return -1;
}

if (tolen < bn_size) {
return -1;
}

bytes_copied = BN_bn2bin(a, to + (tolen - bn_size));

if (bytes_copied != bn_size) {
return -1;
}

memset(to, 0, (size_t)(tolen - bn_size));

return tolen;
}
#endif

#if OPENSSL_VERSION_NUMBER >= 0x30000000L
static int get_bn_param(
const EVP_PKEY* pkey, const char* name, unsigned char* buf, size_t buf_size, BIGNUM** bn)
{
OSSL_PARAM params[2];

params[0] = OSSL_PARAM_construct_BN(name, buf, buf_size);
params[1] = OSSL_PARAM_construct_end();

if (!EVP_PKEY_get_params(pkey, params)) {
return 0;
}

return OSSL_PARAM_get_BN(&params[0], bn);
}
#endif

#endif /* THEMIS_SOTER_BIGNUM_UTILS_H */
Loading

0 comments on commit 542fd9a

Please sign in to comment.