From 2b7a8b656e477f13a2eb4c6ce209761cf0769547 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 27 Jul 2020 15:41:28 -0400 Subject: [PATCH] Port a tiny tiny bit of the ASN.1 parsing to Rust --- .github/workflows/ci.yml | 32 +++++--- .gitignore | 2 + .travis.yml | 28 ++----- MANIFEST.in | 1 + pyproject.toml | 2 + setup.py | 7 +- .../hazmat/backends/openssl/decode_asn1.py | 21 ++---- src/rust/Cargo.toml | 14 ++++ src/rust/src/lib.rs | 73 +++++++++++++++++++ 9 files changed, 131 insertions(+), 49 deletions(-) create mode 100644 src/rust/Cargo.toml create mode 100644 src/rust/src/lib.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b0b89c4cc8af..aa01cb2a1be02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,11 +15,12 @@ jobs: strategy: matrix: PYTHON: - - {VERSION: "2.7", TOXENV: "py27", EXTRA_CFLAGS: ""} - {VERSION: "3.5", TOXENV: "py35", EXTRA_CFLAGS: ""} - {VERSION: "3.6", TOXENV: "py36", EXTRA_CFLAGS: ""} - {VERSION: "3.7", TOXENV: "py37", EXTRA_CFLAGS: ""} - {VERSION: "3.8", TOXENV: "py38", EXTRA_CFLAGS: "-DUSE_OSRANDOM_RNG_FOR_TESTING"} + RUST: + - nightly name: "Python ${{ matrix.PYTHON.VERSION }} on macOS" steps: - uses: actions/checkout@master @@ -28,6 +29,13 @@ jobs: with: python-version: ${{ matrix.PYTHON.VERSION }} + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.RUST }} + override: true + default: true + - run: python -m pip install tox requests coverage - run: git clone https://github.com/google/wycheproof @@ -57,14 +65,15 @@ jobs: strategy: matrix: WINDOWS: - - {ARCH: 'x86', WINDOWS: 'win32'} - - {ARCH: 'x64', WINDOWS: 'win64'} + - {ARCH: 'x86', WINDOWS: 'win32', RUST_TRIPLE: 'i686-pc-windows-msvc'} + - {ARCH: 'x64', WINDOWS: 'win64', RUST_TRIPLE: 'x86_64-pc-windows-msvc'} PYTHON: - - {VERSION: "2.7", TOXENV: "py27", MSVC_VERSION: "2010", CL_FLAGS: ""} - {VERSION: "3.5", TOXENV: "py35", MSVC_VERSION: "2019", CL_FLAGS: ""} - {VERSION: "3.6", TOXENV: "py36", MSVC_VERSION: "2019", CL_FLAGS: ""} - {VERSION: "3.7", TOXENV: "py37", MSVC_VERSION: "2019", CL_FLAGS: ""} - {VERSION: "3.8", TOXENV: "py38", MSVC_VERSION: "2019", CL_FLAGS: "/D USE_OSRANDOM_RNG_FOR_TESTING"} + RUST: + - nightly name: "Python ${{ matrix.PYTHON.VERSION }} on ${{ matrix.WINDOWS.WINDOWS }}" steps: - uses: actions/checkout@master @@ -74,13 +83,14 @@ jobs: python-version: ${{ matrix.PYTHON.VERSION }} architecture: ${{ matrix.WINDOWS.ARCH }} - - name: Install MSVC for Python 2.7 - run: | - Invoke-WebRequest -Uri https://download.microsoft.com/download/7/9/6/796EF2E4-801B-4FC4-AB28-B59FBF6D907B/VCForPython27.msi -OutFile VCForPython27.msi - Start-Process msiexec -Wait -ArgumentList @('/i', 'VCForPython27.msi', '/qn', 'ALLUSERS=1') - Remove-Item VCForPython27.msi -Force - shell: powershell - if: matrix.PYTHON.VERSION == '2.7' + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.RUST }} + override: true + default: true + target: ${{ matrix.WINDOWS.RUST_TRIPLE }} + - run: python -m pip install tox requests coverage - name: Download OpenSSL run: | diff --git a/.gitignore b/.gitignore index cdc4b6ad762e7..333a36d8e111c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ htmlcov/ .eggs/ *.py[cdo] .hypothesis/ +target/ +Cargo.lock \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index efc2b17a95196..2a50a5de799c8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,20 +25,12 @@ matrix: env: TOXENV=py36 # Travis lists available Pythons (including PyPy) by arch and distro here: # https://docs.travis-ci.com/user/languages/python/#python-versions - - python: pypy2.7-7.3.1 - env: TOXENV=pypy-nocoverage - python: pypy3.6-7.3.1 env: TOXENV=pypy3-nocoverage - python: 3.8 env: TOXENV=py38 OPENSSL=1.0.2u - - python: 2.7 - env: TOXENV=py27 OPENSSL=1.1.0l - - python: 2.7 - env: TOXENV=py27-ssh OPENSSL=1.1.0l - python: 3.8 env: TOXENV=py38 OPENSSL=1.1.0l - - python: 2.7 - env: TOXENV=py27 OPENSSL=1.1.1g - python: 3.8 env: TOXENV=py38 OPENSSL=1.1.1g - python: 3.8 @@ -54,21 +46,12 @@ matrix: - python: 3.8 env: TOXENV=py38 LIBRESSL=3.2.0 - - python: 2.7 - services: docker - env: TOXENV=py27 DOCKER=pyca/cryptography-runner-centos7 - - python: 2.7 - services: docker - env: TOXENV=py27 DOCKER=pyca/cryptography-runner-centos8 - python: 3.6 services: docker env: TOXENV=py36 DOCKER=pyca/cryptography-runner-centos8 - python: 3.6 services: docker env: TOXENV=py36 OPENSSL_FORCE_FIPS_MODE=1 DOCKER=pyca/cryptography-runner-centos8-fips - - python: 2.7 - services: docker - env: TOXENV=py27 DOCKER=pyca/cryptography-runner-stretch - python: 3.5 services: docker env: TOXENV=py35 DOCKER=pyca/cryptography-runner-stretch @@ -87,9 +70,6 @@ matrix: - python: 3.8 services: docker env: TOXENV=py38 DOCKER=pyca/cryptography-runner-ubuntu-focal - - python: 2.7 - services: docker - env: TOXENV=py27 DOCKER=pyca/cryptography-runner-ubuntu-rolling - python: 3.8 services: docker env: TOXENV=py38 DOCKER=pyca/cryptography-runner-ubuntu-rolling @@ -120,11 +100,11 @@ matrix: env: DOWNSTREAM=pyopenssl - python: 3.7 env: DOWNSTREAM=twisted OPENSSL=1.1.1g - - python: 2.7 + - python: 3.7 env: DOWNSTREAM=paramiko - - python: 2.7 + - python: 3.7 env: DOWNSTREAM=aws-encryption-sdk - - python: 2.7 + - python: 3.7 # BOTO_CONFIG works around this boto issue on travis: # https://github.com/boto/boto/issues/3717 env: DOWNSTREAM=dynamodb-encryption-sdk BOTO_CONFIG=/dev/null @@ -138,9 +118,11 @@ matrix: dist: xenial install: + - curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain nightly - ./.travis/install.sh script: + - source $HOME/.cargo/env - ./.travis/run.sh after_success: diff --git a/MANIFEST.in b/MANIFEST.in index 7e97167a1b630..4e74f32b32f8f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -11,6 +11,7 @@ include pyproject.toml recursive-include docs * recursive-include src/_cffi_src *.py *.c *.h +recursive-include src/rust Cargo.toml *.rs prune docs/_build recursive-include tests *.py exclude vectors diff --git a/pyproject.toml b/pyproject.toml index 957b1fd04bb03..3e9de880ea3a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,8 @@ requires = [ "wheel", # Must be kept in sync with the `setup_requirements` in `setup.py` "cffi>=1.8,!=1.11.3; platform_python_implementation != 'PyPy'", + + "setuptools-rust>=0.11.1", ] build-backend = "setuptools.build_meta" diff --git a/setup.py b/setup.py index 32fb410d3a78a..91513758a5697 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,8 @@ from setuptools import find_packages, setup from setuptools.command.install import install +from setuptools_rust import RustExtension + if pkg_resources.parse_version( setuptools.__version__ @@ -39,7 +41,7 @@ # `setup_requirements` must be kept in sync with `pyproject.toml` -setup_requirements = ["cffi>=1.8,!=1.11.3"] +setup_requirements = ["cffi>=1.8,!=1.11.3", "setuptools-rust>=0.11.1"] if platform.python_implementation() == "PyPy": if sys.pypy_version_info < (5, 4): @@ -156,6 +158,9 @@ def argument_without_setup_requirements(argv, i): return { "setup_requires": setup_requirements, "cffi_modules": cffi_modules, + "rust_extensions": [ + RustExtension("_rust", "src/rust/Cargo.toml"), + ], } diff --git a/src/cryptography/hazmat/backends/openssl/decode_asn1.py b/src/cryptography/hazmat/backends/openssl/decode_asn1.py index 56dcf26c1b546..c506da670dc8e 100644 --- a/src/cryptography/hazmat/backends/openssl/decode_asn1.py +++ b/src/cryptography/hazmat/backends/openssl/decode_asn1.py @@ -10,8 +10,7 @@ import six from cryptography import x509 -from cryptography.hazmat._der import DERReader, INTEGER, NULL, SEQUENCE -from cryptography.x509.extensions import _TLS_FEATURE_TYPE_TO_ENUM +from cryptography.hazmat.bindings import _rust from cryptography.x509.name import _ASN1_TYPE_TO_ENUM from cryptography.x509.oid import ( CRLEntryExtensionOID, @@ -209,24 +208,18 @@ def parse(self, backend, x509_obj): # The extension contents are a SEQUENCE OF INTEGERs. data = backend._lib.X509_EXTENSION_get_data(ext) data_bytes = _asn1_string_to_bytes(backend, data) - features = DERReader(data_bytes).read_single_element(SEQUENCE) - parsed = [] - while not features.is_empty(): - parsed.append(features.read_element(INTEGER).as_integer()) - # Map the features to their enum value. - value = x509.TLSFeature( - [_TLS_FEATURE_TYPE_TO_ENUM[x] for x in parsed] - ) + value = _rust.parse_tls_feature(data_bytes) + extensions.append(x509.Extension(oid, critical, value)) seen_oids.add(oid) continue elif oid == ExtensionOID.PRECERT_POISON: data = backend._lib.X509_EXTENSION_get_data(ext) - # The contents of the extension must be an ASN.1 NULL. - reader = DERReader(_asn1_string_to_bytes(backend, data)) - reader.read_single_element(NULL).check_empty() + data_bytes = _asn1_string_to_bytes(backend, data) + value = _rust.parse_precert_poison(data_bytes) + extensions.append( - x509.Extension(oid, critical, x509.PrecertPoison()) + x509.Extension(oid, critical, value) ) seen_oids.add(oid) continue diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml new file mode 100644 index 0000000000000..84c1e2fc3a423 --- /dev/null +++ b/src/rust/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "cryptography-rust" +version = "0.1.0" +authors = ["The cryptography developers "] +edition = "2018" +publish = false + +[dependencies] +asn1 = { git = "https://github.com/alex/rust-asn1" } +pyo3 = { version = "0.11", features = ["extension-module"] } + +[lib] +name = "cryptography_rust" +crate-type = ["cdylib"] \ No newline at end of file diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs new file mode 100644 index 0000000000000..553eef81995c2 --- /dev/null +++ b/src/rust/src/lib.rs @@ -0,0 +1,73 @@ +use pyo3::conversion::ToPyObject; + +enum PyAsn1Error { + Asn1(asn1::ParseError), + Py(pyo3::PyErr), +} + +impl From for PyAsn1Error { + fn from(e: asn1::ParseError) -> PyAsn1Error { + PyAsn1Error::Asn1(e) + } +} + +impl From for PyAsn1Error { + fn from(e: pyo3::PyErr) -> PyAsn1Error { + PyAsn1Error::Py(e) + } +} + +impl From for pyo3::PyErr { + fn from(e: PyAsn1Error) -> pyo3::PyErr { + match e { + PyAsn1Error::Asn1(asn1_error) => pyo3::exceptions::ValueError::py_err(format!( + "error parsing asn1 value: {:?}", + asn1_error + )), + PyAsn1Error::Py(py_error) => py_error, + } + } +} + +#[pyo3::prelude::pyfunction] +fn parse_tls_feature(py: pyo3::Python<'_>, data: &[u8]) -> pyo3::PyResult { + let tls_feature_type_to_enum = py + .import("cryptography.x509.extensions")? + .getattr("_TLS_FEATURE_TYPE_TO_ENUM")?; + + let features = asn1::parse::<_, PyAsn1Error, _>(data, |p| { + p.read_element::()?.parse(|p| { + let features = pyo3::types::PyList::empty(py); + while !p.is_empty() { + let feature = p.read_element::()?; + let py_feature = tls_feature_type_to_enum.get_item(feature.to_object(py))?; + features.append(py_feature)? + } + Ok(features) + }) + })?; + + let x509_module = py.import("cryptography.x509")?; + x509_module + .call1("TLSFeature", (features,)) + .map(|o| o.to_object(py)) +} + +#[pyo3::prelude::pyfunction] +fn parse_precert_poison(py: pyo3::Python<'_>, data: &[u8]) -> pyo3::PyResult { + asn1::parse::<_, PyAsn1Error, _>(data, |p| { + p.read_element::<()>()?; + Ok(()) + })?; + + let x509_module = py.import("cryptography.x509")?; + x509_module.call0("PrecertPoison").map(|o| o.to_object(py)) +} + +#[pyo3::prelude::pymodule] +fn _rust(_py: pyo3::Python<'_>, m: &pyo3::types::PyModule) -> pyo3::PyResult<()> { + m.add_wrapped(pyo3::wrap_pyfunction!(parse_tls_feature))?; + m.add_wrapped(pyo3::wrap_pyfunction!(parse_precert_poison))?; + + Ok(()) +}