Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minimal PoC for putting pyOpenSSL functionality in Rust #12378

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
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 @@ -23,6 +23,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 @@ -255,6 +256,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
Loading