diff --git a/src/_cffi_src/openssl/nid.py b/src/_cffi_src/openssl/nid.py index 309991273c83..28135b428d46 100644 --- a/src/_cffi_src/openssl/nid.py +++ b/src/_cffi_src/openssl/nid.py @@ -15,7 +15,6 @@ static const int NID_undef; static const int NID_aes_256_cbc; static const int NID_pbe_WithSHA1And3_Key_TripleDES_CBC; -static const int NID_X25519; static const int NID_X448; static const int NID_ED25519; static const int NID_ED448; diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index 3415863b33d8..53e3486c0da2 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -53,10 +53,6 @@ _X448PrivateKey, _X448PublicKey, ) -from cryptography.hazmat.backends.openssl.x25519 import ( - _X25519PrivateKey, - _X25519PublicKey, -) from cryptography.hazmat.bindings._rust import openssl as rust_openssl from cryptography.hazmat.bindings.openssl import binding from cryptography.hazmat.primitives import hashes, serialization @@ -715,7 +711,9 @@ def _evp_pkey_to_private_key( # EVP_PKEY_X448 is not present in CRYPTOGRAPHY_IS_LIBRESSL return _X448PrivateKey(self, evp_pkey) elif key_type == self._lib.EVP_PKEY_X25519: - return _X25519PrivateKey(self, evp_pkey) + return rust_openssl.x25519.private_key_from_ptr( + int(self._ffi.cast("intptr_t", evp_pkey)) + ) elif key_type == getattr(self._lib, "EVP_PKEY_ED448", None): # EVP_PKEY_ED448 is not present in CRYPTOGRAPHY_IS_LIBRESSL return _Ed448PrivateKey(self, evp_pkey) @@ -772,7 +770,9 @@ def _evp_pkey_to_public_key(self, evp_pkey) -> PublicKeyTypes: # EVP_PKEY_X448 is not present in CRYPTOGRAPHY_IS_LIBRESSL return _X448PublicKey(self, evp_pkey) elif key_type == self._lib.EVP_PKEY_X25519: - return _X25519PublicKey(self, evp_pkey) + return rust_openssl.x25519.public_key_from_ptr( + int(self._ffi.cast("intptr_t", evp_pkey)) + ) elif key_type == getattr(self._lib, "EVP_PKEY_ED448", None): # EVP_PKEY_ED448 is not present in CRYPTOGRAPHY_IS_LIBRESSL return _Ed448PublicKey(self, evp_pkey) @@ -1860,30 +1860,12 @@ def dh_x942_serialization_supported(self) -> bool: return self._lib.Cryptography_HAS_EVP_PKEY_DHX == 1 def x25519_load_public_bytes(self, data: bytes) -> x25519.X25519PublicKey: - if len(data) != 32: - raise ValueError("An X25519 public key is 32 bytes long") - - data_ptr = self._ffi.from_buffer(data) - evp_pkey = self._lib.EVP_PKEY_new_raw_public_key( - self._lib.NID_X25519, self._ffi.NULL, data_ptr, len(data) - ) - self.openssl_assert(evp_pkey != self._ffi.NULL) - evp_pkey = self._ffi.gc(evp_pkey, self._lib.EVP_PKEY_free) - return _X25519PublicKey(self, evp_pkey) + return rust_openssl.x25519.from_public_bytes(data) def x25519_load_private_bytes( self, data: bytes ) -> x25519.X25519PrivateKey: - if len(data) != 32: - raise ValueError("An X25519 private key is 32 bytes long") - - data_ptr = self._ffi.from_buffer(data) - evp_pkey = self._lib.EVP_PKEY_new_raw_private_key( - self._lib.NID_X25519, self._ffi.NULL, data_ptr, len(data) - ) - self.openssl_assert(evp_pkey != self._ffi.NULL) - evp_pkey = self._ffi.gc(evp_pkey, self._lib.EVP_PKEY_free) - return _X25519PrivateKey(self, evp_pkey) + return rust_openssl.x25519.from_private_bytes(data) def _evp_pkey_keygen_gc(self, nid): evp_pkey_ctx = self._lib.EVP_PKEY_CTX_new_id(nid, self._ffi.NULL) @@ -1899,8 +1881,7 @@ def _evp_pkey_keygen_gc(self, nid): return evp_pkey def x25519_generate_key(self) -> x25519.X25519PrivateKey: - evp_pkey = self._evp_pkey_keygen_gc(self._lib.NID_X25519) - return _X25519PrivateKey(self, evp_pkey) + return rust_openssl.x25519.generate_key() def x25519_supported(self) -> bool: if self._fips_enabled: diff --git a/src/cryptography/hazmat/backends/openssl/x25519.py b/src/cryptography/hazmat/backends/openssl/x25519.py deleted file mode 100644 index b7f9406c7b2a..000000000000 --- a/src/cryptography/hazmat/backends/openssl/x25519.py +++ /dev/null @@ -1,120 +0,0 @@ -# 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. - -import typing - -from cryptography.hazmat.backends.openssl.utils import _evp_pkey_derive -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric.x25519 import ( - X25519PrivateKey, - X25519PublicKey, -) - -if typing.TYPE_CHECKING: - from cryptography.hazmat.backends.openssl.backend import Backend - - -_X25519_KEY_SIZE = 32 - - -class _X25519PublicKey(X25519PublicKey): - def __init__(self, backend: "Backend", evp_pkey): - self._backend = backend - self._evp_pkey = evp_pkey - - def public_bytes( - self, - encoding: serialization.Encoding, - format: serialization.PublicFormat, - ) -> bytes: - if ( - encoding is serialization.Encoding.Raw - or format is serialization.PublicFormat.Raw - ): - if ( - encoding is not serialization.Encoding.Raw - or format is not serialization.PublicFormat.Raw - ): - raise ValueError( - "When using Raw both encoding and format must be Raw" - ) - - return self._raw_public_bytes() - - return self._backend._public_key_bytes( - encoding, format, self, self._evp_pkey, None - ) - - def _raw_public_bytes(self) -> bytes: - buf = self._backend._ffi.new("unsigned char []", _X25519_KEY_SIZE) - buflen = self._backend._ffi.new("size_t *", _X25519_KEY_SIZE) - res = self._backend._lib.EVP_PKEY_get_raw_public_key( - self._evp_pkey, buf, buflen - ) - self._backend.openssl_assert(res == 1) - self._backend.openssl_assert(buflen[0] == _X25519_KEY_SIZE) - return self._backend._ffi.buffer(buf, _X25519_KEY_SIZE)[:] - - -class _X25519PrivateKey(X25519PrivateKey): - def __init__(self, backend: "Backend", evp_pkey): - self._backend = backend - self._evp_pkey = evp_pkey - - def public_key(self) -> X25519PublicKey: - bio = self._backend._create_mem_bio_gc() - res = self._backend._lib.i2d_PUBKEY_bio(bio, self._evp_pkey) - self._backend.openssl_assert(res == 1) - evp_pkey = self._backend._lib.d2i_PUBKEY_bio( - bio, self._backend._ffi.NULL - ) - self._backend.openssl_assert(evp_pkey != self._backend._ffi.NULL) - evp_pkey = self._backend._ffi.gc( - evp_pkey, self._backend._lib.EVP_PKEY_free - ) - return _X25519PublicKey(self._backend, evp_pkey) - - def exchange(self, peer_public_key: X25519PublicKey) -> bytes: - if not isinstance(peer_public_key, X25519PublicKey): - raise TypeError("peer_public_key must be X25519PublicKey.") - - return _evp_pkey_derive(self._backend, self._evp_pkey, peer_public_key) - - def private_bytes( - self, - encoding: serialization.Encoding, - format: serialization.PrivateFormat, - encryption_algorithm: serialization.KeySerializationEncryption, - ) -> bytes: - if ( - encoding is serialization.Encoding.Raw - or format is serialization.PrivateFormat.Raw - ): - if ( - format is not serialization.PrivateFormat.Raw - or encoding is not serialization.Encoding.Raw - or not isinstance( - encryption_algorithm, serialization.NoEncryption - ) - ): - raise ValueError( - "When using Raw both encoding and format must be Raw " - "and encryption_algorithm must be NoEncryption()" - ) - - return self._raw_private_bytes() - - return self._backend._private_key_bytes( - encoding, format, encryption_algorithm, self, self._evp_pkey, None - ) - - def _raw_private_bytes(self) -> bytes: - buf = self._backend._ffi.new("unsigned char []", _X25519_KEY_SIZE) - buflen = self._backend._ffi.new("size_t *", _X25519_KEY_SIZE) - res = self._backend._lib.EVP_PKEY_get_raw_private_key( - self._evp_pkey, buf, buflen - ) - self._backend.openssl_assert(res == 1) - self._backend.openssl_assert(buflen[0] == _X25519_KEY_SIZE) - return self._backend._ffi.buffer(buf, _X25519_KEY_SIZE)[:] diff --git a/src/cryptography/hazmat/bindings/_rust/openssl/__init__.pyi b/src/cryptography/hazmat/bindings/_rust/openssl/__init__.pyi index d583500dfc86..c19b6a9bcbeb 100644 --- a/src/cryptography/hazmat/bindings/_rust/openssl/__init__.pyi +++ b/src/cryptography/hazmat/bindings/_rust/openssl/__init__.pyi @@ -4,6 +4,10 @@ import typing +from cryptography.hazmat.bindings._rust.openssl import x25519 + +__all__ = ["openssl_version", "raise_openssl_error", "x25519"] + def openssl_version() -> int: ... def raise_openssl_error() -> typing.NoReturn: ... def capture_error_stack() -> typing.List[OpenSSLError]: ... diff --git a/src/cryptography/hazmat/bindings/_rust/openssl/x25519.pyi b/src/cryptography/hazmat/bindings/_rust/openssl/x25519.pyi new file mode 100644 index 000000000000..90f7cbdda950 --- /dev/null +++ b/src/cryptography/hazmat/bindings/_rust/openssl/x25519.pyi @@ -0,0 +1,14 @@ +# 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. + +from cryptography.hazmat.primitives.asymmetric import x25519 + +class X25519PrivateKey: ... +class X25519PublicKey: ... + +def generate_key() -> x25519.X25519PrivateKey: ... +def private_key_from_ptr(ptr: int) -> x25519.X25519PrivateKey: ... +def public_key_from_ptr(ptr: int) -> x25519.X25519PublicKey: ... +def from_private_bytes(data: bytes) -> x25519.X25519PrivateKey: ... +def from_public_bytes(data: bytes) -> x25519.X25519PublicKey: ... diff --git a/src/cryptography/hazmat/primitives/asymmetric/x25519.py b/src/cryptography/hazmat/primitives/asymmetric/x25519.py index eb964f465316..fb21fe1749a5 100644 --- a/src/cryptography/hazmat/primitives/asymmetric/x25519.py +++ b/src/cryptography/hazmat/primitives/asymmetric/x25519.py @@ -6,6 +6,7 @@ import abc from cryptography.exceptions import UnsupportedAlgorithm, _Reasons +from cryptography.hazmat.bindings._rust import openssl as rust_openssl from cryptography.hazmat.primitives import _serialization @@ -32,14 +33,17 @@ def public_bytes( The serialized bytes of the public key. """ + @abc.abstractmethod def public_bytes_raw(self) -> bytes: """ The raw bytes of the public key. Equivalent to public_bytes(Raw, Raw). """ - return self.public_bytes( - _serialization.Encoding.Raw, _serialization.PublicFormat.Raw - ) + + +# For LibreSSL +if hasattr(rust_openssl, "x25519"): + X25519PublicKey.register(rust_openssl.x25519.X25519PublicKey) class X25519PrivateKey(metaclass=abc.ABCMeta): @@ -83,19 +87,20 @@ def private_bytes( The serialized bytes of the private key. """ + @abc.abstractmethod def private_bytes_raw(self) -> bytes: """ The raw bytes of the private key. Equivalent to private_bytes(Raw, Raw, NoEncryption()). """ - return self.private_bytes( - _serialization.Encoding.Raw, - _serialization.PrivateFormat.Raw, - _serialization.NoEncryption(), - ) @abc.abstractmethod def exchange(self, peer_public_key: X25519PublicKey) -> bytes: """ Performs a key exchange operation using the provided peer's public key. """ + + +# For LibreSSL +if hasattr(rust_openssl, "x25519"): + X25519PrivateKey.register(rust_openssl.x25519.X25519PrivateKey) diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock index 2ce5c9dfa209..dd8c6b0c6fb2 100644 --- a/src/rust/Cargo.lock +++ b/src/rust/Cargo.lock @@ -115,6 +115,7 @@ dependencies = [ "asn1", "cc", "chrono", + "foreign-types-shared", "once_cell", "openssl", "openssl-sys", diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml index 0ef74bd1be91..2b1b94001683 100644 --- a/src/rust/Cargo.toml +++ b/src/rust/Cargo.toml @@ -16,6 +16,7 @@ chrono = { version = "0.4.24", default-features = false, features = ["alloc", "c ouroboros = "0.15" openssl = "0.10.48" openssl-sys = "0.9.72" +foreign-types-shared = "0.1" [build-dependencies] cc = "1.0.72" diff --git a/src/rust/build.rs b/src/rust/build.rs index 0b43d04cdf42..01177ac0e96c 100644 --- a/src/rust/build.rs +++ b/src/rust/build.rs @@ -3,6 +3,7 @@ use std::io::Write; use std::path::Path; use std::process::{Command, Stdio}; +#[allow(clippy::unusual_byte_groupings)] fn main() { let target = env::var("TARGET").unwrap(); let openssl_static = env::var("OPENSSL_STATIC") @@ -71,6 +72,15 @@ fn main() { } build.compile("_openssl.a"); + + if let Ok(version) = env::var("DEP_OPENSSL_LIBRESSL_VERSION_NUMBER") { + let version = u64::from_str_radix(&version, 16).unwrap(); + + println!("cargo:rustc-cfg=CRYPTOGRAPHY_IS_LIBRESSL"); + if version >= 0x3_07_00_00_0 { + println!("cargo:rustc-cfg=CRYPTOGRAPHY_LIBRESSL_370_OR_GREATER") + } + } } /// Run a python script using the specified interpreter binary. diff --git a/src/rust/src/backend/mod.rs b/src/rust/src/backend/mod.rs new file mode 100644 index 000000000000..c7e086b56efb --- /dev/null +++ b/src/rust/src/backend/mod.rs @@ -0,0 +1,13 @@ +// 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. + +#[cfg(any(not(CRYPTOGRAPHY_IS_LIBRESSL), CRYPTOGRAPHY_LIBRESSL_370_OR_GREATER))] +pub(crate) mod x25519; + +pub(crate) fn add_to_module(module: &pyo3::prelude::PyModule) -> pyo3::PyResult<()> { + #[cfg(any(not(CRYPTOGRAPHY_IS_LIBRESSL), CRYPTOGRAPHY_LIBRESSL_370_OR_GREATER))] + module.add_submodule(x25519::create_module(module.py())?)?; + + Ok(()) +} diff --git a/src/rust/src/backend/x25519.rs b/src/rust/src/backend/x25519.rs new file mode 100644 index 000000000000..96a2c7a5cc6e --- /dev/null +++ b/src/rust/src/backend/x25519.rs @@ -0,0 +1,297 @@ +// 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. + +use crate::buf::CffiBuf; +use crate::error::{CryptographyError, CryptographyResult}; +use foreign_types_shared::ForeignTypeRef; + +#[pyo3::prelude::pyclass] +struct X25519PrivateKey { + pkey: openssl::pkey::PKey, +} + +#[pyo3::prelude::pyclass] +struct X25519PublicKey { + pkey: openssl::pkey::PKey, +} + +#[pyo3::prelude::pyfunction] +fn generate_key() -> CryptographyResult { + Ok(X25519PrivateKey { + pkey: openssl::pkey::PKey::generate_x25519()?, + }) +} + +#[pyo3::prelude::pyfunction] +fn private_key_from_ptr(ptr: usize) -> X25519PrivateKey { + let pkey = unsafe { openssl::pkey::PKeyRef::from_ptr(ptr as *mut _) }; + X25519PrivateKey { + pkey: pkey.to_owned(), + } +} + +#[pyo3::prelude::pyfunction] +fn public_key_from_ptr(ptr: usize) -> X25519PublicKey { + let pkey = unsafe { openssl::pkey::PKeyRef::from_ptr(ptr as *mut _) }; + X25519PublicKey { + pkey: pkey.to_owned(), + } +} + +#[pyo3::prelude::pyfunction] +fn from_private_bytes(data: CffiBuf<'_>) -> pyo3::PyResult { + let pkey = + openssl::pkey::PKey::private_key_from_raw_bytes(data.as_bytes(), openssl::pkey::Id::X25519) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!( + "An X25519 private key is 32 bytes long: {}", + e + )) + })?; + Ok(X25519PrivateKey { pkey }) +} +#[pyo3::prelude::pyfunction] +fn from_public_bytes(data: &[u8]) -> pyo3::PyResult { + let pkey = openssl::pkey::PKey::public_key_from_raw_bytes(data, openssl::pkey::Id::X25519) + .map_err(|_| { + pyo3::exceptions::PyValueError::new_err("An X25519 public key is 32 bytes long") + })?; + Ok(X25519PublicKey { pkey }) +} + +#[pyo3::prelude::pymethods] +impl X25519PrivateKey { + fn exchange<'p>( + &self, + py: pyo3::Python<'p>, + public_key: &X25519PublicKey, + ) -> CryptographyResult<&'p pyo3::types::PyBytes> { + let mut deriver = openssl::derive::Deriver::new(&self.pkey)?; + deriver.set_peer(&public_key.pkey)?; + + Ok(pyo3::types::PyBytes::new_with(py, deriver.len()?, |b| { + let n = deriver.derive(b).map_err(|_| { + pyo3::exceptions::PyValueError::new_err("Error computing shared key.") + })?; + assert_eq!(n, b.len()); + Ok(()) + })?) + } + + fn public_key(&self) -> CryptographyResult { + let raw_bytes = self.pkey.raw_public_key()?; + Ok(X25519PublicKey { + pkey: openssl::pkey::PKey::public_key_from_raw_bytes( + &raw_bytes, + openssl::pkey::Id::X25519, + )?, + }) + } + + fn private_bytes_raw<'p>( + &self, + py: pyo3::Python<'p>, + ) -> CryptographyResult<&'p pyo3::types::PyBytes> { + let raw_bytes = self.pkey.raw_private_key()?; + Ok(pyo3::types::PyBytes::new(py, &raw_bytes)) + } + + fn private_bytes<'p>( + &self, + py: pyo3::Python<'p>, + encoding: &pyo3::PyAny, + format: &pyo3::PyAny, + encryption_algorithm: &pyo3::PyAny, + ) -> CryptographyResult<&'p pyo3::types::PyBytes> { + let serialization_mod = py.import("cryptography.hazmat.primitives.serialization")?; + let encoding_class: &pyo3::types::PyType = serialization_mod + .getattr(crate::intern!(py, "Encoding"))? + .extract()?; + let private_format_class: &pyo3::types::PyType = serialization_mod + .getattr(crate::intern!(py, "PrivateFormat"))? + .extract()?; + let no_encryption_class: &pyo3::types::PyType = serialization_mod + .getattr(crate::intern!(py, "NoEncryption"))? + .extract()?; + let best_available_encryption_class: &pyo3::types::PyType = serialization_mod + .getattr(crate::intern!(py, "BestAvailableEncryption"))? + .extract()?; + + if !encoding_class.is_instance(encoding)? { + return Err(CryptographyError::from( + pyo3::exceptions::PyTypeError::new_err( + "encoding must be an item from the Encoding enum", + ), + )); + } + if !private_format_class.is_instance(format)? { + return Err(CryptographyError::from( + pyo3::exceptions::PyTypeError::new_err( + "format must be an item from the PrivateFormat enum", + ), + )); + } + + if encoding == encoding_class.getattr(crate::intern!(py, "Raw"))? + || format == private_format_class.getattr(crate::intern!(py, "Raw"))? + { + if encoding != encoding_class.getattr(crate::intern!(py, "Raw"))? + || format != private_format_class.getattr(crate::intern!(py, "Raw"))? + || !no_encryption_class.is_instance(encryption_algorithm)? + { + return Err(CryptographyError::from(pyo3::exceptions::PyValueError::new_err( + "When using Raw both encoding and format must be Raw and encryption_algorithm must be NoEncryption()" + ))); + } + let raw_bytes = self.pkey.raw_private_key()?; + return Ok(pyo3::types::PyBytes::new(py, &raw_bytes)); + } + + let password = if no_encryption_class.is_instance(encryption_algorithm)? { + b"" + } else if best_available_encryption_class.is_instance(encryption_algorithm)? { + encryption_algorithm + .getattr(crate::intern!(py, "password"))? + .extract::<&[u8]>()? + } else { + return Err(CryptographyError::from( + pyo3::exceptions::PyTypeError::new_err( + "Encryption algorithm must be a KeySerializationEncryption instance", + ), + )); + }; + + if password.len() > 1023 { + return Err(CryptographyError::from( + pyo3::exceptions::PyValueError::new_err( + "Passwords longer than 1023 bytes are not supported by this backend", + ), + )); + } + + if format == private_format_class.getattr(crate::intern!(py, "PKCS8"))? { + if encoding == encoding_class.getattr(crate::intern!(py, "PEM"))? { + let pem_bytes = if password.is_empty() { + self.pkey.private_key_to_pem_pkcs8()? + } else { + self.pkey.private_key_to_pem_pkcs8_passphrase( + openssl::symm::Cipher::aes_256_cbc(), + password, + )? + }; + return Ok(pyo3::types::PyBytes::new(py, &pem_bytes)); + } else if encoding == encoding_class.getattr(crate::intern!(py, "DER"))? { + let der_bytes = if password.is_empty() { + self.pkey.private_key_to_pkcs8()? + } else { + self.pkey.private_key_to_pkcs8_passphrase( + openssl::symm::Cipher::aes_256_cbc(), + password, + )? + }; + return Ok(pyo3::types::PyBytes::new(py, &der_bytes)); + } else { + return Err(CryptographyError::from( + pyo3::exceptions::PyValueError::new_err("Unsupported encoding for PKCS8"), + )); + } + } + + Err(CryptographyError::from( + pyo3::exceptions::PyValueError::new_err("format is invalid with this key"), + )) + } +} + +#[pyo3::prelude::pymethods] +impl X25519PublicKey { + fn public_bytes_raw<'p>( + &self, + py: pyo3::Python<'p>, + ) -> CryptographyResult<&'p pyo3::types::PyBytes> { + let raw_bytes = self.pkey.raw_public_key()?; + Ok(pyo3::types::PyBytes::new(py, &raw_bytes)) + } + + fn public_bytes<'p>( + &self, + py: pyo3::Python<'p>, + encoding: &pyo3::PyAny, + format: &pyo3::PyAny, + ) -> CryptographyResult<&'p pyo3::types::PyBytes> { + let serialization_mod = py.import("cryptography.hazmat.primitives.serialization")?; + let encoding_class: &pyo3::types::PyType = serialization_mod + .getattr(crate::intern!(py, "Encoding"))? + .extract()?; + let public_format_class: &pyo3::types::PyType = serialization_mod + .getattr(crate::intern!(py, "PublicFormat"))? + .extract()?; + + if !encoding_class.is_instance(encoding)? { + return Err(CryptographyError::from( + pyo3::exceptions::PyTypeError::new_err( + "encoding must be an item from the Encoding enum", + ), + )); + } + if !public_format_class.is_instance(format)? { + return Err(CryptographyError::from( + pyo3::exceptions::PyTypeError::new_err( + "format must be an item from the PublicFormat enum", + ), + )); + } + + if encoding == encoding_class.getattr(crate::intern!(py, "Raw"))? + || format == public_format_class.getattr(crate::intern!(py, "Raw"))? + { + if encoding != encoding_class.getattr(crate::intern!(py, "Raw"))? + || format != public_format_class.getattr(crate::intern!(py, "Raw"))? + { + return Err(CryptographyError::from( + pyo3::exceptions::PyValueError::new_err( + "When using Raw both encoding and format must be Raw", + ), + )); + } + let raw_bytes = self.pkey.raw_public_key()?; + return Ok(pyo3::types::PyBytes::new(py, &raw_bytes)); + } + + // SubjectPublicKeyInfo + PEM/DER + if format == public_format_class.getattr(crate::intern!(py, "SubjectPublicKeyInfo"))? { + if encoding == encoding_class.getattr(crate::intern!(py, "PEM"))? { + let pem_bytes = self.pkey.public_key_to_pem()?; + return Ok(pyo3::types::PyBytes::new(py, &pem_bytes)); + } else if encoding == encoding_class.getattr(crate::intern!(py, "DER"))? { + let der_bytes = self.pkey.public_key_to_der()?; + return Ok(pyo3::types::PyBytes::new(py, &der_bytes)); + } else { + return Err(CryptographyError::from( + pyo3::exceptions::PyValueError::new_err( + "SubjectPublicKeyInfo works only with PEM or DER encoding", + ), + )); + } + } + + Err(CryptographyError::from( + pyo3::exceptions::PyValueError::new_err("format is invalid with this key"), + )) + } +} + +pub(crate) fn create_module(py: pyo3::Python<'_>) -> pyo3::PyResult<&pyo3::prelude::PyModule> { + let m = pyo3::prelude::PyModule::new(py, "x25519")?; + m.add_wrapped(pyo3::wrap_pyfunction!(generate_key))?; + m.add_wrapped(pyo3::wrap_pyfunction!(private_key_from_ptr))?; + m.add_wrapped(pyo3::wrap_pyfunction!(public_key_from_ptr))?; + m.add_wrapped(pyo3::wrap_pyfunction!(from_private_bytes))?; + m.add_wrapped(pyo3::wrap_pyfunction!(from_public_bytes))?; + + m.add_class::()?; + m.add_class::()?; + + Ok(m) +} diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs index cec55262123c..2ec4e66bb5c2 100644 --- a/src/rust/src/lib.rs +++ b/src/rust/src/lib.rs @@ -10,6 +10,7 @@ #![allow(unknown_lints, clippy::borrow_deref_ref)] mod asn1; +mod backend; mod buf; mod error; mod intern; @@ -192,6 +193,7 @@ fn _rust(py: pyo3::Python<'_>, m: &pyo3::types::PyModule) -> pyo3::PyResult<()> openssl_mod.add_function(pyo3::wrap_pyfunction!(raise_openssl_error, m)?)?; openssl_mod.add_function(pyo3::wrap_pyfunction!(capture_error_stack, m)?)?; openssl_mod.add_class::()?; + crate::backend::add_to_module(openssl_mod)?; m.add_submodule(openssl_mod)?; Ok(()) diff --git a/tests/hazmat/primitives/test_x25519.py b/tests/hazmat/primitives/test_x25519.py index a0a5083f35e1..3eb642df5542 100644 --- a/tests/hazmat/primitives/test_x25519.py +++ b/tests/hazmat/primitives/test_x25519.py @@ -96,10 +96,24 @@ def test_null_shared_key_raises_error(self, backend): def test_public_bytes_bad_args(self, backend): key = X25519PrivateKey.generate().public_key() - with pytest.raises(ValueError): + with pytest.raises(TypeError): key.public_bytes( None, serialization.PublicFormat.Raw # type: ignore[arg-type] ) + with pytest.raises(ValueError): + key.public_bytes( + serialization.Encoding.DER, serialization.PublicFormat.Raw + ) + with pytest.raises(TypeError): + key.public_bytes( + serialization.Encoding.DER, + None, # type: ignore[arg-type] + ) + with pytest.raises(ValueError): + key.public_bytes( + serialization.Encoding.SMIME, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) # These vectors are also from RFC 7748 # https://tools.ietf.org/html/rfc7748#section-6.1 @@ -202,6 +216,37 @@ def test_invalid_private_bytes(self, backend): serialization.NoEncryption(), ) + with pytest.raises(TypeError): + key.private_bytes(None, None, None) # type: ignore[arg-type] + + with pytest.raises(TypeError): + key.private_bytes( + serialization.Encoding.Raw, + None, # type: ignore[arg-type] + None, # type: ignore[arg-type] + ) + + with pytest.raises(TypeError): + key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.PKCS8, + object(), # type: ignore[arg-type] + ) + + with pytest.raises(ValueError): + key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.PKCS8, + serialization.BestAvailableEncryption(b"a" * 1024), + ) + + with pytest.raises(ValueError): + key.private_bytes( + serialization.Encoding.SMIME, + serialization.PrivateFormat.PKCS8, + serialization.NoEncryption(), + ) + def test_invalid_public_bytes(self, backend): key = X25519PrivateKey.generate().public_key() with pytest.raises(ValueError):