Skip to content

Commit

Permalink
Replace PyOpenSSL with Cryptography
Browse files Browse the repository at this point in the history
  • Loading branch information
kislyuk committed Jul 28, 2024
1 parent 9f06f43 commit 9299dc1
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 151 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
with:
python-version: ${{matrix.python-version}}
- run: |
if [[ $(uname) == Linux ]]; then sudo apt-get install --no-install-recommends python3-openssl python3-lxml python3-certifi; fi
if [[ $(uname) == Linux ]]; then sudo apt-get install --no-install-recommends python3-lxml python3-certifi; fi
- run: make install
- if: ${{matrix.python-version == '3.12'}}
run: make lint
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
SHELL=/bin/bash

lint:
ruff $$(dirname */__init__.py)
ruff check $$(dirname */__init__.py)
mypy --install-types --non-interactive --check-untyped-defs $$(dirname */__init__.py)

test:
Expand Down
26 changes: 6 additions & 20 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ standard, and most recommended ones. Its features are:
<http://www.w3.org/TR/xml-exc-c14n/#def-InclusiveNamespaces-PrefixList>`_, required to verify signatures generated by
some SAML implementations)
* Modern Python compatibility (3.7-3.11+ and PyPy)
* Well-supported, portable, reliable dependencies: `lxml <https://github.com/lxml/lxml>`_,
`cryptography <https://github.com/pyca/cryptography>`_, `pyOpenSSL <https://github.com/pyca/pyopenssl>`_
* Well-supported, portable, reliable dependencies: `lxml <https://github.com/lxml/lxml>`_ and
`cryptography <https://github.com/pyca/cryptography>`_
* Comprehensive testing (including the XMLDSig interoperability suite) and `continuous integration
<https://github.com/XML-Security/signxml/actions>`_
* Simple interface with useful, ergonomic, and secure defaults (no network calls, XSLT or XPath transforms)
Expand All @@ -30,22 +30,6 @@ Installation

pip install signxml

Note: SignXML depends on `lxml <https://github.com/lxml/lxml>`_ and `cryptography
<https://github.com/pyca/cryptography>`_, which in turn depend on `OpenSSL <https://www.openssl.org/>`_, `LibXML
<http://xmlsoft.org/>`_, and Python tools to interface with them. You can install those as follows:

+--------------+----------------------------------------------------------------------------------------------------------------------+
| OS | Command |
+==============+======================================================================================================================+
| Ubuntu | ``apt-get install --no-install-recommends python3-pip python3-wheel python3-setuptools python3-openssl python3-lxml``|
+--------------+----------------------------------------------------------------------------------------------------------------------+
| Red Hat, | ``yum install python3-pip python3-pyOpenSSL python3-lxml`` |
| Amazon Linux,| |
| CentOS | |
+--------------+----------------------------------------------------------------------------------------------------------------------+
| Mac OS | Install `Homebrew <https://brew.sh>`_, then run ``brew install python``. |
+--------------+----------------------------------------------------------------------------------------------------------------------+

Synopsis
--------
SignXML uses the `lxml ElementTree API <https://lxml.de/tutorial.html>`_ to work with XML data.
Expand All @@ -66,9 +50,11 @@ To make this example self-sufficient for test purposes:

- Generate a test certificate and key using
``openssl req -x509 -nodes -subj "/CN=test" -days 1 -newkey rsa -keyout privkey.pem -out cert.pem``
(run ``yum install openssl`` on Red Hat).
(run ``apt-get install openssl``, ``yum install openssl``, or ``brew install openssl`` if the ``openssl`` executable
is not found).
- Pass the ``x509_cert=cert`` keyword argument to ``XMLVerifier.verify()``. (In production, ensure this is replaced with
the correct configuration for the trusted CA or certificate - this determines which signatures your application trusts.)
the correct configuration for the trusted CA or certificate - this determines which signatures your application
trusts.)

.. _verifying-saml-assertions:

Expand Down
1 change: 0 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"python": ("https://docs.python.org/3", None),
"lxml": ("https://lxml.de/apidoc", "https://lxml.de/apidoc/objects.inv"),
"Cryptography": ("https://cryptography.io/en/latest", "https://cryptography.io/en/latest/objects.inv"),
"pyOpenSSL": ("https://www.pyopenssl.org/en/stable", "https://www.pyopenssl.org/en/stable/objects.inv"),
}
templates_path = [""]
ogp_site_url = "https://xml-security.github.io/" + project
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ skip_gitignore = true

[tool.ruff]
line-length = 120

