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

WIP #218

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open

WIP #218

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
26 changes: 10 additions & 16 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,23 @@ on: [push, pull_request]

jobs:
unit_tests:
runs-on: ${{ matrix.os }}
runs-on: ${{matrix.os}}
strategy:
max-parallel: 8
matrix:
os: [ubuntu-18.04, ubuntu-22.04, macos-12]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install
run: |
[[ $(uname) == Linux ]] && sudo apt-get install --no-install-recommends python3-openssl python3-lxml
pip install coverage wheel
make install
- name: Test
run: |
make test
- name: Upload coverage data
run: |
bash <(curl -s https://codecov.io/bash)
python-version: ${{matrix.python-version}}
- run: |
if [[ $(uname) == Linux ]]; then sudo apt-get install --no-install-recommends python3-openssl python3-lxml; fi
- run: make install
- run: make lint
- run: make test
- uses: codecov/codecov-action@v3
black:
runs-on: ubuntu-22.04
steps:
Expand Down
28 changes: 12 additions & 16 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
test_deps:
pip install coverage flake8 wheel mypy types-certifi types-pyOpenSSL lxml-stubs
SHELL=/bin/bash

lint: test_deps
flake8 $$(python setup.py --name) test
mypy $$(python setup.py --name) --check-untyped-defs
lint:
flake8
mypy --install-types --non-interactive --check-untyped-defs $$(dirname */__init__.py)

test: test_deps lint
coverage run --source=$$(python setup.py --name) ./test/test.py
test:
python ./test/test.py -v

init_docs:
cd docs; sphinx-quickstart

docs:
sphinx-build docs docs/html

install: clean
pip install wheel
python setup.py bdist_wheel
pip install --upgrade dist/*.whl
install:
-rm -rf dist
python -m pip install build
python -m build
python -m pip install --upgrade $$(echo dist/*.whl)[tests]

clean:
-rm -rf build dist
-rm -rf *.egg-info

.PHONY: lint test test_deps docs install clean
.PHONY: test lint release docs

include common.mk
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ SignXML uses the `lxml ElementTree API <https://lxml.de/tutorial.html>`_ to work
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:2048 > cert.pem``
``openssl req -x509 -nodes -subj "/CN=test" -days 1 -newkey rsa -keyout privkey.pem -out cert.pem``
(run ``yum install openssl`` on Red Hat).
- 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.)
Expand Down Expand Up @@ -244,7 +244,7 @@ Please report bugs, issues, feature requests, etc. on `GitHub <https://github.co

License
-------
Copyright 2014-2022, Andrey Kislyuk and SignXML contributors. Licensed under the terms of the
Copyright 2014-2023, Andrey Kislyuk and SignXML contributors. Licensed under the terms of the
`Apache License, Version 2.0 <http://www.apache.org/licenses/LICENSE-2.0>`_. Distribution of the LICENSE and NOTICE
files with source copies of this package and derivative works is **REQUIRED** as specified by the Apache License.

Expand Down
2 changes: 0 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
[bdist_wheel]
universal=1
[flake8]
max-line-length=120
extend-ignore=E203
Expand Down
11 changes: 11 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,18 @@
"cryptography >= 3.4.8", # Set to the version in Ubuntu 22.04 due to features we need from cryptography 3.1
"pyOpenSSL >= 17.5.0",
"certifi >= 2018.1.18",
"tsp-client >= 0.1.3",
],
extras_require={
"tests": [
"flake8",
"coverage",
"build",
"wheel",
"mypy",
"lxml-stubs",
]
},
packages=find_packages(exclude=["test"]),
platforms=["MacOS X", "Posix"],
package_data={"signxml": ["schemas/*.xsd", "py.typed"]},
Expand Down
21 changes: 10 additions & 11 deletions signxml/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@
from dataclasses import dataclass
from typing import Any, List, Optional

import certifi
from cryptography.hazmat.primitives import hashes, hmac
from lxml.etree import QName
from OpenSSL.crypto import Error as OpenSSLCryptoError
from OpenSSL.crypto import X509Store, X509StoreContext, X509StoreContextError

from ..exceptions import InvalidCertificate, RedundantCert, SignXMLException

Expand Down Expand Up @@ -207,9 +210,6 @@ def p_sha1(client_b64_bytes, server_b64_bytes):


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:
Expand All @@ -233,22 +233,21 @@ def verify_x509_cert_chain(cert_chain, ca_pem_file=None, ca_path=None):
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)
x509_store = X509Store()
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()
x509_store.load_locations(cafile=ca_pem_file, capath=ca_path)

# FIXME: use X509StoreContext(store=x509_store, certificate=cert, chain=cert_chain).get_verified_chain()
# This requires identifying the signing cert out-of-band

certs = list(reversed(cert_chain))
end_of_chain = None
last_error: Exception = SignXMLException("Invalid certificate chain")
while len(certs) > 0:
for cert in certs:
try:
end_of_chain = _add_cert_to_store(store, cert)
end_of_chain = _add_cert_to_store(x509_store, cert)
certs.remove(cert)
break
except RedundantCert:
Expand Down
3 changes: 3 additions & 0 deletions signxml/verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ def verify(
raise InvalidInput(msg)
else:
cert_chain = [load_certificate(FILETYPE_PEM, add_pem_header(cert)) for cert in certs]
# FIXME: switch to wbondcrypto cert chain verify
signing_cert = verify_x509_cert_chain(cert_chain, ca_pem_file=ca_pem_file, ca_path=ca_path)
elif isinstance(self.x509_cert, X509):
signing_cert = self.x509_cert
Expand All @@ -389,6 +390,8 @@ def verify(

try:
digest_alg_name = str(digest_algorithm_implementations[signature_alg].name)
# FIXME: confirm the specified signature algorithm matches the certificate's public key
# FIXME: switch to cryptography verify
openssl_verify(signing_cert, raw_signature, signed_info_c14n, digest_alg_name)
except OpenSSLCryptoError as e:
try:
Expand Down
34 changes: 29 additions & 5 deletions signxml/xades/xades.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

from lxml.etree import SubElement, _Element
from OpenSSL.crypto import FILETYPE_ASN1, FILETYPE_PEM, X509, dump_certificate, load_certificate
from tsp_client import TSPVerifier

from .. import SignatureConfiguration, VerifyResult, XMLSignatureProcessor, XMLSigner, XMLVerifier
from ..algorithms import DigestAlgorithm
Expand Down Expand Up @@ -264,9 +265,31 @@ class XAdESVerifier(XAdESProcessor, XMLVerifier):
"""

# TODO: document/support SignatureTimeStamp / timestamp attestation
# TODO: allow setting required attributes, including timestamp
# SignatureTimeStamp is required by certain profiles but is an unsigned property
def _verify_signing_time(self, verify_result: VerifyResult):
pass

def _verify_signing_time(self, verify_result: VerifyResult, all_verify_results: List[VerifyResult]):
"""
The Implicit mechanism (see clause 5.1.4.4.1) shall be used for generating this qualifying property.
The input to the computation of the message imprint shall be the result of processing all the ds:Reference
elements within the ds:SignedInfo except the one referencing the SignedProperties element, in their order of
appearance, as follows:
1) process the retrieved ds:Reference element according to the reference-processing model of XMLDSIG [1]
clause 4.4.3.2;
2) if the result is a XML node set, canonicalize it as specified in clause 4.5; and
3) concatenate the resulting octets to those resulting from previously processed ds:Reference elements in
ds:SignedInfo.
"""
ts_path = "xades:SignedDataObjectProperties/xades:AllDataObjectsTimeStamp/xades:EncapsulatedTimeStamp"
if verify_result.signed_xml is None:
return
all_data_objs_ts = verify_result.signed_xml.find(ts_path, namespaces=namespaces)
if all_data_objs_ts is None:
return
print("Will verify", all_data_objs_ts.text)
ts = b64decode(all_data_objs_ts.text) # type: ignore
tsp_message = b"".join(r.signed_data for r in all_verify_results if r != verify_result)
TSPVerifier().verify(ts, message=tsp_message)

def _verify_cert_digest(self, signing_cert_node, expect_cert):
for cert in self._findall(signing_cert_node, "xades:Cert"):
Expand Down Expand Up @@ -320,8 +343,8 @@ def _verify_signature_policy(self, verify_result: VerifyResult, expect_signature
if b64decode(digest_value.text) != b64decode(expect_signature_policy.DigestValue):
raise InvalidInput("Digest mismatch for signature policy hash")

def _verify_signed_properties(self, verify_result):
self._verify_signing_time(verify_result)
def _verify_signed_properties(self, verify_result, *, all_verify_results):
self._verify_signing_time(verify_result, all_verify_results=all_verify_results)
self._verify_cert_digests(verify_result)
if self.expect_signature_policy:
self._verify_signature_policy(
Expand Down Expand Up @@ -364,7 +387,8 @@ def verify( # type: ignore
continue
if verify_result.signed_xml.tag == xades_tag("SignedProperties"):
verify_results[i] = XAdESVerifyResult( # type: ignore
*astuple(verify_result), signed_properties=self._verify_signed_properties(verify_result)
*astuple(verify_result),
signed_properties=self._verify_signed_properties(verify_result, all_verify_results=verify_results),
)
break
else:
Expand Down
79 changes: 79 additions & 0 deletions test/openssl_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env python
from functools import partial
from subprocess import call

import OpenSSL.crypto
from asn1crypto.x509 import TbsCertificate
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, ed25519, padding, rsa

run = partial(call, shell=True, executable="/bin/bash")
message = b"abc"
signatures = {}

pss_padding = padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH)
pkcs_padding = padding.PKCS1v15()

run("openssl req -x509 -nodes -subj '/CN=test' -days 1 -newkey rsa -keyout rsa-key.pem -out rsa-cert.pem")
with open("rsa-cert.pem", "rb") as fh:
rsa_cert = fh.read()
with open("rsa-key.pem", "rb") as fh:
rsa_key = serialization.load_pem_private_key(fh.read(), password=None)
signatures[rsa_cert] = rsa_key.sign(message, pkcs_padding, hashes.SHA512())

# Does not work on LibreSSL
run("openssl req -x509 -nodes -subj '/CN=test' -days 1 -newkey rsa-pss -keyout rsapss-key.pem -out rsapss-cert.pem")
with open("rsapss-cert.pem", "rb") as fh:
rsapss_cert = fh.read()
with open("rsapss-key.pem", "rb") as fh:
rsapss_key = serialization.load_pem_private_key(fh.read(), password=None)
signatures[rsapss_cert] = rsapss_key.sign(message, pss_padding, hashes.SHA512())

run(
"openssl req -x509 -nodes -subj '/CN=test' -days 1 -newkey ec:<(openssl ecparam -name secp384r1) -keyout ec-key.pem -out ec-cert.pem"
)
with open("ec-cert.pem", "rb") as fh:
ec_cert = fh.read()
with open("ec-key.pem", "rb") as fh:
ec_key = serialization.load_pem_private_key(fh.read(), password=None)
signatures[ec_cert] = ec_key.sign(message, ec.ECDSA(hashes.SHA512()))

run(
"openssl req -x509 -nodes -subj '/CN=test' -days 1 -newkey dsa:<(openssl dsaparam 2048) -keyout dsa-key.pem -out dsa-cert.pem"
)
with open("dsa-cert.pem", "rb") as fh:
dsa_cert = fh.read()
with open("dsa-key.pem", "rb") as fh:
dsa_key = serialization.load_pem_private_key(fh.read(), password=None)
signatures[dsa_cert] = dsa_key.sign(message, hashes.SHA512())

# Does not work on LibreSSL
run("openssl req -x509 -nodes -subj '/CN=test' -days 1 -newkey ed25519 -keyout ed25519-key.pem -out ed25519-cert.pem")
with open("ed25519-cert.pem", "rb") as fh:
ed25519_cert = fh.read()
with open("ed25519-key.pem", "rb") as fh:
ed25519_key = serialization.load_pem_private_key(fh.read(), password=None)
signatures[ed25519_cert] = ed25519_key.sign(message)

for cert_pem_bytes, signature in signatures.items():
cert = x509.load_pem_x509_certificate(cert_pem_bytes)
pubkey = cert.public_key()
alg = TbsCertificate.load(cert.tbs_certificate_bytes)["subject_public_key_info"]["algorithm"]
if alg["algorithm"].native == "rsassa_pss":
verify_args = [pss_padding, hashes.SHA512()]
elif alg["algorithm"].native == "rsa":
verify_args = [pkcs_padding, hashes.SHA512()]
elif alg["algorithm"].native == "ec":
verify_args = [ec.ECDSA(hashes.SHA512())]
elif alg["algorithm"].native == "ed25519":
verify_args = []
elif alg["algorithm"].native == "dsa":
verify_args = [hashes.SHA512()]
pubkey.verify(signature, message, *verify_args)

try:
cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_pem_bytes)
OpenSSL.crypto.verify(cert, signature, message, "sha512")
except Exception as e:
print(f"Error in OpenSSL.crypto.verify with {type(pubkey)}: {e}")