├── .devcontainer └── devcontainer.json ├── .github ├── CODEOWNERS ├── pull_request_template.md └── workflows │ ├── main.yml │ └── test.yml ├── .gitignore ├── .pylintrc ├── CHANGELOG.md ├── Dockerfile ├── LICENSE.txt ├── README.md ├── dev-requirements.txt ├── ocspchecker ├── __init__.py ├── __main__.py ├── ocspchecker.py └── utils │ ├── __init__.py │ └── http_proxy_connect.py ├── pyproject.toml └── tests ├── __init__.py ├── certs.py ├── pytest.ini ├── test_ocspchecker.py └── test_responders.py /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.205.2/containers/docker-existing-dockerfile 3 | { 4 | "name": "Existing Dockerfile", 5 | // Sets the run context to one level up instead of the .devcontainer folder. 6 | "context": "..", 7 | // Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename. 8 | "dockerFile": "../Dockerfile", 9 | // Set *default* container specific settings.json values on container create. 10 | "features": { 11 | "ghcr.io/devcontainers/features/common-utils:2": { 12 | "installZsh": "false", 13 | "username": "vscode", 14 | "userUid": "1000", 15 | "userGid": "1000", 16 | "upgradePackages": "true" 17 | }, 18 | "ghcr.io/devcontainers/features/python:1": "none", 19 | "ghcr.io/devcontainers/features/git:1": { 20 | "version": "latest", 21 | "ppa": "false" 22 | } 23 | }, 24 | "customizations": { 25 | "vscode": { 26 | "extensions": [ 27 | "ms-python.vscode-pylance", 28 | "ms-python.python", 29 | "ms-python.isort", 30 | "ms-python.black-formatter", 31 | "ms-python.pylint" 32 | ], 33 | "settings": { 34 | "python.defaultInterpreterPath": "/usr/local/bin/python3", 35 | "pylint.enabled": true, 36 | "pylint.importStrategy": "fromEnvironment", // Manage pylint separate from extension 37 | "pylint.lintOnChange": true, 38 | "pylint.path": [ 39 | "/usr/local/bin/pylint" 40 | ], 41 | "python.formatting.blackPath": "/usr/local/bin/black" 42 | }, 43 | "terminal.integrated.defaultProfile.linux": "bash", 44 | "terminal.integrated.profiles.linux": { 45 | "bash": { 46 | "path": "bash" 47 | } 48 | } 49 | } 50 | }, 51 | "postCreateCommand": "pip install --user -r /home/vscode/OcspChecker/dev-requirements.txt && pip install --user -r /home/vscode/OcspChecker/requirements.txt", 52 | "remoteUser": "vscode" 53 | } -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @gattjoe 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Purpose 2 | 3 | 4 | # Does this introduce a breaking change? 5 | - [ ] Yes 6 | - [ ] No 7 | 8 | # Pull Request Type 9 | 10 | What kind of change does this Pull Request introduce? 11 | 12 | - [ ] Bugfix 13 | - [ ] Feature 14 | - [ ] Code style update (formatting, local variables) 15 | - [ ] Refactoring (no functional changes, no api changes) 16 | - [ ] Documentation content changes 17 | - [ ] Other... Please describe: 18 | 19 | # How Has This Been Tested? 20 | 21 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 22 | 23 | - [ ] Test A 24 | 25 | - [ ] Test B 26 | 27 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/main.yml 2 | 3 | name: build 4 | permissions: {} # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token#defining-access-for-the-github_token-permissions 5 | on: 6 | push: 7 | branches: 8 | - 'master' 9 | 10 | jobs: 11 | build: 12 | if: ${{ (github.repository == 'gattjoe/OCSPChecker') }} 13 | runs-on: ubuntu-latest 14 | permissions: 15 | id-token: write 16 | contents: read 17 | attestations: write 18 | steps: 19 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2 20 | - name: Set up Python 3.x 21 | uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # tag=v5.4.0 22 | with: 23 | python-version: "3.9" 24 | architecture: "x64" 25 | - name: Install tools 26 | run: | 27 | python -m pip install --upgrade pip setuptools wheel 28 | python -m pip install build twine 29 | - name: Build pypy package 30 | run: | 31 | python -m build 32 | - name: Check package 33 | run: | 34 | twine check dist/* 35 | - name: Store the distribution packages 36 | uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # tag=v4.6.1 37 | with: 38 | name: python-package-distributions 39 | path: dist/ 40 | - name: Attest 41 | uses: actions/attest-build-provenance@bd77c077858b8d561b7a36cbe48ef4cc642ca39d # tag=v2.2.2 42 | with: 43 | subject-path: '${{ github.workspace }}/dist' 44 | 45 | publish: 46 | if: ${{ (github.repository == 'gattjoe/OCSPChecker') }} 47 | runs-on: ubuntu-latest 48 | permissions: 49 | id-token: write 50 | needs: 51 | - build 52 | environment: 53 | name: pypi 54 | url: https://pypi.org/p/ocsp-checker 55 | steps: 56 | - name: Download the distribution package 57 | uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # tag=v4.1.9 58 | with: 59 | name: python-package-distributions 60 | path: dist/ 61 | - name: Publish distribution 📦 to PyPI 62 | uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # tag=v1.12.4 63 | with: 64 | verbose: true -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | permissions: {} # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token#defining-access-for-the-github_token-permissions 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | - '!master' 8 | pull_request: 9 | 10 | jobs: 11 | test: 12 | name: ${{ matrix.os }}-python${{ matrix.python-version }} 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 17 | os: 18 | [ 19 | ubuntu-20.04, 20 | ubuntu-22.04, 21 | windows-2022, 22 | windows-2019, 23 | macos-12, 24 | macos-11, 25 | ] 26 | steps: 27 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # tag=v5.4.0 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | - name: Install tools 33 | run: | 34 | python -m pip install --upgrade pip setuptools wheel 35 | - name: Install ocspchecker 36 | run: | 37 | python -m pip install . 38 | - name: Install pytest 39 | run: | 40 | python -m pip install -r dev-requirements.txt 41 | - name: Run pytest 42 | run: | 43 | pytest tests/test_ocspchecker.py -v --junitxml=test-output-${{ matrix.os }}-python${{ matrix.python-version }}.xml 44 | - name: Upload test results 45 | uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # tag=v4.6.1 46 | with: 47 | name: pytest-results for ${{ matrix.os }}-python${{ matrix.python-version }} 48 | path: "**/test-output-${{ matrix.os }}-python${{ matrix.python-version }}.xml" 49 | if: ${{ always() }} 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | include/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 100 | __pypackages__/ 101 | 102 | # Celery stuff 103 | celerybeat-schedule 104 | celerybeat.pid 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | pyvenv.cfg 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | 137 | # pytype static type analyzer 138 | .pytype/ 139 | 140 | # Cython debug symbols 141 | cython_debug/ 142 | 143 | # vscode 144 | .vscode/ 145 | 146 | # pytest 147 | test-output.xml 148 | .pytest_cache/ 149 | 150 | # OS X 151 | .DS_Store 152 | 153 | # vim swap 154 | *.swp 155 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-allow-list= 7 | 8 | # A comma-separated list of package or module names from where C extensions may 9 | # be loaded. Extensions are loading into the active Python interpreter and may 10 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 11 | # for backward compatibility.) 12 | extension-pkg-whitelist= 13 | 14 | # Return non-zero exit code if any of these messages/categories are detected, 15 | # even if score is above --fail-under value. Syntax same as enable. Messages 16 | # specified are enabled, while categories only check already-enabled messages. 17 | fail-on= 18 | 19 | # Specify a score threshold to be exceeded before program exits with error. 20 | fail-under=9.0 21 | 22 | # Files or directories to be skipped. They should be base names, not paths. 23 | ignore=CVS 24 | 25 | # Add files or directories matching the regex patterns to the ignore-list. The 26 | # regex matches against paths. 27 | ignore-paths= 28 | 29 | # Files or directories matching the regex patterns are skipped. The regex 30 | # matches against base names, not paths. 31 | ignore-patterns= 32 | 33 | # Python code to execute, usually for sys.path manipulation such as 34 | # pygtk.require(). 35 | #init-hook= 36 | 37 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 38 | # number of processors available to use. 39 | jobs=1 40 | 41 | # Control the amount of potential inferred values when inferring a single 42 | # object. This can help the performance when dealing with large functions or 43 | # complex, nested conditions. 44 | limit-inference-results=100 45 | 46 | # List of plugins (as comma separated values of python module names) to load, 47 | # usually to register additional checkers. 48 | load-plugins= 49 | 50 | # Pickle collected data for later comparisons. 51 | persistent=yes 52 | 53 | # When enabled, pylint would attempt to guess common misconfiguration and emit 54 | # user-friendly hints instead of false-positive error messages. 55 | suggestion-mode=yes 56 | 57 | # Allow loading of arbitrary C extensions. Extensions are imported into the 58 | # active Python interpreter and may run arbitrary code. 59 | unsafe-load-any-extension=no 60 | 61 | 62 | [MESSAGES CONTROL] 63 | 64 | # Only show warnings with the listed confidence levels. Leave empty to show 65 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 66 | confidence= 67 | 68 | # Disable the message, report, category or checker with the given id(s). You 69 | # can either give multiple identifiers separated by comma (,) or put this 70 | # option multiple times (only on the command line, not in the configuration 71 | # file where it should appear only once). You can also use "--disable=all" to 72 | # disable everything first and then reenable specific checks. For example, if 73 | # you want to run only the similarities checker, you can use "--disable=all 74 | # --enable=similarities". If you want to run only the classes checker, but have 75 | # no Warning level messages displayed, use "--disable=all --enable=classes 76 | # --disable=W". 77 | disable=print-statement, 78 | parameter-unpacking, 79 | unpacking-in-except, 80 | old-raise-syntax, 81 | backtick, 82 | long-suffix, 83 | old-ne-operator, 84 | old-octal-literal, 85 | import-star-module-level, 86 | non-ascii-bytes-literal, 87 | raw-checker-failed, 88 | bad-inline-option, 89 | locally-disabled, 90 | file-ignored, 91 | suppressed-message, 92 | useless-suppression, 93 | deprecated-pragma, 94 | use-symbolic-message-instead, 95 | apply-builtin, 96 | basestring-builtin, 97 | buffer-builtin, 98 | cmp-builtin, 99 | coerce-builtin, 100 | execfile-builtin, 101 | file-builtin, 102 | long-builtin, 103 | raw_input-builtin, 104 | reduce-builtin, 105 | standarderror-builtin, 106 | unicode-builtin, 107 | xrange-builtin, 108 | coerce-method, 109 | delslice-method, 110 | getslice-method, 111 | setslice-method, 112 | no-absolute-import, 113 | old-division, 114 | dict-iter-method, 115 | dict-view-method, 116 | next-method-called, 117 | metaclass-assignment, 118 | indexing-exception, 119 | raising-string, 120 | reload-builtin, 121 | oct-method, 122 | hex-method, 123 | nonzero-method, 124 | cmp-method, 125 | input-builtin, 126 | round-builtin, 127 | intern-builtin, 128 | unichr-builtin, 129 | map-builtin-not-iterating, 130 | zip-builtin-not-iterating, 131 | range-builtin-not-iterating, 132 | filter-builtin-not-iterating, 133 | using-cmp-argument, 134 | eq-without-hash, 135 | div-method, 136 | idiv-method, 137 | rdiv-method, 138 | exception-message-attribute, 139 | invalid-str-codec, 140 | sys-max-int, 141 | bad-python3-import, 142 | deprecated-string-function, 143 | deprecated-str-translate-call, 144 | deprecated-itertools-function, 145 | deprecated-types-field, 146 | next-method-defined, 147 | dict-items-not-iterating, 148 | dict-keys-not-iterating, 149 | dict-values-not-iterating, 150 | deprecated-operator-function, 151 | deprecated-urllib-function, 152 | xreadlines-attribute, 153 | deprecated-sys-function, 154 | exception-escape, 155 | comprehension-escape, 156 | no-name-in-module 157 | 158 | # Enable the message, report, category or checker with the given id(s). You can 159 | # either give multiple identifier separated by comma (,) or put this option 160 | # multiple time (only on the command line, not in the configuration file where 161 | # it should appear only once). See also the "--disable" option for examples. 162 | enable=c-extension-no-member 163 | 164 | 165 | [REPORTS] 166 | 167 | # Python expression which should return a score less than or equal to 10. You 168 | # have access to the variables 'error', 'warning', 'refactor', and 'convention' 169 | # which contain the number of messages in each category, as well as 'statement' 170 | # which is the total number of statements analyzed. This score is used by the 171 | # global evaluation report (RP0004). 172 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 173 | 174 | # Template used to display messages. This is a python new-style format string 175 | # used to format the message information. See doc for all details. 176 | #msg-template= 177 | 178 | # Set the output format. Available formats are text, parseable, colorized, json 179 | # and msvs (visual studio). You can also give a reporter class, e.g. 180 | # mypackage.mymodule.MyReporterClass. 181 | output-format=text 182 | 183 | # Tells whether to display a full report or only the messages. 184 | reports=no 185 | 186 | # Activate the evaluation score. 187 | score=yes 188 | 189 | 190 | [REFACTORING] 191 | 192 | # Maximum number of nested blocks for function / method body 193 | max-nested-blocks=5 194 | 195 | # Complete name of functions that never returns. When checking for 196 | # inconsistent-return-statements if a never returning function is called then 197 | # it will be considered as an explicit return statement and no message will be 198 | # printed. 199 | never-returning-functions=sys.exit,argparse.parse_error 200 | 201 | 202 | [LOGGING] 203 | 204 | # The type of string formatting that logging methods do. `old` means using % 205 | # formatting, `new` is for `{}` formatting. 206 | logging-format-style=old 207 | 208 | # Logging modules to check that the string format arguments are in logging 209 | # function parameter format. 210 | logging-modules=logging 211 | 212 | 213 | [SPELLING] 214 | 215 | # Limits count of emitted suggestions for spelling mistakes. 216 | max-spelling-suggestions=4 217 | 218 | # Spelling dictionary name. Available dictionaries: none. To make it work, 219 | # install the 'python-enchant' package. 220 | spelling-dict= 221 | 222 | # List of comma separated words that should be considered directives if they 223 | # appear and the beginning of a comment and should not be checked. 224 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 225 | 226 | # List of comma separated words that should not be checked. 227 | spelling-ignore-words= 228 | 229 | # A path to a file that contains the private dictionary; one word per line. 230 | spelling-private-dict-file= 231 | 232 | # Tells whether to store unknown words to the private dictionary (see the 233 | # --spelling-private-dict-file option) instead of raising a message. 234 | spelling-store-unknown-words=no 235 | 236 | 237 | [MISCELLANEOUS] 238 | 239 | # List of note tags to take in consideration, separated by a comma. 240 | notes=FIXME, 241 | XXX, 242 | TODO 243 | 244 | # Regular expression of note tags to take in consideration. 245 | #notes-rgx= 246 | 247 | 248 | [TYPECHECK] 249 | 250 | # List of decorators that produce context managers, such as 251 | # contextlib.contextmanager. Add to this list to register other decorators that 252 | # produce valid context managers. 253 | contextmanager-decorators=contextlib.contextmanager 254 | 255 | # List of members which are set dynamically and missed by pylint inference 256 | # system, and so shouldn't trigger E1101 when accessed. Python regular 257 | # expressions are accepted. 258 | generated-members= 259 | 260 | # Tells whether missing members accessed in mixin class should be ignored. A 261 | # mixin class is detected if its name ends with "mixin" (case insensitive). 262 | ignore-mixin-members=yes 263 | 264 | # Tells whether to warn about missing members when the owner of the attribute 265 | # is inferred to be None. 266 | ignore-none=yes 267 | 268 | # This flag controls whether pylint should warn about no-member and similar 269 | # checks whenever an opaque object is returned when inferring. The inference 270 | # can return multiple potential results while evaluating a Python object, but 271 | # some branches might not be evaluated, which results in partial inference. In 272 | # that case, it might be useful to still emit no-member and other checks for 273 | # the rest of the inferred objects. 274 | ignore-on-opaque-inference=yes 275 | 276 | # List of class names for which member attributes should not be checked (useful 277 | # for classes with dynamically set attributes). This supports the use of 278 | # qualified names. 279 | ignored-classes=optparse.Values,thread._local,_thread._local 280 | 281 | # List of module names for which member attributes should not be checked 282 | # (useful for modules/projects where namespaces are manipulated during runtime 283 | # and thus existing member attributes cannot be deduced by static analysis). It 284 | # supports qualified module names, as well as Unix pattern matching. 285 | ignored-modules= 286 | 287 | # Show a hint with possible names when a member name was not found. The aspect 288 | # of finding the hint is based on edit distance. 289 | missing-member-hint=yes 290 | 291 | # The minimum edit distance a name should have in order to be considered a 292 | # similar match for a missing member name. 293 | missing-member-hint-distance=1 294 | 295 | # The total number of similar names that should be taken in consideration when 296 | # showing a hint for a missing member. 297 | missing-member-max-choices=1 298 | 299 | # List of decorators that change the signature of a decorated function. 300 | signature-mutators= 301 | 302 | 303 | [VARIABLES] 304 | 305 | # List of additional names supposed to be defined in builtins. Remember that 306 | # you should avoid defining new builtins when possible. 307 | additional-builtins= 308 | 309 | # Tells whether unused global variables should be treated as a violation. 310 | allow-global-unused-variables=yes 311 | 312 | # List of names allowed to shadow builtins 313 | allowed-redefined-builtins= 314 | 315 | # List of strings which can identify a callback function by name. A callback 316 | # name must start or end with one of those strings. 317 | callbacks=cb_, 318 | _cb 319 | 320 | # A regular expression matching the name of dummy variables (i.e. expected to 321 | # not be used). 322 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 323 | 324 | # Argument names that match this expression will be ignored. Default to name 325 | # with leading underscore. 326 | ignored-argument-names=_.*|^ignored_|^unused_ 327 | 328 | # Tells whether we should check for unused import in __init__ files. 329 | init-import=no 330 | 331 | # List of qualified module names which can have objects that can redefine 332 | # builtins. 333 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 334 | 335 | 336 | [FORMAT] 337 | 338 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 339 | expected-line-ending-format= 340 | 341 | # Regexp for a line that is allowed to be longer than the limit. 342 | ignore-long-lines=^\s*(# )??$ 343 | 344 | # Number of spaces of indent required inside a hanging or continued line. 345 | indent-after-paren=4 346 | 347 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 348 | # tab). 349 | indent-string=' ' 350 | 351 | # Maximum number of characters on a single line. 352 | max-line-length=100 353 | 354 | # Maximum number of lines in a module. 355 | max-module-lines=1000 356 | 357 | # Allow the body of a class to be on the same line as the declaration if body 358 | # contains single statement. 359 | single-line-class-stmt=no 360 | 361 | # Allow the body of an if to be on the same line as the test if there is no 362 | # else. 363 | single-line-if-stmt=no 364 | 365 | 366 | [SIMILARITIES] 367 | 368 | # Ignore comments when computing similarities. 369 | ignore-comments=yes 370 | 371 | # Ignore docstrings when computing similarities. 372 | ignore-docstrings=yes 373 | 374 | # Ignore imports when computing similarities. 375 | ignore-imports=no 376 | 377 | # Ignore function signatures when computing similarities. 378 | ignore-signatures=no 379 | 380 | # Minimum lines number of a similarity. 381 | min-similarity-lines=4 382 | 383 | 384 | [BASIC] 385 | 386 | # Naming style matching correct argument names. 387 | argument-naming-style=snake_case 388 | 389 | # Regular expression matching correct argument names. Overrides argument- 390 | # naming-style. 391 | #argument-rgx= 392 | 393 | # Naming style matching correct attribute names. 394 | attr-naming-style=snake_case 395 | 396 | # Regular expression matching correct attribute names. Overrides attr-naming- 397 | # style. 398 | #attr-rgx= 399 | 400 | # Bad variable names which should always be refused, separated by a comma. 401 | bad-names=foo, 402 | bar, 403 | baz, 404 | toto, 405 | tutu, 406 | tata 407 | 408 | # Bad variable names regexes, separated by a comma. If names match any regex, 409 | # they will always be refused 410 | bad-names-rgxs= 411 | 412 | # Naming style matching correct class attribute names. 413 | class-attribute-naming-style=any 414 | 415 | # Regular expression matching correct class attribute names. Overrides class- 416 | # attribute-naming-style. 417 | #class-attribute-rgx= 418 | 419 | # Naming style matching correct class constant names. 420 | class-const-naming-style=UPPER_CASE 421 | 422 | # Regular expression matching correct class constant names. Overrides class- 423 | # const-naming-style. 424 | #class-const-rgx= 425 | 426 | # Naming style matching correct class names. 427 | class-naming-style=PascalCase 428 | 429 | # Regular expression matching correct class names. Overrides class-naming- 430 | # style. 431 | #class-rgx= 432 | 433 | # Naming style matching correct constant names. 434 | const-naming-style=UPPER_CASE 435 | 436 | # Regular expression matching correct constant names. Overrides const-naming- 437 | # style. 438 | #const-rgx= 439 | 440 | # Minimum line length for functions/classes that require docstrings, shorter 441 | # ones are exempt. 442 | docstring-min-length=-1 443 | 444 | # Naming style matching correct function names. 445 | function-naming-style=snake_case 446 | 447 | # Regular expression matching correct function names. Overrides function- 448 | # naming-style. 449 | #function-rgx= 450 | 451 | # Good variable names which should always be accepted, separated by a comma. 452 | good-names=i, 453 | j, 454 | k, 455 | ex, 456 | Run, 457 | _ 458 | 459 | # Good variable names regexes, separated by a comma. If names match any regex, 460 | # they will always be accepted 461 | good-names-rgxs= 462 | 463 | # Include a hint for the correct naming format with invalid-name. 464 | include-naming-hint=no 465 | 466 | # Naming style matching correct inline iteration names. 467 | inlinevar-naming-style=any 468 | 469 | # Regular expression matching correct inline iteration names. Overrides 470 | # inlinevar-naming-style. 471 | #inlinevar-rgx= 472 | 473 | # Naming style matching correct method names. 474 | method-naming-style=snake_case 475 | 476 | # Regular expression matching correct method names. Overrides method-naming- 477 | # style. 478 | #method-rgx= 479 | 480 | # Naming style matching correct module names. 481 | module-naming-style=snake_case 482 | 483 | # Regular expression matching correct module names. Overrides module-naming- 484 | # style. 485 | #module-rgx= 486 | 487 | # Colon-delimited sets of names that determine each other's naming style when 488 | # the name regexes allow several styles. 489 | name-group= 490 | 491 | # Regular expression which should only match function or class names that do 492 | # not require a docstring. 493 | no-docstring-rgx=^_ 494 | 495 | # List of decorators that produce properties, such as abc.abstractproperty. Add 496 | # to this list to register other decorators that produce valid properties. 497 | # These decorators are taken in consideration only for invalid-name. 498 | property-classes=abc.abstractproperty 499 | 500 | # Naming style matching correct variable names. 501 | variable-naming-style=snake_case 502 | 503 | # Regular expression matching correct variable names. Overrides variable- 504 | # naming-style. 505 | #variable-rgx= 506 | 507 | 508 | [STRING] 509 | 510 | # This flag controls whether inconsistent-quotes generates a warning when the 511 | # character used as a quote delimiter is used inconsistently within a module. 512 | check-quote-consistency=no 513 | 514 | # This flag controls whether the implicit-str-concat should generate a warning 515 | # on implicit string concatenation in sequences defined over several lines. 516 | check-str-concat-over-line-jumps=no 517 | 518 | 519 | [IMPORTS] 520 | 521 | # List of modules that can be imported at any level, not just the top level 522 | # one. 523 | allow-any-import-level= 524 | 525 | # Allow wildcard imports from modules that define __all__. 526 | allow-wildcard-with-all=no 527 | 528 | # Analyse import fallback blocks. This can be used to support both Python 2 and 529 | # 3 compatible code, which means that the block might have code that exists 530 | # only in one or another interpreter, leading to false positives when analysed. 531 | analyse-fallback-blocks=no 532 | 533 | # Deprecated modules which should not be used, separated by a comma. 534 | deprecated-modules= 535 | 536 | # Output a graph (.gv or any supported image format) of external dependencies 537 | # to the given file (report RP0402 must not be disabled). 538 | ext-import-graph= 539 | 540 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 541 | # external) dependencies to the given file (report RP0402 must not be 542 | # disabled). 543 | import-graph= 544 | 545 | # Output a graph (.gv or any supported image format) of internal dependencies 546 | # to the given file (report RP0402 must not be disabled). 547 | int-import-graph= 548 | 549 | # Force import order to recognize a module as part of the standard 550 | # compatibility libraries. 551 | known-standard-library= 552 | 553 | # Force import order to recognize a module as part of a third party library. 554 | known-third-party=enchant 555 | 556 | # Couples of modules and preferred modules, separated by a comma. 557 | preferred-modules= 558 | 559 | 560 | [CLASSES] 561 | 562 | # Warn about protected attribute access inside special methods 563 | check-protected-access-in-special-methods=no 564 | 565 | # List of method names used to declare (i.e. assign) instance attributes. 566 | defining-attr-methods=__init__, 567 | __new__, 568 | setUp, 569 | __post_init__ 570 | 571 | # List of member names, which should be excluded from the protected access 572 | # warning. 573 | exclude-protected=_asdict, 574 | _fields, 575 | _replace, 576 | _source, 577 | _make 578 | 579 | # List of valid names for the first argument in a class method. 580 | valid-classmethod-first-arg=cls 581 | 582 | # List of valid names for the first argument in a metaclass class method. 583 | valid-metaclass-classmethod-first-arg=cls 584 | 585 | 586 | [DESIGN] 587 | 588 | # Maximum number of arguments for function / method. 589 | max-args=5 590 | 591 | # Maximum number of attributes for a class (see R0902). 592 | max-attributes=7 593 | 594 | # Maximum number of boolean expressions in an if statement (see R0916). 595 | max-bool-expr=5 596 | 597 | # Maximum number of branch for function / method body. 598 | max-branches=12 599 | 600 | # Maximum number of locals for function / method body. 601 | max-locals=15 602 | 603 | # Maximum number of parents for a class (see R0901). 604 | max-parents=7 605 | 606 | # Maximum number of public methods for a class (see R0904). 607 | max-public-methods=20 608 | 609 | # Maximum number of return / yield for function / method body. 610 | max-returns=6 611 | 612 | # Maximum number of statements in function / method body. 613 | max-statements=50 614 | 615 | # Minimum number of public methods for a class (see R0903). 616 | min-public-methods=2 617 | 618 | 619 | [EXCEPTIONS] 620 | 621 | # Exceptions that will emit a warning when being caught. Defaults to 622 | # "BaseException, Exception". 623 | overgeneral-exceptions=BaseException, 624 | Exception 625 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for OCSP-StatusChecker 2 | 3 | # v1.0.0 4 | - Initial Release 5 | 6 | # v1.3.0 7 | - Add Server Name Indication (SNI) support 8 | - Add tests to cover each root certificate authority in use 9 | - Fix MITM proxy error reporting 10 | 11 | # v1.4.0 12 | - Added the ability to call from the command line 13 | - Updated cryptography and validators 14 | - Some pylint fixes 15 | 16 | # v1.5.0 17 | - Fixed an uncaught exception when port is > 65535 or not numeric 18 | - Parse out http(s) when submitting a request 19 | 20 | # v1.6.0 21 | - Upgrade cryptography to 3.2 22 | - Upgrade nassl to 3.1 23 | - Added tox tests for Python versions 3.7 and 3.8 24 | 25 | # v1.7.0 26 | - Added support for M1 Mac's 27 | - Added support for Python 3.9 28 | - Upgraded nassl to 4.0.0 29 | - Upgraded cryptography to 3.4.6 30 | - Fixed failing tests 31 | - Added tox tests for Python 3.9 32 | 33 | # v1.8.0 34 | - Fixed a bug to handle a situation when parsing a certificate with an AIA extension but no OCSP URL 35 | - Fixed a bug to handle a situation where the remote host is not using SSL/TLS and we attempt to do a SSL/TLS handshake 36 | - Fixed a bug to handle a situation where the remote host does not respond to a Client Hello 37 | - Prepended all exceptions with the function name for easier troubleshooting 38 | - Upgraded cryptography to 3.4.7 to support the latest versions of OpenSSL 39 | 40 | # v1.8.2 41 | - Pinned all direct and transitive dependencies to a specific version in requirements.txt 42 | - add pylintrc file and pylint fixes 43 | - run black against code base 44 | - move CI from Azure DevOps to Github Actions 45 | - fixed a logic bug when not supplying a port 46 | - increased test coverage 47 | 48 | # v1.9.0 49 | - bump all dependencies 50 | - remove requests library and use built-in urllib module 51 | 52 | # v1.9.9 53 | - created docker development environment in VS Code to work around Apple M1 compatibility issues with NaSSL 54 | - removed a test that will never be able to run in the context of a docker container 55 | - Improved errors returned to the user for various OpenSSL errors 56 | - switch from get_received_chain to the get_verified_chain method in NaSSL to ensure the certificate is validated before we try any operations 57 | 58 | # v1.9.11 59 | - bump all dependencies 60 | - moved to pyproject.toml for project definition 61 | - added tests for python 3.10 and 3.11 62 | - added coverage across macOS, Linux, and Windows 63 | - fixed two broken tests and commented one out for now 64 | 65 | # v1.9.12 66 | - removed validators 67 | - bump all dependencies 68 | - added dev-requirements.txt for CI 69 | - removed tox 70 | 71 | # v1.9.13 72 | - allow usage of http proxy to request both host certificate and perform the ocsp request 73 | - allow configuration of request timeouts as function argument 74 | - add extra info in unknown exceptions 75 | 76 | # v1.9.14 77 | - allow custom path to ca-certs via @vLabayen 78 | - update devcontainer 79 | - fix flaky tests 80 | - update CI 81 | - remove support for Python 3.7 82 | 83 | # v2.0.0 84 | - Relocate repository under gattjoe 85 | - Removed support for Python 3.8 and added support for Python 3.13 86 | - Update dependencies and rely soley on pyproject.toml 87 | - Add CODEOWNERS -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Usually you do not have to specify amd64, but on an Apple M1 you do if you want to use packages 2 | # that are not optimized for arm64 like NaSSL 3 | FROM --platform=amd64 mcr.microsoft.com/devcontainers/python:3.12-bullseye 4 | 5 | #SHELL ["/bin/bash", "--login", "-c"] 6 | 7 | #ENV DEBIAN_FRONTEND noninteractive 8 | #ENV LANG C.UTF-8 9 | 10 | #RUN useradd -m ocspdev 11 | 12 | #RUN apt-get update && apt-get install -y --no-install-recommends \ 13 | # ca-certificates \ 14 | # netbase \ 15 | # curl \ 16 | # git \ 17 | # bash-completion \ 18 | # && rm -rf /var/lib/apt/lists/* 19 | 20 | #USER ocspdev 21 | WORKDIR /home/vscode 22 | 23 | # Copy OcspChecker Folder 24 | COPY --chown=vscode:vscode . /home/vscode/OcspChecker/ 25 | 26 | CMD [ "bash" ] 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-present, MetLife Services and Solutions, LLC 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the Apache License with the above modification is 11 | distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 12 | KIND, either express or implied. See the Apache License for the specific 13 | language governing permissions and limitations under the Apache License. 14 | 15 | -------------------------------------------------------------------------- 16 | 17 | 18 | Apache License 19 | Version 2.0, January 2004 20 | http://www.apache.org/licenses/ 21 | 22 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 23 | 24 | 1. Definitions. 25 | 26 | "License" shall mean the terms and conditions for use, reproduction, 27 | and distribution as defined by Sections 1 through 9 of this document. 28 | 29 | "Licensor" shall mean the copyright owner or entity authorized by 30 | the copyright owner that is granting the License. 31 | 32 | "Legal Entity" shall mean the union of the acting entity and all 33 | other entities that control, are controlled by, or are under common 34 | control with that entity. For the purposes of this definition, 35 | "control" means (i) the power, direct or indirect, to cause the 36 | direction or management of such entity, whether by contract or 37 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 38 | outstanding shares, or (iii) beneficial ownership of such entity. 39 | 40 | "You" (or "Your") shall mean an individual or Legal Entity 41 | exercising permissions granted by this License. 42 | 43 | "Source" form shall mean the preferred form for making modifications, 44 | including but not limited to software source code, documentation 45 | source, and configuration files. 46 | 47 | "Object" form shall mean any form resulting from mechanical 48 | transformation or translation of a Source form, including but 49 | not limited to compiled object code, generated documentation, 50 | and conversions to other media types. 51 | 52 | "Work" shall mean the work of authorship, whether in Source or 53 | Object form, made available under the License, as indicated by a 54 | copyright notice that is included in or attached to the work 55 | (an example is provided in the Appendix below). 56 | 57 | "Derivative Works" shall mean any work, whether in Source or Object 58 | form, that is based on (or derived from) the Work and for which the 59 | editorial revisions, annotations, elaborations, or other modifications 60 | represent, as a whole, an original work of authorship. For the purposes 61 | of this License, Derivative Works shall not include works that remain 62 | separable from, or merely link (or bind by name) to the interfaces of, 63 | the Work and Derivative Works thereof. 64 | 65 | "Contribution" shall mean any work of authorship, including 66 | the original version of the Work and any modifications or additions 67 | to that Work or Derivative Works thereof, that is intentionally 68 | submitted to Licensor for inclusion in the Work by the copyright owner 69 | or by an individual or Legal Entity authorized to submit on behalf of 70 | the copyright owner. For the purposes of this definition, "submitted" 71 | means any form of electronic, verbal, or written communication sent 72 | to the Licensor or its representatives, including but not limited to 73 | communication on electronic mailing lists, source code control systems, 74 | and issue tracking systems that are managed by, or on behalf of, the 75 | Licensor for the purpose of discussing and improving the Work, but 76 | excluding communication that is conspicuously marked or otherwise 77 | designated in writing by the copyright owner as "Not a Contribution." 78 | 79 | "Contributor" shall mean Licensor and any individual or Legal Entity 80 | on behalf of whom a Contribution has been received by Licensor and 81 | subsequently incorporated within the Work. 82 | 83 | 2. Grant of Copyright License. Subject to the terms and conditions of 84 | this License, each Contributor hereby grants to You a perpetual, 85 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 86 | copyright license to reproduce, prepare Derivative Works of, 87 | publicly display, publicly perform, sublicense, and distribute the 88 | Work and such Derivative Works in Source or Object form. 89 | 90 | 3. Grant of Patent License. Subject to the terms and conditions of 91 | this License, each Contributor hereby grants to You a perpetual, 92 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 93 | (except as stated in this section) patent license to make, have made, 94 | use, offer to sell, sell, import, and otherwise transfer the Work, 95 | where such license applies only to those patent claims licensable 96 | by such Contributor that are necessarily infringed by their 97 | Contribution(s) alone or by combination of their Contribution(s) 98 | with the Work to which such Contribution(s) was submitted. If You 99 | institute patent litigation against any entity (including a 100 | cross-claim or counterclaim in a lawsuit) alleging that the Work 101 | or a Contribution incorporated within the Work constitutes direct 102 | or contributory patent infringement, then any patent licenses 103 | granted to You under this License for that Work shall terminate 104 | as of the date such litigation is filed. 105 | 106 | 4. Redistribution. You may reproduce and distribute copies of the 107 | Work or Derivative Works thereof in any medium, with or without 108 | modifications, and in Source or Object form, provided that You 109 | meet the following conditions: 110 | 111 | (a) You must give any other recipients of the Work or 112 | Derivative Works a copy of this License; and 113 | 114 | (b) You must cause any modified files to carry prominent notices 115 | stating that You changed the files; and 116 | 117 | (c) You must retain, in the Source form of any Derivative Works 118 | that You distribute, all copyright, patent, trademark, and 119 | attribution notices from the Source form of the Work, 120 | excluding those notices that do not pertain to any part of 121 | the Derivative Works; and 122 | 123 | (d) If the Work includes a "NOTICE" text file as part of its 124 | distribution, then any Derivative Works that You distribute must 125 | include a readable copy of the attribution notices contained 126 | within such NOTICE file, excluding those notices that do not 127 | pertain to any part of the Derivative Works, in at least one 128 | of the following places: within a NOTICE text file distributed 129 | as part of the Derivative Works; within the Source form or 130 | documentation, if provided along with the Derivative Works; or, 131 | within a display generated by the Derivative Works, if and 132 | wherever such third-party notices normally appear. The contents 133 | of the NOTICE file are for informational purposes only and 134 | do not modify the License. You may add Your own attribution 135 | notices within Derivative Works that You distribute, alongside 136 | or as an addendum to the NOTICE text from the Work, provided 137 | that such additional attribution notices cannot be construed 138 | as modifying the License. 139 | 140 | You may add Your own copyright statement to Your modifications and 141 | may provide additional or different license terms and conditions 142 | for use, reproduction, or distribution of Your modifications, or 143 | for any such Derivative Works as a whole, provided Your use, 144 | reproduction, and distribution of the Work otherwise complies with 145 | the conditions stated in this License. 146 | 147 | 5. Submission of Contributions. Unless You explicitly state otherwise, 148 | any Contribution intentionally submitted for inclusion in the Work 149 | by You to the Licensor shall be under the terms and conditions of 150 | this License, without any additional terms or conditions. 151 | Notwithstanding the above, nothing herein shall supersede or modify 152 | the terms of any separate license agreement you may have executed 153 | with Licensor regarding such Contributions. 154 | 155 | 6. Trademarks. This License does not grant permission to use the trade 156 | names, trademarks, service marks, or product names of the Licensor, 157 | except as required for reasonable and customary use in describing the 158 | origin of the Work and reproducing the content of the NOTICE file. 159 | 160 | 7. Disclaimer of Warranty. Unless required by applicable law or 161 | agreed to in writing, Licensor provides the Work (and each 162 | Contributor provides its Contributions) on an "AS IS" BASIS, 163 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 164 | implied, including, without limitation, any warranties or conditions 165 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 166 | PARTICULAR PURPOSE. You are solely responsible for determining the 167 | appropriateness of using or redistributing the Work and assume any 168 | risks associated with Your exercise of permissions under this License. 169 | 170 | 8. Limitation of Liability. In no event and under no legal theory, 171 | whether in tort (including negligence), contract, or otherwise, 172 | unless required by applicable law (such as deliberate and grossly 173 | negligent acts) or agreed to in writing, shall any Contributor be 174 | liable to You for damages, including any direct, indirect, special, 175 | incidental, or consequential damages of any character arising as a 176 | result of this License or out of the use or inability to use the 177 | Work (including but not limited to damages for loss of goodwill, 178 | work stoppage, computer failure or malfunction, or any and all 179 | other commercial damages or losses), even if such Contributor 180 | has been advised of the possibility of such damages. 181 | 182 | 9. Accepting Warranty or Additional Liability. While redistributing 183 | the Work or Derivative Works thereof, You may choose to offer, 184 | and charge a fee for, acceptance of support, warranty, indemnity, 185 | or other liability obligations and/or rights consistent with this 186 | License. However, in accepting such obligations, You may act only 187 | on Your own behalf and on Your sole responsibility, not on behalf 188 | of any other Contributor, and only if You agree to indemnify, 189 | defend, and hold each Contributor harmless for any liability 190 | incurred by, or claims asserted against, such Contributor by reason 191 | of your accepting any such warranty or additional liability. 192 | 193 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OCSPChecker 2 | 3 | [![Downloads](https://pepy.tech/badge/ocsp-checker/month)](https://pepy.tech/project/ocsp-checker) 4 | [![PyPI Version](https://img.shields.io/pypi/v/ocsp-checker.svg)](https://pypi.org/project/ocsp-checker/) 5 | [![Python version](https://img.shields.io/pypi/pyversions/ocsp-checker.svg)](https://pypi.org/project/ocsp-checker/) 6 | 7 | ## Overview 8 | 9 | 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. 10 | 11 | 12 | ## Pre-requisites 13 | 14 | __Python__ - Python 3.9 (64-bit) and above. 15 | 16 | ## Installation 17 | 18 | It is strongly recommended to run ocsp-checker in a virtual environment. This will prevent you from impacting your system python when installing its dependencies. [venv](https://docs.python.org/3/library/venv.html) is a good option, with an example below: 19 | 20 | ```python -m venv ocsp-checker``` 21 | ```cd ocsp-checker && source bin/activate``` 22 | 23 | Once your virtual environment is activated, install ocsp-checker as follows: 24 | 25 | ```pip install ocsp-checker``` 26 | 27 | ## Usage 28 | 29 | ``` 30 | >>> from ocspchecker import ocspchecker 31 | >>> ocsp_request = ocspchecker.get_ocsp_status("github.com") 32 | ``` 33 | 34 | ## Sample Output 35 | 36 | Sample output below, let me know if you want to add more fields/information to the output. 37 | 38 | ``` 39 | ['Host: github.com:443', 'OCSP URL: http://ocsp.digicert.com', 'OCSP Status: GOOD'] 40 | ``` 41 | 42 | PLEASE NOTE: If you run this on a network with a MITM SSL proxy, you may receive unintended results (see below): 43 | ``` 44 | ["Error: Certificate Authority Information Access (AIA) Extension Missing. Possible MITM Proxy."] 45 | ``` 46 | 47 | ## Command Line Usage 48 | 49 | OCSPChecker can now be used at the command line. The format is: 50 | ``` 51 | usage: ocspchecker [-h] --target target [--port port] 52 | 53 | Check the OCSP revocation status for a x509 digital certificate. 54 | 55 | optional arguments: 56 | -h, --help show this help message and exit 57 | --target target, -t target 58 | The target to test 59 | --port port, -p port The port to test (default is 443) 60 | ``` 61 | 62 | For example: 63 | 64 | ``` ocspchecker -t github.com ``` 65 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==8.3.5 2 | pylint==3.3.1 3 | black==24.10.0 4 | isort==5.13.2 5 | -------------------------------------------------------------------------------- /ocspchecker/__init__.py: -------------------------------------------------------------------------------- 1 | """ Library used to check the OCSP revocation status for a x509 digital certificate. """ 2 | 3 | __title__ = "ocspchecker" 4 | __version__ = "2.0" 5 | __author__ = "Joe Gatt" 6 | 7 | from ocspchecker.ocspchecker import get_ocsp_status 8 | -------------------------------------------------------------------------------- /ocspchecker/__main__.py: -------------------------------------------------------------------------------- 1 | """ Command line module for ocspchecker """ 2 | 3 | import argparse 4 | 5 | from ocspchecker import get_ocsp_status 6 | 7 | # Create the parser 8 | arg_parser = argparse.ArgumentParser( 9 | prog="ocspchecker", 10 | description="""Check the OCSP revocation\ 11 | status for a x509 digital certificate.""", 12 | ) 13 | 14 | # Add the arguments 15 | arg_parser.add_argument( 16 | "--target", 17 | "-t", 18 | metavar="target", 19 | type=str, 20 | required=True, 21 | help="The target to test", 22 | ) 23 | 24 | arg_parser.add_argument( 25 | "--port", 26 | "-p", 27 | metavar="port", 28 | type=int, 29 | required=False, 30 | default=443, 31 | help="The port to test (default is 443)", 32 | ) 33 | 34 | 35 | def main() -> None: 36 | """Main function""" 37 | # Execute the parse_args() method 38 | args = arg_parser.parse_args() 39 | target = args.target 40 | arg_port = args.port 41 | 42 | ocsp_status = get_ocsp_status(target, arg_port) 43 | print(ocsp_status) 44 | 45 | 46 | if __name__ == "__main__": 47 | main() 48 | -------------------------------------------------------------------------------- /ocspchecker/ocspchecker.py: -------------------------------------------------------------------------------- 1 | """ A full cert chain is required to make a proper OCSP request. However, 2 | the ssl module for python 3.x does not support the get_peer_cert_chain() 3 | method. get_peer_cert_chain() is in flight: https://github.com/python/cpython/pull/17938 4 | 5 | For a short-term fix, I will use nassl to grab the full cert chain. """ 6 | 7 | from pathlib import Path 8 | from socket import AF_INET, SOCK_STREAM, gaierror, socket, timeout 9 | from typing import List, Tuple, Union 10 | from urllib import error, request 11 | from urllib.parse import urlparse 12 | 13 | import certifi 14 | from cryptography.hazmat.backends import default_backend 15 | from cryptography.hazmat.primitives import serialization 16 | from cryptography.hazmat.primitives.hashes import SHA1 17 | from cryptography.x509 import ExtensionNotFound, load_pem_x509_certificate, ocsp 18 | from cryptography.x509.oid import ExtensionOID 19 | from nassl._nassl import OpenSSLError 20 | from nassl.cert_chain_verifier import CertificateChainVerificationFailed 21 | from nassl.ssl_client import ( 22 | ClientCertificateRequested, 23 | OpenSslVerifyEnum, 24 | OpenSslVersionEnum, 25 | SslClient, 26 | ) 27 | 28 | from ocspchecker.utils.http_proxy_connect import http_proxy_connect 29 | 30 | 31 | class InitialConnectionError(Exception): 32 | """Custom exception class to differentiate between 33 | initial connection errors and OpenSSL errors""" 34 | 35 | 36 | class OcspResponderError(Exception): 37 | """Custom exception class to identify errors obtaining a response from a CA'a Responder""" 38 | 39 | 40 | openssl_errors: dict = { 41 | # https://github.com/openssl/openssl/issues/6805 42 | "1408F10B": "The remote host is not using SSL/TLS on the port specified." 43 | # TLS Fatal Alert 40 - sender was unable to negotiate an acceptable set of security 44 | # parameters given the options available 45 | , 46 | "14094410": "SSL/TLS Handshake Failure." 47 | # TLS Fatal Alert 112 - the server understood the ClientHello but did not recognize 48 | # the server name per: https://datatracker.ietf.org/doc/html/rfc6066#section-3 49 | , 50 | "14094458": "Unrecognized server name provided. Check your target and try again." 51 | # TLS Fatal Alert 50 - a field was out of the specified range 52 | # or the length of the message was incorrect 53 | , 54 | "1417B109": "Decode Error. Check your target and try again." 55 | # TLS Fatal Alert 80 - Internal Error 56 | , 57 | "14094438": "TLS Fatal Alert 80 - Internal Error." 58 | # Unable to find public key parameters 59 | , 60 | "140070EF": "Unable to find public key parameters.", 61 | } 62 | 63 | 64 | def get_ocsp_status( 65 | host: str, 66 | port: int = 443, 67 | proxy: Union[None, Tuple[str, int]] = None, 68 | request_timeout: float = 3.0, 69 | ) -> List[str]: 70 | """Main function with three inputs: host, port and proxy""" 71 | 72 | results: List[str] = [] 73 | results.append(f"Host: {host}:{port}") 74 | 75 | # pylint: disable=W0703 76 | # All of the exceptions in this function are passed-through 77 | 78 | # Sanitize host 79 | try: 80 | host = verify_host(host) 81 | 82 | except Exception as err: 83 | results.append("Error: " + str(err)) 84 | return results 85 | 86 | try: 87 | # Get the remote certificate chain 88 | cert_chain = get_certificate_chain(host, port, proxy=proxy, request_timeout=request_timeout) 89 | 90 | # Extract OCSP URL from leaf certificate 91 | ocsp_url = extract_ocsp_url(cert_chain) 92 | 93 | # Build OCSP request 94 | ocsp_request = build_ocsp_request(cert_chain) 95 | 96 | # Send OCSP request to responder and get result 97 | ocsp_response = get_ocsp_response( 98 | ocsp_url, ocsp_request, proxy=proxy, request_timeout=request_timeout 99 | ) 100 | 101 | # Extract OCSP result from OCSP response 102 | ocsp_result = extract_ocsp_result(ocsp_response) 103 | 104 | except Exception as err: 105 | results.append("Error: " + str(err)) 106 | return results 107 | 108 | results.append(f"OCSP URL: {ocsp_url}") 109 | results.append(f"{ocsp_result}") 110 | 111 | return results 112 | 113 | 114 | def get_certificate_chain( 115 | host: str, 116 | port: int = 443, 117 | proxy: Union[None, Tuple[str, int]] = None, 118 | request_timeout: float = 3.0, 119 | path_to_ca_certs: Path = Path(certifi.where()), 120 | ) -> List[str]: 121 | """Connect to the host on the port and obtain certificate chain""" 122 | 123 | func_name: str = "get_certificate_chain" 124 | 125 | cert_chain: list = [] 126 | 127 | soc = socket(AF_INET, SOCK_STREAM, proto=0) 128 | soc.settimeout(request_timeout) 129 | 130 | try: 131 | if path_to_ca_certs.is_file(): 132 | pass 133 | except FileNotFoundError: 134 | raise OSError(f"ca cert file {path_to_ca_certs} not found") from None 135 | 136 | try: 137 | if proxy is not None: 138 | http_proxy_connect((host, port), proxy=proxy, soc=soc) 139 | else: 140 | soc.connect((host, port)) 141 | 142 | except gaierror: 143 | raise InitialConnectionError( 144 | f"{func_name}: {host}:{port} is invalid or not known." 145 | ) from None 146 | 147 | except timeout: 148 | soc.close() 149 | raise InitialConnectionError( 150 | f"{func_name}: Connection to {host}:{port} timed out." 151 | ) from None 152 | 153 | except ConnectionRefusedError: 154 | raise InitialConnectionError(f"{func_name}: Connection to {host}:{port} refused.") from None 155 | 156 | except (IOError, OSError) as err: 157 | raise InitialConnectionError( 158 | f"{func_name}: Unable to reach the host {host}. {str(err)}" 159 | ) from None 160 | 161 | except (OverflowError, TypeError): 162 | raise InitialConnectionError( 163 | f"{func_name}: Illegal port: {port}. Port must be between 0-65535." 164 | ) from None 165 | 166 | ssl_client = SslClient( 167 | ssl_version=OpenSslVersionEnum.SSLV23, 168 | underlying_socket=soc, 169 | ssl_verify=OpenSslVerifyEnum.NONE, 170 | ssl_verify_locations=path_to_ca_certs, 171 | ) 172 | 173 | # Add Server Name Indication (SNI) extension to the Client Hello 174 | ssl_client.set_tlsext_host_name(host) 175 | 176 | try: 177 | ssl_client.do_handshake() 178 | cert_chain = ssl_client.get_verified_chain() 179 | 180 | except IOError: 181 | raise ValueError(f"{func_name}: {host} did not respond to the Client Hello.") from None 182 | 183 | except CertificateChainVerificationFailed: 184 | raise ValueError(f"{func_name}: Certificate Verification failed for {host}.") from None 185 | 186 | except ClientCertificateRequested: 187 | raise ValueError(f"{func_name}: Client Certificate Requested for {host}.") from None 188 | 189 | except OpenSSLError as err: 190 | for key, value in openssl_errors.items(): 191 | if key in err.args[0]: 192 | raise ValueError(f"{func_name}: {value}") from None 193 | 194 | raise ValueError(f"{func_name}: {err}") from None 195 | 196 | finally: 197 | # shutdown() will also close the underlying socket 198 | ssl_client.shutdown() 199 | 200 | return cert_chain 201 | 202 | 203 | def extract_ocsp_url(cert_chain: List[str]) -> str: 204 | """Parse the leaf certificate and extract the access method and 205 | access location AUTHORITY_INFORMATION_ACCESS extensions to 206 | get the ocsp url""" 207 | 208 | func_name: str = "extract_ocsp_url" 209 | 210 | ocsp_url: str = "" 211 | 212 | # Convert to a certificate object in cryptography.io 213 | certificate = load_pem_x509_certificate(str.encode(cert_chain[0]), default_backend()) 214 | 215 | # Check to ensure it has an AIA extension and if so, extract ocsp url 216 | try: 217 | aia_extension = certificate.extensions.get_extension_for_oid( 218 | ExtensionOID.AUTHORITY_INFORMATION_ACCESS 219 | ).value 220 | 221 | # pylint: disable=protected-access 222 | for aia_method in iter((aia_extension)): 223 | if aia_method.__getattribute__("access_method")._name == "OCSP": 224 | ocsp_url = aia_method.__getattribute__("access_location").value 225 | 226 | if ocsp_url == "": 227 | raise ValueError(f"{func_name}: OCSP URL missing from Certificate AIA Extension.") 228 | 229 | except ExtensionNotFound: 230 | raise ValueError( 231 | f"{func_name}: Certificate AIA Extension Missing. Possible MITM Proxy." 232 | ) from None 233 | 234 | return ocsp_url 235 | 236 | 237 | def build_ocsp_request(cert_chain: List[str]) -> bytes: 238 | """Build an OCSP request out of the leaf and issuer pem certificates 239 | see: https://cryptography.io/en/latest/x509/ocsp/#cryptography.x509.ocsp.OCSPRequestBuilder 240 | for more information""" 241 | 242 | func_name: str = "build_ocsp_request" 243 | 244 | try: 245 | leaf_cert = load_pem_x509_certificate(str.encode(cert_chain[0]), default_backend()) 246 | issuer_cert = load_pem_x509_certificate(str.encode(cert_chain[1]), default_backend()) 247 | 248 | except ValueError: 249 | raise Exception(f"{func_name}: Unable to load x509 certificate.") from None 250 | 251 | # Build OCSP request 252 | builder = ocsp.OCSPRequestBuilder() 253 | builder = builder.add_certificate(leaf_cert, issuer_cert, SHA1()) 254 | ocsp_data = builder.build() 255 | ocsp_request_data = ocsp_data.public_bytes(serialization.Encoding.DER) 256 | 257 | return ocsp_request_data 258 | 259 | 260 | def get_ocsp_response( 261 | ocsp_url: str, 262 | ocsp_request_data: bytes, 263 | proxy: Union[None, Tuple[str, int]] = None, 264 | request_timeout: float = 3.0, 265 | ): 266 | """Send OCSP request to ocsp responder and retrieve response""" 267 | 268 | func_name: str = "get_ocsp_response" 269 | ocsp_response = None 270 | 271 | try: 272 | ocsp_request = request.Request( 273 | ocsp_url, 274 | data=ocsp_request_data, 275 | headers={"Content-Type": "application/ocsp-request"}, 276 | ) 277 | if proxy is not None: 278 | host, port = proxy 279 | ocsp_request.set_proxy(f"{host}:{port}", "http") 280 | 281 | with request.urlopen(ocsp_request, timeout=request_timeout) as resp: 282 | ocsp_response = resp.read() 283 | 284 | except error.URLError as err: 285 | if isinstance(err.reason, timeout): 286 | raise OcspResponderError(f"{func_name}: Request timeout for {ocsp_url}") 287 | 288 | if isinstance(err.reason, gaierror): 289 | raise OcspResponderError(f"{func_name}: {ocsp_url} is invalid or not known.") 290 | 291 | raise OcspResponderError(f"{func_name}: Connection Error to {ocsp_url}. {str(err)}") 292 | 293 | except ValueError as err: 294 | raise OcspResponderError(f"{func_name}: Connection Error to {ocsp_url}. {str(err)}") 295 | 296 | except timeout: 297 | raise OcspResponderError(f"{func_name}: Request timeout for {ocsp_url}") 298 | 299 | return ocsp_response 300 | 301 | 302 | def extract_ocsp_result(ocsp_response): 303 | """Extract the OCSP result from the provided ocsp_response""" 304 | 305 | func_name: str = "extract_ocsp_result" 306 | 307 | try: 308 | ocsp_response = ocsp.load_der_ocsp_response(ocsp_response) 309 | # OCSP Response Status here: 310 | # https://cryptography.io/en/latest/_modules/cryptography/x509/ocsp/#OCSPResponseStatus 311 | # A status of 0 == OCSPResponseStatus.SUCCESSFUL 312 | if str(ocsp_response.response_status.value) != "0": 313 | # This will return one of five errors, which means connecting 314 | # to the OCSP Responder failed for one of the below reasons: 315 | # MALFORMED_REQUEST = 1 316 | # INTERNAL_ERROR = 2 317 | # TRY_LATER = 3 318 | # SIG_REQUIRED = 5 319 | # UNAUTHORIZED = 6 320 | ocsp_response = str(ocsp_response.response_status) 321 | ocsp_response = ocsp_response.split(".") 322 | raise Exception(f"{func_name}: OCSP Request Error: {ocsp_response[1]}") 323 | 324 | certificate_status = str(ocsp_response.certificate_status) 325 | certificate_status = certificate_status.split(".") 326 | return f"OCSP Status: {certificate_status[1]}" 327 | 328 | except ValueError as err: 329 | return f"{func_name}: {str(err)}" 330 | 331 | 332 | def verify_host(host: str) -> str: 333 | """Parse a DNS name to ensure it does not contain http(s)""" 334 | parsed_name = urlparse(host) 335 | 336 | # The below parses out http(s) from a name 337 | host_candidate = parsed_name.netloc 338 | if host_candidate == "": 339 | host_candidate = parsed_name.path 340 | 341 | return host_candidate 342 | -------------------------------------------------------------------------------- /ocspchecker/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gattjoe/OCSPChecker/8fb866d252b78209717227cf4e3b42cf23f11a00/ocspchecker/utils/__init__.py -------------------------------------------------------------------------------- /ocspchecker/utils/http_proxy_connect.py: -------------------------------------------------------------------------------- 1 | """ 2 | Establish a socket connection through an HTTP proxy. 3 | Author: Fredrik Østrem 4 | License: 5 | Copyright 2013 Fredrik Østrem 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 7 | documentation files (the "Software"), to deal in the Software without restriction, including without 8 | limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 9 | Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions 11 | of the Software. 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 13 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 14 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | """ 18 | 19 | # This file is a copy-paste from https://gist.github.com/frxstrem/4487802 20 | # 21 | # Some fixes were made: 22 | # - remove long since it's just int in python3 (https://gist.github.com/frxstrem/4487802#file-http_proxy_connect-py-L46) 23 | # - replace r+ with rw (https://gist.github.com/frxstrem/4487802#file-http_proxy_connect-py-L73) 24 | # - fix b64encode, as stated in comments (https://gist.github.com/frxstrem/4487802?permalink_comment_id=3635597#gistcomment-3635597) 25 | # 26 | # Some refactors were made: 27 | # - Typing of function arguments 28 | # - Remove type validation of arguments & refactor auth checks to asign proxy-authorization header 29 | # - Make proxy mandatory and remove case where the proxy == None 30 | # - Remove the headers argument, since it's directly overwritten 31 | # - Accept a socket instance to allow custom configuration 32 | # - Return only the socket instance 33 | 34 | 35 | import socket 36 | from base64 import b64encode 37 | from typing import Tuple, Union 38 | 39 | 40 | def http_proxy_connect( 41 | address: Tuple[str, int], 42 | proxy: Tuple[str, int], 43 | auth: Union[None, str, Tuple[str, str]] = None, 44 | soc: Union[None, socket.socket] = None, 45 | ) -> socket.socket: 46 | """Establish a socket connection through an HTTP proxy.""" 47 | 48 | headers = {"host": address[0]} 49 | 50 | if isinstance(auth, str): 51 | headers["proxy-authorization"] = auth 52 | elif isinstance(auth, tuple): 53 | headers["proxy-authorization"] = "Basic " + b64encode( 54 | ("%s:%s" % auth).encode("utf-8") 55 | ).decode("utf-8") 56 | 57 | s = soc if soc is not None else socket.socket() 58 | s.connect(proxy) 59 | fp = s.makefile("rw") 60 | 61 | fp.write("CONNECT %s:%d HTTP/1.0\r\n" % address) 62 | fp.write("\r\n".join("%s: %s" % (k, v) for (k, v) in headers.items()) + "\r\n\r\n") 63 | fp.flush() 64 | 65 | statusline = fp.readline().rstrip("\r\n") 66 | 67 | if statusline.count(" ") < 2: 68 | fp.close() 69 | s.close() 70 | raise IOError("Bad response") 71 | version, status, statusmsg = statusline.split(" ", 2) 72 | if not version in ("HTTP/1.0", "HTTP/1.1"): 73 | fp.close() 74 | s.close() 75 | raise IOError("Unsupported HTTP version") 76 | try: 77 | status = int(status) 78 | except ValueError: 79 | fp.close() 80 | s.close() 81 | raise IOError("Bad response") 82 | 83 | response_headers = {} 84 | 85 | while True: 86 | tl = "" 87 | l = fp.readline().rstrip("\r\n") 88 | if l == "": 89 | break 90 | if not ":" in l: 91 | continue 92 | k, v = l.split(":", 1) 93 | response_headers[k.strip().lower()] = v.strip() 94 | 95 | fp.close() 96 | return s 97 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name="ocsp-checker" 7 | version="2.0.0" 8 | description="Library used to check the OCSP revocation status for a x509 digital certificate." 9 | dependencies = [ 10 | "cryptography>=44.0", 11 | "nassl>=5.3", 12 | "certifi", 13 | ] 14 | 15 | readme= {file = "README.md", content-type = "text/markdown"} 16 | authors=[{ name = "Joe Gatt", email = "gattjoseph@hotmail.com" }] 17 | license= {file = "LICENSE.txt" } 18 | classifiers=[ 19 | "License :: OSI Approved :: Apache Software License", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3.8", 22 | "Programming Language :: Python :: 3.9", 23 | "Programming Language :: Python :: 3.10", 24 | "Programming Language :: Python :: 3.11", 25 | "Programming Language :: Python :: 3.12", 26 | "Programming Language :: Python :: 3.13", 27 | ] 28 | keywords=["ssl, tls, ocsp, python, security"] 29 | requires-python = ">=3.9" 30 | 31 | [project.urls] 32 | "homepage" = "https://github.com/gattjoe/OCSPChecker" 33 | "documentation" = "https://github.com/gattjoe/OCSPChecker/blob/master/README.md" 34 | "repository" = "https://github.com/gattjoe/OCSPChecker" 35 | "changelog" = "https://github.com/gattjoe/OCSPChecker/blob/master/CHANGELOG.md" 36 | 37 | [project.scripts] 38 | ocspchecker = "ocspchecker.__main__:main" 39 | 40 | [tools.setuptools.packages.find] 41 | where = ["."] 42 | 43 | [tool.black] 44 | line-length = 100 45 | target-version = ["py38", "py39", "py310", "py311", "py312", "py13"] 46 | 47 | [tool.isort] 48 | ensure_newline_before_comments = true 49 | force_grid_wrap = 0 50 | force_sort_within_sections = true 51 | include_trailing_comma = true 52 | known_local_folder = ["ocspchecker"] 53 | length_sort = true 54 | line_length = 100 55 | multi_line_output = 3 56 | no_sections = false 57 | profile = "black" 58 | py_version=312 59 | reverse_relative = true 60 | reverse_sort = true 61 | skip_gitignore = true 62 | use_parentheses = true 63 | 64 | [tool.pyright] 65 | root = ["ocspchecker"] 66 | include = ["ocspchecker" , "tests"] 67 | reportMissingImports = true 68 | reportMissingTypeStubs = false 69 | pythonPlatform = "All" 70 | pythonVersion = "3.13" 71 | typeCheckingMode = "basic" 72 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gattjoe/OCSPChecker/8fb866d252b78209717227cf4e3b42cf23f11a00/tests/__init__.py -------------------------------------------------------------------------------- /tests/certs.py: -------------------------------------------------------------------------------- 1 | """ Certs used to validate ocspchecker """ 2 | 3 | github_issuer_pem = """-----BEGIN CERTIFICATE----- 4 | MIIDqDCCAy6gAwIBAgIRAPNkTmtuAFAjfglGvXvh9R0wCgYIKoZIzj0EAwMwgYgx 5 | CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtKZXJz 6 | ZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYDVQQD 7 | EyVVU0VSVHJ1c3QgRUNDIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTE4MTEw 8 | MjAwMDAwMFoXDTMwMTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAkdCMRswGQYDVQQI 9 | ExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoT 10 | D1NlY3RpZ28gTGltaXRlZDE3MDUGA1UEAxMuU2VjdGlnbyBFQ0MgRG9tYWluIFZh 11 | bGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEH 12 | A0IABHkYk8qfbZ5sVwAjBTcLXw9YWsTef1Wj6R7W2SUKiKAgSh16TwUwimNJE4xk 13 | IQeV/To14UrOkPAY9z2vaKb71EijggFuMIIBajAfBgNVHSMEGDAWgBQ64QmG1M8Z 14 | wpZ2dEl23OA1xmNjmjAdBgNVHQ4EFgQU9oUKOxGG4QR9DqoLLNLuzGR7e64wDgYD 15 | VR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYB 16 | BQUHAwEGCCsGAQUFBwMCMBsGA1UdIAQUMBIwBgYEVR0gADAIBgZngQwBAgEwUAYD 17 | VR0fBEkwRzBFoEOgQYY/aHR0cDovL2NybC51c2VydHJ1c3QuY29tL1VTRVJUcnVz 18 | dEVDQ0NlcnRpZmljYXRpb25BdXRob3JpdHkuY3JsMHYGCCsGAQUFBwEBBGowaDA/ 19 | BggrBgEFBQcwAoYzaHR0cDovL2NydC51c2VydHJ1c3QuY29tL1VTRVJUcnVzdEVD 20 | Q0FkZFRydXN0Q0EuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8vb2NzcC51c2VydHJ1 21 | c3QuY29tMAoGCCqGSM49BAMDA2gAMGUCMEvnx3FcsVwJbZpCYF9z6fDWJtS1UVRs 22 | cS0chWBNKPFNpvDKdrdKRe+oAkr2jU+ubgIxAODheSr2XhcA7oz9HmedGdMhlrd9 23 | 4ToKFbZl+/OnFFzqnvOhcjHvClECEQcKmc8fmA== 24 | -----END CERTIFICATE----- 25 | """ 26 | 27 | bad_ssl_fallback = """-----BEGIN CERTIFICATE----- 28 | MIIE8DCCAtigAwIBAgIJAM28Wkrsl2exMA0GCSqGSIb3DQEBCwUAMH8xCzAJBgNV 29 | BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp 30 | c2NvMQ8wDQYDVQQKDAZCYWRTU0wxMjAwBgNVBAMMKUJhZFNTTCBJbnRlcm1lZGlh 31 | dGUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MB4XDTE2MDgwODIxMTcwNVoXDTE4MDgw 32 | ODIxMTcwNVowgagxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYw 33 | FAYDVQQHDA1TYW4gRnJhbmNpc2NvMTYwNAYDVQQKDC1CYWRTU0wgRmFsbGJhY2su 34 | IFVua25vd24gc3ViZG9tYWluIG9yIG5vIFNOSS4xNDAyBgNVBAMMK2JhZHNzbC1m 35 | YWxsYmFjay11bmtub3duLXN1YmRvbWFpbi1vci1uby1zbmkwggEiMA0GCSqGSIb3 36 | DQEBAQUAA4IBDwAwggEKAoIBAQDCBOz4jO4EwrPYUNVwWMyTGOtcqGhJsCK1+ZWe 37 | sSssdj5swEtgTEzqsrTAD4C2sPlyyYYC+VxBXRMrf3HES7zplC5QN6ZnHGGM9kFC 38 | xUbTFocnn3TrCp0RUiYhc2yETHlV5NFr6AY9SBVSrbMo26r/bv9glUp3aznxJNEx 39 | tt1NwMT8U7ltQq21fP6u9RXSM0jnInHHwhR6bCjqN0rf6my1crR+WqIW3GmxV0Tb 40 | ChKr3sMPR3RcQSLhmvkbk+atIgYpLrG6SRwMJ56j+4v3QHIArJII2YxXhFOBBcvm 41 | /mtUmEAnhccQu3Nw72kYQQdFVXz5ZD89LMOpfOuTGkyG0cqFAgMBAAGjRTBDMAkG 42 | A1UdEwQCMAAwNgYDVR0RBC8wLYIrYmFkc3NsLWZhbGxiYWNrLXVua25vd24tc3Vi 43 | ZG9tYWluLW9yLW5vLXNuaTANBgkqhkiG9w0BAQsFAAOCAgEAsuFs0K86D2IB20nB 44 | QNb+4vs2Z6kECmVUuD0vEUBR/dovFE4PfzTr6uUwRoRdjToewx9VCwvTL7toq3dd 45 | oOwHakRjoxvq+lKvPq+0FMTlKYRjOL6Cq3wZNcsyiTYr7odyKbZs383rEBbcNu0N 46 | c666/ozs4y4W7ufeMFrKak9UenrrPlUe0nrEHV3IMSF32iV85nXm95f7aLFvM6Lm 47 | EzAGgWopuRqD+J0QEt3WNODWqBSZ9EYyx9l2l+KI1QcMalG20QXuxDNHmTEzMaCj 48 | 4Zl8k0szexR8rbcQEgJ9J+izxsecLRVp70siGEYDkhq0DgIDOjmmu8ath4yznX6A 49 | pYEGtYTDUxIvsWxwkraBBJAfVxkp2OSg7DiZEVlMM8QxbSeLCz+63kE/d5iJfqde 50 | cGqX7rKEsVW4VLfHPF8sfCyXVi5sWrXrDvJm3zx2b3XToU7EbNONO1C85NsUOWy4 51 | JccoiguV8V6C723IgzkSgJMlpblJ6FVxC6ZX5XJ0ZsMI9TIjibM2L1Z9DkWRCT6D 52 | QjuKbYUeURhScofQBiIx73V7VXnFoc1qHAUd/pGhfkCUnUcuBV1SzCEhjiwjnVKx 53 | HJKvc9OYjJD0ZuvZw9gBrY7qKyBX8g+sglEGFNhruH8/OhqrV8pBXX/EWY0fUZTh 54 | iywmc6GTT7X94Ze2F7iB45jh7WQ= 55 | -----END CERTIFICATE----- 56 | """ 57 | 58 | github_ocsp_data = b"0R0P0N0L0J0\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\x11\x00\xabf\x86\xb5b{\xe8\x05\x96\x82\x130\x12\x86I\xf5" 59 | 60 | 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" 61 | 62 | # List of certificate authorities to test 63 | cert_authorities = [ 64 | "identrust.com", 65 | "digicert.com", 66 | "sectigo.com", 67 | "www.godaddy.com", 68 | "globalsign.com", 69 | "certum.pl", 70 | "actalis.it", 71 | "entrustdatacard.com", 72 | "secom.co.jp", 73 | "letsencrypt.org", 74 | "microsoft.com", 75 | "trustwave.com", 76 | "wisekey.com", 77 | "networksolutions.com", 78 | "amazontrust.com", 79 | "lencr.org", 80 | "comodoca.com", 81 | "www.starfieldtech.com", 82 | ] 83 | -------------------------------------------------------------------------------- /tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | filterwarnings = 3 | ignore:PY_SSIZE_T_CLEAN:DeprecationWarning 4 | -------------------------------------------------------------------------------- /tests/test_ocspchecker.py: -------------------------------------------------------------------------------- 1 | """ Tests """ 2 | 3 | import pytest 4 | 5 | from ocspchecker.ocspchecker import ( 6 | build_ocsp_request, 7 | extract_ocsp_result, 8 | extract_ocsp_url, 9 | get_certificate_chain, 10 | get_ocsp_response, 11 | get_ocsp_status, 12 | ) 13 | 14 | from . import certs 15 | 16 | 17 | def test_get_cert_chain_bad_host(): 18 | """Pass bad host to get_certificate_chain exception""" 19 | 20 | func_name: str = "get_certificate_chain" 21 | 22 | host = "nonexistenthost.com" 23 | port = 443 24 | 25 | with pytest.raises(Exception) as excinfo: 26 | get_certificate_chain(host, port) 27 | 28 | assert str(excinfo.value) == f"{func_name}: {host}:{port} is invalid or not known." 29 | 30 | 31 | def test_get_cert_chain_host_timeout(): 32 | """Pass bad port to get_certificate_chain to force the 33 | connection to time out""" 34 | 35 | func_name: str = "get_certificate_chain" 36 | 37 | host = "espn.com" 38 | port = 65534 39 | 40 | with pytest.raises(Exception) as excinfo: 41 | get_certificate_chain(host, port) 42 | 43 | assert str(excinfo.value) == f"{func_name}: Connection to {host}:{port} timed out." 44 | 45 | 46 | def test_get_cert_chain_success(): 47 | """Validate the issuer for microsoft.com with ms_pem""" 48 | 49 | host = "github.com" 50 | port = 443 51 | 52 | github = get_certificate_chain(host, port) 53 | 54 | assert github[1] == certs.github_issuer_pem 55 | 56 | 57 | def test_get_cert_chain_bad_port(): 58 | """Validate the issuer for microsoft.com with ms_pem""" 59 | 60 | host = "github.com" 61 | port = 80000 62 | 63 | func_name: str = "get_certificate_chain" 64 | 65 | with pytest.raises(Exception) as excinfo: 66 | get_certificate_chain(host, port) 67 | 68 | assert str(excinfo.value) == f"{func_name}: Illegal port: {port}. Port must be between 0-65535." 69 | 70 | 71 | def test_invalid_certificate(): 72 | """edellroot.badssl.com is invalid""" 73 | 74 | func_name: str = "get_certificate_chain" 75 | 76 | host = "edellroot.badssl.com" 77 | error = f"{func_name}: Certificate Verification failed for {host}." 78 | 79 | with pytest.raises(Exception) as excinfo: 80 | get_certificate_chain(host, 443) 81 | 82 | assert str(excinfo.value) == error 83 | 84 | 85 | def test_extract_ocsp_url_success(): 86 | """test a successful extract_ocsp_url function invocation""" 87 | 88 | host = "github.com" 89 | cert_chain = get_certificate_chain(host) 90 | ocsp_url = extract_ocsp_url(cert_chain) 91 | 92 | assert ocsp_url == "http://ocsp.sectigo.com" 93 | 94 | 95 | def test_build_ocsp_request_success(): 96 | """test a successful build_ocsp_request function invocation""" 97 | 98 | host = "github.com" 99 | cert_chain = get_certificate_chain(host) 100 | ocsp_request_data = build_ocsp_request(cert_chain) 101 | 102 | assert ocsp_request_data == certs.github_ocsp_data 103 | 104 | 105 | def test_build_ocsp_request_failure(): 106 | """test an unsuccessful build_ocsp_request function invocation""" 107 | 108 | cert_chain = ["blah", "blah"] 109 | 110 | func_name: str = "build_ocsp_request" 111 | 112 | with pytest.raises(Exception) as excinfo: 113 | build_ocsp_request(cert_chain) 114 | 115 | assert str(excinfo.value) == f"{func_name}: Unable to load x509 certificate." 116 | 117 | 118 | def test_get_ocsp_response_bad_url_format(): 119 | """test an unsuccessful get_ocsp_response function invocation 120 | with a bad url format""" 121 | 122 | func_name: str = "get_ocsp_response" 123 | 124 | ocsp_url = "badurl" 125 | ocsp_request_data = b"dummydata" 126 | 127 | with pytest.raises(Exception) as excinfo: 128 | get_ocsp_response(ocsp_url, ocsp_request_data) 129 | 130 | assert str(excinfo.value) == ( 131 | f"{func_name}: Connection Error to {ocsp_url}. unknown url type: {ocsp_url!r}" 132 | ) 133 | 134 | 135 | def test_get_ocsp_response_connection_error(): 136 | """test an unsuccessful get_ocsp_response function invocation 137 | with a bad url input""" 138 | 139 | func_name: str = "get_ocsp_response" 140 | 141 | ocsp_url = "http://blahhhhhhhh.com" 142 | ocsp_request_data = b"dummydata" 143 | 144 | with pytest.raises(Exception) as excinfo: 145 | get_ocsp_response(ocsp_url, ocsp_request_data) 146 | 147 | assert str(excinfo.value) == f"{func_name}: {ocsp_url} is invalid or not known." 148 | 149 | 150 | def test_get_ocsp_response_timeout(): 151 | """test an unsuccessful get_ocsp_response function invocation 152 | with timeout""" 153 | 154 | func_name: str = "get_ocsp_response" 155 | 156 | ocsp_url = "http://blah.com:65534" 157 | ocsp_request_data = b"dummydata" 158 | 159 | with pytest.raises(Exception) as excinfo: 160 | get_ocsp_response(ocsp_url, ocsp_request_data) 161 | 162 | assert str(excinfo.value) == f"{func_name}: Request timeout for {ocsp_url}" 163 | 164 | 165 | def test_extract_ocsp_result_unauthorized(): 166 | """test an unsuccessful extract_ocsp_result function invocation""" 167 | 168 | func_name: str = "extract_ocsp_result" 169 | 170 | ocsp_response = get_ocsp_response("http://ocsp.digicert.com", certs.unauthorized_ocsp_data) 171 | 172 | with pytest.raises(Exception) as excinfo: 173 | extract_ocsp_result(ocsp_response) 174 | 175 | assert str(excinfo.value) == f"{func_name}: OCSP Request Error: UNAUTHORIZED" 176 | 177 | 178 | def test_extract_ocsp_result_success(): 179 | """test an unsuccessful extract_ocsp_result function invocation""" 180 | 181 | cert_chain = get_certificate_chain("github.com", 443) 182 | ocsp_url = extract_ocsp_url(cert_chain) 183 | ocsp_request = build_ocsp_request(cert_chain) 184 | ocsp_response = get_ocsp_response(ocsp_url, ocsp_request) 185 | 186 | ocsp_result = extract_ocsp_result(ocsp_response) 187 | 188 | assert ocsp_result == "OCSP Status: GOOD" 189 | 190 | 191 | def test_end_to_end_success_test(): 192 | """test the full function end to end""" 193 | 194 | ocsp_result = get_ocsp_status("github.com", 443) 195 | 196 | assert ocsp_result == [ 197 | "Host: github.com:443", 198 | "OCSP URL: http://ocsp.sectigo.com", 199 | "OCSP Status: GOOD", 200 | ] 201 | 202 | 203 | def test_end_to_end_test_bad_host(): 204 | """test the full function end to end""" 205 | 206 | func_name: str = "get_certificate_chain" 207 | 208 | host = "nonexistenthost.com" 209 | ocsp_request = get_ocsp_status(host, 443) 210 | 211 | assert ocsp_request == [ 212 | "Host: nonexistenthost.com:443", 213 | f"Error: {func_name}: nonexistenthost.com:443 is invalid or not known.", 214 | ] 215 | 216 | 217 | def test_end_to_end_test_bad_fqdn(): 218 | """test the full function end to end""" 219 | 220 | host = "nonexistentdomain" 221 | ocsp_request = get_ocsp_status(host, 443) 222 | 223 | assert ocsp_request == [ 224 | "Host: nonexistentdomain:443", 225 | f"Error: get_certificate_chain: {host}:443 is invalid or not known.", 226 | ] 227 | 228 | 229 | def test_end_to_end_test_host_timeout(): 230 | """test the full function end to end""" 231 | 232 | func_name: str = "get_certificate_chain" 233 | 234 | host = "espn.com" 235 | ocsp_request = get_ocsp_status(host, 65534) 236 | 237 | assert ocsp_request == [ 238 | "Host: espn.com:65534", 239 | f"Error: {func_name}: Connection to espn.com:65534 timed out.", 240 | ] 241 | 242 | 243 | def test_bad_port_overflow(): 244 | """Validate passing a bad port results in failure""" 245 | 246 | host = "espn.com" 247 | ocsp_request = get_ocsp_status(host, 80000) 248 | 249 | assert ocsp_request == [ 250 | "Host: espn.com:80000", 251 | "Error: get_certificate_chain: Illegal port: 80000. Port must be between 0-65535.", 252 | ] 253 | 254 | 255 | def test_bad_port_typeerror(): 256 | """Validate passing a bad port results in failure""" 257 | 258 | host = "espn.com" 259 | ocsp_request = get_ocsp_status(host, "a") # type: ignore 260 | 261 | assert ocsp_request == [ 262 | "Host: espn.com:a", 263 | "Error: get_certificate_chain: Illegal port: a. Port must be between 0-65535.", 264 | ] 265 | 266 | 267 | def test_no_port_supplied(): 268 | """Validate that when no port is supplied, the default of 443 is used""" 269 | 270 | host = "github.com" 271 | ocsp_request = get_ocsp_status(host) 272 | 273 | assert ocsp_request == [ 274 | "Host: github.com:443", 275 | "OCSP URL: http://ocsp.sectigo.com", 276 | "OCSP Status: GOOD", 277 | ] 278 | 279 | 280 | def test_strip_http_from_host(): 281 | """Validate stripping http from host""" 282 | 283 | host = "http://github.com" 284 | ocsp_request = get_ocsp_status(host, 443) 285 | 286 | assert ocsp_request == [ 287 | "Host: http://github.com:443", 288 | "OCSP URL: http://ocsp.sectigo.com", 289 | "OCSP Status: GOOD", 290 | ] 291 | 292 | 293 | def test_strip_https_from_host(): 294 | """Validate stripping https from host""" 295 | 296 | host = "https://github.com" 297 | ocsp_request = get_ocsp_status(host, 443) 298 | 299 | assert ocsp_request == [ 300 | "Host: https://github.com:443", 301 | "OCSP URL: http://ocsp.sectigo.com", 302 | "OCSP Status: GOOD", 303 | ] 304 | 305 | 306 | def test_tls_fatal_alert_112(): 307 | """Validate Unrecognized server name provided""" 308 | 309 | host = "nginx.net" 310 | func_name: str = "get_certificate_chain" 311 | 312 | with pytest.raises(Exception) as excinfo: 313 | get_certificate_chain(host, 443) 314 | 315 | assert ( 316 | str(excinfo.value) 317 | == f"{func_name}: Unrecognized server name provided. Check your target and try again." 318 | ) 319 | -------------------------------------------------------------------------------- /tests/test_responders.py: -------------------------------------------------------------------------------- 1 | ''' These global responders are sometimes flaky when testing from GitHub. 2 | To make CI tests in GitHub more reliable, I am making these tests optional. ''' 3 | 4 | import pytest 5 | 6 | from ocspchecker.ocspchecker import ( 7 | build_ocsp_request, 8 | extract_ocsp_result, 9 | extract_ocsp_url, 10 | get_certificate_chain, 11 | get_ocsp_response, 12 | get_ocsp_status, 13 | ) 14 | 15 | from . import certs 16 | 17 | @pytest.mark.parametrize("root_ca", certs.cert_authorities) 18 | def test_a_cert_from_each_root_ca(root_ca): 19 | """Test a cert from each root CA to ensure test coverage""" 20 | 21 | try: 22 | ocsp_request = get_ocsp_status(root_ca, 443) 23 | 24 | except Exception as err: 25 | raise err 26 | 27 | assert ocsp_request[2] == "OCSP Status: GOOD" 28 | 29 | --------------------------------------------------------------------------------