diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5fca3f9..c88b86c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,12 +7,39 @@ // Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename. "dockerFile": "../Dockerfile", // Set *default* container specific settings.json values on container create. + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": "false", + "username": "vscode", + "userUid": "1000", + "userGid": "1000", + "upgradePackages": "true" + }, + "ghcr.io/devcontainers/features/python:1": "none", + "ghcr.io/devcontainers/features/git:1": { + "version": "latest", + "ppa": "false" + } + }, "customizations": { "vscode": { "extensions": [ "ms-python.vscode-pylance", - "visualstudioexptteam.vscodeintellicode" + "ms-python.python", + "ms-python.isort", + "ms-python.black-formatter", + "ms-python.pylint" ], + "settings": { + "python.defaultInterpreterPath": "/usr/local/bin/python3", + "pylint.enabled": true, + "pylint.importStrategy": "fromEnvironment", // Manage pylint separate from extension + "pylint.lintOnChange": true, + "pylint.path": [ + "/usr/local/bin/pylint" + ], + "python.formatting.blackPath": "/usr/local/bin/black" + }, "terminal.integrated.defaultProfile.linux": "bash", "terminal.integrated.profiles.linux": { "bash": { @@ -21,6 +48,6 @@ } } }, - "postCreateCommand": "pip install --no-cache-dir --user --quiet -r /home/ocspdev/OcspChecker/requirements.txt", - "remoteUser": "ocspdev" + "postCreateCommand": "pip install --user -r /home/vscode/OcspChecker/dev-requirements.txt && pip install --user -r /home/vscode/OcspChecker/requirements.txt", + "remoteUser": "vscode" } \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4b73723..81ce3b5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,13 +1,14 @@ -name: OCSP-Checker +name: OCSPChecker on: [push] jobs: test: + name: ${{ matrix.os }}-python${{ matrix.python-version }} runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] os: [ ubuntu-20.04, @@ -18,9 +19,9 @@ jobs: macos-11, ] steps: - - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # tag=v3.3.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # tag=v4.2.1 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # tag=v4.7.1 + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # tag=v5.3.0 with: python-version: ${{ matrix.python-version }} - name: Install tools @@ -28,51 +29,51 @@ jobs: python -m pip install --upgrade pip setuptools wheel - name: Install pytest run: | - python -m pip install pytest pytest-cov + python -m pip install pytest - name: Install requirements run: | python -m pip install -r requirements.txt - name: Run pytest run: | - pytest -v --junitxml=test-output.xml --cov=ocspchecker --cov-report xml + pytest tests/test_ocspchecker.py -v --junitxml=test-output-${{ matrix.os }}-python${{ matrix.python-version }}.xml - name: Upload test results - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # tag=v3.1.2 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # tag=v4.4.3 with: - name: pytest-results for ${{ matrix.python-version }} - path: "**/test-output.xml" - - name: Upload coverage results - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # tag=v3.1.2 - with: - name: pytest-results for ${{ matrix.python-version }} - path: "**/coverage.xml" + name: pytest-results for ${{ matrix.os }}-python${{ matrix.python-version }} + path: "**/test-output-${{ matrix.os }}-python${{ matrix.python-version }}.xml" +# - name: Upload coverage results +# uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # tag=v4.4.3 +# with: +# name: pytest-results for ${{ matrix.python-version }} +# path: "**/coverage.xml" if: ${{ always() }} - build: - runs-on: ubuntu-latest - needs: test - steps: - - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # tag=v3.3.0 - - name: Set up Python 3.x - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # tag=v4.7.1 - with: - python-version: "3.9" - architecture: "x64" - - name: Install tools - run: | - python -m pip install --upgrade pip setuptools wheel - python -m pip install build twine - - name: Build pypy package - run: | - python -m build - - name: Check package description - run: | - twine check dist/* - - name: Generate provenance - uses: slsa-framework/github-actions-demo@v0.1 - with: - artifact_path: dist/ - - name: Upload provenance - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # tag=v3.1.2 - with: - name: OCSP-Checker provenance - path: build.provenance +# build: +# runs-on: ubuntu-latest +# needs: test +# steps: +# - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # tag=v3.3.0 +# - name: Set up Python 3.x +# uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # tag=v4.7.1 +# with: +# python-version: "3.9" +# architecture: "x64" +# - name: Install tools +# run: | +# python -m pip install --upgrade pip setuptools wheel +# python -m pip install build twine +# - name: Build pypy package +# run: | +# python -m build +# - name: Check package description +# run: | +# twine check dist/* +# - name: Generate provenance +# uses: slsa-framework/github-actions-demo@v0.1 +# with: +# artifact_path: dist/ +# - name: Upload provenance +# uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # tag=v3.1.2 +# with: +# name: OCSPChecker provenance +# path: build.provenance diff --git a/CHANGELOG.md b/CHANGELOG.md index e568370..f66c859 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,3 +72,10 @@ - allow usage of http proxy to request both host certificate and perform the ocsp request - allow configuration of request timeouts as function argument - add extra info in unknown exceptions + +# v1.9.14 +- allow custom path to ca-certs via @vLabayen +- update devcontainer +- fix flaky tests +- update CI +- remove support for Python 3.7 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 92244b3..24dd749 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,32 +1,26 @@ # Usually you do not have to specify amd64, but on an Apple M1 you do if you want to use packages # that are not optimized for arm64 like NaSSL -FROM --platform=amd64 python:3.11.1-slim-bullseye +FROM --platform=amd64 mcr.microsoft.com/devcontainers/python:3.12-bullseye -SHELL ["/bin/bash", "--login", "-c"] +#SHELL ["/bin/bash", "--login", "-c"] -ENV DEBIAN_FRONTEND noninteractive -ENV LANG C.UTF-8 +#ENV DEBIAN_FRONTEND noninteractive +#ENV LANG C.UTF-8 -RUN useradd -m ocspdev +#RUN useradd -m ocspdev -RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates \ - netbase \ - curl \ - git \ - bash-completion \ - && rm -rf /var/lib/apt/lists/* +#RUN apt-get update && apt-get install -y --no-install-recommends \ +# ca-certificates \ +# netbase \ +# curl \ +# git \ +# bash-completion \ +# && rm -rf /var/lib/apt/lists/* -RUN pip install --no-cache-dir --quiet --upgrade pip setuptools wheel \ - pip install --no-cache-dir --quiet pytest pytest-cov && \ - pip install --no-cache-dir --quiet twine && \ - pip install --no-cache-dir --quiet pylint && \ - pip install --no-cache-dir --quiet black - -USER ocspdev -WORKDIR /home/ocspdev +#USER ocspdev +WORKDIR /home/vscode # Copy OcspChecker Folder -COPY --chown=ocspdev:ocspdev . /home/ocspdev/OcspChecker/ +COPY --chown=vscode:vscode . /home/vscode/OcspChecker/ CMD [ "bash" ] diff --git a/README.md b/README.md index ec43e3b..16233de 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# OCSP-Checker +# OCSPChecker [![Downloads](https://pepy.tech/badge/ocsp-checker/month)](https://pepy.tech/project/ocsp-checker) [![PyPI Version](https://img.shields.io/pypi/v/ocsp-checker.svg)](https://pypi.org/project/ocsp-checker/) @@ -6,12 +6,12 @@ ## Overview -OCSP-Checker is a python package based on Alban Diquet's [nassl](https://github.com/nabla-c0d3/nassl) wrapper and the Python Cryptographic Authority's [cryptography](https://github.com/pyca/cryptography) package. Relying on a web browser to check the revocation status of a x509 digital certificate [has](https://www.imperialviolet.org/2014/04/19/revchecking.html) [been](https://www.imperialviolet.org/2014/04/29/revocationagain.html) [broken](https://scotthelme.co.uk/revocation-is-broken/) from the beginning, and validating certificates outside of the web browser is a manual process. OCSP-Checker aims to solve this by providing an automated means to check the [OCSP](https://en.wikipedia.org/wiki/Online_Certificate_Status_Protocol) revocation status for a x509 digital certificate. +OCSPChecker is a python package based on Alban Diquet's [nassl](https://github.com/nabla-c0d3/nassl) wrapper and the Python Cryptographic Authority's [cryptography](https://github.com/pyca/cryptography) package. Relying on a web browser to check the revocation status of a x509 digital certificate [has](https://www.imperialviolet.org/2014/04/19/revchecking.html) [been](https://www.imperialviolet.org/2014/04/29/revocationagain.html) [broken](https://scotthelme.co.uk/revocation-is-broken/) from the beginning, and validating certificates outside of the web browser is a manual process. OCSP-Checker aims to solve this by providing an automated means to check the [OCSP](https://en.wikipedia.org/wiki/Online_Certificate_Status_Protocol) revocation status for a x509 digital certificate. ## Pre-requisites -__Python__ - Python 3.7 (64-bit) and above. +__Python__ - Python 3.8 (64-bit) and above. ## Installation @@ -46,7 +46,7 @@ PLEASE NOTE: If you run this on a network with a MITM SSL proxy, you may receive ## Command Line Usage -OCSP-Checker can now be used at the command line. The format is: +OCSPChecker can now be used at the command line. The format is: ``` usage: ocspchecker [-h] --target target [--port port] diff --git a/dev-requirements.txt b/dev-requirements.txt index 7ce490f..0c67a7c 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,5 @@ -pytest==7.4.2 +pytest==8.3.3 twine==4.0.2 -pylint==2.17.2 -black==23.9.1 -isort==5.12.0 +pylint==3.3.1 +black==24.10.0 +isort==5.13.2 diff --git a/ocspchecker/__init__.py b/ocspchecker/__init__.py index 5b46a6e..fc43b78 100644 --- a/ocspchecker/__init__.py +++ b/ocspchecker/__init__.py @@ -1,7 +1,7 @@ """ __init__.py """ -__title__ = "ocsp-checker" -__version__ = "1.9.13" +__title__ = "ocspchecker" +__version__ = "1.9.14" __author__ = "Joe Gatt" from ocspchecker.ocspchecker import get_ocsp_status diff --git a/ocspchecker/ocspchecker.py b/ocspchecker/ocspchecker.py index df74b21..6213e5a 100644 --- a/ocspchecker/ocspchecker.py +++ b/ocspchecker/ocspchecker.py @@ -4,26 +4,26 @@ For a short-term fix, I will use nassl to grab the full cert chain. """ -from socket import gaierror, timeout, socket, SOCK_STREAM, AF_INET -from urllib.parse import urlparse -from urllib import request, error -from typing import List, Tuple, Union from pathlib import Path +from socket import AF_INET, SOCK_STREAM, gaierror, socket, timeout +from typing import List, Tuple, Union +from urllib import error, request +from urllib.parse import urlparse +import certifi +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.hashes import SHA1 +from cryptography.x509 import ExtensionNotFound, load_pem_x509_certificate, ocsp +from cryptography.x509.oid import ExtensionOID +from nassl._nassl import OpenSSLError +from nassl.cert_chain_verifier import CertificateChainVerificationFailed from nassl.ssl_client import ( ClientCertificateRequested, - OpenSslVersionEnum, OpenSslVerifyEnum, + OpenSslVersionEnum, SslClient, ) -from cryptography.x509 import load_pem_x509_certificate, ocsp, ExtensionNotFound -from nassl.cert_chain_verifier import CertificateChainVerificationFailed -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.hashes import SHA1 -from cryptography.x509.oid import ExtensionOID -from nassl._nassl import OpenSSLError -import certifi from ocspchecker.utils.http_proxy_connect import http_proxy_connect @@ -60,7 +60,13 @@ class OcspResponderError(Exception): "140070EF": "Unable to find public key parameters.", } -def get_ocsp_status(host: str, port: int = 443, proxy: Union[None, Tuple[str, int]] = None, request_timeout: float = 3.0) -> List[str]: + +def get_ocsp_status( + host: str, + port: int = 443, + proxy: Union[None, Tuple[str, int]] = None, + request_timeout: float = 3.0, +) -> List[str]: """Main function with three inputs: host, port and proxy""" results: List[str] = [] @@ -88,7 +94,9 @@ def get_ocsp_status(host: str, port: int = 443, proxy: Union[None, Tuple[str, in ocsp_request = build_ocsp_request(cert_chain) # Send OCSP request to responder and get result - ocsp_response = get_ocsp_response(ocsp_url, ocsp_request, proxy=proxy, request_timeout=request_timeout) + ocsp_response = get_ocsp_response( + ocsp_url, ocsp_request, proxy=proxy, request_timeout=request_timeout + ) # Extract OCSP result from OCSP response ocsp_result = extract_ocsp_result(ocsp_response) @@ -103,7 +111,13 @@ def get_ocsp_status(host: str, port: int = 443, proxy: Union[None, Tuple[str, in return results -def get_certificate_chain(host: str, port: int = 443, proxy: Union[None, Tuple[str, int]] = None, request_timeout: float = 3.0, path_to_ca_certs: Path = Path(certifi.where())) -> List[str]: +def get_certificate_chain( + host: str, + port: int = 443, + proxy: Union[None, Tuple[str, int]] = None, + request_timeout: float = 3.0, + path_to_ca_certs: Path = Path(certifi.where()), +) -> List[str]: """Connect to the host on the port and obtain certificate chain""" func_name: str = "get_certificate_chain" @@ -114,8 +128,16 @@ def get_certificate_chain(host: str, port: int = 443, proxy: Union[None, Tuple[s soc.settimeout(request_timeout) try: - if proxy is not None: http_proxy_connect((host, port), proxy=proxy, soc=soc) - else: soc.connect((host, port)) + if path_to_ca_certs.is_file(): + pass + except FileNotFoundError: + raise OSError(f"ca cert file {path_to_ca_certs} not found") from None + + try: + if proxy is not None: + http_proxy_connect((host, port), proxy=proxy, soc=soc) + else: + soc.connect((host, port)) except gaierror: raise InitialConnectionError( @@ -132,7 +154,9 @@ def get_certificate_chain(host: str, port: int = 443, proxy: Union[None, Tuple[s raise InitialConnectionError(f"{func_name}: Connection to {host}:{port} refused.") from None except (IOError, OSError) as err: - raise InitialConnectionError(f"{func_name}: Unable to reach the host {host}. {str(err)}") from None + raise InitialConnectionError( + f"{func_name}: Unable to reach the host {host}. {str(err)}" + ) from None except (OverflowError, TypeError): raise InitialConnectionError( @@ -233,7 +257,12 @@ def build_ocsp_request(cert_chain: List[str]) -> bytes: return ocsp_request_data -def get_ocsp_response(ocsp_url: str, ocsp_request_data: bytes, proxy: Union[None, Tuple[str, int]] = None, request_timeout: float = 3.0): +def get_ocsp_response( + ocsp_url: str, + ocsp_request_data: bytes, + proxy: Union[None, Tuple[str, int]] = None, + request_timeout: float = 3.0, +): """Send OCSP request to ocsp responder and retrieve response""" func_name: str = "get_ocsp_response" @@ -247,7 +276,7 @@ def get_ocsp_response(ocsp_url: str, ocsp_request_data: bytes, proxy: Union[None ) if proxy is not None: host, port = proxy - ocsp_request.set_proxy(f'{host}:{port}', 'http') + ocsp_request.set_proxy(f"{host}:{port}", "http") with request.urlopen(ocsp_request, timeout=request_timeout) as resp: ocsp_response = resp.read() @@ -258,7 +287,7 @@ def get_ocsp_response(ocsp_url: str, ocsp_request_data: bytes, proxy: Union[None if isinstance(err.reason, gaierror): raise OcspResponderError(f"{func_name}: {ocsp_url} is invalid or not known.") - + raise OcspResponderError(f"{func_name}: Connection Error to {ocsp_url}. {str(err)}") except ValueError as err: diff --git a/ocspchecker/utils/http_proxy_connect.py b/ocspchecker/utils/http_proxy_connect.py index f7f795a..402bf46 100644 --- a/ocspchecker/utils/http_proxy_connect.py +++ b/ocspchecker/utils/http_proxy_connect.py @@ -1,4 +1,4 @@ -''' +""" Establish a socket connection through an HTTP proxy. Author: Fredrik Ă˜strem License: @@ -14,7 +14,8 @@ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -''' +""" + # This file is a copy-paste from https://gist.github.com/frxstrem/4487802 # # Some fixes were made: @@ -32,58 +33,64 @@ import socket -from typing import Tuple, Union from base64 import b64encode +from typing import Tuple, Union -def http_proxy_connect(address: Tuple[str, int], proxy: Tuple[str, int], auth: Union[None, str, Tuple[str, str]] = None, soc: Union[None, socket.socket] = None) -> socket.socket: - """Establish a socket connection through an HTTP proxy.""" - headers = { - 'host': address[0] - } +def http_proxy_connect( + address: Tuple[str, int], + proxy: Tuple[str, int], + auth: Union[None, str, Tuple[str, str]] = None, + soc: Union[None, socket.socket] = None, +) -> socket.socket: + """Establish a socket connection through an HTTP proxy.""" - if isinstance(auth, str): - headers['proxy-authorization'] = auth - elif isinstance(auth, tuple): - headers['proxy-authorization'] = 'Basic ' + b64encode(('%s:%s' % auth).encode('utf-8')).decode('utf-8') + headers = {"host": address[0]} - s = soc if soc is not None else socket.socket() - s.connect(proxy) - fp = s.makefile('rw') + if isinstance(auth, str): + headers["proxy-authorization"] = auth + elif isinstance(auth, tuple): + headers["proxy-authorization"] = "Basic " + b64encode( + ("%s:%s" % auth).encode("utf-8") + ).decode("utf-8") - fp.write('CONNECT %s:%d HTTP/1.0\r\n' % address) - fp.write('\r\n'.join('%s: %s' % (k, v) for (k, v) in headers.items()) + '\r\n\r\n') - fp.flush() + s = soc if soc is not None else socket.socket() + s.connect(proxy) + fp = s.makefile("rw") - statusline = fp.readline().rstrip('\r\n') + fp.write("CONNECT %s:%d HTTP/1.0\r\n" % address) + fp.write("\r\n".join("%s: %s" % (k, v) for (k, v) in headers.items()) + "\r\n\r\n") + fp.flush() - if statusline.count(' ') < 2: - fp.close() - s.close() - raise IOError('Bad response') - version, status, statusmsg = statusline.split(' ', 2) - if not version in ('HTTP/1.0', 'HTTP/1.1'): - fp.close() - s.close() - raise IOError('Unsupported HTTP version') - try: - status = int(status) - except ValueError: - fp.close() - s.close() - raise IOError('Bad response') + statusline = fp.readline().rstrip("\r\n") - response_headers = {} + if statusline.count(" ") < 2: + fp.close() + s.close() + raise IOError("Bad response") + version, status, statusmsg = statusline.split(" ", 2) + if not version in ("HTTP/1.0", "HTTP/1.1"): + fp.close() + s.close() + raise IOError("Unsupported HTTP version") + try: + status = int(status) + except ValueError: + fp.close() + s.close() + raise IOError("Bad response") - while True: - tl = '' - l = fp.readline().rstrip('\r\n') - if l == '': - break - if not ':' in l: - continue - k, v = l.split(':', 1) - response_headers[k.strip().lower()] = v.strip() + response_headers = {} - fp.close() - return s \ No newline at end of file + while True: + tl = "" + l = fp.readline().rstrip("\r\n") + if l == "": + break + if not ":" in l: + continue + k, v = l.split(":", 1) + response_headers[k.strip().lower()] = v.strip() + + fp.close() + return s diff --git a/pyproject.toml b/pyproject.toml index 91af6c5..cb1457f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,8 +3,8 @@ requires = ["setuptools>=61.0.0", "wheel"] build-backend = "setuptools.build_meta" [project] -name="ocsp-checker" -version="1.9.13" +name="ocspchecker" +version="1.9.14" description="Library used to check the OCSP revocation status for a x509 digital certificate." readme= {file = "README.md", content-type = "text/markdown"} @@ -13,19 +13,19 @@ license= {file = "LICENSE.txt" } classifiers=[ "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] keywords=["ssl, tls, ocsp, python, security"] dependencies = [ - "cryptography~=41.0", - "nassl~=5.0", + "cryptography~=43.0", + "nassl~=5.2", "certifi", ] -requires-python = ">=3.7" +requires-python = ">=3.8" [project.optional-dependencies] dev = [ @@ -47,7 +47,7 @@ ocspchecker = "ocspchecker.__main__:main" [tool.black] line-length = 100 -target-version = ["py37", "py38", "py39", "py310", "py311"] +target-version = ["py38", "py39", "py310", "py311", "py312"] [tool.isort] ensure_newline_before_comments = true @@ -60,7 +60,7 @@ line_length = 100 multi_line_output = 3 no_sections = false profile = "black" -py_version=311 +py_version=312 reverse_relative = true reverse_sort = true skip_gitignore = true @@ -72,5 +72,5 @@ include = ["ocspchecker" , "tests"] reportMissingImports = true reportMissingTypeStubs = false pythonPlatform = "All" -pythonVersion = "3.11" +pythonVersion = "3.12" typeCheckingMode = "basic" diff --git a/requirements.txt b/requirements.txt index 85c533e..5fbaf12 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ certifi -cffi==1.15.1 -cryptography==41.0.4 -nassl==5.0.1 -pycparser==2.21 +cffi==1.17.1 +cryptography==43.0.3 +nassl==5.2.0 +pycparser==2.22 diff --git a/tests/certs.py b/tests/certs.py index d2892af..20c25cd 100644 --- a/tests/certs.py +++ b/tests/certs.py @@ -1,28 +1,26 @@ """ Certs used to validate ocspchecker """ github_issuer_pem = """-----BEGIN CERTIFICATE----- -MIIEFzCCAv+gAwIBAgIQB/LzXIeod6967+lHmTUlvTANBgkqhkiG9w0BAQwFADBh -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD -QTAeFw0yMTA0MTQwMDAwMDBaFw0zMTA0MTMyMzU5NTlaMFYxCzAJBgNVBAYTAlVT -MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxMDAuBgNVBAMTJ0RpZ2lDZXJ0IFRMUyBI -eWJyaWQgRUNDIFNIQTM4NCAyMDIwIENBMTB2MBAGByqGSM49AgEGBSuBBAAiA2IA -BMEbxppbmNmkKaDp1AS12+umsmxVwP/tmMZJLwYnUcu/cMEFesOxnYeJuq20ExfJ -qLSDyLiQ0cx0NTY8g3KwtdD3ImnI8YDEe0CPz2iHJlw5ifFNkU3aiYvkA8ND5b8v -c6OCAYIwggF+MBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFAq8CCkXjKU5 -bXoOzjPHLrPt+8N6MB8GA1UdIwQYMBaAFAPeUDVW0Uy7ZvCj4hsbw5eyPdFVMA4G -A1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwdgYI -KwYBBQUHAQEEajBoMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j -b20wQAYIKwYBBQUHMAKGNGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdp -Q2VydEdsb2JhbFJvb3RDQS5jcnQwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2Ny -bDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdENBLmNybDA9BgNVHSAE -NjA0MAsGCWCGSAGG/WwCATAHBgVngQwBATAIBgZngQwBAgEwCAYGZ4EMAQICMAgG -BmeBDAECAzANBgkqhkiG9w0BAQwFAAOCAQEAR1mBf9QbH7Bx9phdGLqYR5iwfnYr -6v8ai6wms0KNMeZK6BnQ79oU59cUkqGS8qcuLa/7Hfb7U7CKP/zYFgrpsC62pQsY -kDUmotr2qLcy/JUjS8ZFucTP5Hzu5sn4kL1y45nDHQsFfGqXbbKrAjbYwrwsAZI/ -BKOLdRHHuSm8EdCGupK8JvllyDfNJvaGEwwEqonleLHBTnm8dqMLUeTF0J5q/hos -Vq4GNiejcxwIfZMy0MJEGdqN9A57HSgDKwmKdsp33Id6rHtSJlWncg+d0ohP/rEh -xRqhqjn1VtvChMQ1H3Dau0bwhr9kAMQ+959GG50jBbl9s08PqUU643QwmA== +MIIDqDCCAy6gAwIBAgIRAPNkTmtuAFAjfglGvXvh9R0wCgYIKoZIzj0EAwMwgYgx +CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtKZXJz +ZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYDVQQD +EyVVU0VSVHJ1c3QgRUNDIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTE4MTEw +MjAwMDAwMFoXDTMwMTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAkdCMRswGQYDVQQI +ExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoT +D1NlY3RpZ28gTGltaXRlZDE3MDUGA1UEAxMuU2VjdGlnbyBFQ0MgRG9tYWluIFZh +bGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEH +A0IABHkYk8qfbZ5sVwAjBTcLXw9YWsTef1Wj6R7W2SUKiKAgSh16TwUwimNJE4xk +IQeV/To14UrOkPAY9z2vaKb71EijggFuMIIBajAfBgNVHSMEGDAWgBQ64QmG1M8Z +wpZ2dEl23OA1xmNjmjAdBgNVHQ4EFgQU9oUKOxGG4QR9DqoLLNLuzGR7e64wDgYD +VR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYB +BQUHAwEGCCsGAQUFBwMCMBsGA1UdIAQUMBIwBgYEVR0gADAIBgZngQwBAgEwUAYD +VR0fBEkwRzBFoEOgQYY/aHR0cDovL2NybC51c2VydHJ1c3QuY29tL1VTRVJUcnVz +dEVDQ0NlcnRpZmljYXRpb25BdXRob3JpdHkuY3JsMHYGCCsGAQUFBwEBBGowaDA/ +BggrBgEFBQcwAoYzaHR0cDovL2NydC51c2VydHJ1c3QuY29tL1VTRVJUcnVzdEVD +Q0FkZFRydXN0Q0EuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8vb2NzcC51c2VydHJ1 +c3QuY29tMAoGCCqGSM49BAMDA2gAMGUCMEvnx3FcsVwJbZpCYF9z6fDWJtS1UVRs +cS0chWBNKPFNpvDKdrdKRe+oAkr2jU+ubgIxAODheSr2XhcA7oz9HmedGdMhlrd9 +4ToKFbZl+/OnFFzqnvOhcjHvClECEQcKmc8fmA== -----END CERTIFICATE----- """ @@ -57,9 +55,9 @@ -----END CERTIFICATE----- """ -github_ocsp_data = b"0Q0O0M0K0I0\t\x06\x05+\x0e\x03\x02\x1a\x05\x00\x04\x14+\x1d\x1e\x98\xcc\xf3v\x04\xd6\xc1\xc8\xbd\x15\xa2$\xc8\x04\x13\x008\x04\x14\n\xbc\x08)\x17\x8c\xa59mz\x0e\xce3\xc7.\xb3\xed\xfb\xc3z\x02\x10\x0c\xd0\xa8\xbe\xc62\xcf\xe6E\xec\xa0\xa9\xb0\x84\xfb\x1c" +github_ocsp_data = b"0Q0O0M0K0I0\t\x06\x05+\x0e\x03\x02\x1a\x05\x00\x04\x14\xcf\x94\xdc\\0J\xa7\x94\x85r\x1f\x95ng\x89Z\xc2\x16W\xdd\x04\x14\xf6\x85\n;\x11\x86\xe1\x04}\x0e\xaa\x0b,\xd2\xee\xccd{{\xae\x02\x10N(\xf7\x86\xb6l\x1a;\x94,\xd2\xc4\x0e\xb7B\xa5" -unauthorized_ocsp_data = b"0Q0O0M0K0I0\t\x06\x05+\x0e\x03\x02\x1a\x05\x00\x04\x14\xcf&\xf5\x18\xfa\xc9~\x8f\x8c\xb3B\xe0\x1c/j\x10\x9e\x8e_\n\x04\x14Qh\xff\x90\xaf\x02\x07u<\xcc\xd9edb\xa2\x12\xb8Yr;\x02\x10\x05W\xc8\x0b(&\x83\xa1{\n\x11D)ky" +unauthorized_ocsp_data = b"0Q0O0M0K0I0\t\x06\x05+\x0e\x03\x02\x1a\x05\x00\x04\x14\xcf&\xf5\x18\xfa\xc9~\x8f\x8c\xb3B\xe0\x1c/j\x10\x9e\x8e_\x04\x14Qh\xff\x90\xaf\x02\x07u<\xcc\xd9edb\xa2\x12\xb8Yr;\x02\x10\x05W\xc8\x0b(&\x83\xa1{\x11D)ky" # List of certificate authorities to test cert_authorities = [ diff --git a/tests/test_ocspchecker.py b/tests/test_ocspchecker.py index 803b039..72f8fee 100644 --- a/tests/test_ocspchecker.py +++ b/tests/test_ocspchecker.py @@ -1,5 +1,7 @@ """ Tests """ + import pytest + from ocspchecker.ocspchecker import ( build_ocsp_request, extract_ocsp_result, @@ -63,10 +65,7 @@ def test_get_cert_chain_bad_port(): with pytest.raises(Exception) as excinfo: get_certificate_chain(host, port) - assert ( - str(excinfo.value) - == f"{func_name}: Illegal port: {port}. Port must be between 0-65535." - ) + assert str(excinfo.value) == f"{func_name}: Illegal port: {port}. Port must be between 0-65535." def test_invalid_certificate(): @@ -87,19 +86,17 @@ def test_extract_ocsp_url_success(): """test a successful extract_ocsp_url function invocation""" host = "github.com" - port = 443 - cert_chain = get_certificate_chain(host, port) + cert_chain = get_certificate_chain(host) ocsp_url = extract_ocsp_url(cert_chain) - assert ocsp_url == "http://ocsp.digicert.com" + assert ocsp_url == "http://ocsp.sectigo.com" def test_build_ocsp_request_success(): """test a successful build_ocsp_request function invocation""" host = "github.com" - port = 443 - cert_chain = get_certificate_chain(host, port) + cert_chain = get_certificate_chain(host) ocsp_request_data = build_ocsp_request(cert_chain) assert ocsp_request_data == certs.github_ocsp_data @@ -130,7 +127,10 @@ def test_get_ocsp_response_bad_url_format(): with pytest.raises(Exception) as excinfo: get_ocsp_response(ocsp_url, ocsp_request_data) - assert str(excinfo.value) == (f"{func_name}: Connection Error to {ocsp_url}. unknown url type: {ocsp_url!r}") + assert str(excinfo.value) == ( + f"{func_name}: Connection Error to {ocsp_url}. unknown url type: {ocsp_url!r}" + ) + def test_get_ocsp_response_connection_error(): """test an unsuccessful get_ocsp_response function invocation @@ -167,9 +167,7 @@ def test_extract_ocsp_result_unauthorized(): func_name: str = "extract_ocsp_result" - ocsp_response = get_ocsp_response( - "http://ocsp.digicert.com", certs.unauthorized_ocsp_data - ) + ocsp_response = get_ocsp_response("http://ocsp.digicert.com", certs.unauthorized_ocsp_data) with pytest.raises(Exception) as excinfo: extract_ocsp_result(ocsp_response) @@ -197,7 +195,7 @@ def test_end_to_end_success_test(): assert ocsp_result == [ "Host: github.com:443", - "OCSP URL: http://ocsp.digicert.com", + "OCSP URL: http://ocsp.sectigo.com", "OCSP Status: GOOD", ] @@ -242,19 +240,6 @@ def test_end_to_end_test_host_timeout(): ] -@pytest.mark.parametrize("root_ca", certs.cert_authorities) -def test_a_cert_from_each_root_ca(root_ca): - """Test a cert from each root CA to ensure test coverage""" - - try: - ocsp_request = get_ocsp_status(root_ca, 443) - - except Exception as err: - raise err - - assert ocsp_request[2] == "OCSP Status: GOOD" - - def test_bad_port_overflow(): """Validate passing a bad port results in failure""" @@ -287,7 +272,7 @@ def test_no_port_supplied(): assert ocsp_request == [ "Host: github.com:443", - "OCSP URL: http://ocsp.digicert.com", + "OCSP URL: http://ocsp.sectigo.com", "OCSP Status: GOOD", ] @@ -300,7 +285,7 @@ def test_strip_http_from_host(): assert ocsp_request == [ "Host: http://github.com:443", - "OCSP URL: http://ocsp.digicert.com", + "OCSP URL: http://ocsp.sectigo.com", "OCSP Status: GOOD", ] @@ -313,7 +298,7 @@ def test_strip_https_from_host(): assert ocsp_request == [ "Host: https://github.com:443", - "OCSP URL: http://ocsp.digicert.com", + "OCSP URL: http://ocsp.sectigo.com", "OCSP Status: GOOD", ] @@ -331,19 +316,3 @@ def test_tls_fatal_alert_112(): str(excinfo.value) == f"{func_name}: Unrecognized server name provided. Check your target and try again." ) - - -# Need to find another host with this error -# def test_tls_fatal_alert_50(): -# """Validate SSL/TLS Decode Error""" -# -# host = "pattern-wiki.org" -# func_name: str = "get_certificate_chain" -# -# with pytest.raises(Exception) as excinfo: -# get_certificate_chain(host, 443) -# -# assert ( -# str(excinfo.value) -# == f"{func_name}: Decode Error. Check your target and try again." -# ) diff --git a/tests/test_responders.py b/tests/test_responders.py new file mode 100644 index 0000000..b836f79 --- /dev/null +++ b/tests/test_responders.py @@ -0,0 +1,28 @@ +''' These global responders are sometimes flaky when testing from GitHub. +To make CI tests in GitHub more reliable, I am making these tests optional. ''' + +import pytest + +from ocspchecker.ocspchecker import ( + build_ocsp_request, + extract_ocsp_result, + extract_ocsp_url, + get_certificate_chain, + get_ocsp_response, + get_ocsp_status, +) + +from . import certs + +@pytest.mark.parametrize("root_ca", certs.cert_authorities) +def test_a_cert_from_each_root_ca(root_ca): + """Test a cert from each root CA to ensure test coverage""" + + try: + ocsp_request = get_ocsp_status(root_ca, 443) + + except Exception as err: + raise err + + assert ocsp_request[2] == "OCSP Status: GOOD" +