[tool.ruff.lint]
per-file-ignores = {"signxml/__init__.py" = ["F401"], "signxml/xades/__init__.py" = ["F401"], "signxml/verifier.py" = ["E721"]}
9 changes: 3 additions & 6 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,9 @@
long_description=open("README.rst").read(),
python_requires=">=3.7",
install_requires=[
# Dependencies are restricted by major version range according to semver.
# By default, version minimums are set to be compatible with the oldest supported Ubuntu LTS (currently 20.04).
"lxml >= 4.5.0, < 6",
"cryptography >= 3.4.8", # Set to the version in Ubuntu 22.04 due to features we need from cryptography 3.1
"pyOpenSSL >= 19.0.0",
"certifi >= 2019.11.28",
"lxml >= 5.2.1, < 6", # Ubuntu 24.04 LTS
"cryptography >= 43", # Required to support client certificate validation
"certifi >= 2023.11.17", # Ubuntu 24.04 LTS
# "tsp-client >= 0.1.3",
],
extras_require={
Expand Down
16 changes: 9 additions & 7 deletions signxml/signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
from dataclasses import dataclass, replace
from typing import List, Optional, Union

from cryptography import x509
from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa, utils
from cryptography.hazmat.primitives.asymmetric.padding import MGF1, PSS, PKCS1v15
from cryptography.hazmat.primitives.hmac import HMAC
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.hazmat.primitives.serialization import Encoding, load_pem_private_key
from lxml.etree import Element, SubElement, _Element
from OpenSSL.crypto import FILETYPE_PEM, X509, dump_certificate

from .algorithms import (
CanonicalizationMethod,
Expand Down Expand Up @@ -128,7 +128,7 @@ def sign(
*,
key: Optional[Union[str, bytes, rsa.RSAPrivateKey, dsa.DSAPrivateKey, ec.EllipticCurvePrivateKey]] = None,
passphrase: Optional[bytes] = None,
cert: Optional[Union[str, List[str], List[X509]]] = None,
cert: Optional[Union[str, List[str], List[x509.Certificate]]] = None,
reference_uri: Optional[Union[str, List[str], List[SignatureReference]]] = None,
key_name: Optional[str] = None,
key_info: Optional[_Element] = None,
Expand All @@ -151,8 +151,8 @@ def sign(
:param passphrase: Passphrase to use to decrypt the key, if any.
:param cert:
X.509 certificate to use for signing. This should be a string containing a PEM-formatted certificate, or an
array of strings or :class:`OpenSSL.crypto.X509` objects containing the certificate and a chain of
intermediate certificates.
array of strings or :class:`cryptography.x509.Certificate` objects containing the certificate and a chain
of intermediate certificates.
:param reference_uri:
Custom reference URI or list of reference URIs to incorporate into the signature. When ``method`` is set to
``detached`` or ``enveloped``, reference URIs are set to this value and only the referenced elements are
Expand Down Expand Up @@ -313,7 +313,7 @@ def _add_key_info(self, sig_root, signing_settings: SigningSettings):
if isinstance(cert, (str, bytes)):
x509_certificate.text = strip_pem_header(cert)
else:
x509_certificate.text = strip_pem_header(dump_certificate(FILETYPE_PEM, cert))
x509_certificate.text = strip_pem_header(cert.public_bytes(Encoding.PEM))
else:
sig_root.append(signing_settings.key_info)

Expand Down Expand Up @@ -383,7 +383,9 @@ def _build_transforms_for_reference(self, *, transforms_node: _Element, referenc
SubElement(transforms_node, ds_tag("Transform"), Algorithm=reference.c14n_method.value) # type: ignore
else:
c14n_xform = SubElement(
transforms_node, ds_tag("Transform"), Algorithm=reference.c14n_method.value # type: ignore
transforms_node,
ds_tag("Transform"),
Algorithm=reference.c14n_method.value, # type: ignore
)
if reference.inclusive_ns_prefixes:
SubElement(
Expand Down
107 changes: 58 additions & 49 deletions signxml/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
from dataclasses import dataclass
from typing import Any, List, Optional

import certifi
from cryptography import x509
from cryptography.hazmat.primitives import hashes, hmac
from lxml.etree import QName

from ..exceptions import InvalidCertificate, RedundantCert, SignXMLException
from ..exceptions import InvalidCertificate

PEM_HEADER = "-----BEGIN CERTIFICATE-----"
PEM_FOOTER = "-----END CERTIFICATE-----"
Expand Down Expand Up @@ -159,8 +161,9 @@ def strip_pem_header(cert):
def add_pem_header(bare_base64_cert):
bare_base64_cert = ensure_str(bare_base64_cert)
if bare_base64_cert.startswith(PEM_HEADER):
return bare_base64_cert
return PEM_HEADER + "\n" + textwrap.fill(bare_base64_cert, 64) + "\n" + PEM_FOOTER
return bare_base64_cert.encode()
cert_with_header = PEM_HEADER + "\n" + textwrap.fill(bare_base64_cert, 64) + "\n" + PEM_FOOTER
return cert_with_header.encode()


def iterate_pem(certs):
Expand Down Expand Up @@ -206,24 +209,7 @@ def p_sha1(client_b64_bytes, server_b64_bytes):
return b64encode(raw_p_sha1(client_bytes, server_bytes, (len(client_bytes), len(server_bytes)))[0]).decode()


def _add_cert_to_store(store, cert):
from OpenSSL.crypto import Error as OpenSSLCryptoError
from OpenSSL.crypto import X509StoreContext, X509StoreContextError

try:
X509StoreContext(store, cert).verify_certificate()
except X509StoreContextError as e:
raise InvalidCertificate(e)
try:
store.add_cert(cert)
return cert
except OpenSSLCryptoError as e:
if e.args == ([("x509 certificate routines", "X509_STORE_add_cert", "cert already in hash table")],):
raise RedundantCert(e)
raise


def verify_x509_cert_chain(cert_chain, ca_pem_file=None, ca_path=None):
class X509CertChainVerifier:
"""
Look at certs in the cert chain and add them to the store one by one.
Return the cert at the end of the chain. That is the cert to be used by the caller for verifying.
Expand All @@ -232,35 +218,58 @@ def verify_x509_cert_chain(cert_chain, ca_pem_file=None, ca_path=None):
or being part of a certification chain that terminates in a certificate containing the validation key.
No ordering is implied by the above constraints"
"""
# TODO: migrate to Cryptography (pending cert validation support) or https://github.com/wbond/certvalidator
from OpenSSL import SSL

context = SSL.Context(SSL.TLSv1_METHOD)
if ca_pem_file is None and ca_path is None:
import certifi

ca_pem_file = certifi.where()
context.load_verify_locations(ensure_bytes(ca_pem_file, none_ok=True), capath=ca_path)
store = context.get_cert_store()
certs = list(reversed(cert_chain))
end_of_chain = None
last_error: Exception = SignXMLException("Invalid certificate chain")
while len(certs) > 0:
for cert in certs:

def __init__(self, ca_pem_file=None, ca_path=None, verification_time=None):
if ca_pem_file is None:
ca_pem_file = certifi.where()
self.ca_pem_file = ca_pem_file
self.ca_path = ca_path # FIXME: determine and replicate openssl capath semantics
self.verification_time = verification_time

@property
def store(self):
with open(self.ca_pem_file, "rb") as pems:
certs = x509.load_pem_x509_certificates(pems.read())
return x509.verification.Store(certs)

@property
def builder(self):
builder = x509.verification.PolicyBuilder()
builder = builder.store(self.store)
if self.verification_time is not None:
builder = builder.time(self.verification_time)
# builder.extended_key_usage
return builder

@property
def verifier(self):
v = self.builder.build_client_verifier()
# print("CLIENT VERIFIER EKU:", v.extended_key_usage)
print(dir(self.builder))
return self.builder.build_client_verifier()

def _do_verify(self, cert_chain):
leaf, intermediates = cert_chain[0], cert_chain[1:]
result = self.verifier.verify(leaf=leaf, intermediates=intermediates)
return result.chain[0]

def verify(self, cert_chain):
# [*] leaf, intermediates
# [*] reversed(intermediates), leaf
# if len(cert_chain) > 2:
# [ ] leaf, reversed(intermediates)
# [ ] intermediates, leaf
for cert in cert_chain:
print("***", cert.issuer, cert.subject, cert.not_valid_before_utc, cert.not_valid_after_utc)
eku_oid = x509.oid.ExtensionOID.EXTENDED_KEY_USAGE
print("*** EKUS", cert.subject.get_attributes_for_oid(eku_oid))
try:
return self._do_verify(cert_chain)
except x509.verification.VerificationError:
try:
end_of_chain = _add_cert_to_store(store, cert)
certs.remove(cert)
break
except RedundantCert:
certs.remove(cert)
if end_of_chain is None:
end_of_chain = cert
break
except Exception as e:
last_error = e
else:
raise last_error
return end_of_chain
return self._do_verify(list(reversed(cert_chain)))
except x509.verification.VerificationError as e:
raise InvalidCertificate(e)


def _remove_sig(signature, idempotent=False):
Expand Down
Loading

0 comments on commit 9299dc1

Please sign in to comment.