Skip to content

Commit c9a5682

Browse files
committed
Minimal PoC for putting pyOpenSSL functionality in Rust
1 parent 116c5af commit c9a5682

File tree

8 files changed

+253
-0
lines changed

8 files changed

+253
-0
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# This file is dual licensed under the terms of the Apache License, Version
2+
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
3+
# for complete details.
4+
5+
from __future__ import annotations
6+
7+
import typing
8+
9+
SSLv23_METHOD: int
10+
TLSv1_METHOD: int
11+
TLSv1_1_METHOD: int
12+
TLSv1_2_METHOD: int
13+
TLS_METHOD: int
14+
TLS_SERVER_METHOD: int
15+
TLS_CLIENT_METHOD: int
16+
DTLS_METHOD: int
17+
DTLS_SERVER_METHOD: int
18+
DTLS_CLIENT_METHOD: int
19+
20+
class Context:
21+
def __new__(cls, method: int) -> Context: ...
22+
@property
23+
def _context(self) -> typing.Any: ...

src/rust/src/lib.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub(crate) mod oid;
2323
mod padding;
2424
mod pkcs12;
2525
mod pkcs7;
26+
mod pyopenssl;
2627
mod test_support;
2728
pub(crate) mod types;
2829
pub(crate) mod utils;
@@ -255,6 +256,43 @@ mod _rust {
255256
}
256257
}
257258

