Skip to content

Commit

Permalink
Minimal PoC for putting pyOpenSSL functionality in Rust
Browse files Browse the repository at this point in the history
  • Loading branch information
alex committed Feb 1, 2025
1 parent 13a2e6f commit 0fccee7
Show file tree
Hide file tree
Showing 10 changed files with 260 additions and 8 deletions.
9 changes: 3 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ rust-version = "1.65.0"
[workspace.dependencies]
asn1 = { version = "0.20.0", default-features = false }
pyo3 = { version = "0.23.4", features = ["abi3"] }
openssl = "0.10.69"
openssl-sys = "0.9.104"
# openssl = "0.10.69"
# openssl-sys = "0.9.104"
openssl = { git = "https://github.com/sfackler/rust-openssl" }
openssl-sys = { git = "https://github.com/sfackler/rust-openssl" }

[profile.release]
overflow-checks = true
23 changes: 23 additions & 0 deletions src/cryptography/hazmat/bindings/_rust/pyopenssl.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 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 __future__ import annotations

import typing

SSLv23_METHOD: int
TLSv1_METHOD: int
TLSv1_1_METHOD: int
TLSv1_2_METHOD: int
TLS_METHOD: int
TLS_SERVER_METHOD: int
TLS_CLIENT_METHOD: int
DTLS_METHOD: int
DTLS_SERVER_METHOD: int
DTLS_CLIENT_METHOD: int

class Context:
def __new__(cls, method: int) -> Context: ...
@property
def _context(self) -> typing.Any: ...
38 changes: 38 additions & 0 deletions src/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub(crate) mod oid;
mod padding;
mod pkcs12;
mod pkcs7;
mod pyopenssl;
mod test_support;
pub(crate) mod types;
pub(crate) mod utils;
Expand Down Expand Up @@ -249,6 +250,43 @@ mod _rust {
}
}

