diff --git a/.github/downstream.d/certbot.sh b/.github/downstream.d/certbot.sh index e2890a3a100c..2479d0c25b86 100755 --- a/.github/downstream.d/certbot.sh +++ b/.github/downstream.d/certbot.sh @@ -5,8 +5,8 @@ case "${1}" in git clone --depth=1 https://github.com/certbot/certbot cd certbot git rev-parse HEAD - tools/pip_install_editable.py ./acme[dev] - tools/pip_install_editable.py ./certbot[dev] + tools/pip_install_editable.py ./acme[test] + tools/pip_install_editable.py ./certbot[test] ;; run) cd certbot diff --git a/.github/workflows/build_openssl.sh b/.github/workflows/build_openssl.sh index 99c3f4d33805..440cdcecc69f 100755 --- a/.github/workflows/build_openssl.sh +++ b/.github/workflows/build_openssl.sh @@ -22,6 +22,18 @@ if [[ "${TYPE}" == "openssl" ]]; then # avoid installing the docs (for performance) # https://github.com/openssl/openssl/issues/6685#issuecomment-403838728 make install_sw install_ssldirs + # For OpenSSL 3.0.0 set up the FIPS config. This does not activate it by + # default, but allows programmatic activation at runtime + if [[ "${VERSION}" =~ 3.0.0 && "${CONFIG_FLAGS}" =~ enable-fips ]]; then + # As of alpha16 we have to install it separately and enable it in the config flags + make -j"$(nproc)" install_fips + pushd "${OSSL_PATH}" + # include the conf file generated as part of install_fips + sed -i "s:# .include fipsmodule.cnf:.include $(pwd)/ssl/fipsmodule.cnf:" ssl/openssl.cnf + # uncomment the FIPS section + sed -i 's:# fips = fips_sect:fips = fips_sect:' ssl/openssl.cnf + popd + fi popd elif [[ "${TYPE}" == "libressl" ]]; then curl -O "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-${VERSION}.tar.gz" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd967a3a084c..493571eed857 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,23 +13,22 @@ jobs: linux: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: PYTHON: - {VERSION: "3.9", TOXENV: "flake,rust,docs", COVERAGE: "false"} - {VERSION: "pypy3", TOXENV: "pypy3"} - {VERSION: "3.9", TOXENV: "py39", OPENSSL: {TYPE: "openssl", VERSION: "1.1.0l"}} - - {VERSION: "3.9", TOXENV: "py39", OPENSSL: {TYPE: "openssl", VERSION: "1.1.1i"}} - - {VERSION: "3.9", TOXENV: "py39-ssh", OPENSSL: {TYPE: "openssl", VERSION: "1.1.1i"}} - - {VERSION: "3.9", TOXENV: "py39", OPENSSL: {TYPE: "openssl", VERSION: "1.1.1i", CONFIG_FLAGS: "no-engine no-rc2 no-srtp no-ct"}} - - {VERSION: "3.9", TOXENV: "py39", OPENSSL: {TYPE: "libressl", VERSION: "2.9.2"}} - - {VERSION: "3.9", TOXENV: "py39", OPENSSL: {TYPE: "libressl", VERSION: "3.0.2"}} - - {VERSION: "3.9", TOXENV: "py39", OPENSSL: {TYPE: "libressl", VERSION: "3.1.5"}} - - {VERSION: "3.9", TOXENV: "py39", OPENSSL: {TYPE: "libressl", VERSION: "3.2.3"}} - - {VERSION: "3.9", TOXENV: "py39", OPENSSL: {TYPE: "libressl", VERSION: "3.3.1"}} + - {VERSION: "3.9", TOXENV: "py39", OPENSSL: {TYPE: "openssl", VERSION: "1.1.1m"}} + - {VERSION: "3.9", TOXENV: "py39-ssh", OPENSSL: {TYPE: "openssl", VERSION: "1.1.1m"}} + - {VERSION: "3.9", TOXENV: "py39", OPENSSL: {TYPE: "openssl", VERSION: "1.1.1m", CONFIG_FLAGS: "no-engine no-rc2 no-srtp no-ct"}} + - {VERSION: "3.9", TOXENV: "py39", OPENSSL: {TYPE: "openssl", VERSION: "3.0.1"}} + - {VERSION: "3.9", TOXENV: "py39", TOXARGS: "--enable-fips=1", OPENSSL: {TYPE: "openssl", CONFIG_FLAGS: "enable-fips", VERSION: "3.0.1"}} + - {VERSION: "3.10", TOXENV: "py310"} RUST: - stable - name: "${{ matrix.PYTHON.TOXENV }} ${{ matrix.PYTHON.OPENSSL.TYPE }} ${{ matrix.PYTHON.OPENSSL.VERSION }} ${{ matrix.PYTHON.OPENSSL.CONFIG_FLAGS }}" - timeout-minutes: 30 + name: "${{ matrix.PYTHON.TOXENV }} ${{ matrix.PYTHON.OPENSSL.TYPE }} ${{ matrix.PYTHON.OPENSSL.VERSION }} ${{ matrix.PYTHON.TOXARGS }} ${{ matrix.PYTHON.OPENSSL.CONFIG_FLAGS }}" + timeout-minutes: 20 steps: - uses: actions/checkout@v2 - uses: actions/cache@v2 @@ -71,7 +70,7 @@ jobs: path: ${{ github.workspace }}/osslcache # When altering the openssl build process you may need to increment the value on the end of this cache key # so that you can prevent it from fetching the cache and skipping the build step. - key: ${{ matrix.PYTHON.OPENSSL.TYPE }}-${{ matrix.PYTHON.OPENSSL.VERSION }}-${{ env.CONFIG_HASH }}-1 + key: ${{ matrix.PYTHON.OPENSSL.TYPE }}-${{ matrix.PYTHON.OPENSSL.VERSION }}-${{ env.CONFIG_HASH }}-2 if: matrix.PYTHON.OPENSSL - name: Build custom OpenSSL/LibreSSL run: .github/workflows/build_openssl.sh @@ -81,12 +80,12 @@ jobs: if: matrix.PYTHON.OPENSSL && steps.ossl-cache.outputs.cache-hit != 'true' - name: Set CFLAGS/LDFLAGS run: | - echo "CFLAGS=${CFLAGS} -I${OSSL_PATH}/include" >> $GITHUB_ENV - echo "LDFLAGS=${LDFLAGS} -L${OSSL_PATH}/lib -Wl,-rpath=${OSSL_PATH}/lib" >> $GITHUB_ENV + echo "CFLAGS=${CFLAGS} -Werror=implicit-function-declaration -I${OSSL_PATH}/include" >> $GITHUB_ENV + echo "LDFLAGS=${LDFLAGS} -L${OSSL_PATH}/lib -L${OSSL_PATH}/lib64 -Wl,-rpath=${OSSL_PATH}/lib -Wl,-rpath=${OSSL_PATH}/lib64" >> $GITHUB_ENV if: matrix.PYTHON.OPENSSL - name: Tests run: | - tox -r -- --color=yes --wycheproof-root=wycheproof + tox -r -- --color=yes --wycheproof-root=wycheproof ${{ matrix.PYTHON.TOXARGS }} env: TOXENV: ${{ matrix.PYTHON.TOXENV }} - uses: ./.github/actions/upload-coverage @@ -98,6 +97,7 @@ jobs: runs-on: ubuntu-latest container: ghcr.io/pyca/cryptography-runner-${{ matrix.IMAGE.IMAGE }} strategy: + fail-fast: false matrix: IMAGE: - {IMAGE: "centos8", TOXENV: "py36"} @@ -107,10 +107,10 @@ jobs: - {IMAGE: "sid", TOXENV: "py39"} - {IMAGE: "ubuntu-bionic", TOXENV: "py36"} - {IMAGE: "ubuntu-focal", TOXENV: "py38"} - - {IMAGE: "ubuntu-rolling", TOXENV: "py38"} - - {IMAGE: "ubuntu-rolling", TOXENV: "py38-randomorder"} + - {IMAGE: "ubuntu-rolling", TOXENV: "py39"} + - {IMAGE: "ubuntu-rolling", TOXENV: "py39-randomorder"} - {IMAGE: "fedora", TOXENV: "py39"} - - {IMAGE: "alpine", TOXENV: "py38"} + - {IMAGE: "alpine", TOXENV: "py39"} name: "${{ matrix.IMAGE.TOXENV }} on ${{ matrix.IMAGE.IMAGE }}" timeout-minutes: 30 steps: @@ -139,6 +139,7 @@ jobs: linux-rust: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: PYTHON: - {VERSION: "3.9", TOXENV: "py39"} @@ -184,6 +185,7 @@ jobs: macos: runs-on: macos-latest strategy: + fail-fast: false matrix: PYTHON: - {VERSION: "3.6", TOXENV: "py36", EXTRA_CFLAGS: ""} @@ -239,6 +241,7 @@ jobs: windows: runs-on: windows-latest strategy: + fail-fast: false matrix: WINDOWS: - {ARCH: 'x86', WINDOWS: 'win32', RUST_TRIPLE: 'i686-pc-windows-msvc'} @@ -297,11 +300,11 @@ jobs: linux-downstream: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: DOWNSTREAM: - paramiko - pyopenssl - - twisted - aws-encryption-sdk - dynamodb-encryption-sdk - certbot diff --git a/.github/workflows/wheel-builder.yml b/.github/workflows/wheel-builder.yml index 528c07fd4ac0..08c358925499 100644 --- a/.github/workflows/wheel-builder.yml +++ b/.github/workflows/wheel-builder.yml @@ -10,6 +10,7 @@ jobs: runs-on: ubuntu-latest container: ghcr.io/pyca/${{ matrix.MANYLINUX.CONTAINER }} strategy: + fail-fast: false matrix: PYTHON: ["cp36-cp36m"] MANYLINUX: @@ -55,6 +56,7 @@ jobs: macos: runs-on: macos-latest strategy: + fail-fast: false matrix: PYTHON: - VERSION: '3.8' @@ -106,6 +108,7 @@ jobs: windows: runs-on: windows-latest strategy: + fail-fast: false matrix: WINDOWS: - {ARCH: 'x86', WINDOWS: 'win32', RUST_TRIPLE: 'i686-pc-windows-msvc'} diff --git a/docs/conf.py b/docs/conf.py index 0db9dd8b842d..f79db277abfa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -198,6 +198,10 @@ r"https://info.isl.ntt.co.jp/crypt/eng/camellia/", # Inconsistent small DH params they seem incapable of fixing r"https://www.secg.org/sec1-v2.pdf", + # Incomplete cert chain + r"https://e-trust.gosuslugi.ru", + # Expired cert (1 week at time of writing) + r"https://www.cosic.esat.kuleuven.be", ] autosectionlabel_prefix_document = True diff --git a/src/_cffi_src/build_openssl.py b/src/_cffi_src/build_openssl.py index 08499d66f629..723f82e13dbf 100644 --- a/src/_cffi_src/build_openssl.py +++ b/src/_cffi_src/build_openssl.py @@ -79,6 +79,8 @@ def _extra_compile_args(platform): modules=[ # This goes first so we can define some cryptography-wide symbols. "cryptography", + # Provider comes early as well so we define OSSL_LIB_CTX + "provider", "aes", "asn1", "bignum", diff --git a/src/_cffi_src/openssl/cryptography.py b/src/_cffi_src/openssl/cryptography.py index e2b5a13235ae..06d1e7787ec4 100644 --- a/src/_cffi_src/openssl/cryptography.py +++ b/src/_cffi_src/openssl/cryptography.py @@ -34,6 +34,8 @@ #define CRYPTOGRAPHY_OPENSSL_110F_OR_GREATER \ (OPENSSL_VERSION_NUMBER >= 0x1010006f && !CRYPTOGRAPHY_IS_LIBRESSL) +#define CRYPTOGRAPHY_OPENSSL_300_OR_GREATER \ + (OPENSSL_VERSION_NUMBER >= 0x30000000 && !CRYPTOGRAPHY_IS_LIBRESSL) #define CRYPTOGRAPHY_OPENSSL_LESS_THAN_110J \ (OPENSSL_VERSION_NUMBER < 0x101000af || CRYPTOGRAPHY_IS_LIBRESSL) @@ -53,6 +55,7 @@ TYPES = """ static const int CRYPTOGRAPHY_OPENSSL_110F_OR_GREATER; +static const int CRYPTOGRAPHY_OPENSSL_300_OR_GREATER; static const int CRYPTOGRAPHY_OPENSSL_LESS_THAN_111; static const int CRYPTOGRAPHY_OPENSSL_LESS_THAN_111B; diff --git a/src/_cffi_src/openssl/dh.py b/src/_cffi_src/openssl/dh.py index 979dafa94253..50989e45343a 100644 --- a/src/_cffi_src/openssl/dh.py +++ b/src/_cffi_src/openssl/dh.py @@ -18,7 +18,6 @@ void DH_free(DH *); int DH_size(const DH *); int DH_generate_key(DH *); -int DH_compute_key(unsigned char *, const BIGNUM *, DH *); DH *DHparams_dup(DH *); /* added in 1.1.0 when the DH struct was opaqued */ diff --git a/src/_cffi_src/openssl/err.py b/src/_cffi_src/openssl/err.py index 0634b656c0f4..8d838d4fcac9 100644 --- a/src/_cffi_src/openssl/err.py +++ b/src/_cffi_src/openssl/err.py @@ -18,6 +18,7 @@ static const int ERR_LIB_EVP; static const int ERR_LIB_PEM; +static const int ERR_LIB_PROV; static const int ERR_LIB_ASN1; static const int ERR_LIB_PKCS12; @@ -39,10 +40,14 @@ void ERR_put_error(int, int, int, const char *, int); int ERR_GET_LIB(unsigned long); -int ERR_GET_FUNC(unsigned long); int ERR_GET_REASON(unsigned long); """ CUSTOMIZATIONS = """ +/* This define is tied to provider support and is conditionally + removed if Cryptography_HAS_PROVIDERS is false */ +#ifndef ERR_LIB_PROV +#define ERR_LIB_PROV 0 +#endif """ diff --git a/src/_cffi_src/openssl/evp.py b/src/_cffi_src/openssl/evp.py index 2b2f995e389f..735b8c37cfa2 100644 --- a/src/_cffi_src/openssl/evp.py +++ b/src/_cffi_src/openssl/evp.py @@ -36,6 +36,7 @@ static const int Cryptography_HAS_ONESHOT_EVP_DIGEST_SIGN_VERIFY; static const long Cryptography_HAS_RAW_KEY; static const long Cryptography_HAS_EVP_DIGESTFINAL_XOF; +static const long Cryptography_HAS_300_FIPS; """ FUNCTIONS = """ @@ -165,6 +166,9 @@ size_t); int EVP_PKEY_get_raw_private_key(const EVP_PKEY *, unsigned char *, size_t *); int EVP_PKEY_get_raw_public_key(const EVP_PKEY *, unsigned char *, size_t *); + +int EVP_default_properties_is_fips_enabled(OSSL_LIB_CTX *); +int EVP_default_properties_enable_fips(OSSL_LIB_CTX *, int); """ CUSTOMIZATIONS = """ @@ -269,4 +273,12 @@ #ifndef EVP_PKEY_POLY1305 #define EVP_PKEY_POLY1305 NID_poly1305 #endif + +#if CRYPTOGRAPHY_OPENSSL_300_OR_GREATER +static const long Cryptography_HAS_300_FIPS = 1; +#else +static const long Cryptography_HAS_300_FIPS = 0; +int (*EVP_default_properties_is_fips_enabled)(OSSL_LIB_CTX *) = NULL; +int (*EVP_default_properties_enable_fips)(OSSL_LIB_CTX *, int) = NULL; +#endif """ diff --git a/src/_cffi_src/openssl/fips.py b/src/_cffi_src/openssl/fips.py index b9d0d64d84fb..23c10af92209 100644 --- a/src/_cffi_src/openssl/fips.py +++ b/src/_cffi_src/openssl/fips.py @@ -17,7 +17,7 @@ """ CUSTOMIZATIONS = """ -#if CRYPTOGRAPHY_IS_LIBRESSL +#if CRYPTOGRAPHY_IS_LIBRESSL || CRYPTOGRAPHY_OPENSSL_300_OR_GREATER static const long Cryptography_HAS_FIPS = 0; int (*FIPS_mode_set)(int) = NULL; int (*FIPS_mode)(void) = NULL; diff --git a/src/_cffi_src/openssl/provider.py b/src/_cffi_src/openssl/provider.py new file mode 100644 index 000000000000..d7d659ea5ef4 --- /dev/null +++ b/src/_cffi_src/openssl/provider.py @@ -0,0 +1,40 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + + +INCLUDES = """ +#if CRYPTOGRAPHY_OPENSSL_300_OR_GREATER +#include +#include +#endif +""" + +TYPES = """ +static const long Cryptography_HAS_PROVIDERS; + +typedef ... OSSL_PROVIDER; +typedef ... OSSL_LIB_CTX; + +static const long PROV_R_BAD_DECRYPT; +static const long PROV_R_WRONG_FINAL_BLOCK_LENGTH; +""" + +FUNCTIONS = """ +OSSL_PROVIDER *OSSL_PROVIDER_load(OSSL_LIB_CTX *, const char *); +int OSSL_PROVIDER_unload(OSSL_PROVIDER *prov); +""" + +CUSTOMIZATIONS = """ +#if CRYPTOGRAPHY_OPENSSL_300_OR_GREATER +static const long Cryptography_HAS_PROVIDERS = 1; +#else +static const long Cryptography_HAS_PROVIDERS = 0; +typedef void OSSL_PROVIDER; +typedef void OSSL_LIB_CTX; +static const long PROV_R_BAD_DECRYPT = 0; +static const long PROV_R_WRONG_FINAL_BLOCK_LENGTH = 0; +OSSL_PROVIDER *(*OSSL_PROVIDER_load)(OSSL_LIB_CTX *, const char *) = NULL; +int (*OSSL_PROVIDER_unload)(OSSL_PROVIDER *) = NULL; +#endif +""" diff --git a/src/cryptography/exceptions.py b/src/cryptography/exceptions.py index f5860590571b..3bd98d8252ad 100644 --- a/src/cryptography/exceptions.py +++ b/src/cryptography/exceptions.py @@ -3,10 +3,10 @@ # for complete details. -from enum import Enum +from cryptography import utils -class _Reasons(Enum): +class _Reasons(utils.Enum): BACKEND_MISSING_INTERFACE = 0 UNSUPPORTED_HASH = 1 UNSUPPORTED_CIPHER = 2 diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index 271873d92ad2..2363a6069edb 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -6,6 +6,7 @@ import collections import contextlib import itertools +import typing import warnings from contextlib import contextmanager @@ -195,8 +196,9 @@ class Backend(object): b"aes-256-gcm", } _fips_ciphers = (AES, TripleDES) + # Sometimes SHA1 is still permissible. That logic is contained + # within the various *_supported methods. _fips_hashes = ( - hashes.SHA1, hashes.SHA224, hashes.SHA256, hashes.SHA384, @@ -210,6 +212,12 @@ class Backend(object): hashes.SHAKE128, hashes.SHAKE256, ) + _fips_ecdh_curves = ( + ec.SECP224R1, + ec.SECP256R1, + ec.SECP384R1, + ec.SECP521R1, + ) _fips_rsa_min_key_size = 2048 _fips_rsa_min_public_exponent = 65537 _fips_dsa_min_modulus = 1 << 2048 @@ -237,17 +245,34 @@ def __init__(self): if self._lib.Cryptography_HAS_EVP_PKEY_DHX: self._dh_types.append(self._lib.EVP_PKEY_DHX) + def __repr__(self): + return "".format( + self.openssl_version_text(), self._fips_enabled + ) + def openssl_assert(self, ok, errors=None): return binding._openssl_assert(self._lib, ok, errors=errors) def _is_fips_enabled(self): - fips_mode = getattr(self._lib, "FIPS_mode", lambda: 0) - mode = fips_mode() + if self._lib.Cryptography_HAS_300_FIPS: + mode = self._lib.EVP_default_properties_is_fips_enabled( + self._ffi.NULL + ) + else: + mode = getattr(self._lib, "FIPS_mode", lambda: 0)() + if mode == 0: # OpenSSL without FIPS pushes an error on the error stack self._lib.ERR_clear_error() return bool(mode) + def _enable_fips(self): + # This function enables FIPS mode for OpenSSL 3.0.0 on installs that + # have the FIPS provider installed properly. + self._binding._enable_fips() + assert self._is_fips_enabled() + self._fips_enabled = self._is_fips_enabled() + def activate_builtin_random(self): if self._lib.CRYPTOGRAPHY_NEEDS_OSRANDOM_ENGINE: # Obtain a new structural reference. @@ -342,15 +367,32 @@ def hash_supported(self, algorithm): evp_md = self._evp_md_from_algorithm(algorithm) return evp_md != self._ffi.NULL + def scrypt_supported(self): + if self._fips_enabled: + return False + else: + return self._lib.Cryptography_HAS_SCRYPT == 1 + def hmac_supported(self, algorithm): + # FIPS mode still allows SHA1 for HMAC + if self._fips_enabled and isinstance(algorithm, hashes.SHA1): + return True + return self.hash_supported(algorithm) def create_hash_ctx(self, algorithm): return _HashContext(self, algorithm) def cipher_supported(self, cipher, mode): - if self._fips_enabled and not isinstance(cipher, self._fips_ciphers): - return False + if self._fips_enabled: + # FIPS mode requires AES or TripleDES, but only CBC/ECB allowed + # in TripleDES mode. + if not isinstance(cipher, self._fips_ciphers) or ( + isinstance(cipher, TripleDES) + and not isinstance(mode, (CBC, ECB)) + ): + return False + try: adapter = self._cipher_registry[type(cipher), type(mode)] except KeyError: @@ -766,7 +808,13 @@ def rsa_padding_supported(self, padding): if isinstance(padding, PKCS1v15): return True elif isinstance(padding, PSS) and isinstance(padding._mgf, MGF1): - return self.hash_supported(padding._mgf._algorithm) + # SHA1 is permissible in MGF1 in FIPS + if self._fips_enabled and isinstance( + padding._mgf._algorithm, hashes.SHA1 + ): + return True + else: + return self.hash_supported(padding._mgf._algorithm) elif isinstance(padding, OAEP) and isinstance(padding._mgf, MGF1): return ( self._oaep_hash_supported(padding._mgf._algorithm) @@ -1280,6 +1328,11 @@ def load_der_private_key(self, data, password): def _evp_pkey_from_der_traditional_key(self, bio_data, password): key = self._lib.d2i_PrivateKey_bio(bio_data.bio, self._ffi.NULL) if key != self._ffi.NULL: + # In OpenSSL 3.0.0-alpha15 there exist scenarios where the key will + # successfully load but errors are still put on the stack. Tracked + # as https://github.com/openssl/openssl/issues/14996 + self._consume_errors() + key = self._ffi.gc(key, self._lib.EVP_PKEY_free) if password is not None: raise TypeError( @@ -1447,6 +1500,11 @@ def _load_key(self, openssl_read_func, convert_func, data, password): else: self._handle_key_loading_error() + # In OpenSSL 3.0.0-alpha15 there exist scenarios where the key will + # successfully load but errors are still put on the stack. Tracked + # as https://github.com/openssl/openssl/issues/14996 + self._consume_errors() + evp_pkey = self._ffi.gc(evp_pkey, self._lib.EVP_PKEY_free) if password is not None and userdata.called == 0: @@ -1469,11 +1527,22 @@ def _handle_key_loading_error(self): "incorrect format or it may be encrypted with an unsupported " "algorithm." ) - elif errors[0]._lib_reason_match( - self._lib.ERR_LIB_EVP, self._lib.EVP_R_BAD_DECRYPT - ) or errors[0]._lib_reason_match( - self._lib.ERR_LIB_PKCS12, - self._lib.PKCS12_R_PKCS12_CIPHERFINAL_ERROR, + + elif ( + errors[0]._lib_reason_match( + self._lib.ERR_LIB_EVP, self._lib.EVP_R_BAD_DECRYPT + ) + or errors[0]._lib_reason_match( + self._lib.ERR_LIB_PKCS12, + self._lib.PKCS12_R_PKCS12_CIPHERFINAL_ERROR, + ) + or ( + self._lib.Cryptography_HAS_PROVIDERS + and errors[0]._lib_reason_match( + self._lib.ERR_LIB_PROV, + self._lib.PROV_R_BAD_DECRYPT, + ) + ) ): raise ValueError("Bad decrypt. Incorrect password?") @@ -1487,10 +1556,12 @@ def _handle_key_loading_error(self): raise ValueError("Unsupported public key algorithm.") else: + errors = binding._errors_with_text(errors) raise ValueError( "Could not deserialize key data. The data may be in an " "incorrect format or it may be encrypted with an unsupported " - "algorithm." + "algorithm.", + errors, ) def elliptic_curve_supported(self, curve): @@ -1772,6 +1843,11 @@ def create_ocsp_response( return _OCSPResponse(self, ocsp_resp) def elliptic_curve_exchange_algorithm_supported(self, algorithm, curve): + if self._fips_enabled and not isinstance( + curve, self._fips_ecdh_curves + ): + return False + return self.elliptic_curve_supported(curve) and isinstance( algorithm, ec.ECDH ) @@ -2519,7 +2595,16 @@ def load_key_and_certificates_from_pkcs12(self, data, password): if sk_x509_ptr[0] != self._ffi.NULL: sk_x509 = self._ffi.gc(sk_x509_ptr[0], self._lib.sk_X509_free) num = self._lib.sk_X509_num(sk_x509_ptr[0]) - for i in range(num): + + # In OpenSSL < 3.0.0 PKCS12 parsing reverses the order of the + # certificates. + indices: typing.Iterable[int] + if self._lib.CRYPTOGRAPHY_OPENSSL_300_OR_GREATER: + indices = range(num) + else: + indices = reversed(range(num)) + + for i in indices: x509 = self._lib.sk_X509_value(sk_x509, i) self.openssl_assert(x509 != self._ffi.NULL) x509 = self._ffi.gc(x509, self._lib.X509_free) @@ -2562,9 +2647,7 @@ def serialize_key_and_certificates_to_pkcs12( sk_x509 = self._lib.sk_X509_new_null() sk_x509 = self._ffi.gc(sk_x509, self._lib.sk_X509_free) - # reverse the list when building the stack so that they're encoded - # in the order they were originally provided. it is a mystery - for ca in reversed(cas): + for ca in cas: res = self._lib.sk_X509_push(sk_x509, ca._x509) backend.openssl_assert(res >= 1) diff --git a/src/cryptography/hazmat/backends/openssl/ciphers.py b/src/cryptography/hazmat/backends/openssl/ciphers.py index 0f96795fdc73..a2dd68944fd7 100644 --- a/src/cryptography/hazmat/backends/openssl/ciphers.py +++ b/src/cryptography/hazmat/backends/openssl/ciphers.py @@ -145,7 +145,13 @@ def update_into(self, data: bytes, buf) -> int: res = self._backend._lib.EVP_CipherUpdate( self._ctx, outbuf, outlen, inbuf, inlen ) - self._backend.openssl_assert(res != 0) + if res == 0 and isinstance(self._mode, modes.XTS): + raise ValueError( + "In XTS mode you must supply at least a full block in the " + "first update call. For AES this is 16 bytes." + ) + else: + self._backend.openssl_assert(res != 0) data_processed += inlen total_out += outlen[0] @@ -174,6 +180,13 @@ def finalize(self) -> bytes: errors[0]._lib_reason_match( self._backend._lib.ERR_LIB_EVP, self._backend._lib.EVP_R_DATA_NOT_MULTIPLE_OF_BLOCK_LENGTH, + ) + or ( + self._backend._lib.Cryptography_HAS_PROVIDERS + and errors[0]._lib_reason_match( + self._backend._lib.ERR_LIB_PROV, + self._backend._lib.PROV_R_WRONG_FINAL_BLOCK_LENGTH, + ) ), errors=errors, ) diff --git a/src/cryptography/hazmat/backends/openssl/dh.py b/src/cryptography/hazmat/backends/openssl/dh.py index 65ddaeec5fe2..b928f024f56f 100644 --- a/src/cryptography/hazmat/backends/openssl/dh.py +++ b/src/cryptography/hazmat/backends/openssl/dh.py @@ -127,35 +127,48 @@ def private_numbers(self) -> dh.DHPrivateNumbers: ) def exchange(self, peer_public_key: dh.DHPublicKey) -> bytes: - buf = self._backend._ffi.new("unsigned char[]", self._key_size_bytes) - pub_key = self._backend._ffi.new("BIGNUM **") - self._backend._lib.DH_get0_key( - peer_public_key._dh_cdata, # type: ignore[attr-defined] - pub_key, - self._backend._ffi.NULL, + if not isinstance(peer_public_key, _DHPublicKey): + raise TypeError("peer_public_key must be a DHPublicKey") + + ctx = self._backend._lib.EVP_PKEY_CTX_new( + self._evp_pkey, self._backend._ffi.NULL ) - self._backend.openssl_assert(pub_key[0] != self._backend._ffi.NULL) - res = self._backend._lib.DH_compute_key( - buf, pub_key[0], self._dh_cdata + self._backend.openssl_assert(ctx != self._backend._ffi.NULL) + ctx = self._backend._ffi.gc(ctx, self._backend._lib.EVP_PKEY_CTX_free) + res = self._backend._lib.EVP_PKEY_derive_init(ctx) + self._backend.openssl_assert(res == 1) + res = self._backend._lib.EVP_PKEY_derive_set_peer( + ctx, peer_public_key._evp_pkey + ) + # Invalid kex errors here in OpenSSL 3.0 because checks were moved + # to EVP_PKEY_derive_set_peer + self._exchange_assert(res == 1) + keylen = self._backend._ffi.new("size_t *") + res = self._backend._lib.EVP_PKEY_derive( + ctx, self._backend._ffi.NULL, keylen ) + # Invalid kex errors here in OpenSSL < 3 + self._exchange_assert(res == 1) + self._backend.openssl_assert(keylen[0] > 0) + buf = self._backend._ffi.new("unsigned char[]", keylen[0]) + res = self._backend._lib.EVP_PKEY_derive(ctx, buf, keylen) + self._backend.openssl_assert(res == 1) - if res == -1: + key = self._backend._ffi.buffer(buf, keylen[0])[:] + pad = self._key_size_bytes - len(key) + + if pad > 0: + key = (b"\x00" * pad) + key + + return key + + def _exchange_assert(self, ok): + if not ok: errors_with_text = self._backend._consume_errors_with_text() raise ValueError( - "Error computing shared key. Public key is likely invalid " - "for this exchange.", + "Error computing shared key.", errors_with_text, ) - else: - self._backend.openssl_assert(res >= 1) - - key = self._backend._ffi.buffer(buf)[:res] - pad = self._key_size_bytes - len(key) - - if pad > 0: - key = (b"\x00" * pad) + key - - return key def public_key(self) -> dh.DHPublicKey: dh_cdata = _dh_params_dup(self._dh_cdata, self._backend) diff --git a/src/cryptography/hazmat/backends/openssl/poly1305.py b/src/cryptography/hazmat/backends/openssl/poly1305.py index 35f6819ce87e..2ddae9847a43 100644 --- a/src/cryptography/hazmat/backends/openssl/poly1305.py +++ b/src/cryptography/hazmat/backends/openssl/poly1305.py @@ -51,7 +51,7 @@ def update(self, data): def finalize(self): buf = self._backend._ffi.new("unsigned char[]", _POLY1305_TAG_SIZE) - outlen = self._backend._ffi.new("size_t *") + outlen = self._backend._ffi.new("size_t *", _POLY1305_TAG_SIZE) res = self._backend._lib.EVP_DigestSignFinal(self._ctx, buf, outlen) self._backend.openssl_assert(res != 0) self._backend.openssl_assert(outlen[0] == _POLY1305_TAG_SIZE) diff --git a/src/cryptography/hazmat/bindings/openssl/_conditional.py b/src/cryptography/hazmat/bindings/openssl/_conditional.py index 8654835796b6..dfddafb4acb6 100644 --- a/src/cryptography/hazmat/bindings/openssl/_conditional.py +++ b/src/cryptography/hazmat/bindings/openssl/_conditional.py @@ -270,6 +270,23 @@ def cryptography_has_get_proto_version(): ] +def cryptography_has_providers(): + return [ + "OSSL_PROVIDER_load", + "OSSL_PROVIDER_unload", + "ERR_LIB_PROV", + "PROV_R_WRONG_FINAL_BLOCK_LENGTH", + "PROV_R_BAD_DECRYPT", + ] + + +def cryptography_has_300_fips(): + return [ + "EVP_default_properties_is_fips_enabled", + "EVP_default_properties_enable_fips", + ] + + # This is a mapping of # {condition: function-returning-names-dependent-on-that-condition} so we can # loop over them and delete unsupported names at runtime. It will be removed @@ -318,4 +335,6 @@ def cryptography_has_get_proto_version(): "Cryptography_HAS_VERIFIED_CHAIN": cryptography_has_verified_chain, "Cryptography_HAS_SRTP": cryptography_has_srtp, "Cryptography_HAS_GET_PROTO_VERSION": cryptography_has_get_proto_version, + "Cryptography_HAS_PROVIDERS": cryptography_has_providers, + "Cryptography_HAS_300_FIPS": cryptography_has_300_fips, } diff --git a/src/cryptography/hazmat/bindings/openssl/binding.py b/src/cryptography/hazmat/bindings/openssl/binding.py index a2bc36a83a71..92d5b2448a48 100644 --- a/src/cryptography/hazmat/bindings/openssl/binding.py +++ b/src/cryptography/hazmat/bindings/openssl/binding.py @@ -15,15 +15,14 @@ from cryptography.hazmat.bindings.openssl._conditional import CONDITIONAL_NAMES _OpenSSLErrorWithText = collections.namedtuple( - "_OpenSSLErrorWithText", ["code", "lib", "func", "reason", "reason_text"] + "_OpenSSLErrorWithText", ["code", "lib", "reason", "reason_text"] ) class _OpenSSLError(object): - def __init__(self, code, lib, func, reason): + def __init__(self, code, lib, reason): self._code = code self._lib = lib - self._func = func self._reason = reason def _lib_reason_match(self, lib, reason): @@ -31,7 +30,6 @@ def _lib_reason_match(self, lib, reason): code = utils.read_only_property("_code") lib = utils.read_only_property("_lib") - func = utils.read_only_property("_func") reason = utils.read_only_property("_reason") @@ -43,10 +41,9 @@ def _consume_errors(lib): break err_lib = lib.ERR_GET_LIB(code) - err_func = lib.ERR_GET_FUNC(code) err_reason = lib.ERR_GET_REASON(code) - errors.append(_OpenSSLError(code, err_lib, err_func, err_reason)) + errors.append(_OpenSSLError(code, err_lib, err_reason)) return errors @@ -60,7 +57,7 @@ def _errors_with_text(errors): errors_with_text.append( _OpenSSLErrorWithText( - err.code, err.lib, err.func, err.reason, err_text_reason + err.code, err.lib, err.reason, err_text_reason ) ) @@ -113,10 +110,28 @@ class Binding(object): ffi = ffi _lib_loaded = False _init_lock = threading.Lock() + _legacy_provider: typing.Any = None + _default_provider: typing.Any = None def __init__(self): self._ensure_ffi_initialized() + def _enable_fips(self): + # This function enables FIPS mode for OpenSSL 3.0.0 on installs that + # have the FIPS provider installed properly. + _openssl_assert(self.lib, self.lib.CRYPTOGRAPHY_OPENSSL_300_OR_GREATER) + self._base_provider = self.lib.OSSL_PROVIDER_load( + self.ffi.NULL, b"base" + ) + _openssl_assert(self.lib, self._base_provider != self.ffi.NULL) + self.lib._fips_provider = self.lib.OSSL_PROVIDER_load( + self.ffi.NULL, b"fips" + ) + _openssl_assert(self.lib, self.lib._fips_provider != self.ffi.NULL) + + res = self.lib.EVP_default_properties_enable_fips(self.ffi.NULL, 1) + _openssl_assert(self.lib, res == 1) + @classmethod def _register_osrandom_engine(cls): # Clear any errors extant in the queue before we start. In many @@ -140,6 +155,24 @@ def _ensure_ffi_initialized(cls): # adds all ciphers/digests for EVP cls.lib.OpenSSL_add_all_algorithms() cls._register_osrandom_engine() + # As of OpenSSL 3.0.0 we must register a legacy cipher provider + # to get RC2 (needed for junk asymmetric private key + # serialization), RC4, Blowfish, IDEA, SEED, etc. These things + # are ugly legacy, but we aren't going to get rid of them + # any time soon. + if cls.lib.CRYPTOGRAPHY_OPENSSL_300_OR_GREATER: + cls._legacy_provider = cls.lib.OSSL_PROVIDER_load( + cls.ffi.NULL, b"legacy" + ) + _openssl_assert( + cls.lib, cls._legacy_provider != cls.ffi.NULL + ) + cls._default_provider = cls.lib.OSSL_PROVIDER_load( + cls.ffi.NULL, b"default" + ) + _openssl_assert( + cls.lib, cls._default_provider != cls.ffi.NULL + ) @classmethod def init_static_locks(cls): diff --git a/src/cryptography/hazmat/primitives/_serialization.py b/src/cryptography/hazmat/primitives/_serialization.py index 96a5ed9b75d9..160a6b89c089 100644 --- a/src/cryptography/hazmat/primitives/_serialization.py +++ b/src/cryptography/hazmat/primitives/_serialization.py @@ -3,13 +3,14 @@ # for complete details. import abc -from enum import Enum + +from cryptography import utils # This exists to break an import cycle. These classes are normally accessible # from the serialization module. -class Encoding(Enum): +class Encoding(utils.Enum): PEM = "PEM" DER = "DER" OpenSSH = "OpenSSH" @@ -18,14 +19,14 @@ class Encoding(Enum): SMIME = "S/MIME" -class PrivateFormat(Enum): +class PrivateFormat(utils.Enum): PKCS8 = "PKCS8" TraditionalOpenSSL = "TraditionalOpenSSL" Raw = "Raw" OpenSSH = "OpenSSH" -class PublicFormat(Enum): +class PublicFormat(utils.Enum): SubjectPublicKeyInfo = "X.509 subjectPublicKeyInfo with PKCS#1" PKCS1 = "Raw PKCS#1" OpenSSH = "OpenSSH" @@ -34,7 +35,7 @@ class PublicFormat(Enum): UncompressedPoint = "X9.62 Uncompressed Point" -class ParameterFormat(Enum): +class ParameterFormat(utils.Enum): PKCS3 = "PKCS3" diff --git a/src/cryptography/hazmat/primitives/kdf/kbkdf.py b/src/cryptography/hazmat/primitives/kdf/kbkdf.py index ac36474fd7ae..75fe7d518910 100644 --- a/src/cryptography/hazmat/primitives/kdf/kbkdf.py +++ b/src/cryptography/hazmat/primitives/kdf/kbkdf.py @@ -4,7 +4,6 @@ import typing -from enum import Enum from cryptography import utils from cryptography.exceptions import ( @@ -19,11 +18,11 @@ from cryptography.hazmat.primitives.kdf import KeyDerivationFunction -class Mode(Enum): +class Mode(utils.Enum): CounterMode = "ctr" -class CounterLocation(Enum): +class CounterLocation(utils.Enum): BeforeFixed = "before_fixed" AfterFixed = "after_fixed" diff --git a/src/cryptography/hazmat/primitives/serialization/pkcs7.py b/src/cryptography/hazmat/primitives/serialization/pkcs7.py index bcd9e330d58d..57aac7e34645 100644 --- a/src/cryptography/hazmat/primitives/serialization/pkcs7.py +++ b/src/cryptography/hazmat/primitives/serialization/pkcs7.py @@ -3,8 +3,8 @@ # for complete details. import typing -from enum import Enum +from cryptography import utils from cryptography import x509 from cryptography.hazmat.backends import _get_backend from cryptography.hazmat.primitives import hashes, serialization @@ -35,7 +35,7 @@ def load_der_pkcs7_certificates(data: bytes) -> typing.List[x509.Certificate]: ] -class PKCS7Options(Enum): +class PKCS7Options(utils.Enum): Text = "Add text/plain MIME type" Binary = "Don't translate input data into canonical MIME format" DetachedSignature = "Don't embed data in the PKCS7 structure" diff --git a/src/cryptography/utils.py b/src/cryptography/utils.py index ef0fc44332d0..9e571cfdd153 100644 --- a/src/cryptography/utils.py +++ b/src/cryptography/utils.py @@ -4,6 +4,7 @@ import abc +import enum import inspect import sys import typing @@ -162,3 +163,13 @@ def inner(instance): "int_from_bytes is deprecated, use int.from_bytes instead", DeprecatedIn34, ) + + +# Python 3.10 changed representation of enums. We use well-defined object +# representation and string representation from Python 3.9. +class Enum(enum.Enum): + def __repr__(self): + return f"<{self.__class__.__name__}.{self._name_}: {self._value_!r}>" + + def __str__(self): + return f"{self.__class__.__name__}.{self._name_}" diff --git a/src/cryptography/x509/base.py b/src/cryptography/x509/base.py index 5505fa3b6d5e..26ec43d56493 100644 --- a/src/cryptography/x509/base.py +++ b/src/cryptography/x509/base.py @@ -7,8 +7,8 @@ import datetime import os import typing -from enum import Enum +from cryptography import utils from cryptography.hazmat._types import _PRIVATE_KEY_TYPES, _PUBLIC_KEY_TYPES from cryptography.hazmat.backends import _get_backend from cryptography.hazmat.primitives import hashes, serialization @@ -66,7 +66,7 @@ def _convert_to_naive_utc_time(time: datetime.datetime) -> datetime.datetime: return time -class Version(Enum): +class Version(utils.Enum): v1 = 0 v3 = 2 diff --git a/src/cryptography/x509/certificate_transparency.py b/src/cryptography/x509/certificate_transparency.py index d51bee92effc..d80f051a68ae 100644 --- a/src/cryptography/x509/certificate_transparency.py +++ b/src/cryptography/x509/certificate_transparency.py @@ -5,15 +5,16 @@ import abc import datetime -from enum import Enum +from cryptography import utils -class LogEntryType(Enum): + +class LogEntryType(utils.Enum): X509_CERTIFICATE = 0 PRE_CERTIFICATE = 1 -class Version(Enum): +class Version(utils.Enum): v1 = 0 diff --git a/src/cryptography/x509/extensions.py b/src/cryptography/x509/extensions.py index 6cae016a1c60..742f1fa29319 100644 --- a/src/cryptography/x509/extensions.py +++ b/src/cryptography/x509/extensions.py @@ -8,7 +8,6 @@ import hashlib import ipaddress import typing -from enum import Enum from cryptography import utils from cryptography.hazmat._der import ( @@ -634,7 +633,7 @@ def __hash__(self): crl_issuer = utils.read_only_property("_crl_issuer") -class ReasonFlags(Enum): +class ReasonFlags(utils.Enum): unspecified = "unspecified" key_compromise = "keyCompromise" ca_compromise = "cACompromise" @@ -978,7 +977,7 @@ def __hash__(self): return hash(tuple(self._features)) -class TLSFeatureType(Enum): +class TLSFeatureType(utils.Enum): # status_request is defined in RFC 6066 and is used for what is commonly # called OCSP Must-Staple when present in the TLS Feature extension in an # X.509 certificate. diff --git a/src/cryptography/x509/name.py b/src/cryptography/x509/name.py index a579aa219638..9069a9f4b71c 100644 --- a/src/cryptography/x509/name.py +++ b/src/cryptography/x509/name.py @@ -3,14 +3,13 @@ # for complete details. import typing -from enum import Enum from cryptography import utils from cryptography.hazmat.backends import _get_backend from cryptography.x509.oid import NameOID, ObjectIdentifier -class _ASN1Type(Enum): +class _ASN1Type(utils.Enum): UTF8String = 12 NumericString = 18 PrintableString = 19 diff --git a/src/cryptography/x509/ocsp.py b/src/cryptography/x509/ocsp.py index 1c5de73e45b1..bcf210c1eb31 100644 --- a/src/cryptography/x509/ocsp.py +++ b/src/cryptography/x509/ocsp.py @@ -6,8 +6,8 @@ import abc import datetime import typing -from enum import Enum +from cryptography import utils from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.x509.base import ( @@ -27,12 +27,12 @@ } -class OCSPResponderEncoding(Enum): +class OCSPResponderEncoding(utils.Enum): HASH = "By Hash" NAME = "By Name" -class OCSPResponseStatus(Enum): +class OCSPResponseStatus(utils.Enum): SUCCESSFUL = 0 MALFORMED_REQUEST = 1 INTERNAL_ERROR = 2 @@ -58,7 +58,7 @@ def _verify_algorithm(algorithm): ) -class OCSPCertStatus(Enum): +class OCSPCertStatus(utils.Enum): GOOD = 0 REVOKED = 1 UNKNOWN = 2 diff --git a/tests/conftest.py b/tests/conftest.py index 43debdd61a85..32a4f0360b6a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,11 @@ from .utils import check_backend_support +def pytest_configure(config): + if config.getoption("--enable-fips"): + openssl_backend._enable_fips() + + def pytest_report_header(config): return "\n".join( [ @@ -21,6 +26,7 @@ def pytest_report_header(config): def pytest_addoption(parser): parser.addoption("--wycheproof-root", default=None) + parser.addoption("--enable-fips", default=False) def pytest_runtest_setup(item): diff --git a/tests/hazmat/backends/test_openssl_memleak.py b/tests/hazmat/backends/test_openssl_memleak.py index 0c96516fa19f..0316b5d9602e 100644 --- a/tests/hazmat/backends/test_openssl_memleak.py +++ b/tests/hazmat/backends/test_openssl_memleak.py @@ -82,7 +82,7 @@ def free(ptr, path, line): assert result == 1 # Trigger a bunch of initialization stuff. - import cryptography.hazmat.backends.openssl + from cryptography.hazmat.backends.openssl.backend import backend start_heap = set(heap) @@ -91,6 +91,10 @@ def free(ptr, path, line): gc.collect() gc.collect() + if lib.CRYPTOGRAPHY_OPENSSL_300_OR_GREATER: + lib.OSSL_PROVIDER_unload(backend._binding._legacy_provider) + lib.OSSL_PROVIDER_unload(backend._binding._default_provider) + if lib.Cryptography_HAS_OPENSSL_CLEANUP: lib.OPENSSL_cleanup() diff --git a/tests/hazmat/bindings/test_openssl.py b/tests/hazmat/bindings/test_openssl.py index fb9a1e363742..1d9b87bad52f 100644 --- a/tests/hazmat/bindings/test_openssl.py +++ b/tests/hazmat/bindings/test_openssl.py @@ -91,9 +91,10 @@ def test_openssl_assert_error_on_stack(self): _openssl_assert(b.lib, False) error = exc_info.value.err_code[0] - assert error.code == 101183626 + # As of 3.0.0 OpenSSL no longer sets func codes (which we now also + # ignore), so the combined code is a different value + assert error.code in (101183626, 50331786) assert error.lib == b.lib.ERR_LIB_EVP - assert error.func == b.lib.EVP_F_EVP_ENCRYPTFINAL_EX assert error.reason == b.lib.EVP_R_DATA_NOT_MULTIPLE_OF_BLOCK_LENGTH assert b"data not multiple of block length" in error.reason_text diff --git a/tests/hazmat/primitives/test_dh.py b/tests/hazmat/primitives/test_dh.py index 131807fc0860..f6bf8fea3c20 100644 --- a/tests/hazmat/primitives/test_dh.py +++ b/tests/hazmat/primitives/test_dh.py @@ -180,7 +180,23 @@ def test_dh_parameters_allows_rfc3526_groups(self, backend, vector): params = dh.DHParameterNumbers(p, int(vector["g"])) param = params.parameters(backend) key = param.generate_private_key() - assert key.private_numbers().public_numbers.parameter_numbers == params + # In OpenSSL 3.0.0 OpenSSL maps to known groups. This results in + # a scenario where loading a known group with p and g returns a + # re-serialized form that has q as well (the Sophie Germain prime of + # that group). This makes a naive comparison of the parameter numbers + # objects fail, so we have to be a bit smarter + serialized_params = ( + key.private_numbers().public_numbers.parameter_numbers + ) + if serialized_params.q is None: + # This is the path OpenSSL < 3.0 takes + assert serialized_params == params + else: + assert serialized_params.p == params.p + assert serialized_params.g == params.g + # p = 2q + 1 since it is a Sophie Germain prime, so we can compute + # what we expect OpenSSL to have done here. + assert serialized_params.q == (params.p - 1) // 2 @pytest.mark.skip_fips(reason="non-FIPS parameters") @pytest.mark.parametrize( @@ -280,6 +296,12 @@ def test_generate_dh(self, backend, with_q): assert isinstance(key.private_numbers(), dh.DHPrivateNumbers) assert isinstance(key.parameters(), dh.DHParameters) + def test_exchange_wrong_type(self, backend): + parameters = FFDH3072_P.parameters(backend) + key1 = parameters.generate_private_key() + with pytest.raises(TypeError): + key1.exchange(b"invalidtype") # type: ignore[arg-type] + def test_exchange(self, backend): parameters = FFDH3072_P.parameters(backend) assert isinstance(parameters, dh.DHParameters) @@ -370,18 +392,19 @@ def test_bad_exchange(self, backend, vector): key2 = private2.private_key(backend) pub_key2 = key2.public_key() - if pub_key2.public_numbers().y >= parameters1.p: - with pytest.raises(ValueError): - key1.exchange(pub_key2) - else: - symkey1 = key1.exchange(pub_key2) - assert symkey1 - - symkey2 = key2.exchange(pub_key1) + with pytest.raises(ValueError): + key1.exchange(pub_key2) - assert symkey1 != symkey2 + with pytest.raises(ValueError): + key2.exchange(pub_key1) @pytest.mark.skip_fips(reason="key_size too small for FIPS") + @pytest.mark.supported( + only_if=lambda backend: ( + not backend._lib.CRYPTOGRAPHY_OPENSSL_300_OR_GREATER + ), + skip_message="256-bit DH keys are not supported in OpenSSL 3.0.0+", + ) def test_load_256bit_key_from_pkcs8(self, backend): data = load_vectors_from_file( os.path.join("asymmetric", "DH", "dh_key_256.pem"), @@ -652,6 +675,7 @@ def test_public_bytes(self, backend, encoding, loader_func): pub_num = key.public_numbers() assert loaded_pub_num == pub_num + @pytest.mark.skip_fips(reason="non-FIPS parameters") @pytest.mark.parametrize( ("key_path", "loader_func", "encoding", "is_dhx"), [ @@ -695,6 +719,7 @@ def test_public_bytes_match( ) assert serialized == key_bytes + @pytest.mark.skip_fips(reason="non-FIPS parameters") @pytest.mark.parametrize( ("key_path", "loader_func", "vec_path", "is_dhx"), [ diff --git a/tests/hazmat/primitives/test_pkcs12.py b/tests/hazmat/primitives/test_pkcs12.py index b5de09f95ca4..65f5b283018a 100644 --- a/tests/hazmat/primitives/test_pkcs12.py +++ b/tests/hazmat/primitives/test_pkcs12.py @@ -4,13 +4,15 @@ import os +from datetime import datetime import pytest from cryptography import x509 from cryptography.hazmat.backends.interfaces import DERSerializationBackend from cryptography.hazmat.backends.openssl.backend import _RC2 -from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.serialization import load_pem_private_key from cryptography.hazmat.primitives.serialization.pkcs12 import ( load_key_and_certificates, @@ -22,6 +24,9 @@ @pytest.mark.requires_backend_interface(interface=DERSerializationBackend) +@pytest.mark.skip_fips( + reason="PKCS12 unsupported in FIPS mode. So much bad crypto in it." +) class TestPKCS12Loading(object): def _test_load_pkcs12_ec_keys(self, filename, password, backend): cert = load_vectors_from_file( @@ -70,7 +75,6 @@ def test_load_pkcs12_ec_keys(self, filename, password, backend): only_if=lambda backend: backend.cipher_supported(_RC2(), None), skip_message="Does not support RC2", ) - @pytest.mark.skip_fips(reason="Unsupported algorithm in FIPS mode") def test_load_pkcs12_ec_keys_rc2(self, filename, password, backend): self._test_load_pkcs12_ec_keys(filename, password, backend) @@ -167,6 +171,9 @@ def _load_ca(backend): return cert, key +@pytest.mark.skip_fips( + reason="PKCS12 unsupported in FIPS mode. So much bad crypto in it." +) class TestPKCS12Creation(object): @pytest.mark.parametrize("name", [None, b"name"]) @pytest.mark.parametrize( @@ -273,3 +280,60 @@ def test_generate_unsupported_encryption_type(self, backend): DummyKeySerializationEncryption(), ) assert str(exc.value) == "Unsupported key encryption type" + + +@pytest.mark.skip_fips( + reason="PKCS12 unsupported in FIPS mode. So much bad crypto in it." +) +def test_pkcs12_ordering(): + """ + In OpenSSL < 3.0.0 PKCS12 parsing reverses the order. However, we + accidentally thought it was **encoding** that did it, leading to bug + https://github.com/pyca/cryptography/issues/5872 + This test ensures our ordering is correct going forward. + """ + + def make_cert(name): + key = ec.generate_private_key(ec.SECP256R1()) + subject = x509.Name( + [ + x509.NameAttribute(x509.NameOID.COMMON_NAME, name), + ] + ) + now = datetime.utcnow() + cert = ( + x509.CertificateBuilder() + .subject_name(subject) + .issuer_name(subject) + .public_key(key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(now) + .not_valid_after(now) + .sign(key, hashes.SHA256()) + ) + return (key, cert) + + # Make some certificates with distinct names. + a_name = "A" * 20 + b_name = "B" * 20 + c_name = "C" * 20 + a_key, a_cert = make_cert(a_name) + _, b_cert = make_cert(b_name) + _, c_cert = make_cert(c_name) + + # Bundle them in a PKCS#12 file in order A, B, C. + p12 = serialize_key_and_certificates( + b"p12", a_key, a_cert, [b_cert, c_cert], serialization.NoEncryption() + ) + + # Parse them out. The API should report them in the same order. + (key, cert, certs) = load_key_and_certificates(p12, None) + assert cert == a_cert + assert certs == [b_cert, c_cert] + + # The ordering in the PKCS#12 file itself should also match. + a_idx = p12.index(a_name.encode("utf-8")) + b_idx = p12.index(b_name.encode("utf-8")) + c_idx = p12.index(c_name.encode("utf-8")) + + assert a_idx < b_idx < c_idx diff --git a/tests/hazmat/primitives/test_pkcs7.py b/tests/hazmat/primitives/test_pkcs7.py index 6bc65eef3104..2f1ecb3bb469 100644 --- a/tests/hazmat/primitives/test_pkcs7.py +++ b/tests/hazmat/primitives/test_pkcs7.py @@ -331,6 +331,9 @@ def test_sign_pem(self, backend): def test_sign_alternate_digests_der( self, hash_alg, expected_value, backend ): + if isinstance(hash_alg, hashes.SHA1) and backend._fips_enabled: + pytest.skip("SHA1 not supported in FIPS mode") + data = b"hello world" cert, key = _load_cert_key() builder = ( @@ -354,7 +357,12 @@ def test_sign_alternate_digests_der( (hashes.SHA512(), b"sha-512"), ], ) - def test_sign_alternate_digests_detached(self, hash_alg, expected_value): + def test_sign_alternate_digests_detached( + self, hash_alg, expected_value, backend + ): + if isinstance(hash_alg, hashes.SHA1) and backend._fips_enabled: + pytest.skip("SHA1 not supported in FIPS mode") + data = b"hello world" cert, key = _load_cert_key() builder = ( diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py index ca969e031cfc..44bb55acd4d5 100644 --- a/tests/hazmat/primitives/test_serialization.py +++ b/tests/hazmat/primitives/test_serialization.py @@ -59,8 +59,19 @@ def _skip_fips_format(key_path, password, backend): if backend._fips_enabled: if key_path[0] == "Traditional_OpenSSL_Serialization": pytest.skip("Traditional OpenSSL format blocked in FIPS mode") - if key_path[0] == "PEM_Serialization" and password is not None: - pytest.skip("Encrypted PEM_Serialization blocked in FIPS mode") + if ( + key_path[0] in ("PEM_Serialization", "PKCS8") + and password is not None + ): + pytest.skip( + "The encrypted PEM vectors currently have encryption " + "that is not FIPS approved in the 3.0 provider" + ) + if key_path[0] == "DER_Serialization" and password is not None: + pytest.skip( + "The encrypted PKCS8 DER vectors currently have encryption " + "that is not FIPS approved in the 3.0 provider" + ) class TestBufferProtocolSerialization(object): @@ -75,6 +86,7 @@ class TestBufferProtocolSerialization(object): ], ) def test_load_der_rsa_private_key(self, key_path, password, backend): + _skip_fips_format(key_path, password, backend) data = load_vectors_from_file( os.path.join("asymmetric", *key_path), lambda derfile: derfile.read(), @@ -128,6 +140,7 @@ class TestDERSerialization(object): ], ) def test_load_der_rsa_private_key(self, key_path, password, backend): + _skip_fips_format(key_path, password, backend) key = load_vectors_from_file( os.path.join("asymmetric", *key_path), lambda derfile: load_der_private_key( @@ -837,6 +850,7 @@ def test_pks8_encrypted_corrupt_format(self, backend): with pytest.raises(ValueError): load_pem_private_key(key_data, password, backend) + @pytest.mark.skip_fips(reason="non-FIPS parameters") def test_rsa_pkcs8_encrypted_values(self, backend): pkey = load_vectors_from_file( os.path.join("asymmetric", "PKCS8", "enc-rsa-pkcs8.pem"), diff --git a/tests/test_cryptography_utils.py b/tests/test_cryptography_utils.py index 6b795e0c683a..803997ac06ea 100644 --- a/tests/test_cryptography_utils.py +++ b/tests/test_cryptography_utils.py @@ -2,6 +2,7 @@ # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. +import enum import typing import pytest @@ -51,3 +52,13 @@ def t(self): assert len(accesses) == 1 assert t.t == 14 assert len(accesses) == 1 + + +def test_enum(): + class TestEnum(utils.Enum): + value = "something" + + assert issubclass(TestEnum, enum.Enum) + assert isinstance(TestEnum.value, enum.Enum) + assert repr(TestEnum.value) == "" + assert str(TestEnum.value) == "TestEnum.value" diff --git a/tests/wycheproof/test_hmac.py b/tests/wycheproof/test_hmac.py index bfc690795122..84b0c19a0539 100644 --- a/tests/wycheproof/test_hmac.py +++ b/tests/wycheproof/test_hmac.py @@ -41,7 +41,7 @@ def test_hmac(backend, wycheproof): hash_algo = _HMAC_ALGORITHMS[wycheproof.testfiledata["algorithm"]] if wycheproof.testgroup["tagSize"] // 8 != hash_algo.digest_size: pytest.skip("Truncated HMAC not supported") - if not backend.hash_supported(hash_algo): + if not backend.hmac_supported(hash_algo): pytest.skip("Hash {} not supported".format(hash_algo.name)) h = hmac.HMAC( diff --git a/tests/wycheproof/test_rsa.py b/tests/wycheproof/test_rsa.py index 0b0983da3590..8694b61ce3ff 100644 --- a/tests/wycheproof/test_rsa.py +++ b/tests/wycheproof/test_rsa.py @@ -104,6 +104,13 @@ def test_rsa_pkcs1v15_signature_generation(backend, wycheproof): assert isinstance(key, rsa.RSAPrivateKey) digest = _DIGESTS[wycheproof.testgroup["sha"]] assert digest is not None + if backend._fips_enabled: + if key.key_size < 2048 or isinstance(digest, hashes.SHA1): + pytest.skip( + "Invalid params for FIPS. key: {} bits, digest: {}".format( + key.key_size, digest.name + ) + ) sig = key.sign( binascii.unhexlify(wycheproof.testcase["msg"]), diff --git a/tox.ini b/tox.ini index 93f4b253ab8d..e5e0955af55a 100644 --- a/tox.ini +++ b/tox.ini @@ -46,6 +46,7 @@ extras = ssh deps = mypy + types-pytz check-manifest commands = flake8 . @@ -63,7 +64,7 @@ commands = cargo clippy -- -D warnings [flake8] -ignore = E203,E211,W503,W504 +ignore = E203,E211,W503,W504,N818 exclude = .tox,*.egg,.git,_build,.hypothesis select = E,W,F,N,I application-import-names = cryptography,cryptography_vectors,tests