259+
#[pyo3::pymodule]
260+
mod pyopenssl {
261+
use pyo3::prelude::PyModuleMethods;
262+
263+
#[pymodule_export]
264+
use crate::pyopenssl::error::Error;
265+
#[pymodule_export]
266+
use crate::pyopenssl::ssl::Context;
267+
268+
#[pymodule_init]
269+
fn init(pyopenssl_mod: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> {
270+
use crate::pyopenssl::ssl::{
271+
DTLS_CLIENT_METHOD, DTLS_METHOD, DTLS_SERVER_METHOD, SSLV23_METHOD, TLSV1_1_METHOD,
272+
TLSV1_2_METHOD, TLSV1_METHOD, TLS_CLIENT_METHOD, TLS_METHOD, TLS_SERVER_METHOD,
273+
};
274+
275+
macro_rules! add_const {
276+
($name:ident) => {
277+
pyopenssl_mod.add(stringify!($name), $name)?;
278+
};
279+
}
280+
281+
pyopenssl_mod.add("SSLv23_METHOD", SSLV23_METHOD)?;
282+
pyopenssl_mod.add("TLSv1_METHOD", TLSV1_METHOD)?;
283+
pyopenssl_mod.add("TLSv1_1_METHOD", TLSV1_1_METHOD)?;
284+
pyopenssl_mod.add("TLSv1_2_METHOD", TLSV1_2_METHOD)?;
285+
add_const!(TLS_METHOD);
286+
add_const!(TLS_SERVER_METHOD);
287+
add_const!(TLS_CLIENT_METHOD);
288+
add_const!(DTLS_METHOD);
289+
add_const!(DTLS_SERVER_METHOD);
290+
add_const!(DTLS_CLIENT_METHOD);
291+
292+
Ok(())
293+
}
294+
}
295+
258296
#[pymodule_init]
259297
fn init(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> {
260298
m.add_submodule(&cryptography_cffi::create_module(m.py())?)?;

src/rust/src/pyopenssl/error.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// This file is dual licensed under the terms of the Apache License, Version
2+
// 2.0, and the BSD License. See the LICENSE file in the root of this repository
3+
// for complete details.
4+
5+
use pyo3::types::PyListMethods;
6+
7+
pyo3::create_exception!(
8+
OpenSSL.SSL,
9+
Error,
10+
pyo3::exceptions::PyException,
11+
"An error occurred in an `OpenSSL.SSL` API."
12+
);
13+
14+
pub(crate) enum PyOpenSslError {
15+
Py(pyo3::PyErr),
16+
OpenSSL(openssl::error::ErrorStack),
17+
}
18+
19+
impl From<pyo3::PyErr> for PyOpenSslError {
20+
fn from(e: pyo3::PyErr) -> PyOpenSslError {
21+
PyOpenSslError::Py(e)
22+
}
23+
}
24+
25+
impl From<openssl::error::ErrorStack> for PyOpenSslError {
26+
fn from(e: openssl::error::ErrorStack) -> PyOpenSslError {
27+
PyOpenSslError::OpenSSL(e)
28+
}
29+
}
30+
31+
impl From<PyOpenSslError> for pyo3::PyErr {
32+
fn from(e: PyOpenSslError) -> pyo3::PyErr {
33+
match e {
34+
PyOpenSslError::Py(e) => e,
35+
PyOpenSslError::OpenSSL(e) => pyo3::Python::with_gil(|py| {
36+
let errs = pyo3::types::PyList::empty(py);
37+
for err in e.errors() {
38+
errs.append((
39+
err.library().unwrap_or(""),
40+
err.function().unwrap_or(""),
41+
err.reason().unwrap_or(""),
42+
))?;
43+
}
44+
Ok(Error::new_err(errs.unbind()))
45+
})
46+
.unwrap_or_else(|e| e),
47+
}
48+
}
49+
}
50+
51+
pub(crate) type PyOpenSslResult<T> = Result<T, PyOpenSslError>;
52+
53+
#[cfg(test)]
54+
mod tests {
55+
use super::{Error, PyOpenSslError};
56+
57+
#[test]
58+
fn test_pyopenssl_error_from_openssl_error() {
59+
pyo3::Python::with_gil(|py| {
60+
// Literally anything that returns a non-empty error stack
61+
let err = openssl::x509::X509::from_der(b"").unwrap_err();
62+
63+
let py_err: pyo3::PyErr = PyOpenSslError::from(err).into();
64+
assert!(py_err.is_instance_of::<Error>(py));
65+
assert!(py_err.to_string().starts_with("Error: [("),);
66+
});
67+
}
68+
}

src/rust/src/pyopenssl/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// This file is dual licensed under the terms of the Apache License, Version
2+
// 2.0, and the BSD License. See the LICENSE file in the root of this repository
3+
// for complete details.
4+
5+
pub(crate) mod error;
6+
pub(crate) mod ssl;

src/rust/src/pyopenssl/ssl.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// This file is dual licensed under the terms of the Apache License, Version
2+
// 2.0, and the BSD License. See the LICENSE file in the root of this repository
3+
// for complete details.
4+
5+
use pyo3::types::PyAnyMethods;
6+
7+
use crate::pyopenssl::error::{PyOpenSslError, PyOpenSslResult};
8+
use crate::types;
9+
10+
pub(crate) const SSLV23_METHOD: u32 = 3;
11+
pub(crate) const TLSV1_METHOD: u32 = 4;
12+
pub(crate) const TLSV1_1_METHOD: u32 = 5;
13+
pub(crate) const TLSV1_2_METHOD: u32 = 6;
14+
pub(crate) const TLS_METHOD: u32 = 7;
15+
pub(crate) const TLS_SERVER_METHOD: u32 = 8;
16+
pub(crate) const TLS_CLIENT_METHOD: u32 = 9;
17+
pub(crate) const DTLS_METHOD: u32 = 10;
18+
pub(crate) const DTLS_SERVER_METHOD: u32 = 11;
19+
pub(crate) const DTLS_CLIENT_METHOD: u32 = 12;
20+
21+
#[pyo3::pyclass(subclass, module = "OpenSSL.SSL")]
22+
pub(crate) struct Context {
23+
ssl_ctx: openssl::ssl::SslContextBuilder,
24+
}
25+
26+
#[pyo3::pymethods]
27+
impl Context {
28+
#[new]
29+
fn new(method: u32) -> PyOpenSslResult<Self> {
30+
let (ssl_method, version) = match method {
31+
SSLV23_METHOD => (openssl::ssl::SslMethod::tls(), None),
32+
TLSV1_METHOD => (
33+
openssl::ssl::SslMethod::tls(),
34+
Some(openssl::ssl::SslVersion::TLS1),
35+
),
36+
TLSV1_1_METHOD => (
37+
openssl::ssl::SslMethod::tls(),
38+
Some(openssl::ssl::SslVersion::TLS1_1),
39+
),
40+
TLSV1_2_METHOD => (
41+
openssl::ssl::SslMethod::tls(),
42+
Some(openssl::ssl::SslVersion::TLS1_2),
43+
),
44+
TLS_METHOD => (openssl::ssl::SslMethod::tls(), None),
45+
TLS_SERVER_METHOD => (openssl::ssl::SslMethod::tls_server(), None),
46+
TLS_CLIENT_METHOD => (openssl::ssl::SslMethod::tls_client(), None),
47+
DTLS_METHOD => (openssl::ssl::SslMethod::dtls(), None),
48+
DTLS_SERVER_METHOD => (openssl::ssl::SslMethod::dtls_server(), None),
49+
DTLS_CLIENT_METHOD => (openssl::ssl::SslMethod::dtls_client(), None),
50+
_ => {
51+
return Err(PyOpenSslError::from(
52+
pyo3::exceptions::PyValueError::new_err("No such protocol"),
53+
))
54+
}
55+
};
56+
let mut ssl_ctx = openssl::ssl::SslContext::builder(ssl_method)?;
57+
if let Some(version) = version {
58+
ssl_ctx.set_min_proto_version(Some(version))?;
59+
ssl_ctx.set_max_proto_version(Some(version))?;
60+
}
61+
62+
Ok(Context { ssl_ctx })
63+
}
64+
65+
#[getter]
66+
fn _context<'p>(&self, py: pyo3::Python<'p>) -> PyOpenSslResult<pyo3::Bound<'p, pyo3::PyAny>> {
67+
Ok(types::FFI.get(py)?.call_method1(
68+
pyo3::intern!(py, "cast"),
69+
(
70+
pyo3::intern!(py, "SSL_CTX *"),
71+
self.ssl_ctx.as_ptr() as usize,
72+
),
73+
)?)
74+
}
75+
}

src/rust/src/types.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ impl LazyPyImport {
3232
}
3333
}
3434

