Visitar URL original
Fix legacy cipher support detection with partial OpenSSL providers by arpitkjaiswal · Pull Request #15928 · pyca/cryptography · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Changelog

.. note:: This version is not yet released and is under active development.

* Fixed legacy cipher support detection when a loaded OpenSSL provider does not
implement every legacy algorithm.
* Require CFFI 2.1.1 or newer on Python 3.15 to avoid an ABI issue in
earlier CFFI releases.
* :func:`~cryptography.hazmat.primitives.serialization.load_der_private_key`
Expand Down
63 changes: 41 additions & 22 deletions src/rust/src/backend/cipher_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,27 @@ impl<'p> RegistryBuilder<'p> {
Ok(())
}

fn add_legacy(
&mut self,
algorithm: &pyo3::Bound<'_, pyo3::PyAny>,
mode: &pyo3::Bound<'_, pyo3::PyAny>,
key_size: Option<u16>,
cipher: &'static openssl::cipher::CipherRef,
) -> CryptographyResult<()> {
cfg_if::cfg_if! {
if #[cfg(any(CRYPTOGRAPHY_IS_LIBRESSL, CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC))] {
self.add(algorithm, mode, key_size, cipher)
} else {
// A static OpenSSL 3 cipher descriptor does not guarantee that
// a loaded provider implements the algorithm.
if let Ok(fetched) = Cipher::fetch(None, cipher.nid().short_name()?, None) {
self.add(algorithm, mode, key_size, fetched)?;
}
Ok(())
}
}
}