#[pyo3::pymodule]
mod pyopenssl {
use pyo3::prelude::PyModuleMethods;

#[pymodule_export]
use crate::pyopenssl::error::Error;
#[pymodule_export]
use crate::pyopenssl::ssl::Context;

#[pymodule_init]
fn init(pyopenssl_mod: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> {
use crate::pyopenssl::ssl::{
DTLS_CLIENT_METHOD, DTLS_METHOD, DTLS_SERVER_METHOD, SSLV23_METHOD, TLSV1_1_METHOD,
TLSV1_2_METHOD, TLSV1_METHOD, TLS_CLIENT_METHOD, TLS_METHOD, TLS_SERVER_METHOD,
};

macro_rules! add_const {
($name:ident) => {
pyopenssl_mod.add(stringify!($name), $name)?;
};
}

pyopenssl_mod.add("SSLv23_METHOD", SSLV23_METHOD)?;
pyopenssl_mod.add("TLSv1_METHOD", TLSV1_METHOD)?;
pyopenssl_mod.add("TLSv1_1_METHOD", TLSV1_1_METHOD)?;
pyopenssl_mod.add("TLSv1_2_METHOD", TLSV1_2_METHOD)?;
add_const!(TLS_METHOD);
add_const!(TLS_SERVER_METHOD);
add_const!(TLS_CLIENT_METHOD);
add_const!(DTLS_METHOD);
add_const!(DTLS_SERVER_METHOD);
add_const!(DTLS_CLIENT_METHOD);

Ok(())
}
}

#[pymodule_init]
fn init(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> {
m.add_submodule(&cryptography_cffi::create_module(m.py())?)?;
Expand Down
68 changes: 68 additions & 0 deletions src/rust/src/pyopenssl/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// 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 pyo3::types::PyListMethods;

pyo3::create_exception!(
OpenSSL.SSL,
Error,
pyo3::exceptions::PyException,
"An error occurred in an `OpenSSL.SSL` API."
);

pub(crate) enum PyOpenSslError {
Py(pyo3::PyErr),
OpenSSL(openssl::error::ErrorStack),
}

impl From<pyo3::PyErr> for PyOpenSslError {
fn from(e: pyo3::PyErr) -> PyOpenSslError {
PyOpenSslError::Py(e)
}
}

impl From<openssl::error::ErrorStack> for PyOpenSslError {
fn from(e: openssl::error::ErrorStack) -> PyOpenSslError {
PyOpenSslError::OpenSSL(e)
}
}

impl From<PyOpenSslError> for pyo3::PyErr {
fn from(e: PyOpenSslError) -> pyo3::PyErr {
match e {
PyOpenSslError::Py(e) => e,
PyOpenSslError::OpenSSL(e) => pyo3::Python::with_gil(|py| {
let errs = pyo3::types::PyList::empty(py);
for err in e.errors() {
errs.append((
err.library().unwrap_or(""),
err.function().unwrap_or(""),
err.reason().unwrap_or(""),
))?;
}
Ok(Error::new_err(errs.unbind()))
})
.unwrap_or_else(|e| e),
}
}
}

pub(crate) type PyOpenSslResult<T> = Result<T, PyOpenSslError>;

#[cfg(test)]
mod tests {
use super::{Error, PyOpenSslError};

#[test]
fn test_pyopenssl_error_from_openssl_error() {
pyo3::Python::with_gil(|py| {
// Literally anything that returns a non-empty error stack
let err = openssl::x509::X509::from_der(b"").unwrap_err();

let py_err: pyo3::PyErr = PyOpenSslError::from(err).into();
assert!(py_err.is_instance_of::<Error>(py));
assert!(py_err.to_string().starts_with("Error: [("),);
});
}
}
6 changes: 6 additions & 0 deletions src/rust/src/pyopenssl/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// 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.

pub(crate) mod error;
pub(crate) mod ssl;
75 changes: 75 additions & 0 deletions src/rust/src/pyopenssl/ssl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// 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 pyo3::types::PyAnyMethods;

use crate::pyopenssl::error::{PyOpenSslError, PyOpenSslResult};
use crate::types;

pub(crate) const SSLV23_METHOD: u32 = 3;
pub(crate) const TLSV1_METHOD: u32 = 4;
pub(crate) const TLSV1_1_METHOD: u32 = 5;
pub(crate) const TLSV1_2_METHOD: u32 = 6;
pub(crate) const TLS_METHOD: u32 = 7;
pub(crate) const TLS_SERVER_METHOD: u32 = 8;
pub(crate) const TLS_CLIENT_METHOD: u32 = 9;
pub(crate) const DTLS_METHOD: u32 = 10;
pub(crate) const DTLS_SERVER_METHOD: u32 = 11;
pub(crate) const DTLS_CLIENT_METHOD: u32 = 12;

#[pyo3::pyclass(subclass, module = "OpenSSL.SSL")]
pub(crate) struct Context {
ssl_ctx: openssl::ssl::SslContextBuilder,
}

#[pyo3::pymethods]
impl Context {
#[new]
fn new(method: u32) -> PyOpenSslResult<Self> {
let (ssl_method, version) = match method {
SSLV23_METHOD => (openssl::ssl::SslMethod::tls(), None),
TLSV1_METHOD => (
openssl::ssl::SslMethod::tls(),
Some(openssl::ssl::SslVersion::TLS1),
),
TLSV1_1_METHOD => (
openssl::ssl::SslMethod::tls(),
Some(openssl::ssl::SslVersion::TLS1_1),
),
TLSV1_2_METHOD => (
openssl::ssl::SslMethod::tls(),
Some(openssl::ssl::SslVersion::TLS1_2),
),
TLS_METHOD => (openssl::ssl::SslMethod::tls(), None),
TLS_SERVER_METHOD => (openssl::ssl::SslMethod::tls_server(), None),
TLS_CLIENT_METHOD => (openssl::ssl::SslMethod::tls_client(), None),
DTLS_METHOD => (openssl::ssl::SslMethod::dtls(), None),
DTLS_SERVER_METHOD => (openssl::ssl::SslMethod::dtls_server(), None),
DTLS_CLIENT_METHOD => (openssl::ssl::SslMethod::dtls_client(), None),
_ => {
return Err(PyOpenSslError::from(
pyo3::exceptions::PyValueError::new_err("No such protocol"),
))
}
};
let mut ssl_ctx = openssl::ssl::SslContext::builder(ssl_method)?;
if let Some(version) = version {
ssl_ctx.set_min_proto_version(Some(version))?;
ssl_ctx.set_max_proto_version(Some(version))?;
}

Ok(Context { ssl_ctx })
}

#[getter]
fn _context<'p>(&self, py: pyo3::Python<'p>) -> PyOpenSslResult<pyo3::Bound<'p, pyo3::PyAny>> {
Ok(types::FFI.get(py)?.call_method1(
pyo3::intern!(py, "cast"),
(
pyo3::intern!(py, "SSL_CTX *"),
self.ssl_ctx.as_ptr() as usize,
),
)?)
}
}
3 changes: 3 additions & 0 deletions src/rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ impl LazyPyImport {
}
}

pub static FFI: LazyPyImport =
LazyPyImport::new("cryptography.hazmat.bindings._rust", &["_openssl", "ffi"]);

pub static DATETIME_DATETIME: LazyPyImport = LazyPyImport::new("datetime", &["datetime"]);
pub static DATETIME_TIMEZONE_UTC: LazyPyImport =
LazyPyImport::new("datetime", &["timezone", "utc"]);
Expand Down
3 changes: 3 additions & 0 deletions tests/pyopenssl/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# 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.
37 changes: 37 additions & 0 deletions tests/pyopenssl/test_ssl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# 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 pytest

from cryptography.hazmat.bindings._rust import _openssl, pyopenssl


class TestContext:
def test_create(self):
for method in [
pyopenssl.SSLv23_METHOD,
pyopenssl.TLSv1_METHOD,
pyopenssl.TLSv1_1_METHOD,
pyopenssl.TLSv1_2_METHOD,
pyopenssl.TLS_METHOD,
pyopenssl.TLS_SERVER_METHOD,
pyopenssl.TLS_CLIENT_METHOD,
pyopenssl.DTLS_METHOD,
pyopenssl.DTLS_SERVER_METHOD,
pyopenssl.DTLS_CLIENT_METHOD,
]:
ctx = pyopenssl.Context(method)
assert ctx

with pytest.raises(TypeError):
pyopenssl.Context(object()) # type: ignore[arg-type]

with pytest.raises(ValueError):
pyopenssl.Context(12324213)

def test__context(self):
ctx = pyopenssl.Context(pyopenssl.TLS_METHOD)
assert ctx._context
assert _openssl.ffi.typeof(ctx._context).cname == "SSL_CTX *"
assert _openssl.ffi.cast("uintptr_t", ctx._context) > 0

0 comments on commit 0fccee7

Please sign in to comment.