35+
pub static FFI: LazyPyImport =
36+
LazyPyImport::new("cryptography.hazmat.bindings._rust", &["_openssl", "ffi"]);
37+
3538
pub static DATETIME_DATETIME: LazyPyImport = LazyPyImport::new("datetime", &["datetime"]);
3639
pub static DATETIME_TIMEZONE_UTC: LazyPyImport =
3740
LazyPyImport::new("datetime", &["timezone", "utc"]);

tests/pyopenssl/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# This file is dual licensed under the terms of the Apache License, Version
2+
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
3+
# for complete details.

tests/pyopenssl/test_ssl.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# This file is dual licensed under the terms of the Apache License, Version
2+
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
3+
# for complete details.
4+
5+
import pytest
6+
7+
from cryptography.hazmat.bindings._rust import _openssl, pyopenssl
8+
9+
10+
class TestContext:
11+
def test_create(self):
12+
for method in [
13+
pyopenssl.SSLv23_METHOD,
14+
pyopenssl.TLSv1_METHOD,
15+
pyopenssl.TLSv1_1_METHOD,
16+
pyopenssl.TLSv1_2_METHOD,
17+
pyopenssl.TLS_METHOD,
18+
pyopenssl.TLS_SERVER_METHOD,
19+
pyopenssl.TLS_CLIENT_METHOD,
20+
pyopenssl.DTLS_METHOD,
21+
pyopenssl.DTLS_SERVER_METHOD,
22+
pyopenssl.DTLS_CLIENT_METHOD,
23+
]:
24+
ctx = pyopenssl.Context(method)
25+
assert ctx
26+
27+
with pytest.raises(TypeError):
28+
pyopenssl.Context(object()) # type: ignore[arg-type]
29+
30+
with pytest.raises(ValueError):
31+
pyopenssl.Context(12324213)
32+
33+
def test__context(self):
34+
ctx = pyopenssl.Context(pyopenssl.TLS_METHOD)
35+
assert ctx._context
36+
assert _openssl.ffi.typeof(ctx._context).cname == "SSL_CTX *"
37+
assert _openssl.ffi.cast("uintptr_t", ctx._context) > 0

0 commit comments

Comments
 (0)