fn build(self) -> HashMap<RegistryKey, RegistryCipher> {
self.m
}
Expand Down Expand Up @@ -271,9 +292,7 @@ fn get_cipher_registry(
#[cfg(not(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC)))]
m.add(&chacha20, none_type.as_any(), None, Cipher::chacha20())?;

// Don't register legacy ciphers if they're unavailable. In theory
// this shouldn't be necessary but OpenSSL 3 will return an EVP_CIPHER
// even when the cipher is unavailable.
// The legacy provider may be absent or implement only some ciphers.
if cfg!(any(
CRYPTOGRAPHY_IS_LIBRESSL,
CRYPTOGRAPHY_IS_BORINGSSL,
Expand All @@ -282,42 +301,42 @@ fn get_cipher_registry(
{
#[cfg(not(CRYPTOGRAPHY_OSSLCONF = "OPENSSL_NO_BF"))]
{
m.add(&blowfish, &cbc, None, Cipher::bf_cbc())?;
m.add(&blowfish, &cfb, None, Cipher::bf_cfb64())?;
m.add(&blowfish, &ofb, None, Cipher::bf_ofb())?;
m.add(&blowfish, &ecb, None, Cipher::bf_ecb())?;
m.add_legacy(&blowfish, &cbc, None, Cipher::bf_cbc())?;
m.add_legacy(&blowfish, &cfb, None, Cipher::bf_cfb64())?;
m.add_legacy(&blowfish, &ofb, None, Cipher::bf_ofb())?;
m.add_legacy(&blowfish, &ecb, None, Cipher::bf_ecb())?;
}
#[cfg(not(CRYPTOGRAPHY_OSSLCONF = "OPENSSL_NO_SEED"))]
{
m.add(&seed, &cbc, Some(128), Cipher::seed_cbc())?;
m.add(&seed, &cfb, Some(128), Cipher::seed_cfb128())?;
m.add(&seed, &ofb, Some(128), Cipher::seed_ofb())?;
m.add(&seed, &ecb, Some(128), Cipher::seed_ecb())?;
m.add_legacy(&seed, &cbc, Some(128), Cipher::seed_cbc())?;
m.add_legacy(&seed, &cfb, Some(128), Cipher::seed_cfb128())?;
m.add_legacy(&seed, &ofb, Some(128), Cipher::seed_ofb())?;
m.add_legacy(&seed, &ecb, Some(128), Cipher::seed_ecb())?;
}

#[cfg(not(CRYPTOGRAPHY_OSSLCONF = "OPENSSL_NO_CAST"))]
{
m.add(&cast5, &cbc, None, Cipher::cast5_cbc())?;
m.add(&cast5, &ecb, None, Cipher::cast5_ecb())?;
m.add(&cast5, &ofb, None, Cipher::cast5_ofb())?;
m.add(&cast5, &cfb, None, Cipher::cast5_cfb64())?;
m.add_legacy(&cast5, &cbc, None, Cipher::cast5_cbc())?;
m.add_legacy(&cast5, &ecb, None, Cipher::cast5_ecb())?;
m.add_legacy(&cast5, &ofb, None, Cipher::cast5_ofb())?;
m.add_legacy(&cast5, &cfb, None, Cipher::cast5_cfb64())?;
}

#[cfg(not(CRYPTOGRAPHY_OSSLCONF = "OPENSSL_NO_IDEA"))]
{
m.add(&idea, &cbc, Some(128), Cipher::idea_cbc())?;
m.add(&idea, &ecb, Some(128), Cipher::idea_ecb())?;
m.add(&idea, &ofb, Some(128), Cipher::idea_ofb())?;
m.add(&idea, &cfb, Some(128), Cipher::idea_cfb64())?;
m.add_legacy(&idea, &cbc, Some(128), Cipher::idea_cbc())?;
m.add_legacy(&idea, &ecb, Some(128), Cipher::idea_ecb())?;
m.add_legacy(&idea, &ofb, Some(128), Cipher::idea_ofb())?;
m.add_legacy(&idea, &cfb, Some(128), Cipher::idea_cfb64())?;
}

#[cfg(not(CRYPTOGRAPHY_OSSLCONF = "OPENSSL_NO_RC4"))]
m.add(&arc4, none_type.as_any(), None, Cipher::rc4())?;
m.add_legacy(&arc4, none_type.as_any(), None, Cipher::rc4())?;

m.add(&des, &cbc, Some(64), Cipher::des_cbc())?;
m.add_legacy(&des, &cbc, Some(64), Cipher::des_cbc())?;

if let Some(rc2_cbc) = Cipher::from_nid(openssl::nid::Nid::RC2_CBC) {
m.add(&rc2, &cbc, Some(128), rc2_cbc)?;
m.add_legacy(&rc2, &cbc, Some(128), rc2_cbc)?;
}
}

Expand Down
66 changes: 66 additions & 0 deletions tests/hazmat/backends/test_openssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@


import itertools
import os
import subprocess
import sys
import textwrap

import pytest

Expand Down Expand Up @@ -108,6 +112,68 @@ def test_evp_ciphers_registered(self):
cipher = backend._lib.EVP_get_cipherbyname(b"aes-256-cbc")
assert cipher != backend._ffi.NULL

@pytest.mark.skipif(
rust_openssl.CRYPTOGRAPHY_IS_LIBRESSL
or rust_openssl.CRYPTOGRAPHY_IS_BORINGSSL
or rust_openssl.CRYPTOGRAPHY_IS_AWSLC,
reason="Requires OpenSSL provider configuration",
)
def test_legacy_ciphers_require_provider_implementation(self, tmp_path):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a better way we can test this? I'd prefer to avoid using confs if possible.

config = tmp_path / "openssl.cnf"
config.write_text(
"openssl_conf = openssl_init\n"
"[openssl_init]\n"
"alg_section = algorithm_section\n"
"[algorithm_section]\n"
"default_properties = provider=default\n"
)
script = textwrap.dedent(
"""
from cryptography.exceptions import UnsupportedAlgorithm, _Reasons
from cryptography.hazmat.backends.openssl.backend import backend
from cryptography.hazmat.bindings import _rust
from cryptography.hazmat.decrepit.ciphers.algorithms import (
ARC4, Blowfish, CAST5, RC2,
)
from cryptography.hazmat.primitives.ciphers import (
Cipher, algorithms, modes,
)

if not _rust.openssl._legacy_provider_loaded:
raise SystemExit(77)

assert backend.cipher_supported(
algorithms.AES(b"\\x00" * 16), modes.CBC(b"\\x00" * 16)
)
for algorithm, mode in (
(ARC4(b"\\x00" * 16), None),
(Blowfish(b"\\x00" * 16), modes.CBC(b"\\x00" * 8)),
(CAST5(b"\\x00" * 16), modes.CBC(b"\\x00" * 8)),
(RC2(b"\\x00" * 16), modes.CBC(b"\\x00" * 8)),
):
assert not backend.cipher_supported(algorithm, mode), (
algorithm.name
)
try:
Cipher(algorithm, mode).encryptor()
except UnsupportedAlgorithm as error:
assert error._reason is _Reasons.UNSUPPORTED_CIPHER
else:
raise AssertionError(
f"{algorithm.name} unexpectedly initialized"
)
"""
)
result = subprocess.run(
[sys.executable, "-c", script],
env={**os.environ, "OPENSSL_CONF": str(config)},
capture_output=True,
text=True,
)
if result.returncode == 77:
pytest.skip("OpenSSL legacy provider is unavailable")
assert result.returncode == 0, result.stderr


class TestOpenSSLRSA:
def test_rsa_padding_unsupported_pss_mgf1_hash(self):
Expand Down
Loading