├── .circleci └── config.yml ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── changelog.md ├── dev ├── __init__.py ├── _import.py ├── _pep425.py ├── _task.py ├── api_docs.py ├── build.py ├── ci-cleanup.py ├── ci-driver.py ├── ci.py ├── codecov.json ├── coverage.py ├── deps.py ├── lint.py ├── pyenv-install.py ├── python-install.py ├── release.py ├── tests.py └── version.py ├── docs ├── api.md └── readme.md ├── ocspbuilder ├── __init__.py └── version.py ├── readme.md ├── requires ├── api_docs ├── ci ├── coverage ├── lint └── release ├── run.py ├── scripts └── create_pair.py ├── setup.py ├── tests ├── __init__.py ├── _unittest_compat.py ├── fixtures │ ├── test-inter.crt │ ├── test-ocsp.crt │ ├── test-ocsp.key │ ├── test-third.crt │ ├── test-third.key │ ├── test.crt │ └── test.key ├── test_ocsp_request_builder.py └── test_ocsp_response_builder.py └── tox.ini /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | python2.7: 4 | machine: 5 | image: ubuntu-2004:202101-01 6 | resource_class: arm.medium 7 | steps: 8 | - checkout 9 | - run: python run.py deps 10 | - run: python run.py ci-driver 11 | python3.9: 12 | machine: 13 | image: ubuntu-2004:202101-01 14 | resource_class: arm.medium 15 | steps: 16 | - checkout 17 | - run: python run.py deps 18 | - run: python3 run.py ci-driver 19 | workflows: 20 | version: 2 21 | arm64: 22 | jobs: 23 | - python2.7 24 | - python3.9 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build-windows: 6 | name: Python ${{ matrix.python }} on windows-2019 ${{ matrix.arch }} 7 | runs-on: windows-2019 8 | strategy: 9 | matrix: 10 | python: 11 | - '3.11' 12 | # - 'pypy-3.7-v7.3.5' 13 | arch: 14 | - 'x86' 15 | - 'x64' 16 | exclude: 17 | - python: 'pypy-3.7-v7.3.5' 18 | arch: x86 19 | steps: 20 | - uses: actions/checkout@master 21 | - uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python }} 24 | architecture: ${{ matrix.arch }} 25 | - name: Install dependencies 26 | run: python run.py deps 27 | - name: Run test suite 28 | run: python run.py ci-driver 29 | - name: Run test suite (Windows legacy API) 30 | run: python run.py ci-driver winlegacy 31 | 32 | build-windows-old: 33 | name: Python ${{ matrix.python }} on windows-2019 ${{ matrix.arch }} 34 | runs-on: windows-2019 35 | strategy: 36 | matrix: 37 | python: 38 | - '2.6' 39 | - '2.7' 40 | - '3.3' 41 | arch: 42 | - 'x86' 43 | - 'x64' 44 | steps: 45 | - uses: actions/checkout@master 46 | 47 | - name: Cache Python 48 | id: cache-python 49 | uses: actions/cache@v2 50 | with: 51 | path: ~/AppData/Local/Python${{ matrix.python }}-${{ matrix.arch }} 52 | key: windows-2019-python-${{ matrix.python }}-${{ matrix.arch }} 53 | 54 | - name: Install Python ${{ matrix.python }} 55 | run: python run.py python-install ${{ matrix.python }} ${{ matrix.arch }} | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append 56 | 57 | - name: Install dependencies 58 | run: python run.py deps 59 | - name: Run test suite 60 | run: python run.py ci-driver 61 | - name: Run test suite (Windows legacy API) 62 | run: python run.py ci-driver winlegacy 63 | 64 | build-mac: 65 | name: Python ${{ matrix.python }} on macos-13 66 | runs-on: macos-13 67 | strategy: 68 | matrix: 69 | python: 70 | - '3.11' 71 | steps: 72 | - uses: actions/checkout@master 73 | - uses: actions/setup-python@v2 74 | with: 75 | python-version: ${{ matrix.python }} 76 | architecture: x64 77 | - name: Install dependencies 78 | run: python run.py deps 79 | - name: Run test suite 80 | run: python run.py ci-driver 81 | - name: Run test suite (Mac cffi) 82 | run: python run.py ci-driver cffi 83 | - name: Run test suite (Mac OpenSSL) 84 | run: python run.py ci-driver openssl 85 | - name: Run test suite (Mac OpenSSL/cffi) 86 | run: python run.py ci-driver cffi openssl 87 | 88 | build-mac-legacy: 89 | name: Python ${{ matrix.python }} on macos-11 90 | runs-on: macos-11 91 | strategy: 92 | matrix: 93 | python: 94 | - '3.7' 95 | - '3.11' 96 | steps: 97 | - uses: actions/checkout@master 98 | - uses: actions/setup-python@v2 99 | with: 100 | python-version: ${{ matrix.python }} 101 | architecture: x64 102 | - name: Install dependencies 103 | run: python run.py deps 104 | - name: Run test suite 105 | run: python run.py ci-driver 106 | - name: Run test suite (Mac cffi) 107 | run: python run.py ci-driver cffi 108 | - name: Run test suite (Mac OpenSSL) 109 | run: python run.py ci-driver openssl 110 | - name: Run test suite (Mac OpenSSL/cffi) 111 | run: python run.py ci-driver cffi openssl 112 | 113 | build-mac-old: 114 | name: Python ${{ matrix.python }} on macos-11 115 | runs-on: macos-11 116 | strategy: 117 | matrix: 118 | python: 119 | - '2.6' 120 | - '2.7' 121 | - '3.3' 122 | env: 123 | PYTHONIOENCODING: 'utf-8:surrogateescape' 124 | steps: 125 | - uses: actions/checkout@master 126 | 127 | - name: Check pyenv 128 | id: check-pyenv 129 | uses: actions/cache@v2 130 | with: 131 | path: ~/.pyenv 132 | key: macos-11-${{ matrix.python }}-pyenv 133 | 134 | - name: Install Python ${{ matrix.python }} 135 | run: python run.py pyenv-install ${{ matrix.python }} >> $GITHUB_PATH 136 | 137 | - name: Install dependencies 138 | run: python run.py deps 139 | - name: Run test suite 140 | run: python run.py ci-driver 141 | - name: Run test suite (Mac cffi) 142 | run: python run.py ci-driver cffi 143 | - name: Run test suite (Mac OpenSSL) 144 | run: python run.py ci-driver openssl 145 | - name: Run test suite (Mac OpenSSL/cffi) 146 | run: python run.py ci-driver cffi openssl 147 | 148 | build-mac-openssl3: 149 | name: Python ${{ matrix.python }} on macos-11 with OpenSSL 3.0 150 | runs-on: macos-11 151 | strategy: 152 | matrix: 153 | python: 154 | - '3.6' 155 | - '3.11' 156 | steps: 157 | - uses: actions/checkout@master 158 | - uses: actions/setup-python@v2 159 | with: 160 | python-version: ${{ matrix.python }} 161 | architecture: x64 162 | - name: Install OpenSSL 3.0 163 | run: brew install openssl@3 164 | - name: Install dependencies 165 | run: python run.py deps 166 | - name: Run test suite (Mac OpenSSL 3.0) 167 | run: python run.py ci-driver openssl3 168 | - name: Run test suite (Mac OpenSSL 3.0/cffi) 169 | run: python run.py ci-driver cffi openssl3 170 | 171 | build-ubuntu: 172 | name: Python ${{ matrix.python }} on ubuntu-20.04 x64 173 | runs-on: ubuntu-20.04 174 | strategy: 175 | matrix: 176 | python: 177 | - '3.9' 178 | - '3.10' 179 | - '3.11' 180 | - 'pypy-3.7-v7.3.5' 181 | steps: 182 | - uses: actions/checkout@master 183 | - uses: actions/setup-python@v2 184 | with: 185 | python-version: ${{ matrix.python }} 186 | architecture: x64 187 | - name: Install dependencies 188 | run: python run.py deps 189 | - name: Run test suite 190 | run: python run.py ci-driver 191 | 192 | build-ubuntu-openssl3-py3: 193 | name: Python 3 on (Docker) ubuntu-22.04 x64 194 | runs-on: ubuntu-latest 195 | container: ubuntu:22.04 196 | steps: 197 | - uses: actions/checkout@master 198 | - name: Install Python and OpenSSL 199 | run: DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y --no-install-recommends python3 python3-setuptools python-is-python3 openssl curl ca-certificates git 200 | - name: Install dependencies 201 | run: python run.py deps 202 | - name: Run test suite 203 | run: python run.py ci-driver 204 | - name: Run test suite (cffi) 205 | run: python run.py ci-driver cffi 206 | 207 | build-ubuntu-openssl3-py2: 208 | name: Python 2 on (Docker) ubuntu-22.04 x64 209 | runs-on: ubuntu-latest 210 | container: ubuntu:22.04 211 | steps: 212 | - uses: actions/checkout@master 213 | - name: Install Python and OpenSSL 214 | run: DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y --no-install-recommends python2 python-setuptools openssl curl ca-certificates git 215 | - name: Install dependencies 216 | run: python2 run.py deps 217 | - name: Run test suite 218 | run: python2 run.py ci-driver 219 | - name: Run test suite (cffi) 220 | run: python2 run.py ci-driver cffi 221 | 222 | 223 | build-ubuntu-old: 224 | name: Python ${{ matrix.python }} on ubuntu-20.04 x64 225 | runs-on: ubuntu-20.04 226 | strategy: 227 | matrix: 228 | python: 229 | - '3.6' 230 | - '3.7' 231 | steps: 232 | - uses: actions/checkout@master 233 | - name: Setup deadsnakes/ppa 234 | run: sudo apt-add-repository ppa:deadsnakes/ppa 235 | - name: Update apt 236 | run: sudo apt-get update 237 | - name: Install Python ${{matrix.python}} 238 | run: sudo apt-get install python${{matrix.python}} python${{matrix.python}}-distutils 239 | - name: Install dependencies 240 | run: python${{matrix.python}} run.py deps 241 | - name: Run test suite 242 | run: python${{matrix.python}} run.py ci-driver 243 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | .cache/ 3 | .tox/ 4 | __pycache__/ 5 | build/ 6 | dist/ 7 | tests/output/ 8 | *.pyc 9 | *.swp 10 | .coverage 11 | .DS_Store 12 | .python-version 13 | coverage.xml 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2018 Will Bond 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # changelog 2 | 3 | ## 0.10.2 4 | 5 | - Updated [asn1crypto](https://github.com/wbond/asn1crypto) dependency to 6 | `0.18.1`, [oscrypto](https://github.com/wbond/oscrypto) dependency to 7 | `0.16.1`. 8 | 9 | ## 0.10.1 10 | 11 | - `OCSPResponseBuilder()` no longer requires the `certificate` and 12 | `certificate_status` parameters when the `response_status` is not 13 | `"successful"` 14 | 15 | ## 0.10.0 16 | 17 | - Added the options `unknown` and `revoked` to the `certificate_status` 18 | parameter of `OCSPResponseBuilder()` 19 | 20 | ## 0.9.1 21 | 22 | - Package metadata updates 23 | 24 | ## 0.9.0 25 | 26 | - Initial release 27 | -------------------------------------------------------------------------------- /dev/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals, division, absolute_import, print_function 3 | 4 | import os 5 | 6 | 7 | package_name = "ocspbuilder" 8 | 9 | other_packages = [] 10 | 11 | task_keyword_args = [] 12 | 13 | requires_oscrypto = True 14 | has_tests_package = False 15 | 16 | package_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 17 | build_root = os.path.abspath(os.path.join(package_root, '..')) 18 | 19 | md_source_map = { 20 | 'docs/api.md': ['ocspbuilder/__init__.py'], 21 | } 22 | 23 | definition_replacements = {} 24 | -------------------------------------------------------------------------------- /dev/_import.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals, division, absolute_import, print_function 3 | 4 | import sys 5 | import os 6 | 7 | from . import build_root, package_name, package_root 8 | 9 | if sys.version_info < (3, 5): 10 | import imp 11 | else: 12 | import importlib 13 | import importlib.abc 14 | import importlib.util 15 | 16 | 17 | if sys.version_info < (3,): 18 | getcwd = os.getcwdu 19 | else: 20 | getcwd = os.getcwd 21 | 22 | 23 | if sys.version_info >= (3, 5): 24 | class ModCryptoMetaFinder(importlib.abc.MetaPathFinder): 25 | def setup(self): 26 | self.modules = {} 27 | sys.meta_path.insert(0, self) 28 | 29 | def add_module(self, package_name, package_path): 30 | if package_name not in self.modules: 31 | self.modules[package_name] = package_path 32 | 33 | def find_spec(self, fullname, path, target=None): 34 | name_parts = fullname.split('.') 35 | if name_parts[0] not in self.modules: 36 | return None 37 | 38 | package = name_parts[0] 39 | package_path = self.modules[package] 40 | 41 | fullpath = os.path.join(package_path, *name_parts[1:]) 42 | 43 | if os.path.isdir(fullpath): 44 | filename = os.path.join(fullpath, "__init__.py") 45 | submodule_locations = [fullpath] 46 | else: 47 | filename = fullpath + ".py" 48 | submodule_locations = None 49 | 50 | if not os.path.exists(filename): 51 | return None 52 | 53 | return importlib.util.spec_from_file_location( 54 | fullname, 55 | filename, 56 | loader=None, 57 | submodule_search_locations=submodule_locations 58 | ) 59 | 60 | CUSTOM_FINDER = ModCryptoMetaFinder() 61 | CUSTOM_FINDER.setup() 62 | 63 | 64 | def _import_from(mod, path, mod_dir=None, allow_error=False): 65 | """ 66 | Imports a module from a specific path 67 | 68 | :param mod: 69 | A unicode string of the module name 70 | 71 | :param path: 72 | A unicode string to the directory containing the module 73 | 74 | :param mod_dir: 75 | If the sub directory of "path" is different than the "mod" name, 76 | pass the sub directory as a unicode string 77 | 78 | :param allow_error: 79 | If an ImportError should be raised when the module can't be imported 80 | 81 | :return: 82 | None if not loaded, otherwise the module 83 | """ 84 | 85 | if mod in sys.modules: 86 | return sys.modules[mod] 87 | 88 | if mod_dir is None: 89 | full_mod = mod 90 | else: 91 | full_mod = mod_dir.replace(os.sep, '.') 92 | 93 | if mod_dir is None: 94 | mod_dir = mod.replace('.', os.sep) 95 | 96 | if not os.path.exists(path): 97 | return None 98 | 99 | source_path = os.path.join(path, mod_dir, '__init__.py') 100 | if not os.path.exists(source_path): 101 | source_path = os.path.join(path, mod_dir + '.py') 102 | 103 | if not os.path.exists(source_path): 104 | return None 105 | 106 | if os.sep in mod_dir: 107 | append, mod_dir = mod_dir.rsplit(os.sep, 1) 108 | path = os.path.join(path, append) 109 | 110 | try: 111 | if sys.version_info < (3, 5): 112 | mod_info = imp.find_module(mod_dir, [path]) 113 | return imp.load_module(mod, *mod_info) 114 | 115 | else: 116 | package = mod.split('.', 1)[0] 117 | package_dir = full_mod.split('.', 1)[0] 118 | package_path = os.path.join(path, package_dir) 119 | CUSTOM_FINDER.add_module(package, package_path) 120 | 121 | return importlib.import_module(mod) 122 | 123 | except ImportError: 124 | if allow_error: 125 | raise 126 | return None 127 | 128 | 129 | def _preload(require_oscrypto, print_info): 130 | """ 131 | Preloads asn1crypto and optionally oscrypto from a local source checkout, 132 | or from a normal install 133 | 134 | :param require_oscrypto: 135 | A bool if oscrypto needs to be preloaded 136 | 137 | :param print_info: 138 | A bool if info about asn1crypto and oscrypto should be printed 139 | """ 140 | 141 | if print_info: 142 | print('Working dir: ' + getcwd()) 143 | print('Python ' + sys.version.replace('\n', '')) 144 | 145 | asn1crypto = None 146 | oscrypto = None 147 | 148 | if require_oscrypto: 149 | # Some CI services don't use the package name for the dir 150 | if package_name == 'oscrypto': 151 | oscrypto_dir = package_root 152 | else: 153 | oscrypto_dir = os.path.join(build_root, 'oscrypto') 154 | oscrypto_tests = None 155 | if os.path.exists(oscrypto_dir): 156 | oscrypto_tests = _import_from('oscrypto_tests', oscrypto_dir, 'tests') 157 | if oscrypto_tests is None: 158 | import oscrypto_tests 159 | asn1crypto, oscrypto = oscrypto_tests.local_oscrypto() 160 | 161 | else: 162 | if package_name == 'asn1crypto': 163 | asn1crypto_dir = package_root 164 | else: 165 | asn1crypto_dir = os.path.join(build_root, 'asn1crypto') 166 | if os.path.exists(asn1crypto_dir): 167 | asn1crypto = _import_from('asn1crypto', asn1crypto_dir) 168 | if asn1crypto is None: 169 | import asn1crypto 170 | 171 | if print_info: 172 | print( 173 | '\nasn1crypto: %s, %s' % ( 174 | asn1crypto.__version__, 175 | os.path.dirname(asn1crypto.__file__) 176 | ) 177 | ) 178 | if require_oscrypto: 179 | backend = oscrypto.backend() 180 | if backend == 'openssl': 181 | from oscrypto._openssl._libcrypto import libcrypto_version 182 | backend = '%s (%s)' % (backend, libcrypto_version) 183 | 184 | print( 185 | 'oscrypto: %s, %s backend, %s' % ( 186 | oscrypto.__version__, 187 | backend, 188 | os.path.dirname(oscrypto.__file__) 189 | ) 190 | ) 191 | -------------------------------------------------------------------------------- /dev/_pep425.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | This file was originally derived from 5 | https://github.com/pypa/pip/blob/3e713708088aedb1cde32f3c94333d6e29aaf86e/src/pip/_internal/pep425tags.py 6 | 7 | The following license covers that code: 8 | 9 | Copyright (c) 2008-2018 The pip developers (see AUTHORS.txt file) 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining 12 | a copy of this software and associated documentation files (the 13 | "Software"), to deal in the Software without restriction, including 14 | without limitation the rights to use, copy, modify, merge, publish, 15 | distribute, sublicense, and/or sell copies of the Software, and to 16 | permit persons to whom the Software is furnished to do so, subject to 17 | the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be 20 | included in all copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 23 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 24 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 25 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 26 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 27 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 28 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 29 | """ 30 | 31 | from __future__ import unicode_literals, division, absolute_import, print_function 32 | 33 | import sys 34 | import os 35 | import ctypes 36 | import re 37 | import platform 38 | 39 | if sys.version_info >= (2, 7): 40 | import sysconfig 41 | 42 | if sys.version_info < (3,): 43 | str_cls = unicode # noqa 44 | else: 45 | str_cls = str 46 | 47 | 48 | def _pep425_implementation(): 49 | """ 50 | :return: 51 | A 2 character unicode string of the implementation - 'cp' for cpython 52 | or 'pp' for PyPy 53 | """ 54 | 55 | return 'pp' if hasattr(sys, 'pypy_version_info') else 'cp' 56 | 57 | 58 | def _pep425_version(): 59 | """ 60 | :return: 61 | A tuple of integers representing the Python version number 62 | """ 63 | 64 | if hasattr(sys, 'pypy_version_info'): 65 | return (sys.version_info[0], sys.pypy_version_info.major, 66 | sys.pypy_version_info.minor) 67 | else: 68 | return (sys.version_info[0], sys.version_info[1]) 69 | 70 | 71 | def _pep425_supports_manylinux(): 72 | """ 73 | :return: 74 | A boolean indicating if the machine can use manylinux1 packages 75 | """ 76 | 77 | try: 78 | import _manylinux 79 | return bool(_manylinux.manylinux1_compatible) 80 | except (ImportError, AttributeError): 81 | pass 82 | 83 | # Check for glibc 2.5 84 | try: 85 | proc = ctypes.CDLL(None) 86 | gnu_get_libc_version = proc.gnu_get_libc_version 87 | gnu_get_libc_version.restype = ctypes.c_char_p 88 | 89 | ver = gnu_get_libc_version() 90 | if not isinstance(ver, str_cls): 91 | ver = ver.decode('ascii') 92 | match = re.match(r'(\d+)\.(\d+)', ver) 93 | return match and match.group(1) == '2' and int(match.group(2)) >= 5 94 | 95 | except (AttributeError): 96 | return False 97 | 98 | 99 | def _pep425_get_abi(): 100 | """ 101 | :return: 102 | A unicode string of the system abi. Will be something like: "cp27m", 103 | "cp33m", etc. 104 | """ 105 | 106 | try: 107 | soabi = sysconfig.get_config_var('SOABI') 108 | if soabi: 109 | if soabi.startswith('cpython-'): 110 | return 'cp%s' % soabi.split('-')[1] 111 | return soabi.replace('.', '_').replace('-', '_') 112 | except (IOError, NameError): 113 | pass 114 | 115 | impl = _pep425_implementation() 116 | suffix = '' 117 | if impl == 'cp': 118 | suffix += 'm' 119 | if sys.maxunicode == 0x10ffff and sys.version_info < (3, 3): 120 | suffix += 'u' 121 | return '%s%s%s' % (impl, ''.join(map(str_cls, _pep425_version())), suffix) 122 | 123 | 124 | def _pep425tags(): 125 | """ 126 | :return: 127 | A list of 3-element tuples with unicode strings or None: 128 | [0] implementation tag - cp33, pp27, cp26, py2, py2.py3 129 | [1] abi tag - cp26m, None 130 | [2] arch tag - linux_x86_64, macosx_10_10_x85_64, etc 131 | """ 132 | 133 | tags = [] 134 | 135 | versions = [] 136 | version_info = _pep425_version() 137 | major = version_info[:-1] 138 | for minor in range(version_info[-1], -1, -1): 139 | versions.append(''.join(map(str, major + (minor,)))) 140 | 141 | impl = _pep425_implementation() 142 | 143 | abis = [] 144 | abi = _pep425_get_abi() 145 | if abi: 146 | abis.append(abi) 147 | abi3 = _pep425_implementation() == 'cp' and sys.version_info >= (3,) 148 | if abi3: 149 | abis.append('abi3') 150 | abis.append('none') 151 | 152 | if sys.platform == 'darwin': 153 | plat_ver = platform.mac_ver() 154 | ver_parts = plat_ver[0].split('.') 155 | minor = int(ver_parts[1]) 156 | arch = plat_ver[2] 157 | if sys.maxsize == 2147483647: 158 | arch = 'i386' 159 | arches = [] 160 | while minor > 5: 161 | arches.append('macosx_10_%s_%s' % (minor, arch)) 162 | arches.append('macosx_10_%s_intel' % (minor,)) 163 | arches.append('macosx_10_%s_universal' % (minor,)) 164 | minor -= 1 165 | else: 166 | if sys.platform == 'win32': 167 | if 'amd64' in sys.version.lower(): 168 | arches = ['win_amd64'] 169 | else: 170 | arches = [sys.platform] 171 | elif hasattr(os, 'uname'): 172 | (plat, _, _, _, machine) = os.uname() 173 | plat = plat.lower().replace('/', '') 174 | machine.replace(' ', '_').replace('/', '_') 175 | if plat == 'linux' and sys.maxsize == 2147483647 and 'arm' not in machine: 176 | machine = 'i686' 177 | arch = '%s_%s' % (plat, machine) 178 | if _pep425_supports_manylinux(): 179 | arches = [arch.replace('linux', 'manylinux1'), arch] 180 | else: 181 | arches = [arch] 182 | 183 | for abi in abis: 184 | for arch in arches: 185 | tags.append(('%s%s' % (impl, versions[0]), abi, arch)) 186 | 187 | if abi3: 188 | for version in versions[1:]: 189 | for arch in arches: 190 | tags.append(('%s%s' % (impl, version), 'abi3', arch)) 191 | 192 | for arch in arches: 193 | tags.append(('py%s' % (versions[0][0]), 'none', arch)) 194 | 195 | tags.append(('%s%s' % (impl, versions[0]), 'none', 'any')) 196 | tags.append(('%s%s' % (impl, versions[0][0]), 'none', 'any')) 197 | 198 | for i, version in enumerate(versions): 199 | tags.append(('py%s' % (version,), 'none', 'any')) 200 | if i == 0: 201 | tags.append(('py%s' % (version[0]), 'none', 'any')) 202 | 203 | tags.append(('py2.py3', 'none', 'any')) 204 | 205 | return tags 206 | -------------------------------------------------------------------------------- /dev/_task.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals, division, absolute_import, print_function 3 | 4 | import ast 5 | import _ast 6 | import os 7 | import sys 8 | 9 | from . import package_root, task_keyword_args 10 | from ._import import _import_from 11 | 12 | 13 | if sys.version_info < (3,): 14 | byte_cls = str 15 | else: 16 | byte_cls = bytes 17 | 18 | 19 | def _list_tasks(): 20 | """ 21 | Fetches a list of all valid tasks that may be run, and the args they 22 | accept. Does not actually import the task module to prevent errors if a 23 | user does not have the dependencies installed for every task. 24 | 25 | :return: 26 | A list of 2-element tuples: 27 | 0: a unicode string of the task name 28 | 1: a list of dicts containing the parameter definitions 29 | """ 30 | 31 | out = [] 32 | dev_path = os.path.join(package_root, 'dev') 33 | for fname in sorted(os.listdir(dev_path)): 34 | if fname.startswith('.') or fname.startswith('_'): 35 | continue 36 | if not fname.endswith('.py'): 37 | continue 38 | name = fname[:-3] 39 | args = () 40 | 41 | full_path = os.path.join(package_root, 'dev', fname) 42 | with open(full_path, 'rb') as f: 43 | full_code = f.read() 44 | if sys.version_info >= (3,): 45 | full_code = full_code.decode('utf-8') 46 | 47 | task_node = ast.parse(full_code, filename=full_path) 48 | for node in ast.iter_child_nodes(task_node): 49 | if isinstance(node, _ast.Assign): 50 | if len(node.targets) == 1 \ 51 | and isinstance(node.targets[0], _ast.Name) \ 52 | and node.targets[0].id == 'run_args': 53 | args = ast.literal_eval(node.value) 54 | break 55 | 56 | out.append((name, args)) 57 | return out 58 | 59 | 60 | def show_usage(): 61 | """ 62 | Prints to stderr the valid options for invoking tasks 63 | """ 64 | 65 | valid_tasks = [] 66 | for task in _list_tasks(): 67 | usage = task[0] 68 | for run_arg in task[1]: 69 | usage += ' ' 70 | name = run_arg.get('name', '') 71 | if run_arg.get('required', False): 72 | usage += '{%s}' % name 73 | else: 74 | usage += '[%s]' % name 75 | valid_tasks.append(usage) 76 | 77 | out = 'Usage: run.py' 78 | for karg in task_keyword_args: 79 | out += ' [%s=%s]' % (karg['name'], karg['placeholder']) 80 | out += ' (%s)' % ' | '.join(valid_tasks) 81 | 82 | print(out, file=sys.stderr) 83 | sys.exit(1) 84 | 85 | 86 | def _get_arg(num): 87 | """ 88 | :return: 89 | A unicode string of the requested command line arg 90 | """ 91 | 92 | if len(sys.argv) < num + 1: 93 | return None 94 | arg = sys.argv[num] 95 | if isinstance(arg, byte_cls): 96 | arg = arg.decode('utf-8') 97 | return arg 98 | 99 | 100 | def run_task(): 101 | """ 102 | Parses the command line args, invoking the requested task 103 | """ 104 | 105 | arg_num = 1 106 | task = None 107 | args = [] 108 | kwargs = {} 109 | 110 | # We look for the task name, processing any global task keyword args 111 | # by setting the appropriate env var 112 | while True: 113 | val = _get_arg(arg_num) 114 | if val is None: 115 | break 116 | 117 | next_arg = False 118 | for karg in task_keyword_args: 119 | if val.startswith(karg['name'] + '='): 120 | os.environ[karg['env_var']] = val[len(karg['name']) + 1:] 121 | next_arg = True 122 | break 123 | 124 | if next_arg: 125 | arg_num += 1 126 | continue 127 | 128 | task = val 129 | break 130 | 131 | if task is None: 132 | show_usage() 133 | 134 | task_mod = _import_from('dev.%s' % task, package_root, allow_error=True) 135 | if task_mod is None: 136 | show_usage() 137 | 138 | run_args = task_mod.__dict__.get('run_args', []) 139 | max_args = arg_num + 1 + len(run_args) 140 | 141 | if len(sys.argv) > max_args: 142 | show_usage() 143 | 144 | for i, run_arg in enumerate(run_args): 145 | val = _get_arg(arg_num + 1 + i) 146 | if val is None: 147 | if run_arg.get('required', False): 148 | show_usage() 149 | break 150 | 151 | if run_arg.get('cast') == 'int' and val.isdigit(): 152 | val = int(val) 153 | 154 | kwarg = run_arg.get('kwarg') 155 | if kwarg: 156 | kwargs[kwarg] = val 157 | else: 158 | args.append(val) 159 | 160 | run = task_mod.__dict__.get('run') 161 | 162 | result = run(*args, **kwargs) 163 | sys.exit(int(not result)) 164 | -------------------------------------------------------------------------------- /dev/api_docs.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals, division, absolute_import, print_function 3 | 4 | import ast 5 | import _ast 6 | import os 7 | import re 8 | import sys 9 | import textwrap 10 | 11 | import commonmark 12 | from collections import OrderedDict 13 | 14 | from . import package_name, package_root, md_source_map, definition_replacements 15 | 16 | 17 | if hasattr(commonmark, 'DocParser'): 18 | raise EnvironmentError("commonmark must be version 0.6.0 or newer") 19 | 20 | 21 | def _get_func_info(docstring, def_lineno, code_lines, prefix): 22 | """ 23 | Extracts the function signature and description of a Python function 24 | 25 | :param docstring: 26 | A unicode string of the docstring for the function 27 | 28 | :param def_lineno: 29 | An integer line number that function was defined on 30 | 31 | :param code_lines: 32 | A list of unicode string lines from the source file the function was 33 | defined in 34 | 35 | :param prefix: 36 | A prefix to prepend to all output lines 37 | 38 | :return: 39 | A 2-element tuple: 40 | 41 | - [0] A unicode string of the function signature with a docstring of 42 | parameter info 43 | - [1] A markdown snippet of the function description 44 | """ 45 | 46 | def_index = def_lineno - 1 47 | definition = code_lines[def_index] 48 | definition = definition.rstrip() 49 | while not definition.endswith(':'): 50 | def_index += 1 51 | definition += '\n' + code_lines[def_index].rstrip() 52 | 53 | definition = textwrap.dedent(definition).rstrip(':') 54 | definition = definition.replace('\n', '\n' + prefix) 55 | 56 | description = '' 57 | found_colon = False 58 | 59 | params = '' 60 | 61 | for line in docstring.splitlines(): 62 | if line and line[0] == ':': 63 | found_colon = True 64 | if not found_colon: 65 | if description: 66 | description += '\n' 67 | description += line 68 | else: 69 | if params: 70 | params += '\n' 71 | params += line 72 | 73 | description = description.strip() 74 | description_md = '' 75 | if description: 76 | description_md = "%s%s" % (prefix, description.replace('\n', '\n' + prefix)) 77 | description_md = re.sub('\n>(\\s+)\n', '\n>\n', description_md) 78 | 79 | params = params.strip() 80 | if params: 81 | definition += (':\n%s """\n%s ' % (prefix, prefix)) 82 | definition += params.replace('\n', '\n%s ' % prefix) 83 | definition += ('\n%s """' % prefix) 84 | definition = re.sub('\n>(\\s+)\n', '\n>\n', definition) 85 | 86 | for search, replace in definition_replacements.items(): 87 | definition = definition.replace(search, replace) 88 | 89 | return (definition, description_md) 90 | 91 | 92 | def _find_sections(md_ast, sections, last, last_class, total_lines=None): 93 | """ 94 | Walks through a commonmark AST to find section headers that delineate 95 | content that should be updated by this script 96 | 97 | :param md_ast: 98 | The AST of the markdown document 99 | 100 | :param sections: 101 | A dict to store the start and end lines of a section. The key will be 102 | a two-element tuple of the section type ("class", "function", 103 | "method" or "attribute") and identifier. The values are a two-element 104 | tuple of the start and end line number in the markdown document of the 105 | section. 106 | 107 | :param last: 108 | A dict containing information about the last section header seen. 109 | Includes the keys "type_name", "identifier", "start_line". 110 | 111 | :param last_class: 112 | A unicode string of the name of the last class found - used when 113 | processing methods and attributes. 114 | 115 | :param total_lines: 116 | An integer of the total number of lines in the markdown document - 117 | used to work around a bug in the API of the Python port of commonmark 118 | """ 119 | 120 | def child_walker(node): 121 | for child, entering in node.walker(): 122 | if child == node: 123 | continue 124 | yield child, entering 125 | 126 | for child, entering in child_walker(md_ast): 127 | if child.t == 'heading': 128 | start_line = child.sourcepos[0][0] 129 | 130 | if child.level == 2: 131 | if last: 132 | sections[(last['type_name'], last['identifier'])] = (last['start_line'], start_line - 1) 133 | last.clear() 134 | 135 | if child.level in set([3, 5]): 136 | heading_elements = [] 137 | for heading_child, _ in child_walker(child): 138 | heading_elements.append(heading_child) 139 | if len(heading_elements) != 2: 140 | continue 141 | first = heading_elements[0] 142 | second = heading_elements[1] 143 | if first.t != 'code': 144 | continue 145 | if second.t != 'text': 146 | continue 147 | 148 | type_name = second.literal.strip() 149 | identifier = first.literal.strip().replace('()', '').lstrip('.') 150 | 151 | if last: 152 | sections[(last['type_name'], last['identifier'])] = (last['start_line'], start_line - 1) 153 | last.clear() 154 | 155 | if type_name == 'function': 156 | if child.level != 3: 157 | continue 158 | 159 | if type_name == 'class': 160 | if child.level != 3: 161 | continue 162 | last_class.append(identifier) 163 | 164 | if type_name in set(['method', 'attribute']): 165 | if child.level != 5: 166 | continue 167 | identifier = last_class[-1] + '.' + identifier 168 | 169 | last.update({ 170 | 'type_name': type_name, 171 | 'identifier': identifier, 172 | 'start_line': start_line, 173 | }) 174 | 175 | elif child.t == 'block_quote': 176 | find_sections(child, sections, last, last_class) 177 | 178 | if last: 179 | sections[(last['type_name'], last['identifier'])] = (last['start_line'], total_lines) 180 | 181 | 182 | find_sections = _find_sections 183 | 184 | 185 | def walk_ast(node, code_lines, sections, md_chunks): 186 | """ 187 | A callback used to walk the Python AST looking for classes, functions, 188 | methods and attributes. Generates chunks of markdown markup to replace 189 | the existing content. 190 | 191 | :param node: 192 | An _ast module node object 193 | 194 | :param code_lines: 195 | A list of unicode strings - the source lines of the Python file 196 | 197 | :param sections: 198 | A dict of markdown document sections that need to be updated. The key 199 | will be a two-element tuple of the section type ("class", "function", 200 | "method" or "attribute") and identifier. The values are a two-element 201 | tuple of the start and end line number in the markdown document of the 202 | section. 203 | 204 | :param md_chunks: 205 | A dict with keys from the sections param and the values being a unicode 206 | string containing a chunk of markdown markup. 207 | """ 208 | 209 | if isinstance(node, _ast.FunctionDef): 210 | key = ('function', node.name) 211 | if key not in sections: 212 | return 213 | 214 | docstring = ast.get_docstring(node) 215 | def_lineno = node.lineno + len(node.decorator_list) 216 | 217 | definition, description_md = _get_func_info(docstring, def_lineno, code_lines, '> ') 218 | 219 | md_chunk = textwrap.dedent(""" 220 | ### `%s()` function 221 | 222 | > ```python 223 | > %s 224 | > ``` 225 | > 226 | %s 227 | """).strip() % ( 228 | node.name, 229 | definition, 230 | description_md 231 | ) + "\n" 232 | 233 | md_chunks[key] = md_chunk.replace('>\n\n', '') 234 | 235 | elif isinstance(node, _ast.ClassDef): 236 | if ('class', node.name) not in sections: 237 | return 238 | 239 | for subnode in node.body: 240 | if isinstance(subnode, _ast.FunctionDef): 241 | node_id = node.name + '.' + subnode.name 242 | 243 | method_key = ('method', node_id) 244 | is_method = method_key in sections 245 | 246 | attribute_key = ('attribute', node_id) 247 | is_attribute = attribute_key in sections 248 | 249 | is_constructor = subnode.name == '__init__' 250 | 251 | if not is_constructor and not is_attribute and not is_method: 252 | continue 253 | 254 | docstring = ast.get_docstring(subnode) 255 | def_lineno = subnode.lineno 256 | if sys.version_info < (3, 8): 257 | def_lineno += len(subnode.decorator_list) 258 | 259 | if not docstring: 260 | continue 261 | 262 | if is_method or is_constructor: 263 | definition, description_md = _get_func_info(docstring, def_lineno, code_lines, '> > ') 264 | 265 | if is_constructor: 266 | key = ('class', node.name) 267 | 268 | class_docstring = ast.get_docstring(node) or '' 269 | class_description = textwrap.dedent(class_docstring).strip() 270 | if class_description: 271 | class_description_md = "> %s\n>" % (class_description.replace("\n", "\n> ")) 272 | else: 273 | class_description_md = '' 274 | 275 | md_chunk = textwrap.dedent(""" 276 | ### `%s()` class 277 | 278 | %s 279 | > ##### constructor 280 | > 281 | > > ```python 282 | > > %s 283 | > > ``` 284 | > > 285 | %s 286 | """).strip() % ( 287 | node.name, 288 | class_description_md, 289 | definition, 290 | description_md 291 | ) 292 | 293 | md_chunk = md_chunk.replace('\n\n\n', '\n\n') 294 | 295 | else: 296 | key = method_key 297 | 298 | md_chunk = textwrap.dedent(""" 299 | > 300 | > ##### `.%s()` method 301 | > 302 | > > ```python 303 | > > %s 304 | > > ``` 305 | > > 306 | %s 307 | """).strip() % ( 308 | subnode.name, 309 | definition, 310 | description_md 311 | ) 312 | 313 | if md_chunk[-5:] == '\n> >\n': 314 | md_chunk = md_chunk[0:-5] 315 | 316 | else: 317 | key = attribute_key 318 | 319 | description = textwrap.dedent(docstring).strip() 320 | description_md = "> > %s" % (description.replace("\n", "\n> > ")) 321 | 322 | md_chunk = textwrap.dedent(""" 323 | > 324 | > ##### `.%s` attribute 325 | > 326 | %s 327 | """).strip() % ( 328 | subnode.name, 329 | description_md 330 | ) 331 | 332 | md_chunks[key] = re.sub('[ \\t]+\n', '\n', md_chunk.rstrip()) 333 | 334 | elif isinstance(node, _ast.If): 335 | for subast in node.body: 336 | walk_ast(subast, code_lines, sections, md_chunks) 337 | for subast in node.orelse: 338 | walk_ast(subast, code_lines, sections, md_chunks) 339 | 340 | 341 | def run(): 342 | """ 343 | Looks through the docs/ dir and parses each markdown document, looking for 344 | sections to update from Python docstrings. Looks for section headers in 345 | the format: 346 | 347 | - ### `ClassName()` class 348 | - ##### `.method_name()` method 349 | - ##### `.attribute_name` attribute 350 | - ### `function_name()` function 351 | 352 | The markdown content following these section headers up until the next 353 | section header will be replaced by new markdown generated from the Python 354 | docstrings of the associated source files. 355 | 356 | By default maps docs/{name}.md to {modulename}/{name}.py. Allows for 357 | custom mapping via the md_source_map variable. 358 | """ 359 | 360 | print('Updating API docs...') 361 | 362 | md_files = [] 363 | for root, _, filenames in os.walk(os.path.join(package_root, 'docs')): 364 | for filename in filenames: 365 | if not filename.endswith('.md'): 366 | continue 367 | md_files.append(os.path.join(root, filename)) 368 | 369 | parser = commonmark.Parser() 370 | 371 | for md_file in md_files: 372 | md_file_relative = md_file[len(package_root) + 1:] 373 | if md_file_relative in md_source_map: 374 | py_files = md_source_map[md_file_relative] 375 | py_paths = [os.path.join(package_root, py_file) for py_file in py_files] 376 | else: 377 | py_files = [os.path.basename(md_file).replace('.md', '.py')] 378 | py_paths = [os.path.join(package_root, package_name, py_files[0])] 379 | 380 | if not os.path.exists(py_paths[0]): 381 | continue 382 | 383 | with open(md_file, 'rb') as f: 384 | markdown = f.read().decode('utf-8') 385 | 386 | original_markdown = markdown 387 | md_lines = list(markdown.splitlines()) 388 | md_ast = parser.parse(markdown) 389 | 390 | last_class = [] 391 | last = {} 392 | sections = OrderedDict() 393 | find_sections(md_ast, sections, last, last_class, markdown.count("\n") + 1) 394 | 395 | md_chunks = {} 396 | 397 | for index, py_file in enumerate(py_files): 398 | py_path = py_paths[index] 399 | 400 | with open(os.path.join(py_path), 'rb') as f: 401 | code = f.read().decode('utf-8') 402 | module_ast = ast.parse(code, filename=py_file) 403 | code_lines = list(code.splitlines()) 404 | 405 | for node in ast.iter_child_nodes(module_ast): 406 | walk_ast(node, code_lines, sections, md_chunks) 407 | 408 | added_lines = 0 409 | 410 | def _replace_md(key, sections, md_chunk, md_lines, added_lines): 411 | start, end = sections[key] 412 | start -= 1 413 | start += added_lines 414 | end += added_lines 415 | new_lines = md_chunk.split('\n') 416 | added_lines += len(new_lines) - (end - start) 417 | 418 | # Ensure a newline above each class header 419 | if start > 0 and md_lines[start][0:4] == '### ' and md_lines[start - 1][0:1] == '>': 420 | added_lines += 1 421 | new_lines.insert(0, '') 422 | 423 | md_lines[start:end] = new_lines 424 | return added_lines 425 | 426 | for key in sections: 427 | if key not in md_chunks: 428 | raise ValueError('No documentation found for %s' % key[1]) 429 | added_lines = _replace_md(key, sections, md_chunks[key], md_lines, added_lines) 430 | 431 | markdown = '\n'.join(md_lines).strip() + '\n' 432 | 433 | if original_markdown != markdown: 434 | with open(md_file, 'wb') as f: 435 | f.write(markdown.encode('utf-8')) 436 | 437 | 438 | if __name__ == '__main__': 439 | run() 440 | -------------------------------------------------------------------------------- /dev/build.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals, division, absolute_import, print_function 3 | 4 | import os 5 | import tarfile 6 | import zipfile 7 | 8 | import setuptools.sandbox 9 | 10 | from . import package_root, package_name, has_tests_package 11 | from ._import import _import_from 12 | 13 | 14 | def _list_zip(filename): 15 | """ 16 | Prints all of the files in a .zip file 17 | """ 18 | 19 | zf = zipfile.ZipFile(filename, 'r') 20 | for name in zf.namelist(): 21 | print(' %s' % name) 22 | 23 | 24 | def _list_tgz(filename): 25 | """ 26 | Prints all of the files in a .tar.gz file 27 | """ 28 | 29 | tf = tarfile.open(filename, 'r:gz') 30 | for name in tf.getnames(): 31 | print(' %s' % name) 32 | 33 | 34 | def run(): 35 | """ 36 | Creates a sdist .tar.gz and a bdist_wheel --univeral .whl 37 | 38 | :return: 39 | A bool - if the packaging process was successful 40 | """ 41 | 42 | setup = os.path.join(package_root, 'setup.py') 43 | tests_root = os.path.join(package_root, 'tests') 44 | tests_setup = os.path.join(tests_root, 'setup.py') 45 | 46 | # Trying to call setuptools.sandbox.run_setup(setup, ['--version']) 47 | # resulted in a segfault, so we do this instead 48 | package_dir = os.path.join(package_root, package_name) 49 | version_mod = _import_from('%s.version' % package_name, package_dir, 'version') 50 | 51 | pkg_name_info = (package_name, version_mod.__version__) 52 | print('Building %s-%s' % pkg_name_info) 53 | 54 | sdist = '%s-%s.tar.gz' % pkg_name_info 55 | whl = '%s-%s-py2.py3-none-any.whl' % pkg_name_info 56 | setuptools.sandbox.run_setup(setup, ['-q', 'sdist']) 57 | print(' - created %s' % sdist) 58 | _list_tgz(os.path.join(package_root, 'dist', sdist)) 59 | setuptools.sandbox.run_setup(setup, ['-q', 'bdist_wheel', '--universal']) 60 | print(' - created %s' % whl) 61 | _list_zip(os.path.join(package_root, 'dist', whl)) 62 | setuptools.sandbox.run_setup(setup, ['-q', 'clean']) 63 | 64 | if has_tests_package: 65 | print('Building %s_tests-%s' % (package_name, version_mod.__version__)) 66 | 67 | tests_sdist = '%s_tests-%s.tar.gz' % pkg_name_info 68 | tests_whl = '%s_tests-%s-py2.py3-none-any.whl' % pkg_name_info 69 | setuptools.sandbox.run_setup(tests_setup, ['-q', 'sdist']) 70 | print(' - created %s' % tests_sdist) 71 | _list_tgz(os.path.join(tests_root, 'dist', tests_sdist)) 72 | setuptools.sandbox.run_setup(tests_setup, ['-q', 'bdist_wheel', '--universal']) 73 | print(' - created %s' % tests_whl) 74 | _list_zip(os.path.join(tests_root, 'dist', tests_whl)) 75 | setuptools.sandbox.run_setup(tests_setup, ['-q', 'clean']) 76 | 77 | dist_dir = os.path.join(package_root, 'dist') 78 | tests_dist_dir = os.path.join(tests_root, 'dist') 79 | os.rename( 80 | os.path.join(tests_dist_dir, tests_sdist), 81 | os.path.join(dist_dir, tests_sdist) 82 | ) 83 | os.rename( 84 | os.path.join(tests_dist_dir, tests_whl), 85 | os.path.join(dist_dir, tests_whl) 86 | ) 87 | os.rmdir(tests_dist_dir) 88 | 89 | return True 90 | -------------------------------------------------------------------------------- /dev/ci-cleanup.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals, division, absolute_import, print_function 3 | 4 | import os 5 | import shutil 6 | 7 | from . import build_root, other_packages 8 | 9 | 10 | def run(): 11 | """ 12 | Cleans up CI dependencies - used for persistent GitHub Actions 13 | Runners since they don't clean themselves up. 14 | """ 15 | 16 | print("Removing ci dependencies") 17 | deps_dir = os.path.join(build_root, 'modularcrypto-deps') 18 | if os.path.exists(deps_dir): 19 | shutil.rmtree(deps_dir, ignore_errors=True) 20 | 21 | print("Removing modularcrypto packages") 22 | for other_package in other_packages: 23 | pkg_dir = os.path.join(build_root, other_package) 24 | if os.path.exists(pkg_dir): 25 | shutil.rmtree(pkg_dir, ignore_errors=True) 26 | print() 27 | 28 | return True 29 | -------------------------------------------------------------------------------- /dev/ci-driver.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals, division, absolute_import, print_function 3 | 4 | import os 5 | import platform 6 | import sys 7 | import subprocess 8 | 9 | 10 | run_args = [ 11 | { 12 | 'name': 'cffi', 13 | 'kwarg': 'cffi', 14 | }, 15 | { 16 | 'name': 'openssl', 17 | 'kwarg': 'openssl', 18 | }, 19 | { 20 | 'name': 'winlegacy', 21 | 'kwarg': 'winlegacy', 22 | }, 23 | ] 24 | 25 | 26 | def _write_env(env, key, value): 27 | sys.stdout.write("%s: %s\n" % (key, value)) 28 | sys.stdout.flush() 29 | if sys.version_info < (3,): 30 | env[key.encode('utf-8')] = value.encode('utf-8') 31 | else: 32 | env[key] = value 33 | 34 | 35 | def run(**_): 36 | """ 37 | Runs CI, setting various env vars 38 | 39 | :return: 40 | A bool - if the CI ran successfully 41 | """ 42 | 43 | env = os.environ.copy() 44 | options = set(sys.argv[2:]) 45 | 46 | newline = False 47 | if 'cffi' not in options: 48 | _write_env(env, 'OSCRYPTO_USE_CTYPES', 'true') 49 | newline = True 50 | if 'openssl' in options and sys.platform == 'darwin': 51 | mac_version_info = tuple(map(int, platform.mac_ver()[0].split('.')[:2])) 52 | if mac_version_info < (10, 15): 53 | _write_env(env, 'OSCRYPTO_USE_OPENSSL', '/usr/lib/libcrypto.dylib,/usr/lib/libssl.dylib') 54 | else: 55 | _write_env(env, 'OSCRYPTO_USE_OPENSSL', '/usr/lib/libcrypto.35.dylib,/usr/lib/libssl.35.dylib') 56 | newline = True 57 | if 'openssl3' in options and sys.platform == 'darwin': 58 | _write_env( 59 | env, 60 | 'OSCRYPTO_USE_OPENSSL', 61 | '/usr/local/opt/openssl@3/lib/libcrypto.dylib,/usr/local/opt/openssl@3/lib/libssl.dylib' 62 | ) 63 | if 'winlegacy' in options: 64 | _write_env(env, 'OSCRYPTO_USE_WINLEGACY', 'true') 65 | newline = True 66 | 67 | if newline: 68 | sys.stdout.write("\n") 69 | 70 | proc = subprocess.Popen( 71 | [ 72 | sys.executable, 73 | 'run.py', 74 | 'ci', 75 | ], 76 | env=env 77 | ) 78 | proc.communicate() 79 | return proc.returncode == 0 80 | -------------------------------------------------------------------------------- /dev/ci.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals, division, absolute_import, print_function 3 | 4 | import os 5 | import site 6 | import sys 7 | 8 | from . import build_root, requires_oscrypto 9 | from ._import import _preload 10 | 11 | 12 | deps_dir = os.path.join(build_root, 'modularcrypto-deps') 13 | if os.path.exists(deps_dir): 14 | site.addsitedir(deps_dir) 15 | # In case any of the deps are installed system-wide 16 | sys.path.insert(0, deps_dir) 17 | 18 | if sys.version_info[0:2] not in [(2, 6), (3, 2)]: 19 | from .lint import run as run_lint 20 | else: 21 | run_lint = None 22 | 23 | if sys.version_info[0:2] != (3, 2): 24 | from .coverage import run as run_coverage 25 | from .coverage import coverage 26 | run_tests = None 27 | 28 | else: 29 | from .tests import run as run_tests 30 | run_coverage = None 31 | 32 | 33 | def run(): 34 | """ 35 | Runs the linter and tests 36 | 37 | :return: 38 | A bool - if the linter and tests ran successfully 39 | """ 40 | 41 | _preload(requires_oscrypto, True) 42 | 43 | if run_lint: 44 | print('') 45 | lint_result = run_lint() 46 | else: 47 | lint_result = True 48 | 49 | if run_coverage: 50 | print('\nRunning tests (via coverage.py %s)' % coverage.__version__) 51 | sys.stdout.flush() 52 | tests_result = run_coverage(ci=True) 53 | else: 54 | print('\nRunning tests') 55 | sys.stdout.flush() 56 | tests_result = run_tests(ci=True) 57 | sys.stdout.flush() 58 | 59 | return lint_result and tests_result 60 | -------------------------------------------------------------------------------- /dev/codecov.json: -------------------------------------------------------------------------------- 1 | { 2 | "slug": "wbond/ocspbuilder", 3 | "token": "f1a824a8-7ff0-4ede-9293-9894e3c070c8", 4 | "disabled": true 5 | } 6 | -------------------------------------------------------------------------------- /dev/coverage.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals, division, absolute_import, print_function 3 | 4 | import cgi 5 | import codecs 6 | import coverage 7 | import json 8 | import os 9 | import unittest 10 | import re 11 | import sys 12 | import tempfile 13 | import time 14 | import platform as _plat 15 | import subprocess 16 | from fnmatch import fnmatch 17 | 18 | from . import package_name, package_root, other_packages 19 | from ._import import _import_from 20 | 21 | if sys.version_info < (3,): 22 | str_cls = unicode # noqa 23 | from urllib2 import URLError 24 | from urllib import urlencode 25 | from io import open 26 | else: 27 | str_cls = str 28 | from urllib.error import URLError 29 | from urllib.parse import urlencode 30 | 31 | if sys.version_info < (3, 7): 32 | Pattern = re._pattern_type 33 | else: 34 | Pattern = re.Pattern 35 | 36 | 37 | def run(ci=False): 38 | """ 39 | Runs the tests while measuring coverage 40 | 41 | :param ci: 42 | If coverage is being run in a CI environment - this triggers trying to 43 | run the tests for the rest of modularcrypto and uploading coverage data 44 | 45 | :return: 46 | A bool - if the tests ran successfully 47 | """ 48 | 49 | xml_report_path = os.path.join(package_root, 'coverage.xml') 50 | if os.path.exists(xml_report_path): 51 | os.unlink(xml_report_path) 52 | 53 | cov = coverage.Coverage(include='%s/*.py' % package_name) 54 | cov.start() 55 | 56 | from .tests import run as run_tests 57 | result = run_tests(ci=ci) 58 | print() 59 | 60 | if ci: 61 | suite = unittest.TestSuite() 62 | loader = unittest.TestLoader() 63 | for other_package in other_packages: 64 | for test_class in _load_package_tests(other_package): 65 | suite.addTest(loader.loadTestsFromTestCase(test_class)) 66 | 67 | if suite.countTestCases() > 0: 68 | print('Running tests from other modularcrypto packages') 69 | sys.stdout.flush() 70 | runner_result = unittest.TextTestRunner(stream=sys.stdout, verbosity=1).run(suite) 71 | result = runner_result.wasSuccessful() and result 72 | print() 73 | sys.stdout.flush() 74 | 75 | cov.stop() 76 | cov.save() 77 | 78 | cov.report(show_missing=False) 79 | print() 80 | sys.stdout.flush() 81 | if ci: 82 | cov.xml_report() 83 | 84 | if ci and result and os.path.exists(xml_report_path): 85 | _codecov_submit() 86 | print() 87 | 88 | return result 89 | 90 | 91 | def _load_package_tests(name): 92 | """ 93 | Load the test classes from another modularcrypto package 94 | 95 | :param name: 96 | A unicode string of the other package name 97 | 98 | :return: 99 | A list of unittest.TestCase classes of the tests for the package 100 | """ 101 | 102 | package_dir = os.path.join('..', name) 103 | if not os.path.exists(package_dir): 104 | return [] 105 | 106 | return _import_from('%s_tests' % name, package_dir, 'tests').test_classes() 107 | 108 | 109 | def _env_info(): 110 | """ 111 | :return: 112 | A two-element tuple of unicode strings. The first is the name of the 113 | environment, the second the root of the repo. The environment name 114 | will be one of: "ci-travis", "ci-circle", "ci-appveyor", 115 | "ci-github-actions", "local" 116 | """ 117 | 118 | if os.getenv('CI') == 'true' and os.getenv('TRAVIS') == 'true': 119 | return ('ci-travis', os.getenv('TRAVIS_BUILD_DIR')) 120 | 121 | if os.getenv('CI') == 'True' and os.getenv('APPVEYOR') == 'True': 122 | return ('ci-appveyor', os.getenv('APPVEYOR_BUILD_FOLDER')) 123 | 124 | if os.getenv('CI') == 'true' and os.getenv('CIRCLECI') == 'true': 125 | return ('ci-circle', os.getcwdu() if sys.version_info < (3,) else os.getcwd()) 126 | 127 | if os.getenv('GITHUB_ACTIONS') == 'true': 128 | return ('ci-github-actions', os.getenv('GITHUB_WORKSPACE')) 129 | 130 | return ('local', package_root) 131 | 132 | 133 | def _codecov_submit(): 134 | env_name, root = _env_info() 135 | 136 | try: 137 | with open(os.path.join(root, 'dev/codecov.json'), 'rb') as f: 138 | json_data = json.loads(f.read().decode('utf-8')) 139 | except (OSError, ValueError, UnicodeDecodeError, KeyError): 140 | print('error reading codecov.json') 141 | return 142 | 143 | if json_data.get('disabled'): 144 | return 145 | 146 | if env_name == 'ci-travis': 147 | # http://docs.travis-ci.com/user/environment-variables/#Default-Environment-Variables 148 | build_url = 'https://travis-ci.org/%s/jobs/%s' % (os.getenv('TRAVIS_REPO_SLUG'), os.getenv('TRAVIS_JOB_ID')) 149 | query = { 150 | 'service': 'travis', 151 | 'branch': os.getenv('TRAVIS_BRANCH'), 152 | 'build': os.getenv('TRAVIS_JOB_NUMBER'), 153 | 'pr': os.getenv('TRAVIS_PULL_REQUEST'), 154 | 'job': os.getenv('TRAVIS_JOB_ID'), 155 | 'tag': os.getenv('TRAVIS_TAG'), 156 | 'slug': os.getenv('TRAVIS_REPO_SLUG'), 157 | 'commit': os.getenv('TRAVIS_COMMIT'), 158 | 'build_url': build_url, 159 | } 160 | 161 | elif env_name == 'ci-appveyor': 162 | # http://www.appveyor.com/docs/environment-variables 163 | build_url = 'https://ci.appveyor.com/project/%s/build/%s' % ( 164 | os.getenv('APPVEYOR_REPO_NAME'), 165 | os.getenv('APPVEYOR_BUILD_VERSION') 166 | ) 167 | query = { 168 | 'service': "appveyor", 169 | 'branch': os.getenv('APPVEYOR_REPO_BRANCH'), 170 | 'build': os.getenv('APPVEYOR_JOB_ID'), 171 | 'pr': os.getenv('APPVEYOR_PULL_REQUEST_NUMBER'), 172 | 'job': '/'.join(( 173 | os.getenv('APPVEYOR_ACCOUNT_NAME'), 174 | os.getenv('APPVEYOR_PROJECT_SLUG'), 175 | os.getenv('APPVEYOR_BUILD_VERSION') 176 | )), 177 | 'tag': os.getenv('APPVEYOR_REPO_TAG_NAME'), 178 | 'slug': os.getenv('APPVEYOR_REPO_NAME'), 179 | 'commit': os.getenv('APPVEYOR_REPO_COMMIT'), 180 | 'build_url': build_url, 181 | } 182 | 183 | elif env_name == 'ci-circle': 184 | # https://circleci.com/docs/environment-variables 185 | query = { 186 | 'service': 'circleci', 187 | 'branch': os.getenv('CIRCLE_BRANCH'), 188 | 'build': os.getenv('CIRCLE_BUILD_NUM'), 189 | 'pr': os.getenv('CIRCLE_PR_NUMBER'), 190 | 'job': os.getenv('CIRCLE_BUILD_NUM') + "." + os.getenv('CIRCLE_NODE_INDEX'), 191 | 'tag': os.getenv('CIRCLE_TAG'), 192 | 'slug': os.getenv('CIRCLE_PROJECT_USERNAME') + "/" + os.getenv('CIRCLE_PROJECT_REPONAME'), 193 | 'commit': os.getenv('CIRCLE_SHA1'), 194 | 'build_url': os.getenv('CIRCLE_BUILD_URL'), 195 | } 196 | 197 | elif env_name == 'ci-github-actions': 198 | branch = '' 199 | tag = '' 200 | ref = os.getenv('GITHUB_REF', '') 201 | if ref.startswith('refs/tags/'): 202 | tag = ref[10:] 203 | elif ref.startswith('refs/heads/'): 204 | branch = ref[11:] 205 | 206 | impl = _plat.python_implementation() 207 | major, minor = _plat.python_version_tuple()[0:2] 208 | build_name = '%s %s %s.%s' % (_platform_name(), impl, major, minor) 209 | 210 | query = { 211 | 'service': 'custom', 212 | 'token': json_data['token'], 213 | 'branch': branch, 214 | 'tag': tag, 215 | 'slug': os.getenv('GITHUB_REPOSITORY'), 216 | 'commit': os.getenv('GITHUB_SHA'), 217 | 'build_url': 'https://github.com/wbond/oscrypto/commit/%s/checks' % os.getenv('GITHUB_SHA'), 218 | 'name': 'GitHub Actions %s on %s' % (build_name, os.getenv('RUNNER_OS')) 219 | } 220 | 221 | else: 222 | if not os.path.exists(os.path.join(root, '.git')): 223 | print('git repository not found, not submitting coverage data') 224 | return 225 | git_status = _git_command(['status', '--porcelain'], root) 226 | if git_status != '': 227 | print('git repository has uncommitted changes, not submitting coverage data') 228 | return 229 | 230 | branch = _git_command(['rev-parse', '--abbrev-ref', 'HEAD'], root) 231 | commit = _git_command(['rev-parse', '--verify', 'HEAD'], root) 232 | tag = _git_command(['name-rev', '--tags', '--name-only', commit], root) 233 | impl = _plat.python_implementation() 234 | major, minor = _plat.python_version_tuple()[0:2] 235 | build_name = '%s %s %s.%s' % (_platform_name(), impl, major, minor) 236 | query = { 237 | 'branch': branch, 238 | 'commit': commit, 239 | 'slug': json_data['slug'], 240 | 'token': json_data['token'], 241 | 'build': build_name, 242 | } 243 | if tag != 'undefined': 244 | query['tag'] = tag 245 | 246 | payload = 'PLATFORM=%s\n' % _platform_name() 247 | payload += 'PYTHON_VERSION=%s %s\n' % (_plat.python_version(), _plat.python_implementation()) 248 | if 'oscrypto' in sys.modules: 249 | payload += 'OSCRYPTO_BACKEND=%s\n' % sys.modules['oscrypto'].backend() 250 | payload += '<<<<<< ENV\n' 251 | 252 | for path in _list_files(root): 253 | payload += path + '\n' 254 | payload += '<<<<<< network\n' 255 | 256 | payload += '# path=coverage.xml\n' 257 | with open(os.path.join(root, 'coverage.xml'), 'r', encoding='utf-8') as f: 258 | payload += f.read() + '\n' 259 | payload += '<<<<<< EOF\n' 260 | 261 | url = 'https://codecov.io/upload/v4' 262 | headers = { 263 | 'Accept': 'text/plain' 264 | } 265 | filtered_query = {} 266 | for key in query: 267 | value = query[key] 268 | if value == '' or value is None: 269 | continue 270 | filtered_query[key] = value 271 | 272 | print('Submitting coverage info to codecov.io') 273 | info = _do_request( 274 | 'POST', 275 | url, 276 | headers, 277 | query_params=filtered_query 278 | ) 279 | 280 | encoding = info[1] or 'utf-8' 281 | text = info[2].decode(encoding).strip() 282 | parts = text.split() 283 | upload_url = parts[1] 284 | 285 | headers = { 286 | 'Content-Type': 'text/plain', 287 | 'x-amz-acl': 'public-read', 288 | 'x-amz-storage-class': 'REDUCED_REDUNDANCY' 289 | } 290 | 291 | print('Uploading coverage data to codecov.io S3 bucket') 292 | _do_request( 293 | 'PUT', 294 | upload_url, 295 | headers, 296 | data=payload.encode('utf-8') 297 | ) 298 | 299 | 300 | def _git_command(params, cwd): 301 | """ 302 | Executes a git command, returning the output 303 | 304 | :param params: 305 | A list of the parameters to pass to git 306 | 307 | :param cwd: 308 | The working directory to execute git in 309 | 310 | :return: 311 | A 2-element tuple of (stdout, stderr) 312 | """ 313 | 314 | proc = subprocess.Popen( 315 | ['git'] + params, 316 | stdout=subprocess.PIPE, 317 | stderr=subprocess.STDOUT, 318 | cwd=cwd 319 | ) 320 | stdout, stderr = proc.communicate() 321 | code = proc.wait() 322 | if code != 0: 323 | e = OSError('git exit code was non-zero') 324 | e.stdout = stdout 325 | raise e 326 | return stdout.decode('utf-8').strip() 327 | 328 | 329 | def _parse_env_var_file(data): 330 | """ 331 | Parses a basic VAR="value data" file contents into a dict 332 | 333 | :param data: 334 | A unicode string of the file data 335 | 336 | :return: 337 | A dict of parsed name/value data 338 | """ 339 | 340 | output = {} 341 | for line in data.splitlines(): 342 | line = line.strip() 343 | if not line or '=' not in line: 344 | continue 345 | parts = line.split('=') 346 | if len(parts) != 2: 347 | continue 348 | name = parts[0] 349 | value = parts[1] 350 | if len(value) > 1: 351 | if value[0] == '"' and value[-1] == '"': 352 | value = value[1:-1] 353 | output[name] = value 354 | return output 355 | 356 | 357 | def _platform_name(): 358 | """ 359 | Returns information about the current operating system and version 360 | 361 | :return: 362 | A unicode string containing the OS name and version 363 | """ 364 | 365 | if sys.platform == 'darwin': 366 | version = _plat.mac_ver()[0] 367 | _plat_ver_info = tuple(map(int, version.split('.'))) 368 | if _plat_ver_info < (10, 12): 369 | name = 'OS X' 370 | else: 371 | name = 'macOS' 372 | return '%s %s' % (name, version) 373 | 374 | elif sys.platform == 'win32': 375 | _win_ver = sys.getwindowsversion() 376 | _plat_ver_info = (_win_ver[0], _win_ver[1]) 377 | return 'Windows %s' % _plat.win32_ver()[0] 378 | 379 | elif sys.platform in ['linux', 'linux2']: 380 | if os.path.exists('/etc/os-release'): 381 | with open('/etc/os-release', 'r', encoding='utf-8') as f: 382 | pairs = _parse_env_var_file(f.read()) 383 | if 'NAME' in pairs and 'VERSION_ID' in pairs: 384 | return '%s %s' % (pairs['NAME'], pairs['VERSION_ID']) 385 | version = pairs['VERSION_ID'] 386 | elif 'PRETTY_NAME' in pairs: 387 | return pairs['PRETTY_NAME'] 388 | elif 'NAME' in pairs: 389 | return pairs['NAME'] 390 | else: 391 | raise ValueError('No suitable version info found in /etc/os-release') 392 | elif os.path.exists('/etc/lsb-release'): 393 | with open('/etc/lsb-release', 'r', encoding='utf-8') as f: 394 | pairs = _parse_env_var_file(f.read()) 395 | if 'DISTRIB_DESCRIPTION' in pairs: 396 | return pairs['DISTRIB_DESCRIPTION'] 397 | else: 398 | raise ValueError('No suitable version info found in /etc/lsb-release') 399 | else: 400 | return 'Linux' 401 | 402 | else: 403 | return '%s %s' % (_plat.system(), _plat.release()) 404 | 405 | 406 | def _list_files(root): 407 | """ 408 | Lists all of the files in a directory, taking into account any .gitignore 409 | file that is present 410 | 411 | :param root: 412 | A unicode filesystem path 413 | 414 | :return: 415 | A list of unicode strings, containing paths of all files not ignored 416 | by .gitignore with root, using relative paths 417 | """ 418 | 419 | dir_patterns, file_patterns = _gitignore(root) 420 | paths = [] 421 | prefix = os.path.abspath(root) + os.sep 422 | for base, dirs, files in os.walk(root): 423 | for d in dirs: 424 | for dir_pattern in dir_patterns: 425 | if fnmatch(d, dir_pattern): 426 | dirs.remove(d) 427 | break 428 | for f in files: 429 | skip = False 430 | for file_pattern in file_patterns: 431 | if fnmatch(f, file_pattern): 432 | skip = True 433 | break 434 | if skip: 435 | continue 436 | full_path = os.path.join(base, f) 437 | if full_path[:len(prefix)] == prefix: 438 | full_path = full_path[len(prefix):] 439 | paths.append(full_path) 440 | return sorted(paths) 441 | 442 | 443 | def _gitignore(root): 444 | """ 445 | Parses a .gitignore file and returns patterns to match dirs and files. 446 | Only basic gitignore patterns are supported. Pattern negation, ** wildcards 447 | and anchored patterns are not currently implemented. 448 | 449 | :param root: 450 | A unicode string of the path to the git repository 451 | 452 | :return: 453 | A 2-element tuple: 454 | - 0: a list of unicode strings to match against dirs 455 | - 1: a list of unicode strings to match against dirs and files 456 | """ 457 | 458 | gitignore_path = os.path.join(root, '.gitignore') 459 | 460 | dir_patterns = ['.git'] 461 | file_patterns = [] 462 | 463 | if not os.path.exists(gitignore_path): 464 | return (dir_patterns, file_patterns) 465 | 466 | with open(gitignore_path, 'r', encoding='utf-8') as f: 467 | for line in f.readlines(): 468 | line = line.strip() 469 | if not line: 470 | continue 471 | if line.startswith('#'): 472 | continue 473 | if '**' in line: 474 | raise NotImplementedError('gitignore ** wildcards are not implemented') 475 | if line.startswith('!'): 476 | raise NotImplementedError('gitignore pattern negation is not implemented') 477 | if line.startswith('/'): 478 | raise NotImplementedError('gitignore anchored patterns are not implemented') 479 | if line.startswith('\\#'): 480 | line = '#' + line[2:] 481 | if line.startswith('\\!'): 482 | line = '!' + line[2:] 483 | if line.endswith('/'): 484 | dir_patterns.append(line[:-1]) 485 | else: 486 | file_patterns.append(line) 487 | 488 | return (dir_patterns, file_patterns) 489 | 490 | 491 | def _do_request(method, url, headers, data=None, query_params=None, timeout=20): 492 | """ 493 | Performs an HTTP request 494 | 495 | :param method: 496 | A unicode string of 'POST' or 'PUT' 497 | 498 | :param url; 499 | A unicode string of the URL to request 500 | 501 | :param headers: 502 | A dict of unicode strings, where keys are header names and values are 503 | the header values. 504 | 505 | :param data: 506 | A dict of unicode strings (to be encoded as 507 | application/x-www-form-urlencoded), or a byte string of data. 508 | 509 | :param query_params: 510 | A dict of unicode keys and values to pass as query params 511 | 512 | :param timeout: 513 | An integer number of seconds to use as the timeout 514 | 515 | :return: 516 | A 3-element tuple: 517 | - 0: A unicode string of the response content-type 518 | - 1: A unicode string of the response encoding, or None 519 | - 2: A byte string of the response body 520 | """ 521 | 522 | if query_params: 523 | url += '?' + urlencode(query_params).replace('+', '%20') 524 | 525 | if isinstance(data, dict): 526 | data_bytes = {} 527 | for key in data: 528 | data_bytes[key.encode('utf-8')] = data[key].encode('utf-8') 529 | data = urlencode(data_bytes) 530 | headers['Content-Type'] = 'application/x-www-form-urlencoded' 531 | if isinstance(data, str_cls): 532 | raise TypeError('data must be a byte string') 533 | 534 | try: 535 | tempfd, tempf_path = tempfile.mkstemp('-coverage') 536 | os.write(tempfd, data or b'') 537 | os.close(tempfd) 538 | 539 | if sys.platform == 'win32': 540 | powershell_exe = os.path.join('system32\\WindowsPowerShell\\v1.0\\powershell.exe') 541 | code = "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12;" 542 | code += "$wc = New-Object Net.WebClient;" 543 | for key in headers: 544 | code += "$wc.Headers.add('%s','%s');" % (key, headers[key]) 545 | code += "$out = $wc.UploadFile('%s', '%s', '%s');" % (url, method, tempf_path) 546 | code += "[System.Text.Encoding]::GetEncoding('ISO-8859-1').GetString($wc.ResponseHeaders.ToByteArray())" 547 | 548 | # To properly obtain bytes, we use BitConverter to get hex dash 549 | # encoding (e.g. AE-09-3F) and they decode in python 550 | code += " + [System.BitConverter]::ToString($out);" 551 | stdout, stderr = _execute( 552 | [powershell_exe, '-Command', code], 553 | os.getcwd(), 554 | re.compile(r'Unable to connect to|TLS|Internal Server Error'), 555 | 6 556 | ) 557 | if stdout[-2:] == b'\r\n' and b'\r\n\r\n' in stdout: 558 | # An extra trailing crlf is added at the end by powershell 559 | stdout = stdout[0:-2] 560 | parts = stdout.split(b'\r\n\r\n', 1) 561 | if len(parts) == 2: 562 | stdout = parts[0] + b'\r\n\r\n' + codecs.decode(parts[1].replace(b'-', b''), 'hex_codec') 563 | 564 | else: 565 | args = [ 566 | 'curl', 567 | '--http1.1', 568 | '--connect-timeout', '5', 569 | '--request', 570 | method, 571 | '--location', 572 | '--silent', 573 | '--show-error', 574 | '--include', 575 | # Prevent curl from asking for an HTTP "100 Continue" response 576 | '--header', 'Expect:' 577 | ] 578 | for key in headers: 579 | args.append('--header') 580 | args.append("%s: %s" % (key, headers[key])) 581 | args.append('--data-binary') 582 | args.append('@%s' % tempf_path) 583 | args.append(url) 584 | stdout, stderr = _execute( 585 | args, 586 | os.getcwd(), 587 | re.compile(r'Failed to connect to|TLS|SSLRead|outstanding|cleanly|timed out'), 588 | 6 589 | ) 590 | finally: 591 | if tempf_path and os.path.exists(tempf_path): 592 | os.remove(tempf_path) 593 | 594 | if len(stderr) > 0: 595 | raise URLError("Error %sing %s:\n%s" % (method, url, stderr)) 596 | 597 | parts = stdout.split(b'\r\n\r\n', 1) 598 | if len(parts) != 2: 599 | raise URLError("Error %sing %s, response data malformed:\n%s" % (method, url, stdout)) 600 | header_block, body = parts 601 | 602 | content_type_header = None 603 | content_len_header = None 604 | for hline in header_block.decode('iso-8859-1').splitlines(): 605 | hline_parts = hline.split(':', 1) 606 | if len(hline_parts) != 2: 607 | continue 608 | name, val = hline_parts 609 | name = name.strip().lower() 610 | val = val.strip() 611 | if name == 'content-type': 612 | content_type_header = val 613 | if name == 'content-length': 614 | content_len_header = val 615 | 616 | if content_type_header is None and content_len_header != '0': 617 | raise URLError("Error %sing %s, no content-type header:\n%s" % (method, url, stdout)) 618 | 619 | if content_type_header is None: 620 | content_type = 'text/plain' 621 | encoding = 'utf-8' 622 | else: 623 | content_type, params = cgi.parse_header(content_type_header) 624 | encoding = params.get('charset') 625 | 626 | return (content_type, encoding, body) 627 | 628 | 629 | def _execute(params, cwd, retry=None, retries=0, backoff=2): 630 | """ 631 | Executes a subprocess 632 | 633 | :param params: 634 | A list of the executable and arguments to pass to it 635 | 636 | :param cwd: 637 | The working directory to execute the command in 638 | 639 | :param retry: 640 | If this string is present in stderr, or regex pattern matches stderr, retry the operation 641 | 642 | :param retries: 643 | An integer number of times to retry 644 | 645 | :return: 646 | A 2-element tuple of (stdout, stderr) 647 | """ 648 | 649 | proc = subprocess.Popen( 650 | params, 651 | stdout=subprocess.PIPE, 652 | stderr=subprocess.PIPE, 653 | cwd=cwd 654 | ) 655 | stdout, stderr = proc.communicate() 656 | code = proc.wait() 657 | if code != 0: 658 | if retry and retries > 0: 659 | stderr_str = stderr.decode('utf-8') 660 | if isinstance(retry, Pattern): 661 | if retry.search(stderr_str) is not None: 662 | time.sleep(backoff) 663 | return _execute(params, cwd, retry, retries - 1, backoff * 2) 664 | elif retry in stderr_str: 665 | time.sleep(backoff) 666 | return _execute(params, cwd, retry, retries - 1, backoff * 2) 667 | e = OSError('subprocess exit code for "%s" was %d: %s' % (' '.join(params), code, stderr)) 668 | e.stdout = stdout 669 | e.stderr = stderr 670 | raise e 671 | return (stdout, stderr) 672 | 673 | 674 | if __name__ == '__main__': 675 | _codecov_submit() 676 | -------------------------------------------------------------------------------- /dev/deps.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals, division, absolute_import, print_function 3 | 4 | import os 5 | import subprocess 6 | import sys 7 | import shutil 8 | import re 9 | import json 10 | import tarfile 11 | import zipfile 12 | 13 | from . import package_root, build_root, other_packages 14 | from ._pep425 import _pep425tags, _pep425_implementation 15 | 16 | if sys.version_info < (3,): 17 | str_cls = unicode # noqa 18 | else: 19 | str_cls = str 20 | 21 | 22 | def run(): 23 | """ 24 | Installs required development dependencies. Uses git to checkout other 25 | modularcrypto repos for more accurate coverage data. 26 | """ 27 | 28 | deps_dir = os.path.join(build_root, 'modularcrypto-deps') 29 | if os.path.exists(deps_dir): 30 | shutil.rmtree(deps_dir, ignore_errors=True) 31 | os.mkdir(deps_dir) 32 | 33 | try: 34 | print("Staging ci dependencies") 35 | _stage_requirements(deps_dir, os.path.join(package_root, 'requires', 'ci')) 36 | 37 | print("Checking out modularcrypto packages for coverage") 38 | for other_package in other_packages: 39 | pkg_url = 'https://github.com/wbond/%s.git' % other_package 40 | pkg_dir = os.path.join(build_root, other_package) 41 | if os.path.exists(pkg_dir): 42 | print("%s is already present" % other_package) 43 | continue 44 | print("Cloning %s" % pkg_url) 45 | _execute(['git', 'clone', pkg_url], build_root) 46 | print() 47 | 48 | except (Exception): 49 | if os.path.exists(deps_dir): 50 | shutil.rmtree(deps_dir, ignore_errors=True) 51 | raise 52 | 53 | return True 54 | 55 | 56 | def _download(url, dest): 57 | """ 58 | Downloads a URL to a directory 59 | 60 | :param url: 61 | The URL to download 62 | 63 | :param dest: 64 | The path to the directory to save the file in 65 | 66 | :return: 67 | The filesystem path to the saved file 68 | """ 69 | 70 | print('Downloading %s' % url) 71 | filename = os.path.basename(url) 72 | dest_path = os.path.join(dest, filename) 73 | 74 | if sys.platform == 'win32': 75 | powershell_exe = os.path.join('system32\\WindowsPowerShell\\v1.0\\powershell.exe') 76 | code = "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12;" 77 | code += "(New-Object Net.WebClient).DownloadFile('%s', '%s');" % (url, dest_path) 78 | _execute([powershell_exe, '-Command', code], dest, 'Unable to connect to') 79 | 80 | else: 81 | _execute( 82 | ['curl', '-L', '--silent', '--show-error', '-O', url], 83 | dest, 84 | 'Failed to connect to' 85 | ) 86 | 87 | return dest_path 88 | 89 | 90 | def _tuple_from_ver(version_string): 91 | """ 92 | :param version_string: 93 | A unicode dotted version string 94 | 95 | :return: 96 | A tuple of integers 97 | """ 98 | 99 | match = re.search( 100 | r'(\d+(?:\.\d+)*)' 101 | r'([-._]?(?:alpha|a|beta|b|preview|pre|c|rc)\.?\d*)?' 102 | r'(-\d+|(?:[-._]?(?:rev|r|post)\.?\d*))?' 103 | r'([-._]?dev\.?\d*)?', 104 | version_string 105 | ) 106 | if not match: 107 | return tuple() 108 | 109 | nums = tuple(map(int, match.group(1).split('.'))) 110 | 111 | pre = match.group(2) 112 | if pre: 113 | pre = pre.replace('alpha', 'a') 114 | pre = pre.replace('beta', 'b') 115 | pre = pre.replace('preview', 'rc') 116 | pre = pre.replace('pre', 'rc') 117 | pre = re.sub(r'(?= (3,): 368 | env['PYTHONPATH'] = deps_dir 369 | else: 370 | env[b'PYTHONPATH'] = deps_dir.encode('utf-8') 371 | 372 | _execute( 373 | [ 374 | sys.executable, 375 | 'setup.py', 376 | 'install', 377 | '--root=%s' % root, 378 | '--install-lib=%s' % install_lib, 379 | '--no-compile' 380 | ], 381 | setup_dir, 382 | env=env 383 | ) 384 | 385 | finally: 386 | if ar: 387 | ar.close() 388 | if staging_dir: 389 | shutil.rmtree(staging_dir) 390 | 391 | 392 | def _sort_pep440_versions(releases, include_prerelease): 393 | """ 394 | :param releases: 395 | A list of unicode string PEP 440 version numbers 396 | 397 | :param include_prerelease: 398 | A boolean indicating if prerelease versions should be included 399 | 400 | :return: 401 | A sorted generator of 2-element tuples: 402 | 0: A unicode string containing a PEP 440 version number 403 | 1: A tuple of tuples containing integers - this is the output of 404 | _tuple_from_ver() for the PEP 440 version number and is intended 405 | for comparing versions 406 | """ 407 | 408 | parsed_versions = [] 409 | for v in releases: 410 | t = _tuple_from_ver(v) 411 | if not include_prerelease and t[1][0] < 0: 412 | continue 413 | parsed_versions.append((v, t)) 414 | 415 | return sorted(parsed_versions, key=lambda v: v[1]) 416 | 417 | 418 | def _is_valid_python_version(python_version, requires_python): 419 | """ 420 | Verifies the "python_version" and "requires_python" keys from a PyPi 421 | download record are applicable to the current version of Python 422 | 423 | :param python_version: 424 | The "python_version" value from a PyPi download JSON structure. This 425 | should be one of: "py2", "py3", "py2.py3" or "source". 426 | 427 | :param requires_python: 428 | The "requires_python" value from a PyPi download JSON structure. This 429 | will be None, or a comma-separated list of conditions that must be 430 | true. Ex: ">=3.5", "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 431 | """ 432 | 433 | if python_version == "py2" and sys.version_info >= (3,): 434 | return False 435 | if python_version == "py3" and sys.version_info < (3,): 436 | return False 437 | 438 | if requires_python is not None: 439 | 440 | def _ver_tuples(ver_str): 441 | ver_str = ver_str.strip() 442 | if ver_str.endswith('.*'): 443 | ver_str = ver_str[:-2] 444 | cond_tup = tuple(map(int, ver_str.split('.'))) 445 | return (sys.version_info[:len(cond_tup)], cond_tup) 446 | 447 | for part in map(str_cls.strip, requires_python.split(',')): 448 | if part.startswith('!='): 449 | sys_tup, cond_tup = _ver_tuples(part[2:]) 450 | if sys_tup == cond_tup: 451 | return False 452 | elif part.startswith('>='): 453 | sys_tup, cond_tup = _ver_tuples(part[2:]) 454 | if sys_tup < cond_tup: 455 | return False 456 | elif part.startswith('>'): 457 | sys_tup, cond_tup = _ver_tuples(part[1:]) 458 | if sys_tup <= cond_tup: 459 | return False 460 | elif part.startswith('<='): 461 | sys_tup, cond_tup = _ver_tuples(part[2:]) 462 | if sys_tup > cond_tup: 463 | return False 464 | elif part.startswith('<'): 465 | sys_tup, cond_tup = _ver_tuples(part[1:]) 466 | if sys_tup >= cond_tup: 467 | return False 468 | elif part.startswith('=='): 469 | sys_tup, cond_tup = _ver_tuples(part[2:]) 470 | if sys_tup != cond_tup: 471 | return False 472 | 473 | return True 474 | 475 | 476 | def _locate_suitable_download(downloads): 477 | """ 478 | :param downloads: 479 | A list of dicts containing a key "url", "python_version" and 480 | "requires_python" 481 | 482 | :return: 483 | A unicode string URL, or None if not a valid release for the current 484 | version of Python 485 | """ 486 | 487 | valid_tags = _pep425tags() 488 | 489 | exe_suffix = None 490 | if sys.platform == 'win32' and _pep425_implementation() == 'cp': 491 | win_arch = 'win32' if sys.maxsize == 2147483647 else 'win-amd64' 492 | version_info = sys.version_info 493 | exe_suffix = '.%s-py%d.%d.exe' % (win_arch, version_info[0], version_info[1]) 494 | 495 | wheels = {} 496 | whl = None 497 | tar_bz2 = None 498 | tar_gz = None 499 | exe = None 500 | for download in downloads: 501 | if not _is_valid_python_version(download.get('python_version'), download.get('requires_python')): 502 | continue 503 | 504 | if exe_suffix and download['url'].endswith(exe_suffix): 505 | exe = download['url'] 506 | if download['url'].endswith('.whl'): 507 | parts = os.path.basename(download['url']).split('-') 508 | tag_impl = parts[-3] 509 | tag_abi = parts[-2] 510 | tag_arch = parts[-1].split('.')[0] 511 | wheels[(tag_impl, tag_abi, tag_arch)] = download['url'] 512 | if download['url'].endswith('.tar.bz2'): 513 | tar_bz2 = download['url'] 514 | if download['url'].endswith('.tar.gz'): 515 | tar_gz = download['url'] 516 | 517 | # Find the most-specific wheel possible 518 | for tag in valid_tags: 519 | if tag in wheels: 520 | whl = wheels[tag] 521 | break 522 | 523 | if exe_suffix and exe: 524 | url = exe 525 | elif whl: 526 | url = whl 527 | elif tar_bz2: 528 | url = tar_bz2 529 | elif tar_gz: 530 | url = tar_gz 531 | else: 532 | return None 533 | 534 | return url 535 | 536 | 537 | def _stage_requirements(deps_dir, path): 538 | """ 539 | Installs requirements without using Python to download, since 540 | different services are limiting to TLS 1.2, and older version of 541 | Python do not support that 542 | 543 | :param deps_dir: 544 | A unicode path to a temporary directory to use for downloads 545 | 546 | :param path: 547 | A unicode filesystem path to a requirements file 548 | """ 549 | 550 | packages = _parse_requires(path) 551 | for p in packages: 552 | url = None 553 | pkg = p['pkg'] 554 | pkg_sub_dir = None 555 | if p['type'] == 'url': 556 | anchor = None 557 | if '#' in pkg: 558 | pkg, anchor = pkg.split('#', 1) 559 | if '&' in anchor: 560 | parts = anchor.split('&') 561 | else: 562 | parts = [anchor] 563 | for part in parts: 564 | param, value = part.split('=') 565 | if param == 'subdirectory': 566 | pkg_sub_dir = value 567 | 568 | if pkg.endswith('.zip') or pkg.endswith('.tar.gz') or pkg.endswith('.tar.bz2') or pkg.endswith('.whl'): 569 | url = pkg 570 | else: 571 | raise Exception('Unable to install package from URL that is not an archive') 572 | else: 573 | pypi_json_url = 'https://pypi.org/pypi/%s/json' % pkg 574 | json_dest = _download(pypi_json_url, deps_dir) 575 | with open(json_dest, 'rb') as f: 576 | pkg_info = json.loads(f.read().decode('utf-8')) 577 | if os.path.exists(json_dest): 578 | os.remove(json_dest) 579 | 580 | if p['type'] == '==': 581 | if p['ver'] not in pkg_info['releases']: 582 | raise Exception('Unable to find version %s of %s' % (p['ver'], pkg)) 583 | url = _locate_suitable_download(pkg_info['releases'][p['ver']]) 584 | if not url: 585 | raise Exception('Unable to find a compatible download of %s == %s' % (pkg, p['ver'])) 586 | else: 587 | p_ver_tup = _tuple_from_ver(p['ver']) 588 | for ver_str, ver_tup in reversed(_sort_pep440_versions(pkg_info['releases'], False)): 589 | if p['type'] == '>=' and ver_tup < p_ver_tup: 590 | break 591 | url = _locate_suitable_download(pkg_info['releases'][ver_str]) 592 | if url: 593 | break 594 | if not url: 595 | if p['type'] == '>=': 596 | raise Exception('Unable to find a compatible download of %s >= %s' % (pkg, p['ver'])) 597 | else: 598 | raise Exception('Unable to find a compatible download of %s' % pkg) 599 | 600 | local_path = _download(url, deps_dir) 601 | 602 | _extract_package(deps_dir, local_path, pkg_sub_dir) 603 | 604 | os.remove(local_path) 605 | 606 | 607 | def _parse_requires(path): 608 | """ 609 | Does basic parsing of pip requirements files, to allow for 610 | using something other than Python to do actual TLS requests 611 | 612 | :param path: 613 | A path to a requirements file 614 | 615 | :return: 616 | A list of dict objects containing the keys: 617 | - 'type' ('any', 'url', '==', '>=') 618 | - 'pkg' 619 | - 'ver' (if 'type' == '==' or 'type' == '>=') 620 | """ 621 | 622 | python_version = '.'.join(map(str_cls, sys.version_info[0:2])) 623 | sys_platform = sys.platform 624 | 625 | packages = [] 626 | 627 | with open(path, 'rb') as f: 628 | contents = f.read().decode('utf-8') 629 | 630 | for line in re.split(r'\r?\n', contents): 631 | line = line.strip() 632 | if not len(line): 633 | continue 634 | if re.match(r'^\s*#', line): 635 | continue 636 | if ';' in line: 637 | package, cond = line.split(';', 1) 638 | package = package.strip() 639 | cond = cond.strip() 640 | cond = cond.replace('sys_platform', repr(sys_platform)) 641 | cond = re.sub( 642 | r'[\'"]' 643 | r'(\d+(?:\.\d+)*)' 644 | r'([-._]?(?:alpha|a|beta|b|preview|pre|c|rc)\.?\d*)?' 645 | r'(-\d+|(?:[-._]?(?:rev|r|post)\.?\d*))?' 646 | r'([-._]?dev\.?\d*)?' 647 | r'[\'"]', 648 | r'_tuple_from_ver(\g<0>)', 649 | cond 650 | ) 651 | cond = cond.replace('python_version', '_tuple_from_ver(%r)' % python_version) 652 | if not eval(cond): 653 | continue 654 | else: 655 | package = line.strip() 656 | 657 | if re.match(r'^\s*-r\s*', package): 658 | sub_req_file = re.sub(r'^\s*-r\s*', '', package) 659 | sub_req_file = os.path.abspath(os.path.join(os.path.dirname(path), sub_req_file)) 660 | packages.extend(_parse_requires(sub_req_file)) 661 | continue 662 | 663 | if re.match(r'https?://', package): 664 | packages.append({'type': 'url', 'pkg': package}) 665 | continue 666 | 667 | if '>=' in package: 668 | parts = package.split('>=') 669 | package = parts[0].strip() 670 | ver = parts[1].strip() 671 | packages.append({'type': '>=', 'pkg': package, 'ver': ver}) 672 | continue 673 | 674 | if '==' in package: 675 | parts = package.split('==') 676 | package = parts[0].strip() 677 | ver = parts[1].strip() 678 | packages.append({'type': '==', 'pkg': package, 'ver': ver}) 679 | continue 680 | 681 | if re.search(r'[^ a-zA-Z0-9\-]', package): 682 | raise Exception('Unsupported requirements format version constraint: %s' % package) 683 | 684 | packages.append({'type': 'any', 'pkg': package}) 685 | 686 | return packages 687 | 688 | 689 | def _execute(params, cwd, retry=None, env=None): 690 | """ 691 | Executes a subprocess 692 | 693 | :param params: 694 | A list of the executable and arguments to pass to it 695 | 696 | :param cwd: 697 | The working directory to execute the command in 698 | 699 | :param retry: 700 | If this string is present in stderr, retry the operation 701 | 702 | :return: 703 | A 2-element tuple of (stdout, stderr) 704 | """ 705 | 706 | proc = subprocess.Popen( 707 | params, 708 | stdout=subprocess.PIPE, 709 | stderr=subprocess.PIPE, 710 | cwd=cwd, 711 | env=env 712 | ) 713 | stdout, stderr = proc.communicate() 714 | code = proc.wait() 715 | if code != 0: 716 | if retry and retry in stderr.decode('utf-8'): 717 | return _execute(params, cwd) 718 | e = OSError('subprocess exit code for "%s" was %d: %s' % (' '.join(params), code, stderr)) 719 | e.stdout = stdout 720 | e.stderr = stderr 721 | raise e 722 | return (stdout, stderr) 723 | -------------------------------------------------------------------------------- /dev/lint.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals, division, absolute_import, print_function 3 | 4 | import os 5 | 6 | from . import package_name, package_root 7 | 8 | import flake8 9 | if not hasattr(flake8, '__version_info__') or flake8.__version_info__ < (3,): 10 | from flake8.engine import get_style_guide 11 | else: 12 | from flake8.api.legacy import get_style_guide 13 | 14 | 15 | def run(): 16 | """ 17 | Runs flake8 lint 18 | 19 | :return: 20 | A bool - if flake8 did not find any errors 21 | """ 22 | 23 | print('Running flake8 %s' % flake8.__version__) 24 | 25 | flake8_style = get_style_guide(config_file=os.path.join(package_root, 'tox.ini')) 26 | 27 | paths = [] 28 | for _dir in [package_name, 'dev', 'tests']: 29 | for root, _, filenames in os.walk(_dir): 30 | for filename in filenames: 31 | if not filename.endswith('.py'): 32 | continue 33 | paths.append(os.path.join(root, filename)) 34 | report = flake8_style.check_files(paths) 35 | success = report.total_errors == 0 36 | if success: 37 | print('OK') 38 | return success 39 | -------------------------------------------------------------------------------- /dev/pyenv-install.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals, division, absolute_import, print_function 3 | 4 | import os 5 | import subprocess 6 | import sys 7 | 8 | 9 | run_args = [ 10 | { 11 | 'name': 'version', 12 | 'kwarg': 'version', 13 | }, 14 | ] 15 | 16 | 17 | def _write_env(env, key, value): 18 | sys.stdout.write("%s: %s\n" % (key, value)) 19 | sys.stdout.flush() 20 | if sys.version_info < (3,): 21 | env[key.encode('utf-8')] = value.encode('utf-8') 22 | else: 23 | env[key] = value 24 | 25 | 26 | def _shell_subproc(args): 27 | proc = subprocess.Popen( 28 | args, 29 | shell=True, 30 | stdout=subprocess.PIPE, 31 | stderr=subprocess.PIPE 32 | ) 33 | so, se = proc.communicate() 34 | stdout = so.decode('utf-8') 35 | stderr = se.decode('utf-8') 36 | return proc.returncode == 0, stdout, stderr 37 | 38 | 39 | def run(version=None): 40 | """ 41 | Installs a version of Python on Mac using pyenv 42 | 43 | :return: 44 | A bool - if Python was installed successfully 45 | """ 46 | 47 | if sys.platform == 'win32': 48 | raise ValueError('pyenv-install is not designed for Windows') 49 | 50 | if version not in set(['2.6', '2.7', '3.3']): 51 | raise ValueError('Invalid version: %r' % version) 52 | 53 | python_path = os.path.expanduser('~/.pyenv/versions/%s/bin' % version) 54 | if os.path.exists(os.path.join(python_path, 'python')): 55 | print(python_path) 56 | return True 57 | 58 | stdout = "" 59 | stderr = "" 60 | 61 | has_pyenv, _, _ = _shell_subproc('command -v pyenv') 62 | if not has_pyenv: 63 | success, stdout, stderr = _shell_subproc('brew install pyenv') 64 | if not success: 65 | print(stdout) 66 | print(stderr, file=sys.stderr) 67 | return False 68 | 69 | has_zlib, _, _ = _shell_subproc('brew list zlib') 70 | if not has_zlib: 71 | success, stdout, stderr = _shell_subproc('brew install zlib') 72 | if not success: 73 | print(stdout) 74 | print(stderr, file=sys.stderr) 75 | return False 76 | 77 | success, stdout, stderr = _shell_subproc('brew --prefix zlib') 78 | if not success: 79 | print(stdout) 80 | print(stderr, file=sys.stderr) 81 | return False 82 | zlib_prefix = stdout.strip() 83 | 84 | pyenv_script = './%s' % version 85 | try: 86 | with open(pyenv_script, 'wb') as f: 87 | if version == '2.6': 88 | contents = '#require_gcc\n' \ 89 | 'install_package "openssl-1.0.2k" "https://www.openssl.org/source/old/1.0.2/openssl-1.0.2k.tar.gz' \ 90 | '#6b3977c61f2aedf0f96367dcfb5c6e578cf37e7b8d913b4ecb6643c3cb88d8c0" mac_openssl\n' \ 91 | 'install_package "readline-8.0" "https://ftpmirror.gnu.org/readline/readline-8.0.tar.gz' \ 92 | '#e339f51971478d369f8a053a330a190781acb9864cf4c541060f12078948e461" mac_readline' \ 93 | ' --if has_broken_mac_readline\n' \ 94 | 'install_package "Python-2.6.9" "https://www.python.org/ftp/python/2.6.9/Python-2.6.9.tgz' \ 95 | '#7277b1285d8a82f374ef6ebaac85b003266f7939b3f2a24a3af52f9523ac94db" standard verify_py26' 96 | elif version == '2.7': 97 | contents = '#require_gcc\n' \ 98 | 'export PYTHON_BUILD_HOMEBREW_OPENSSL_FORMULA="openssl@1.1 openssl@1.0 openssl"\n' \ 99 | 'install_package "openssl-1.0.2q" "https://www.openssl.org/source/old/1.0.2/openssl-1.0.2q.tar.gz' \ 100 | '#5744cfcbcec2b1b48629f7354203bc1e5e9b5466998bbccc5b5fcde3b18eb684" mac_openssl ' \ 101 | '--if has_broken_mac_openssl\n' \ 102 | 'install_package "readline-8.0" "https://ftpmirror.gnu.org/readline/readline-8.0.tar.gz' \ 103 | '#e339f51971478d369f8a053a330a190781acb9864cf4c541060f12078948e461" mac_readline ' \ 104 | '--if has_broken_mac_readline\n' \ 105 | 'install_package "Python-2.7.18" "https://www.python.org/ftp/python/2.7.18/Python-2.7.18.tgz' \ 106 | '#da3080e3b488f648a3d7a4560ddee895284c3380b11d6de75edb986526b9a814" standard verify_py27 ' \ 107 | 'copy_python_gdb ensurepip\n' 108 | elif version == '3.3': 109 | contents = '#require_gcc\n' \ 110 | 'install_package "openssl-1.0.2k" "https://www.openssl.org/source/old/1.0.2/openssl-1.0.2k.tar.gz' \ 111 | '#6b3977c61f2aedf0f96367dcfb5c6e578cf37e7b8d913b4ecb6643c3cb88d8c0" mac_openssl\n' \ 112 | 'install_package "readline-8.0" "https://ftpmirror.gnu.org/readline/readline-8.0.tar.gz' \ 113 | '#e339f51971478d369f8a053a330a190781acb9864cf4c541060f12078948e461" mac_readline' \ 114 | ' --if has_broken_mac_readline\n' \ 115 | 'install_package "Python-3.3.7" "https://www.python.org/ftp/python/3.3.7/Python-3.3.7.tar.xz' \ 116 | '#85f60c327501c36bc18c33370c14d472801e6af2f901dafbba056f61685429fe" standard verify_py33' 117 | f.write(contents.encode('utf-8')) 118 | 119 | args = ['pyenv', 'install', pyenv_script] 120 | stdin = None 121 | stdin_contents = None 122 | env = os.environ.copy() 123 | 124 | _write_env(env, 'CFLAGS', '-I' + zlib_prefix + '/include') 125 | _write_env(env, 'LDFLAGS', '-L' + zlib_prefix + '/lib') 126 | 127 | if version == '2.6': 128 | _write_env(env, 'PYTHON_CONFIGURE_OPTS', '--enable-ipv6') 129 | stdin = subprocess.PIPE 130 | stdin_contents = '--- configure 2021-08-05 20:17:26.000000000 -0400\n' \ 131 | '+++ configure 2021-08-05 20:21:30.000000000 -0400\n' \ 132 | '@@ -10300,17 +10300,8 @@\n' \ 133 | ' rm -f core conftest.err conftest.$ac_objext \\\n' \ 134 | ' conftest$ac_exeext conftest.$ac_ext\n' \ 135 | ' \n' \ 136 | '-if test "$buggygetaddrinfo" = "yes"; then\n' \ 137 | '-\tif test "$ipv6" = "yes"; then\n' \ 138 | '-\t\techo \'Fatal: You must get working getaddrinfo() function.\'\n' \ 139 | '-\t\techo \' or you can specify "--disable-ipv6"\'.\n' \ 140 | '-\t\texit 1\n' \ 141 | '-\tfi\n' \ 142 | '-else\n' \ 143 | '-\n' \ 144 | ' $as_echo "#define HAVE_GETADDRINFO 1" >>confdefs.h\n' \ 145 | ' \n' \ 146 | '-fi\n' \ 147 | ' for ac_func in getnameinfo\n' \ 148 | ' do :\n' \ 149 | ' ac_fn_c_check_func "$LINENO" "getnameinfo" "ac_cv_func_getnameinfo"' 150 | stdin_contents = stdin_contents.encode('ascii') 151 | args.append('--patch') 152 | elif version == '3.3': 153 | stdin = subprocess.PIPE 154 | stdin_contents = '--- configure\n' \ 155 | '+++ configure\n' \ 156 | '@@ -3391,7 +3391,7 @@ $as_echo "#define _BSD_SOURCE 1" >>confdefs.h\n' \ 157 | ' # has no effect, don\'t bother defining them\n' \ 158 | ' Darwin/[6789].*)\n' \ 159 | ' define_xopen_source=no;;\n' \ 160 | '- Darwin/1[0-9].*)\n' \ 161 | '+ Darwin/[12][0-9].*)\n' \ 162 | ' define_xopen_source=no;;\n' \ 163 | ' # On AIX 4 and 5.1, mbstate_t is defined only when _XOPEN_SOURCE == 500 but\n' \ 164 | ' # used in wcsnrtombs() and mbsnrtowcs() even if _XOPEN_SOURCE is not defined\n' \ 165 | '--- configure.ac\n' \ 166 | '+++ configure.ac\n' \ 167 | '@@ 480,7 +480,7 @@ case $ac_sys_system/$ac_sys_release in\n' \ 168 | ' # has no effect, don\'t bother defining them\n' \ 169 | ' Darwin/@<:@6789@:>@.*)\n' \ 170 | ' define_xopen_source=no;;\n' \ 171 | '- Darwin/1@<:@0-9@:>@.*)\n' \ 172 | '+ Darwin/@<:@[12]@:>@@<:@0-9@:>@.*)\n' \ 173 | ' define_xopen_source=no;;\n' \ 174 | ' # On AIX 4 and 5.1, mbstate_t is defined only when _XOPEN_SOURCE == 500 but\n' \ 175 | ' # used in wcsnrtombs() and mbsnrtowcs() even if _XOPEN_SOURCE is not defined\n' 176 | stdin_contents = stdin_contents.encode('ascii') 177 | args.append('--patch') 178 | 179 | proc = subprocess.Popen( 180 | args, 181 | stdout=subprocess.PIPE, 182 | stderr=subprocess.PIPE, 183 | stdin=stdin, 184 | env=env 185 | ) 186 | so, se = proc.communicate(stdin_contents) 187 | stdout += so.decode('utf-8') 188 | stderr += se.decode('utf-8') 189 | 190 | if proc.returncode != 0: 191 | print(stdout) 192 | print(stderr, file=sys.stderr) 193 | return False 194 | 195 | finally: 196 | if os.path.exists(pyenv_script): 197 | os.unlink(pyenv_script) 198 | 199 | print(python_path) 200 | return True 201 | -------------------------------------------------------------------------------- /dev/python-install.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals, division, absolute_import, print_function 3 | 4 | import os 5 | import shutil 6 | import subprocess 7 | import sys 8 | from urllib.parse import urlparse 9 | from urllib.request import urlopen 10 | 11 | 12 | run_args = [ 13 | { 14 | 'name': 'version', 15 | 'kwarg': 'version', 16 | }, 17 | { 18 | 'name': 'arch', 19 | 'kwarg': 'arch', 20 | }, 21 | ] 22 | 23 | 24 | def run(version=None, arch=None): 25 | """ 26 | Installs a version of Python on Windows 27 | 28 | :return: 29 | A bool - if Python was installed successfully 30 | """ 31 | 32 | if sys.platform != 'win32': 33 | raise ValueError('python-install is only designed for Windows') 34 | 35 | if version not in set(['2.6', '2.7', '3.3']): 36 | raise ValueError('Invalid version: %r' % version) 37 | 38 | if arch not in set(['x86', 'x64']): 39 | raise ValueError('Invalid arch: %r' % arch) 40 | 41 | if version == '2.6': 42 | if arch == 'x64': 43 | url = 'https://www.python.org/ftp/python/2.6.6/python-2.6.6.amd64.msi' 44 | else: 45 | url = 'https://www.python.org/ftp/python/2.6.6/python-2.6.6.msi' 46 | elif version == '2.7': 47 | if arch == 'x64': 48 | url = 'https://www.python.org/ftp/python/2.7.18/python-2.7.18.amd64.msi' 49 | else: 50 | url = 'https://www.python.org/ftp/python/2.7.18/python-2.7.18.msi' 51 | else: 52 | if arch == 'x64': 53 | url = 'https://www.python.org/ftp/python/3.3.5/python-3.3.5.amd64.msi' 54 | else: 55 | url = 'https://www.python.org/ftp/python/3.3.5/python-3.3.5.msi' 56 | 57 | home = os.environ.get('USERPROFILE') 58 | msi_filename = os.path.basename(urlparse(url).path) 59 | msi_path = os.path.join(home, msi_filename) 60 | install_path = os.path.join(os.environ.get('LOCALAPPDATA'), 'Python%s-%s' % (version, arch)) 61 | 62 | if os.path.exists(os.path.join(install_path, 'python.exe')): 63 | print(install_path) 64 | return True 65 | 66 | try: 67 | with urlopen(url) as r, open(msi_path, 'wb') as f: 68 | shutil.copyfileobj(r, f) 69 | 70 | proc = subprocess.Popen( 71 | 'msiexec /passive /a %s TARGETDIR=%s' % (msi_filename, install_path), 72 | shell=True, 73 | cwd=home 74 | ) 75 | proc.communicate() 76 | 77 | finally: 78 | if os.path.exists(msi_path): 79 | os.unlink(msi_path) 80 | 81 | print(install_path) 82 | return True 83 | -------------------------------------------------------------------------------- /dev/release.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals, division, absolute_import, print_function 3 | 4 | import subprocess 5 | import sys 6 | 7 | import twine.cli 8 | 9 | from . import package_name, package_root, has_tests_package 10 | from .build import run as build 11 | 12 | 13 | def run(): 14 | """ 15 | Creates a sdist .tar.gz and a bdist_wheel --univeral .whl and uploads 16 | them to pypi 17 | 18 | :return: 19 | A bool - if the packaging and upload process was successful 20 | """ 21 | 22 | git_wc_proc = subprocess.Popen( 23 | ['git', 'status', '--porcelain', '-uno'], 24 | stdout=subprocess.PIPE, 25 | stderr=subprocess.STDOUT, 26 | cwd=package_root 27 | ) 28 | git_wc_status, _ = git_wc_proc.communicate() 29 | 30 | if len(git_wc_status) > 0: 31 | print(git_wc_status.decode('utf-8').rstrip(), file=sys.stderr) 32 | print('Unable to perform release since working copy is not clean', file=sys.stderr) 33 | return False 34 | 35 | git_tag_proc = subprocess.Popen( 36 | ['git', 'tag', '-l', '--contains', 'HEAD'], 37 | stdout=subprocess.PIPE, 38 | stderr=subprocess.PIPE, 39 | cwd=package_root 40 | ) 41 | tag, tag_error = git_tag_proc.communicate() 42 | 43 | if len(tag_error) > 0: 44 | print(tag_error.decode('utf-8').rstrip(), file=sys.stderr) 45 | print('Error looking for current git tag', file=sys.stderr) 46 | return False 47 | 48 | if len(tag) == 0: 49 | print('No git tag found on HEAD', file=sys.stderr) 50 | return False 51 | 52 | tag = tag.decode('ascii').strip() 53 | 54 | build() 55 | 56 | twine.cli.dispatch(['upload', 'dist/%s-%s*' % (package_name, tag)]) 57 | if has_tests_package: 58 | twine.cli.dispatch(['upload', 'dist/%s_tests-%s*' % (package_name, tag)]) 59 | 60 | return True 61 | -------------------------------------------------------------------------------- /dev/tests.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals, division, absolute_import, print_function 3 | 4 | import unittest 5 | import re 6 | import sys 7 | import warnings 8 | 9 | from . import requires_oscrypto 10 | from ._import import _preload 11 | 12 | from tests import test_classes 13 | 14 | if sys.version_info < (3,): 15 | range = xrange # noqa 16 | from cStringIO import StringIO 17 | else: 18 | from io import StringIO 19 | 20 | 21 | run_args = [ 22 | { 23 | 'name': 'regex', 24 | 'kwarg': 'matcher', 25 | }, 26 | { 27 | 'name': 'repeat_count', 28 | 'kwarg': 'repeat', 29 | 'cast': 'int', 30 | }, 31 | ] 32 | 33 | 34 | def run(matcher=None, repeat=1, ci=False): 35 | """ 36 | Runs the tests 37 | 38 | :param matcher: 39 | A unicode string containing a regular expression to use to filter test 40 | names by. A value of None will cause no filtering. 41 | 42 | :param repeat: 43 | An integer - the number of times to run the tests 44 | 45 | :param ci: 46 | A bool, indicating if the tests are being run as part of CI 47 | 48 | :return: 49 | A bool - if the tests succeeded 50 | """ 51 | 52 | _preload(requires_oscrypto, not ci) 53 | 54 | warnings.filterwarnings("error") 55 | 56 | loader = unittest.TestLoader() 57 | # We have to manually track the list of applicable tests because for 58 | # some reason with Python 3.4 on Windows, the tests in a suite are replaced 59 | # with None after being executed. This breaks the repeat functionality. 60 | test_list = [] 61 | for test_class in test_classes(): 62 | if matcher: 63 | names = loader.getTestCaseNames(test_class) 64 | for name in names: 65 | if re.search(matcher, name): 66 | test_list.append(test_class(name)) 67 | else: 68 | test_list.append(loader.loadTestsFromTestCase(test_class)) 69 | 70 | stream = sys.stdout 71 | verbosity = 1 72 | if matcher and repeat == 1: 73 | verbosity = 2 74 | elif repeat > 1: 75 | stream = StringIO() 76 | 77 | for _ in range(0, repeat): 78 | suite = unittest.TestSuite() 79 | for test in test_list: 80 | suite.addTest(test) 81 | result = unittest.TextTestRunner(stream=stream, verbosity=verbosity).run(suite) 82 | 83 | if len(result.errors) > 0 or len(result.failures) > 0: 84 | if repeat > 1: 85 | print(stream.getvalue()) 86 | return False 87 | 88 | if repeat > 1: 89 | stream.truncate(0) 90 | 91 | return True 92 | -------------------------------------------------------------------------------- /dev/version.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals, division, absolute_import, print_function 3 | 4 | import codecs 5 | import os 6 | import re 7 | 8 | from . import package_root, package_name, has_tests_package 9 | 10 | 11 | run_args = [ 12 | { 13 | 'name': 'pep440_version', 14 | 'required': True 15 | }, 16 | ] 17 | 18 | 19 | def run(new_version): 20 | """ 21 | Updates the package version in the various locations 22 | 23 | :param new_version: 24 | A unicode string of the new library version as a PEP 440 version 25 | 26 | :return: 27 | A bool - if the version number was successfully bumped 28 | """ 29 | 30 | # We use a restricted form of PEP 440 versions 31 | version_match = re.match( 32 | r'(\d+)\.(\d+)\.(\d)+(?:\.((?:dev|a|b|rc)\d+))?$', 33 | new_version 34 | ) 35 | if not version_match: 36 | raise ValueError('Invalid PEP 440 version: %s' % new_version) 37 | 38 | new_version_info = ( 39 | int(version_match.group(1)), 40 | int(version_match.group(2)), 41 | int(version_match.group(3)), 42 | ) 43 | if version_match.group(4): 44 | new_version_info += (version_match.group(4),) 45 | 46 | version_path = os.path.join(package_root, package_name, 'version.py') 47 | setup_path = os.path.join(package_root, 'setup.py') 48 | setup_tests_path = os.path.join(package_root, 'tests', 'setup.py') 49 | tests_path = os.path.join(package_root, 'tests', '__init__.py') 50 | 51 | file_paths = [version_path, setup_path] 52 | if has_tests_package: 53 | file_paths.extend([setup_tests_path, tests_path]) 54 | 55 | for file_path in file_paths: 56 | orig_source = '' 57 | with codecs.open(file_path, 'r', encoding='utf-8') as f: 58 | orig_source = f.read() 59 | 60 | found = 0 61 | new_source = '' 62 | for line in orig_source.splitlines(True): 63 | if line.startswith('__version__ = '): 64 | found += 1 65 | new_source += '__version__ = %r\n' % new_version 66 | elif line.startswith('__version_info__ = '): 67 | found += 1 68 | new_source += '__version_info__ = %r\n' % (new_version_info,) 69 | elif line.startswith('PACKAGE_VERSION = '): 70 | found += 1 71 | new_source += 'PACKAGE_VERSION = %r\n' % new_version 72 | else: 73 | new_source += line 74 | 75 | if found == 0: 76 | raise ValueError('Did not find any versions in %s' % file_path) 77 | 78 | s = 's' if found > 1 else '' 79 | rel_path = file_path[len(package_root) + 1:] 80 | was_were = 'was' if found == 1 else 'were' 81 | if new_source != orig_source: 82 | print('Updated %d version%s in %s' % (found, s, rel_path)) 83 | with codecs.open(file_path, 'w', encoding='utf-8') as f: 84 | f.write(new_source) 85 | else: 86 | print('%d version%s in %s %s up-to-date' % (found, s, rel_path, was_were)) 87 | 88 | return True 89 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # ocspbuilder API Documentation 2 | 3 | - [`OCSPRequestBuilder()`](#ocsprequestbuilder-class) 4 | - [`OCSPResponseBuilder()`](#ocspresponsebuilder-class) 5 | 6 | ### `OCSPRequestBuilder()` class 7 | 8 | > ##### constructor 9 | > 10 | > > ```python 11 | > > def __init__(self, certificate, issuer): 12 | > > """ 13 | > > :param certificate: 14 | > > An asn1crypto.x509.Certificate or oscrypto.asymmetric.Certificate 15 | > > object to create the request for 16 | > > 17 | > > :param issuer: 18 | > > An asn1crypto.x509.Certificate or oscrypto.asymmetric.Certificate 19 | > > object for the issuer of the certificate 20 | > > """ 21 | > > ``` 22 | > 23 | > ##### `.certificate` attribute 24 | > 25 | > > An asn1crypto.x509.Certificate or oscrypto.asymmetric.Certificate object 26 | > > of the certificate to create the request for. 27 | > 28 | > ##### `.issuer` attribute 29 | > 30 | > > An asn1crypto.x509.Certificate or oscrypto.asymmetric.Certificate object 31 | > > of the issuer. 32 | > 33 | > ##### `.hash_algo` attribute 34 | > 35 | > > A unicode string of the hash algorithm to use when signing the 36 | > > request - "sha1", "sha256" (default) or "sha512". 37 | > 38 | > ##### `.key_hash_algo` attribute 39 | > 40 | > > A unicode string of the hash algorithm to use when creating the 41 | > > certificate identifier - "sha1" (default), or "sha256". 42 | > 43 | > ##### `.nonce` attribute 44 | > 45 | > > A bool - if the nonce extension should be used to prevent replay 46 | > > attacks. 47 | > 48 | > ##### `.set_extension()` method 49 | > 50 | > > ```python 51 | > > def set_extension(self, name, value): 52 | > > """ 53 | > > :param name: 54 | > > A unicode string of an extension id name from 55 | > > asn1crypto.ocsp.TBSRequestExtensionId or 56 | > > asn1crypto.ocsp.RequestExtensionId. If the extension is not one 57 | > > defined in those classes, this must be an instance of one of the 58 | > > classes instead of a unicode string. 59 | > > 60 | > > :param value: 61 | > > A value object per the specs defined by 62 | > > asn1crypto.ocsp.TBSRequestExtension or 63 | > > asn1crypto.ocsp.RequestExtension 64 | > > """ 65 | > > ``` 66 | > > 67 | > > Sets the value for an extension using a fully constructed 68 | > > asn1crypto.core.Asn1Value object. Normally this should not be needed, 69 | > > and the convenience attributes should be sufficient. 70 | > > 71 | > > See the definition of asn1crypto.ocsp.TBSRequestExtension and 72 | > > asn1crypto.ocsp.RequestExtension to determine the appropriate object 73 | > > type for a given extension. Extensions are marked as critical when RFC 74 | > > 6960 indicates so. 75 | > 76 | > ##### `.build()` method 77 | > 78 | > > ```python 79 | > > def build(self, requestor_private_key=None, requestor_certificate=None, other_certificates=None): 80 | > > """ 81 | > > :param requestor_private_key: 82 | > > An asn1crypto.keys.PrivateKeyInfo or oscrypto.asymmetric.PrivateKey 83 | > > object for the private key to sign the request with 84 | > > 85 | > > :param requestor_certificate: 86 | > > An asn1crypto.x509.Certificate or oscrypto.asymmetric.Certificate 87 | > > object of the certificate associated with the private key 88 | > > 89 | > > :param other_certificates: 90 | > > A list of asn1crypto.x509.Certificate or 91 | > > oscrypto.asymmetric.Certificate objects that may be useful for the 92 | > > OCSP server to verify the request signature. Intermediate 93 | > > certificates would be specified here. 94 | > > 95 | > > :return: 96 | > > An asn1crypto.ocsp.OCSPRequest object of the request 97 | > > """ 98 | > > ``` 99 | > > 100 | > > Validates the request information, constructs the ASN.1 structure and 101 | > > then optionally signs it. 102 | > > 103 | > > The requestor_private_key, requestor_certificate and other_certificates 104 | > > params are all optional and only necessary if the request needs to be 105 | > > signed. Signing a request is uncommon for OCSP requests related to web 106 | > > TLS connections. 107 | 108 | ### `OCSPResponseBuilder()` class 109 | 110 | > ##### constructor 111 | > 112 | > > ```python 113 | > > def __init__(self, response_status, certificate=None, certificate_status=None, revocation_date=None): 114 | > > """ 115 | > > :param response_status: 116 | > > A unicode string of OCSP response type: 117 | > > 118 | > > - "successful" - when the response includes information about the certificate 119 | > > - "malformed_request" - when the request could not be understood 120 | > > - "internal_error" - when an internal error occured with the OCSP responder 121 | > > - "try_later" - when the OCSP responder is temporarily unavailable 122 | > > - "sign_required" - when the OCSP request must be signed 123 | > > - "unauthorized" - when the responder is not the correct responder for the certificate 124 | > > 125 | > > :param certificate: 126 | > > An asn1crypto.x509.Certificate or oscrypto.asymmetric.Certificate 127 | > > object of the certificate the response is about. Only required if 128 | > > the response_status is "successful". 129 | > > 130 | > > :param certificate_status: 131 | > > A unicode string of the status of the certificate. Only required if 132 | > > the response_status is "successful". 133 | > > 134 | > > - "good" - when the certificate is in good standing 135 | > > - "revoked" - when the certificate is revoked without a reason code 136 | > > - "key_compromise" - when a private key is compromised 137 | > > - "ca_compromise" - when the CA issuing the certificate is compromised 138 | > > - "affiliation_changed" - when the certificate subject name changed 139 | > > - "superseded" - when the certificate was replaced with a new one 140 | > > - "cessation_of_operation" - when the certificate is no longer needed 141 | > > - "certificate_hold" - when the certificate is temporarily invalid 142 | > > - "remove_from_crl" - only delta CRLs - when temporary hold is removed 143 | > > - "privilege_withdrawn" - one of the usages for a certificate was removed 144 | > > - "unknown" - the responder doesn't know about the certificate being requested 145 | > > 146 | > > :param revocation_date: 147 | > > A datetime.datetime object of when the certificate was revoked, if 148 | > > the response_status is "successful" and the certificate status is 149 | > > not "good" or "unknown". 150 | > > """ 151 | > > ``` 152 | > > 153 | > > Unless changed, responses will use SHA-256 for the signature, 154 | > > and will be valid from the moment created for one week. 155 | > 156 | > ##### `.response_status` attribute 157 | > 158 | > > The overall status of the response. Only a "successful" response will 159 | > > include information about the certificate. Other response types are for 160 | > > signaling info about the OCSP responder. Valid values include: 161 | > > 162 | > > - "successful" - when the response includes information about the certificate 163 | > > - "malformed_request" - when the request could not be understood 164 | > > - "internal_error" - when an internal error occured with the OCSP responder 165 | > > - "try_later" - when the OCSP responder is temporarily unavailable 166 | > > - "sign_required" - when the OCSP request must be signed 167 | > > - "unauthorized" - when the responder is not the correct responder for the certificate 168 | > 169 | > ##### `.certificate` attribute 170 | > 171 | > > An asn1crypto.x509.Certificate or oscrypto.asymmetric.Certificate object 172 | > > of the certificate the response is about. 173 | > 174 | > ##### `.certificate_status` attribute 175 | > 176 | > > A unicode string of the status of the certificate. Valid values include: 177 | > > 178 | > > - "good" - when the certificate is in good standing 179 | > > - "revoked" - when the certificate is revoked without a reason code 180 | > > - "key_compromise" - when a private key is compromised 181 | > > - "ca_compromise" - when the CA issuing the certificate is compromised 182 | > > - "affiliation_changed" - when the certificate subject name changed 183 | > > - "superseded" - when the certificate was replaced with a new one 184 | > > - "cessation_of_operation" - when the certificate is no longer needed 185 | > > - "certificate_hold" - when the certificate is temporarily invalid 186 | > > - "remove_from_crl" - only delta CRLs - when temporary hold is removed 187 | > > - "privilege_withdrawn" - one of the usages for a certificate was removed 188 | > > - "unknown" - when the responder doesn't know about the certificate being requested 189 | > 190 | > ##### `.revocation_date` attribute 191 | > 192 | > > A datetime.datetime object of when the certificate was revoked, if the 193 | > > status is not "good" or "unknown". 194 | > 195 | > ##### `.certificate_issuer` attribute 196 | > 197 | > > An asn1crypto.x509.Certificate object of the issuer of the certificate. 198 | > > This should only be set if the OCSP responder is not the issuer of 199 | > > the certificate, but instead a special certificate only for OCSP 200 | > > responses. 201 | > 202 | > ##### `.hash_algo` attribute 203 | > 204 | > > A unicode string of the hash algorithm to use when signing the 205 | > > request - "sha1", "sha256" (default) or "sha512". 206 | > 207 | > ##### `.key_hash_algo` attribute 208 | > 209 | > > A unicode string of the hash algorithm to use when creating the 210 | > > certificate identifier - "sha1" (default), or "sha256". 211 | > 212 | > ##### `.nonce` attribute 213 | > 214 | > > The nonce that was provided during the request. 215 | > 216 | > ##### `.this_update` attribute 217 | > 218 | > > A datetime.datetime object of when the response was generated. 219 | > 220 | > ##### `.next_update` attribute 221 | > 222 | > > A datetime.datetime object of when the response may next change. This 223 | > > should only be set if responses are cached. If responses are generated 224 | > > fresh on every request, this should not be set. 225 | > 226 | > ##### `.set_extension()` method 227 | > 228 | > > ```python 229 | > > def set_extension(self, name, value): 230 | > > """ 231 | > > :param name: 232 | > > A unicode string of an extension id name from 233 | > > asn1crypto.ocsp.SingleResponseExtensionId or 234 | > > asn1crypto.ocsp.ResponseDataExtensionId. If the extension is not one 235 | > > defined in those classes, this must be an instance of one of the 236 | > > classes instead of a unicode string. 237 | > > 238 | > > :param value: 239 | > > A value object per the specs defined by 240 | > > asn1crypto.ocsp.SingleResponseExtension or 241 | > > asn1crypto.ocsp.ResponseDataExtension 242 | > > """ 243 | > > ``` 244 | > > 245 | > > Sets the value for an extension using a fully constructed 246 | > > asn1crypto.core.Asn1Value object. Normally this should not be needed, 247 | > > and the convenience attributes should be sufficient. 248 | > > 249 | > > See the definition of asn1crypto.ocsp.SingleResponseExtension and 250 | > > asn1crypto.ocsp.ResponseDataExtension to determine the appropriate 251 | > > object type for a given extension. Extensions are marked as critical 252 | > > when RFC 6960 indicates so. 253 | > 254 | > ##### `.build()` method 255 | > 256 | > > ```python 257 | > > def build(self, responder_private_key=None, responder_certificate=None): 258 | > > """ 259 | > > :param responder_private_key: 260 | > > An asn1crypto.keys.PrivateKeyInfo or oscrypto.asymmetric.PrivateKey 261 | > > object for the private key to sign the response with 262 | > > 263 | > > :param responder_certificate: 264 | > > An asn1crypto.x509.Certificate or oscrypto.asymmetric.Certificate 265 | > > object of the certificate associated with the private key 266 | > > 267 | > > :return: 268 | > > An asn1crypto.ocsp.OCSPResponse object of the response 269 | > > """ 270 | > > ``` 271 | > > 272 | > > Validates the request information, constructs the ASN.1 structure and 273 | > > signs it. 274 | > > 275 | > > The responder_private_key and responder_certificate parameters are only 276 | > > required if the response_status is "successful". 277 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # ocspbuilder Documentation 2 | 3 | *ocspbuilder* is a Python library for constructing OCSP requests and responses. 4 | It provides a high-level interface with knowledge of RFC 6960 to produce, valid, 5 | correct OCSP messages without terrible APIs or hunting through RFCs. 6 | 7 | Since its only dependencies are the 8 | [*asn1crypto*](https://github.com/wbond/asn1crypto#readme) and 9 | [*oscrypto*](https://github.com/wbond/oscrypto#readme) libraries, it is 10 | easy to install and use on Windows, OS X, Linux and the BSDs. 11 | 12 | The documentation consists of the following topics: 13 | 14 | - [Generating a Request](#generating-a-request) 15 | - [Constructing a Response](#constructing-a-response) 16 | - [API Documentation](api.md) 17 | 18 | ## Generating a Request 19 | 20 | A basic OCSP request requires the certificate to obtain the status of, and the 21 | issuer certificate: 22 | 23 | ```python 24 | from oscrypto import asymmetric 25 | from ocspbuilder import OCSPRequestBuilder 26 | 27 | 28 | subject_cert = asymmetric.load_certificate('/path/to/certificate.crt') 29 | issuer_cert = asymmetric.load_certificate('/path/to/issuer.crt') 30 | 31 | builder = OCSPRequestBuilder(subject_cert, issuer_cert) 32 | ocsp_request = builder.build() 33 | 34 | with open('/path/to/cached_request.der', 'wb') as f: 35 | f.write(ocsp_request.dump()) 36 | ``` 37 | 38 | ## Constructing a Response 39 | 40 | To construct a OCSP response, a few pieces of information are necessary: 41 | 42 | - subject certificate 43 | - certificate status 44 | - revocation date and reason (if revoked) 45 | - issuer certificate and key, or purpose-created OCSP responder certificate 46 | and key 47 | 48 | The following code shows examples of constructing the response for a certificate 49 | in good standing, a revoked certificate and finally a response from an OCSP 50 | responder certificate, instead of the certificate issuer. 51 | 52 | ```python 53 | from datetime import datetime 54 | from asn1crypto.util import timezone 55 | from oscrypto import asymmetric 56 | from ocspbuilder import OCSPResponseBuilder 57 | 58 | 59 | subject_cert = asymmetric.load_certificate('/path/to/certificate.crt') 60 | issuer_cert = asymmetric.load_certificate('/path/to/issuer.crt') 61 | issuer_key = asymmetric.load_private_key('/path/to/issuer.key') 62 | 63 | 64 | # A response for a certificate in good standing 65 | builder = OCSPResponseBuilder('successful', subject_cert, 'good') 66 | ocsp_response = builder.build(issuer_key, issuer_cert) 67 | 68 | with open('/path/to/cached_response.der', 'wb') as f: 69 | f.write(ocsp_response.dump()) 70 | 71 | 72 | # A response for a certificate that has been revoked 73 | revocation_date = datetime(2015, 10, 20, 12, 0, 0, tzinfo=timezone.utc) 74 | builder = OCSPResponseBuilder('successful', subject_cert, 'key_compromise', revocation_date) 75 | ocsp_response = builder.build(issuer_key, issuer_cert) 76 | 77 | with open('/path/to/cached_revoked_response.der', 'wb') as f: 78 | f.write(ocsp_response.dump()) 79 | 80 | 81 | # A response from a special OCSP response certificate/key 82 | responder_cert = asymmetric.load_certificate('/path/to/responder.crt') 83 | responder_key = asymmetric.load_private_key('/path/to/responder.key') 84 | 85 | builder = OCSPResponseBuilder('successful', subject_cert, 'good') 86 | builder.certificate_issuer = issuer_cert 87 | ocsp_response = builder.build(responder_key, responder_cert) 88 | 89 | with open('/path/to/cached_responder_response.der', 'wb') as f: 90 | f.write(ocsp_response.dump()) 91 | ``` 92 | -------------------------------------------------------------------------------- /ocspbuilder/version.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals, division, absolute_import, print_function 3 | 4 | 5 | __version__ = '0.11.0.dev1' 6 | __version_info__ = (0, 11, 0, 'dev1') 7 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ocspbuilder 2 | 3 | A Python library for creating and signing online certificate status protocol 4 | (OCSP) requests and responses for X.509 certificates. 5 | 6 | - [Related Crypto Libraries](#related-crypto-libraries) 7 | - [Current Release](#current-release) 8 | - [Dependencies](#dependencies) 9 | - [Installation](#installation) 10 | - [License](#license) 11 | - [Documentation](#documentation) 12 | - [Continuous Integration](#continuous-integration) 13 | - [Testing](#testing) 14 | - [Development](#development) 15 | - [CI Tasks](#ci-tasks) 16 | 17 | [![GitHub Actions CI](https://github.com/wbond/ocspbuilder/workflows/CI/badge.svg)](https://github.com/wbond/ocspbuilder/actions?workflow=CI) 18 | [![CircleCI](https://circleci.com/gh/wbond/ocspbuilder.svg?style=shield)](https://circleci.com/gh/wbond/ocspbuilder) 19 | [![PyPI](https://img.shields.io/pypi/v/ocspbuilder.svg)](https://pypi.python.org/pypi/ocspbuilder) 20 | 21 | ## Related Crypto Libraries 22 | 23 | *ocspbuilder* is part of the modularcrypto family of Python packages: 24 | 25 | - [asn1crypto](https://github.com/wbond/asn1crypto) 26 | - [oscrypto](https://github.com/wbond/oscrypto) 27 | - [csrbuilder](https://github.com/wbond/csrbuilder) 28 | - [certbuilder](https://github.com/wbond/certbuilder) 29 | - [crlbuilder](https://github.com/wbond/crlbuilder) 30 | - [ocspbuilder](https://github.com/wbond/ocspbuilder) 31 | - [certvalidator](https://github.com/wbond/certvalidator) 32 | 33 | ## Current Release 34 | 35 | 0.10.2 - [changelog](changelog.md) 36 | 37 | ## Dependencies 38 | 39 | - [*asn1crypto*](https://github.com/wbond/asn1crypto) 40 | - [*oscrypto*](https://github.com/wbond/oscrypto) 41 | - Python 2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9 or pypy 42 | 43 | ## Installation 44 | 45 | ```bash 46 | pip install ocspbuilder 47 | ``` 48 | 49 | ## License 50 | 51 | *ocspbuilder* is licensed under the terms of the MIT license. See the 52 | [LICENSE](LICENSE) file for the exact license text. 53 | 54 | ## Documentation 55 | 56 | [*ocspbuilder* documentation](docs/readme.md) 57 | 58 | ## Continuous Integration 59 | 60 | - [macOS, Linux, Windows](https://github.com/wbond/ocspbuilder/actions/workflows/ci.yml) via GitHub Actions 61 | - [arm64](https://circleci.com/gh/wbond/ocspbuilder) via CircleCI 62 | 63 | ## Testing 64 | 65 | Tests are written using `unittest` and require no third-party packages. 66 | 67 | Depending on what type of source is available for the package, the following 68 | commands can be used to run the test suite. 69 | 70 | ### Git Repository 71 | 72 | When working within a Git working copy, or an archive of the Git repository, 73 | the full test suite is run via: 74 | 75 | ```bash 76 | python run.py tests 77 | ``` 78 | 79 | To run only some tests, pass a regular expression as a parameter to `tests`. 80 | 81 | ```bash 82 | python run.py tests build 83 | ``` 84 | 85 | ### PyPi Source Distribution 86 | 87 | When working within an extracted source distribution (aka `.tar.gz`) from 88 | PyPi, the full test suite is run via: 89 | 90 | ```bash 91 | python setup.py test 92 | ``` 93 | 94 | ## Development 95 | 96 | To install the package used for linting, execute: 97 | 98 | ```bash 99 | pip install --user -r requires/lint 100 | ``` 101 | 102 | The following command will run the linter: 103 | 104 | ```bash 105 | python run.py lint 106 | ``` 107 | 108 | Support for code coverage can be installed via: 109 | 110 | ```bash 111 | pip install --user -r requires/coverage 112 | ``` 113 | 114 | Coverage is measured by running: 115 | 116 | ```bash 117 | python run.py coverage 118 | ``` 119 | 120 | To install the packages requires to generate the API documentation, run: 121 | 122 | ```bash 123 | pip install --user -r requires/api_docs 124 | ``` 125 | 126 | The documentation can then be generated by running: 127 | 128 | ```bash 129 | python run.py api_docs 130 | ``` 131 | 132 | To change the version number of the package, run: 133 | 134 | ```bash 135 | python run.py version {pep440_version} 136 | ``` 137 | 138 | To install the necessary packages for releasing a new version on PyPI, run: 139 | 140 | ```bash 141 | pip install --user -r requires/release 142 | ``` 143 | 144 | Releases are created by: 145 | 146 | - Making a git tag in [PEP 440](https://www.python.org/dev/peps/pep-0440/#examples-of-compliant-version-schemes) format 147 | - Running the command: 148 | 149 | ```bash 150 | python run.py release 151 | ``` 152 | 153 | Existing releases can be found at https://pypi.org/project/ocspbuilder. 154 | 155 | ## CI Tasks 156 | 157 | A task named `deps` exists to ensure a modern version of `pip` is installed, 158 | along with all necessary testing dependencies. 159 | 160 | The `ci` task runs `lint` (if flake8 is avaiable for the version of Python) and 161 | `coverage` (or `tests` if coverage is not available for the version of Python). 162 | If the current directory is a clean git working copy, the coverage data is 163 | submitted to codecov.io. 164 | 165 | ```bash 166 | python run.py deps 167 | python run.py ci 168 | ``` 169 | -------------------------------------------------------------------------------- /requires/api_docs: -------------------------------------------------------------------------------- 1 | CommonMark >= 0.6.0 2 | -------------------------------------------------------------------------------- /requires/ci: -------------------------------------------------------------------------------- 1 | setuptools == 36.8.0 ; python_version == '2.6' 2 | setuptools == 44.1.1 ; python_version == '2.7' and sys_platform == 'win32' 3 | setuptools == 18.4 ; python_version == '3.2' 4 | setuptools == 39.2.0 ; python_version == '3.3' 5 | https://github.com/wbond/asn1crypto/archive/master.zip 6 | https://github.com/wbond/oscrypto/archive/master.zip 7 | https://github.com/wbond/oscrypto/archive/master.zip#egg=oscrypto_tests&subdirectory=tests 8 | -r ./coverage 9 | -r ./lint 10 | # cffi 3.15.0 is required for Python 3.10 11 | cffi == 1.15.0 ; (python_version == '2.7' or python_version >= '3.6') and sys_platform == 'darwin' 12 | pycparser == 2.19 ; (python_version == '2.7' or python_version >= '3.6') and sys_platform == 'darwin' 13 | -------------------------------------------------------------------------------- /requires/coverage: -------------------------------------------------------------------------------- 1 | coverage == 4.4.1 ; python_version == '2.6' 2 | coverage == 4.5.4 ; python_version == '3.3' or python_version == '3.4' 3 | coverage == 5.5 ; python_version == '2.7' or python_version == '3.5' or python_version == '3.6' 4 | coverage == 6.5.0 ; python_version == '3.7' 5 | coverage == 7.3.0 ; python_version >= '3.8' 6 | -------------------------------------------------------------------------------- /requires/lint: -------------------------------------------------------------------------------- 1 | setuptools >= 39.0.1 ; python_version == '2.7' or python_version >= '3.3' 2 | enum34 == 1.1.6 ; python_version == '2.7' or python_version == '3.3' 3 | configparser == 3.5.0 ; python_version == '2.7' 4 | mccabe == 0.6.1 ; python_version == '3.3' 5 | pycodestyle == 2.3.1 ; python_version == '3.3' 6 | pyflakes == 1.6.0 ; python_version == '3.3' 7 | flake8 == 3.5.0 ; python_version == '3.3' 8 | mccabe == 0.6.1 ; python_version == '2.7' or python_version >= '3.4' 9 | pycodestyle == 2.5.0 ; python_version == '2.7' or python_version >= '3.4' 10 | pyflakes == 2.1.1 ; python_version == '2.7' or python_version >= '3.4' 11 | functools32 == 3.2.3-2 ; python_version == '2.7' 12 | typing == 3.7.4.1 ; python_version == '2.7' or python_version == '3.4' 13 | entrypoints == 0.3 ; python_version == '2.7' or python_version >= '3.4' 14 | flake8 == 3.7.9 ; python_version == '2.7' or python_version >= '3.4' 15 | -------------------------------------------------------------------------------- /requires/release: -------------------------------------------------------------------------------- 1 | wheel >= 0.31.0 2 | twine >= 1.11.0 3 | setuptools >= 38.6.0 4 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from __future__ import unicode_literals, division, absolute_import, print_function 4 | 5 | from dev._task import run_task 6 | 7 | 8 | run_task() 9 | -------------------------------------------------------------------------------- /scripts/create_pair.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals, division, absolute_import, print_function 3 | 4 | import os 5 | 6 | from oscrypto import asymmetric 7 | from certbuilder import CertificateBuilder 8 | 9 | 10 | fixtures_dir = os.path.join(os.path.dirname(__file__), '..', 'tests', 'fixtures') 11 | 12 | 13 | root_ca_private_key = asymmetric.load_private_key(os.path.join(fixtures_dir, 'test.key')) 14 | root_ca_certificate = asymmetric.load_certificate(os.path.join(fixtures_dir, 'test.crt')) 15 | 16 | root_ocsp_public_key, root_ocsp_private_key = asymmetric.generate_pair('rsa', bit_size=2048) 17 | 18 | with open(os.path.join(fixtures_dir, 'test-ocsp.key'), 'wb') as f: 19 | f.write(asymmetric.dump_private_key(root_ocsp_private_key, 'password', target_ms=20)) 20 | 21 | builder = CertificateBuilder( 22 | { 23 | 'country_name': 'US', 24 | 'state_or_province_name': 'Massachusetts', 25 | 'locality_name': 'Newbury', 26 | 'organization_name': 'Codex Non Sufficit LC', 27 | 'organization_unit_name': 'Testing', 28 | 'common_name': 'CodexNS OCSP Responder', 29 | }, 30 | root_ocsp_public_key 31 | ) 32 | builder.extended_key_usage = set(['ocsp_signing']) 33 | builder.issuer = root_ca_certificate 34 | root_ocsp_certificate = builder.build(root_ca_private_key) 35 | 36 | with open(os.path.join(fixtures_dir, 'test-ocsp.crt'), 'wb') as f: 37 | f.write(asymmetric.dump_certificate(root_ocsp_certificate)) 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os 3 | import shutil 4 | import sys 5 | import warnings 6 | 7 | import setuptools 8 | from setuptools import find_packages, setup, Command 9 | from setuptools.command.egg_info import egg_info 10 | 11 | 12 | PACKAGE_NAME = 'ocspbuilder' 13 | PACKAGE_VERSION = '0.11.0.dev1' 14 | PACKAGE_ROOT = os.path.dirname(os.path.abspath(__file__)) 15 | 16 | 17 | # setuptools 38.6.0 and newer know about long_description_content_type, but 18 | # distutils still complains about it, so silence the warning 19 | sv = setuptools.__version__ 20 | svi = tuple(int(o) if o.isdigit() else o for o in sv.split('.')) 21 | if svi >= (38, 6): 22 | warnings.filterwarnings( 23 | 'ignore', 24 | "Unknown distribution option: 'long_description_content_type'", 25 | module='distutils.dist' 26 | ) 27 | 28 | 29 | # This allows us to send the LICENSE and docs when creating a sdist. Wheels 30 | # automatically include the LICENSE, and don't need the docs. For these 31 | # to be included, the command must be "python setup.py sdist". 32 | package_data = {} 33 | if sys.argv[1:] == ['sdist'] or sorted(sys.argv[1:]) == ['-q', 'sdist']: 34 | package_data[PACKAGE_NAME] = [ 35 | '../LICENSE', 36 | '../*.md', 37 | '../docs/*.md', 38 | ] 39 | 40 | 41 | # Ensures a copy of the LICENSE is included with the egg-info for 42 | # install and bdist_egg commands 43 | class EggInfoCommand(egg_info): 44 | def run(self): 45 | egg_info_path = os.path.join( 46 | PACKAGE_ROOT, 47 | '%s.egg-info' % PACKAGE_NAME 48 | ) 49 | if not os.path.exists(egg_info_path): 50 | os.mkdir(egg_info_path) 51 | shutil.copy2( 52 | os.path.join(PACKAGE_ROOT, 'LICENSE'), 53 | os.path.join(egg_info_path, 'LICENSE') 54 | ) 55 | egg_info.run(self) 56 | 57 | 58 | class CleanCommand(Command): 59 | user_options = [ 60 | ('all', 'a', '(Compatibility with original clean command)'), 61 | ] 62 | 63 | def initialize_options(self): 64 | self.all = False 65 | 66 | def finalize_options(self): 67 | pass 68 | 69 | def run(self): 70 | sub_folders = ['build', 'temp', '%s.egg-info' % PACKAGE_NAME] 71 | if self.all: 72 | sub_folders.append('dist') 73 | for sub_folder in sub_folders: 74 | full_path = os.path.join(PACKAGE_ROOT, sub_folder) 75 | if os.path.exists(full_path): 76 | shutil.rmtree(full_path) 77 | for root, dirs, files in os.walk(os.path.join(PACKAGE_ROOT, PACKAGE_NAME)): 78 | for filename in files: 79 | if filename[-4:] == '.pyc': 80 | os.unlink(os.path.join(root, filename)) 81 | for dirname in list(dirs): 82 | if dirname == '__pycache__': 83 | shutil.rmtree(os.path.join(root, dirname)) 84 | 85 | 86 | readme = '' 87 | with codecs.open(os.path.join(PACKAGE_ROOT, 'readme.md'), 'r', 'utf-8') as f: 88 | readme = f.read() 89 | 90 | 91 | setup( 92 | name=PACKAGE_NAME, 93 | version=PACKAGE_VERSION, 94 | 95 | description=( 96 | 'Creates and signs online certificate status protocol (OCSP) ' 97 | 'requests and responses for X.509 certificates' 98 | ), 99 | long_description=readme, 100 | long_description_content_type='text/markdown', 101 | 102 | url='https://github.com/wbond/ocspbuilder', 103 | 104 | author='wbond', 105 | author_email='will@wbond.net', 106 | 107 | license='MIT', 108 | 109 | classifiers=[ 110 | 'Development Status :: 4 - Beta', 111 | 112 | 'Intended Audience :: Developers', 113 | 114 | 'License :: OSI Approved :: MIT License', 115 | 116 | 'Programming Language :: Python :: 2', 117 | 'Programming Language :: Python :: 2.6', 118 | 'Programming Language :: Python :: 2.7', 119 | 'Programming Language :: Python :: 3', 120 | 'Programming Language :: Python :: 3.2', 121 | 'Programming Language :: Python :: 3.3', 122 | 'Programming Language :: Python :: 3.4', 123 | 'Programming Language :: Python :: 3.5', 124 | 'Programming Language :: Python :: 3.6', 125 | 'Programming Language :: Python :: 3.7', 126 | 'Programming Language :: Python :: 3.8', 127 | 'Programming Language :: Python :: 3.9', 128 | 'Programming Language :: Python :: Implementation :: PyPy', 129 | 130 | 'Topic :: Security :: Cryptography', 131 | ], 132 | 133 | keywords='crypto pki x509 certificate ocsp', 134 | 135 | install_requires=[ 136 | 'asn1crypto>=1.2.0', 137 | 'oscrypto>=1.1.0' 138 | ], 139 | packages=[PACKAGE_NAME], 140 | package_data=package_data, 141 | 142 | test_suite='tests.make_suite', 143 | 144 | cmdclass={ 145 | 'clean': CleanCommand, 146 | 'egg_info': EggInfoCommand, 147 | } 148 | ) 149 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals, division, absolute_import, print_function 3 | 4 | import os 5 | import sys 6 | import unittest 7 | 8 | if sys.version_info < (3, 5): 9 | import imp 10 | else: 11 | import importlib 12 | import importlib.abc 13 | import importlib.util 14 | 15 | 16 | if sys.version_info >= (3, 5): 17 | class ModCryptoMetaFinder(importlib.abc.MetaPathFinder): 18 | def setup(self): 19 | self.modules = {} 20 | sys.meta_path.insert(0, self) 21 | 22 | def add_module(self, package_name, package_path): 23 | if package_name not in self.modules: 24 | self.modules[package_name] = package_path 25 | 26 | def find_spec(self, fullname, path, target=None): 27 | name_parts = fullname.split('.') 28 | if name_parts[0] not in self.modules: 29 | return None 30 | 31 | package = name_parts[0] 32 | package_path = self.modules[package] 33 | 34 | fullpath = os.path.join(package_path, *name_parts[1:]) 35 | 36 | if os.path.isdir(fullpath): 37 | filename = os.path.join(fullpath, "__init__.py") 38 | submodule_locations = [fullpath] 39 | else: 40 | filename = fullpath + ".py" 41 | submodule_locations = None 42 | 43 | if not os.path.exists(filename): 44 | return None 45 | 46 | return importlib.util.spec_from_file_location( 47 | fullname, 48 | filename, 49 | loader=None, 50 | submodule_search_locations=submodule_locations 51 | ) 52 | 53 | CUSTOM_FINDER = ModCryptoMetaFinder() 54 | CUSTOM_FINDER.setup() 55 | 56 | 57 | def _import_from(mod, path, mod_dir=None): 58 | """ 59 | Imports a module from a specific path 60 | 61 | :param mod: 62 | A unicode string of the module name 63 | 64 | :param path: 65 | A unicode string to the directory containing the module 66 | 67 | :param mod_dir: 68 | If the sub directory of "path" is different than the "mod" name, 69 | pass the sub directory as a unicode string 70 | 71 | :return: 72 | None if not loaded, otherwise the module 73 | """ 74 | 75 | if mod in sys.modules: 76 | return sys.modules[mod] 77 | 78 | if mod_dir is None: 79 | full_mod = mod 80 | else: 81 | full_mod = mod_dir.replace(os.sep, '.') 82 | 83 | if mod_dir is None: 84 | mod_dir = mod.replace('.', os.sep) 85 | 86 | if not os.path.exists(path): 87 | return None 88 | 89 | source_path = os.path.join(path, mod_dir, '__init__.py') 90 | if not os.path.exists(source_path): 91 | source_path = os.path.join(path, mod_dir + '.py') 92 | 93 | if not os.path.exists(source_path): 94 | return None 95 | 96 | if os.sep in mod_dir: 97 | append, mod_dir = mod_dir.rsplit(os.sep, 1) 98 | path = os.path.join(path, append) 99 | 100 | try: 101 | if sys.version_info < (3, 5): 102 | mod_info = imp.find_module(mod_dir, [path]) 103 | return imp.load_module(mod, *mod_info) 104 | 105 | else: 106 | package = mod.split('.', 1)[0] 107 | package_dir = full_mod.split('.', 1)[0] 108 | package_path = os.path.join(path, package_dir) 109 | CUSTOM_FINDER.add_module(package, package_path) 110 | 111 | return importlib.import_module(mod) 112 | 113 | except ImportError: 114 | return None 115 | 116 | 117 | def make_suite(): 118 | """ 119 | Constructs a unittest.TestSuite() of all tests for the package. For use 120 | with setuptools. 121 | 122 | :return: 123 | A unittest.TestSuite() object 124 | """ 125 | 126 | loader = unittest.TestLoader() 127 | suite = unittest.TestSuite() 128 | for test_class in test_classes(): 129 | tests = loader.loadTestsFromTestCase(test_class) 130 | suite.addTests(tests) 131 | return suite 132 | 133 | 134 | def test_classes(): 135 | """ 136 | Returns a list of unittest.TestCase classes for the package 137 | 138 | :return: 139 | A list of unittest.TestCase classes 140 | """ 141 | 142 | # Make sure the module is loaded from this source folder 143 | tests_dir = os.path.dirname(os.path.abspath(__file__)) 144 | 145 | _import_from( 146 | 'ocspbuilder', 147 | os.path.join(tests_dir, '..') 148 | ) 149 | 150 | from .test_ocsp_response_builder import OCSPResponseBuilderTests 151 | from .test_ocsp_request_builder import OCSPRequestBuilderTests 152 | 153 | return [OCSPResponseBuilderTests, OCSPRequestBuilderTests] 154 | -------------------------------------------------------------------------------- /tests/_unittest_compat.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals, division, absolute_import, print_function 3 | 4 | import sys 5 | import unittest 6 | import re 7 | 8 | 9 | if sys.version_info < (3,): 10 | str_cls = unicode # noqa 11 | else: 12 | str_cls = str 13 | 14 | 15 | _non_local = {'patched': False} 16 | 17 | 18 | def patch(): 19 | if sys.version_info >= (3, 0): 20 | return 21 | 22 | if _non_local['patched']: 23 | return 24 | 25 | if sys.version_info < (2, 7): 26 | unittest.TestCase.assertIsInstance = _assert_is_instance 27 | unittest.TestCase.assertRegex = _assert_regex 28 | unittest.TestCase.assertRaises = _assert_raises 29 | unittest.TestCase.assertRaisesRegex = _assert_raises_regex 30 | unittest.TestCase.assertGreaterEqual = _assert_greater_equal 31 | unittest.TestCase.assertLess = _assert_less 32 | unittest.TestCase.assertLessEqual = _assert_less_equal 33 | unittest.TestCase.assertIn = _assert_in 34 | unittest.TestCase.assertNotIn = _assert_not_in 35 | else: 36 | unittest.TestCase.assertRegex = unittest.TestCase.assertRegexpMatches 37 | unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp 38 | _non_local['patched'] = True 39 | 40 | 41 | def _safe_repr(obj): 42 | try: 43 | return repr(obj) 44 | except Exception: 45 | return object.__repr__(obj) 46 | 47 | 48 | def _format_message(msg, standard_msg): 49 | return msg or standard_msg 50 | 51 | 52 | def _assert_greater_equal(self, a, b, msg=None): 53 | if not a >= b: 54 | standard_msg = '%s not greater than or equal to %s' % (_safe_repr(a), _safe_repr(b)) 55 | self.fail(_format_message(msg, standard_msg)) 56 | 57 | 58 | def _assert_less(self, a, b, msg=None): 59 | if not a < b: 60 | standard_msg = '%s not less than %s' % (_safe_repr(a), _safe_repr(b)) 61 | self.fail(_format_message(msg, standard_msg)) 62 | 63 | 64 | def _assert_less_equal(self, a, b, msg=None): 65 | if not a <= b: 66 | standard_msg = '%s not less than or equal to %s' % (_safe_repr(a), _safe_repr(b)) 67 | self.fail(_format_message(msg, standard_msg)) 68 | 69 | 70 | def _assert_is_instance(self, obj, cls, msg=None): 71 | if not isinstance(obj, cls): 72 | if not msg: 73 | msg = '%s is not an instance of %r' % (obj, cls) 74 | self.fail(msg) 75 | 76 | 77 | def _assert_in(self, member, container, msg=None): 78 | if member not in container: 79 | standard_msg = '%s not found in %s' % (_safe_repr(member), _safe_repr(container)) 80 | self.fail(_format_message(msg, standard_msg)) 81 | 82 | 83 | def _assert_not_in(self, member, container, msg=None): 84 | if member in container: 85 | standard_msg = '%s found in %s' % (_safe_repr(member), _safe_repr(container)) 86 | self.fail(_format_message(msg, standard_msg)) 87 | 88 | 89 | def _assert_regex(self, text, expected_regexp, msg=None): 90 | """Fail the test unless the text matches the regular expression.""" 91 | if isinstance(expected_regexp, str_cls): 92 | expected_regexp = re.compile(expected_regexp) 93 | if not expected_regexp.search(text): 94 | msg = msg or "Regexp didn't match" 95 | msg = '%s: %r not found in %r' % (msg, expected_regexp.pattern, text) 96 | self.fail(msg) 97 | 98 | 99 | def _assert_raises(self, excClass, callableObj=None, *args, **kwargs): # noqa 100 | context = _AssertRaisesContext(excClass, self) 101 | if callableObj is None: 102 | return context 103 | with context: 104 | callableObj(*args, **kwargs) 105 | 106 | 107 | def _assert_raises_regex(self, expected_exception, expected_regexp, callable_obj=None, *args, **kwargs): 108 | if expected_regexp is not None: 109 | expected_regexp = re.compile(expected_regexp) 110 | context = _AssertRaisesContext(expected_exception, self, expected_regexp) 111 | if callable_obj is None: 112 | return context 113 | with context: 114 | callable_obj(*args, **kwargs) 115 | 116 | 117 | class _AssertRaisesContext(object): 118 | def __init__(self, expected, test_case, expected_regexp=None): 119 | self.expected = expected 120 | self.failureException = test_case.failureException 121 | self.expected_regexp = expected_regexp 122 | 123 | def __enter__(self): 124 | return self 125 | 126 | def __exit__(self, exc_type, exc_value, tb): 127 | if exc_type is None: 128 | try: 129 | exc_name = self.expected.__name__ 130 | except AttributeError: 131 | exc_name = str(self.expected) 132 | raise self.failureException( 133 | "{0} not raised".format(exc_name)) 134 | if not issubclass(exc_type, self.expected): 135 | # let unexpected exceptions pass through 136 | return False 137 | self.exception = exc_value # store for later retrieval 138 | if self.expected_regexp is None: 139 | return True 140 | 141 | expected_regexp = self.expected_regexp 142 | if not expected_regexp.search(str(exc_value)): 143 | raise self.failureException( 144 | '"%s" does not match "%s"' % 145 | (expected_regexp.pattern, str(exc_value)) 146 | ) 147 | return True 148 | -------------------------------------------------------------------------------- /tests/fixtures/test-inter.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEBDCCAuygAwIBAgIDGEN5MA0GCSqGSIb3DQEBCwUAMIGdMQswCQYDVQQGEwJV 3 | UzEWMBQGA1UECBMNTWFzc2FjaHVzZXR0czEQMA4GA1UEBxMHTmV3YnVyeTEeMBwG 4 | A1UEChMVQ29kZXggTm9uIFN1ZmZpY2l0IExDMRAwDgYDVQQLEwdUZXN0aW5nMRIw 5 | EAYDVQQDEwlXaWxsIEJvbmQxHjAcBgkqhkiG9w0BCQEWD3dpbGxAY29kZXhucy5p 6 | bzAeFw0xNTA1MDkwMDQxMzlaFw0yNTA1MDYwMDQxMzlaMIGYMQswCQYDVQQGEwJV 7 | UzEWMBQGA1UECBMNTWFzc2FjaHVzZXR0czEeMBwGA1UEChMVQ29kZXggTm9uIFN1 8 | ZmZpY2l0IExDMR0wGwYDVQQLExRUZXN0aW5nIEludGVybWVkaWF0ZTESMBAGA1UE 9 | AxMJV2lsbCBCb25kMR4wHAYJKoZIhvcNAQkBFg93aWxsQGNvZGV4bnMuaW8wggEi 10 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/PVOdogtqe2gqXZ20dWB5NFxj 11 | JX6z6A57zGqT11OfiNmckIqxMRK8eToR69/DJmVNbQTJy/cFexZ33db+UrtEItxO 12 | klFFbG/6TjmqsOYF87TkJqGX7zXs/Z7D1Xl0/2CNh6vPPPbi/o1FHtKnk4HYOnyU 13 | LRMxxtt06O/u//wdnWXiSAOKOasLT7eP6Fuok28+BeQ29GB3UNL8oC7biyhv89yN 14 | VhDlHAkk+zfrGP4dNqxOY8SkwI0GYki9skEckwf7Nyz+vA8zVJaIymte/eRicCFF 15 | YFxh+QtQhyxF7DwsB5/XIgEWKRXnd/ivyORMlIVc83CHCM4ryvXgG0+I7WuZAgMB 16 | AAGjUDBOMB0GA1UdDgQWBBTSCv0uJdG3IddQfrukfb8071JeAjAfBgNVHSMEGDAW 17 | gBS+QoU9zP/j+SgCj35YVrT9A1zqSzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEB 18 | CwUAA4IBAQAKnbphxDozjob2qobtO5QRfZNwIANr/Pyz6SoD8UnJDfZAg9y1jb/I 19 | OutcURjYfV4vyLKI5oyhepyBMkuwvnB/lRpLERAwv5lJgq4sIGDn5Sp56yYcxazC 20 | QXLyG5YJ2Q29kVuq9//IE/dkLVaP444iJZZ7IgrGN4EFQwvFRVa6toeVpwzp8kkn 21 | 5eQaiKQYt/1dVsQ5mv8ksT/gNJRMD4xJ7xvlats+Jc6cyygJk18h+JqMYDoFTTu6 22 | fJmjtnjPFg+XU0q9GnJfrW+FjxYGMv+5tv0pJrujEq7+8gz5A4viSCzHHoc4O/Qs 23 | 0Jt6dQ/xmGYswxjrR/ZgsSjPx3lib7XC 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /tests/fixtures/test-ocsp.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID/zCCAumgAwIBAgIIVkZXquZ41NIwCwYJKoZIhvcNAQELMIGdMQswCQYDVQQG 3 | EwJVUzEWMBQGA1UECBMNTWFzc2FjaHVzZXR0czEQMA4GA1UEBxMHTmV3YnVyeTEe 4 | MBwGA1UEChMVQ29kZXggTm9uIFN1ZmZpY2l0IExDMRAwDgYDVQQLEwdUZXN0aW5n 5 | MRIwEAYDVQQDEwlXaWxsIEJvbmQxHjAcBgkqhkiG9w0BCQEWD3dpbGxAY29kZXhu 6 | cy5pbzAeFw0xNTExMTMyMTM1MzhaFw0xNjExMTIyMTM1MzhaMHAxbjAJBgNVBAYT 7 | AlVTMBQGA1UECAwNTWFzc2FjaHVzZXR0czAOBgNVBAcMB05ld2J1cnkwHAYDVQQK 8 | DBVDb2RleCBOb24gU3VmZmljaXQgTEMwHQYDVQQDDBZDb2RleE5TIE9DU1AgUmVz 9 | cG9uZGVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4QoiJo5OMex+ 10 | OcEXqGgOQBD62SGNFAAPuSLwxVflGV9gLzoKg1ipyKgYfrrct4soFJqEiayfYEB5 11 | psB48pwq8Kz6GtbE7p/MQ4g9srhDEkDThz3LAjKalHF+OfM+aF9mLVK6ZBRi5ICb 12 | 2q5UDZbYFsQDhPE97hRditQRA5MKfoPx5h0OaRhV15cmgURVQUsN1mNHWETLYcJr 13 | /Pe+SvsPnCEsmttPuqaQPibmJAisMhhLSHRrMrEO1k6wsbSF/y67csLrdUbfmVHW 14 | rhCD+KW5cFEOpidHQ+6ese09FKqZ4vvRvTWbfbN9vMCKV/H33KnpJBXLtNoXvoPf 15 | HwdAmOg0SQIDAQABo3MwcTAfBgNVHSMEGDAWgBS+QoU9zP/j+SgCj35YVrT9A1zq 16 | SzAJBgNVHRMEAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMJMB0GA1UdDgQWBBQ17XC8 17 | K8tzRB3nx8yXm69gM1MdtjAPBgNVHQ8BAf8EBQMDB6AAMAsGCSqGSIb3DQEBCwOC 18 | AQEALkYqxg2+FXxGc0FU/1+zWYDjX+Tt4zlffGV8UtLqSUibMLdrv7nQ5P296nZc 19 | EGC92yhWUWTwGBvRpErh7vG00eZJH7j36uC+NQfFKsdvyO51bDc9nW/s+dCZbX7E 20 | jTH4wvj3qYsIg93SW/qURzTg32mu+grOCTG0Ad07tFCuZMYucx9Bma43QVz5tX5S 21 | wBT9f1q52XXMPGHB1xYWAfq3KWo3hxZJJwcjS5YXF8bUAo8p16nl8255w5fpOP7Y 22 | oVghHQuUsempbnmbLLr5HXMdAFfwHJRXc7YBm0FOtnt31LQvYObKgr9bpsFzVctU 23 | FNK5LK4oPeFyM05IQOKD0rlgsg== 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /tests/fixtures/test-ocsp.key: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIIFRTBvBgkqhkiG9w0BBQ0wYjBBBgkqhkiG9w0BBQwwNAQgcKLCTVXKCNTTnY8u 3 | AxeyuaZBZGUwj7mh8itQcfh8I8oCAk4gMAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUD 4 | BAEqBBBgFfPRMB/jhHUI2AXw5UTZBIIE0Bz5QqEnvEip8KtBfWmVgLM08102UNgx 5 | wnfEjbfdjsT7ziWrDHE/0liAN7YZIPnHlCUDA4i2tnuud5Q2fMJewyfLOjz8BdnQ 6 | yMp1C1xoXhsaQMTk1v1qTKSdxAUAvqDfJOwM/mCcZUnKJEPBzV2MVBQOfktqIDsG 7 | 2TDDo1v3jLk13Il7V+J6IM8kFoAdbM8cdisVRDTymA2T+jPpMuHZNMS31dWO0P40 8 | rl31+g7zuT2n7KE36r+Mkr+w92CNtOhLiJQqZZGlCqGW0Q+ExdBNjkZCL32uHhJx 9 | fDVeMajRh2DTfdrF/jbNgeC1qrxXUiUKXfdosCtfUBf4OmfVjY2HuzFNPQZCSiJ3 10 | yTPW1y03gotXC5gbxgF9exFPINtLFA09qpbD9bmwmH5AxUNwA99IwHWNPhhHBbGN 11 | mjI9eBMGXCRoDxxoo9w/DoOVxgX6uBtXKLEfLZZEduOXj/JTZRVSKy6uwEw27W24 12 | SjVWk7G9HTzp31DEZ9ofiY0HpF2g4iivBiVJrf7HAwkXWoUPX1vTefMIeVgxl9Xa 13 | /U5ijb8yn0CfJ9GgWgNT953koCDgyIal1Mv9ByNSyyRSp3c45P20kitnj+6HBwES 14 | GwZqBa83P0jpx0OWg/xiV9lSOjbfdNrXFWDOuhOHtexXX4neIBeOLfHMIJYNXgCE 15 | rxoSvPr+TSSk399hS1t8g41728us0btfdHKoe5daHO4s/eaFWKs/8bCQ212uyW9Z 16 | MkjtJvviDbTzfacnRP9S4EPVQ4yl9z2jlLIh1p+qnt7txDDwmNHHt9ra3Th1/lq8 17 | YCd13qPoNWA9Z/NOTWRxYP6gtKA2sDbOkFwGF2tYMBmPQJkOPjOdw5fFBsYqkBb+ 18 | iAvf78pUs7srBt4bKqxfXU1auRVCHrMXNf+n6JDKK63Xu7KaKtPJYa+YglwjR7lP 19 | pJRDgq1SPEruUwfM1oTZJ426An1Zg/i34/eq+FJxSpx2M6BLO513jtC76TIzn6Ev 20 | ao7gBSNELEXuQlVFRJSxssKUjiHvIK95nFotMuglDDYMOzMIljvGl96xDXvt8E+4 21 | X0nKMMQTd8aAdpnHXI+ho/mdPA/Du1oS4NdxSJNOu+TaM9/326YnvLfxKuZDDyGT 22 | uRDDcNiaEgXauhC8SOZK+ssZcKfbUgjerQ+O/Mc9VlCgmW5XQKu/isNkABZoPCJ0 23 | S1HQSugS+my2GpJp/yYJWaUK2Ev4usUTsY4yGasfCjbqdn3hX28A9cdetQt5bTGa 24 | Nsb+YvrqlFMcYIUB+7CYIAOZCMBzBsQ6nfdVSTGTmpNHlLfLMqtx0zd2BcDTqi9X 25 | bR61HgIVXKwh3ao7nCbA0r3GtFVs/3lKpwNp2Is5HVJgsGfIGszs3/4iCaehY33C 26 | k9r0oQO7ds8I+JeqGFkhbgIyeu+g0BjVHGQleh8biNTXK1NvnlQ3iNX6b8TqyeOJ 27 | y1wIPG7HJVPuQA4/pjtJOmZg6QqUmlrrYPjLYGYYA4TuVgY90fWl9jgj44ujf8HG 28 | sBLgKjjMnUCSBh1G4FOh9e+O8jv+jvU+IKo4haZaN+XKgcrd9fK7lJuUnFSRd8vb 29 | EKSZ9LNXz3hmNp9GtFipTi7ax3UMaiMQCFi+XRbYIOyosvU0ekks62aO9cqZoqdj 30 | PEmXCD3dKjur 31 | -----END ENCRYPTED PRIVATE KEY----- 32 | -------------------------------------------------------------------------------- /tests/fixtures/test-third.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID+zCCAuOgAwIBAgIFAJOEAykwDQYJKoZIhvcNAQELBQAwgZgxCzAJBgNVBAYT 3 | AlVTMRYwFAYDVQQIEw1NYXNzYWNodXNldHRzMR4wHAYDVQQKExVDb2RleCBOb24g 4 | U3VmZmljaXQgTEMxHTAbBgNVBAsTFFRlc3RpbmcgSW50ZXJtZWRpYXRlMRIwEAYD 5 | VQQDEwlXaWxsIEJvbmQxHjAcBgkqhkiG9w0BCQEWD3dpbGxAY29kZXhucy5pbzAe 6 | Fw0xNTA1MTEyMTQyNTZaFw0yNTA1MDgyMTQyNTZaMIGgMQswCQYDVQQGEwJVUzEW 7 | MBQGA1UECBMNTWFzc2FjaHVzZXR0czEeMBwGA1UEChMVQ29kZXggTm9uIFN1ZmZp 8 | Y2l0IExDMSUwIwYDVQQLExxUZXN0IFRoaXJkLUxldmVsIENlcnRpZmljYXRlMRIw 9 | EAYDVQQDEwlXaWxsIEJvbmQxHjAcBgkqhkiG9w0BCQEWD3dpbGxAY29kZXhucy5p 10 | bzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMAKLNVdAlEUbBeAKfSz 11 | 3Ymd78tt6l2jjEdz4QMqFkYDSbl5pMB0RP+yRKHVSr+gL99F3wAXHoMb81bFVItL 12 | dyx3vK1qK2FhMvmuw8yNUy3o4aIwE/Neobp4eWLWNWwdqmPyNpvc8g8HdVsnk97u 13 | 9ZUGoiHEblvLZ8uijvbxK5hbCkKHjOhHCDT5egtPKOyFqOWJ1WzINICRfEeIjZM0 14 | cuLUWVJmzb9QJ8CmN4O6R1rYK131eIZ6FWlJTpx1n7MvygTOmDiyVE0NMfJFTzdj 15 | a8kQxiwdyqN9I9OfHqRefh8TCQBuAguuwPUyAu9Ve45eDqzfeWrgoyinZ0KXiqrH 16 | 5DUCAwEAAaNCMEAwHQYDVR0OBBYEFEQ44OAmhb+Yhtwb4R31MjC+q6wNMB8GA1Ud 17 | IwQYMBaAFNIK/S4l0bch11B+u6R9vzTvUl4CMA0GCSqGSIb3DQEBCwUAA4IBAQAb 18 | Wq6ueTRDgY5hDAcVn3j5Eaco88rzhhzgfnH4GSES/QOQlOEFbj0/zzAk7LCgcXq3 19 | 7ud/cbA0tuoFmra9Q4D3YEcL0xHiDlXkvzcy2MJ6MysRiQftvHE9o1f8lxWqEfHK 20 | EtL3espMfVE8zisiA7kS07Jm4t7OSBte21JVwqC+dlqYca2T0coJ47Fw/yVvlaQU 21 | O5h6wvHuUS4L0myNdW+/V2yoUex8C9OK3qs8H61ULcgoVnvn/DJJerad53LBfJR3 22 | jmmamq6Pu7COWU07N7MOGkrG8bwal64ncdzwvTtBj9NdoDataw9LvjyAGxYDSY6X 23 | KiqBZoOHm108hjAfW+XC 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /tests/fixtures/test-third.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAwAos1V0CURRsF4Ap9LPdiZ3vy23qXaOMR3PhAyoWRgNJuXmk 3 | wHRE/7JEodVKv6Av30XfABcegxvzVsVUi0t3LHe8rWorYWEy+a7DzI1TLejhojAT 4 | 816hunh5YtY1bB2qY/I2m9zyDwd1WyeT3u71lQaiIcRuW8tny6KO9vErmFsKQoeM 5 | 6EcINPl6C08o7IWo5YnVbMg0gJF8R4iNkzRy4tRZUmbNv1AnwKY3g7pHWtgrXfV4 6 | hnoVaUlOnHWfsy/KBM6YOLJUTQ0x8kVPN2NryRDGLB3Ko30j058epF5+HxMJAG4C 7 | C67A9TIC71V7jl4OrN95auCjKKdnQpeKqsfkNQIDAQABAoIBAQCyxkYqcqV/eXWP 8 | Ax8L0I3CWScsyCxP87rZocStP3bwworVgaqgBx1ctEY0Ke2mKqemQNNysBMVluWX 9 | t6gW7LAK04TwI1AzHVtpGQrp1/7BVHUImZ1ZCJWilBjcq/Gbrpo65Pd1beBhoV3c 10 | +CEufmJc04oHyWe7SMZdyf0xYh5le2yq6qbRjihjOdGtGxJF/BkGT7MVGZwOE21Z 11 | wTtW9hto26rCSFoUDhnHNQLEBNSsxwIov2VJ1lp7VN6ruvisN3SddJl9068dvorn 12 | cLyYcelR7i9efM1/VdsI9xmQOaLF9p8HizYex/euQ7vyMp3APAepf5pCT4DkpZpD 13 | PmlJoUCBAoGBAPLwQpliNUofOIfmqiQ3/v0c5tTapoG9NwFyRtRfnm46Ou810OPs 14 | e/L89Wreo2+sfW54Y7J2ftqI6+XmuGRGCv0AgeZaYC8Hx7+o3uUitNTcwvVFqdKM 15 | SmDAhzy/WZGhHE5dDEAmXEDF0NNSgHWjNVwrewT6+RHdpv4EMavum5oRAoGBAMpd 16 | W62XfLVCVNxUCjRCMwozf46Cybx3NLwVtJKbB2kGDqUXYLs4f4F0OzFt5XPIluFA 17 | kQSro5CrexVXMhcIigOadk6dGegJPGFaAmw5ZSjyGLjr1wmaskCzTBqguDUta1Up 18 | IA6swmYK9eR8tHZNNFRBkL/KNEY9+ZwWdQ62WuPlAoGBAN0nMrmG2ZQcT84HgaNv 19 | BkVM5iWm1iUNJuG+MhRq50LY54WTrBGQ2lUdShx7iLTEhXrnRXrUvC4crwKewgUm 20 | biJbL+WPKDgoEQK8rAxTR+LvBNtbC3mMFLl3CqWWW+diju4XbmuHgDvG2I9Hb4Gn 21 | jY/WVSr3fX1yFe7vyngFwsjBAoGAZkO9k8EtTXBi8CEsMvKNVodl27/ucOaQ6MfT 22 | RA9CNGnSNs3UnWhUzzfMvhL6VIO288gsQP74HqD6B3PUJV20WVPSm7G6qM8aC1xw 23 | Qv7SR1no8nKEbh8WG6pAOGimDoGQby3kPGZDq0u4ranzjKFBY57qpnFp72FcZevX 24 | ZgLzdZ0CgYEAzhkOEjC82l1tm9oWbkrdLdx/Sox2mBEnxkkvt30oRUTt89xA2epS 25 | q/4zBCTSCg+OhHfASg+ek8zMOtKnOQnis8F2Z6lvboA1Hg/iWv6mZ7EYj78C5yZh 26 | nsdKM+U/5sxUa4RCc0Aj5s8irpZZfc0SxwmYZH/NMQuY3e3HMuyGLKM= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /tests/fixtures/test.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIExzCCA6+gAwIBAgIJAL3l2aQQMVyCMA0GCSqGSIb3DQEBCwUAMIGdMQswCQYD 3 | VQQGEwJVUzEWMBQGA1UECBMNTWFzc2FjaHVzZXR0czEQMA4GA1UEBxMHTmV3YnVy 4 | eTEeMBwGA1UEChMVQ29kZXggTm9uIFN1ZmZpY2l0IExDMRAwDgYDVQQLEwdUZXN0 5 | aW5nMRIwEAYDVQQDEwlXaWxsIEJvbmQxHjAcBgkqhkiG9w0BCQEWD3dpbGxAY29k 6 | ZXhucy5pbzAeFw0xNTA1MDYxNDM3MTZaFw0yNTA1MDMxNDM3MTZaMIGdMQswCQYD 7 | VQQGEwJVUzEWMBQGA1UECBMNTWFzc2FjaHVzZXR0czEQMA4GA1UEBxMHTmV3YnVy 8 | eTEeMBwGA1UEChMVQ29kZXggTm9uIFN1ZmZpY2l0IExDMRAwDgYDVQQLEwdUZXN0 9 | aW5nMRIwEAYDVQQDEwlXaWxsIEJvbmQxHjAcBgkqhkiG9w0BCQEWD3dpbGxAY29k 10 | ZXhucy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL1bKAd04uZK 11 | LAIqvVDeeBeq7FA2fpS5xkWcqHbarzvD1//EG/kCQirJr302nusjJFxdji3aVDRG 12 | Px0+WWwGajy+k2vYm0t7mSP/bmVGCM06ofvDZUMWV1Ld4SyInHruS1Qj4xHlB7/Z 13 | +mAWYpCudmAFIJEgtlHDzezqu6kLEVNB1lbLH+lPNyunwXC9FSYWhekjAyBaflFB 14 | koQV90jXfuTG7Ph0m4DAfZn5n5r/YpvmKEDkPkaW1mAt8qel4b8RklAh8t8vTSfv 15 | QuTeyw3GFcKe7KymKHIaDDxwwnALfGWNa3t7YoVZP9fVrghkR73DBCnHIx22uDHU 16 | TkwBmIdUL18CAwEAAaOCAQYwggECMB0GA1UdDgQWBBS+QoU9zP/j+SgCj35YVrT9 17 | A1zqSzCB0gYDVR0jBIHKMIHHgBS+QoU9zP/j+SgCj35YVrT9A1zqS6GBo6SBoDCB 18 | nTELMAkGA1UEBhMCVVMxFjAUBgNVBAgTDU1hc3NhY2h1c2V0dHMxEDAOBgNVBAcT 19 | B05ld2J1cnkxHjAcBgNVBAoTFUNvZGV4IE5vbiBTdWZmaWNpdCBMQzEQMA4GA1UE 20 | CxMHVGVzdGluZzESMBAGA1UEAxMJV2lsbCBCb25kMR4wHAYJKoZIhvcNAQkBFg93 21 | aWxsQGNvZGV4bnMuaW+CCQC95dmkEDFcgjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3 22 | DQEBCwUAA4IBAQA/5fmvWSMRWqD6JalrNr3Saio5+c/LzxbQ7kZZX1EH5Bo+vhD6 23 | Pfs6lrhzE5OVS86sOItPTtT2KQIE0wTxNfX9wQzjekH4Q2sZRpWaKvK6cH+YzqUK 24 | n3DiICpu8vau4wHDrlfV/C2XhKf5u5qIB+SyOhGDo+XhY0cjgh/FS2//1/qGxrfx 25 | TeOHxoRN5YH4yKNGNapL3XF+utpwVFddmSRUWCBr7Fof9RJlzPCcVP/svTAXDi/y 26 | dhHevPEr21oYqX0KnJJa0JvJx92K8YKFTVj2x5PPCn9qze5C/Re/JFbCIuwq7kcX 27 | 3WQRd+S9zHbtG/4g3DnGibdczSw9529fZZw3 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /tests/fixtures/test.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAvVsoB3Ti5kosAiq9UN54F6rsUDZ+lLnGRZyodtqvO8PX/8Qb 3 | +QJCKsmvfTae6yMkXF2OLdpUNEY/HT5ZbAZqPL6Ta9ibS3uZI/9uZUYIzTqh+8Nl 4 | QxZXUt3hLIiceu5LVCPjEeUHv9n6YBZikK52YAUgkSC2UcPN7Oq7qQsRU0HWVssf 5 | 6U83K6fBcL0VJhaF6SMDIFp+UUGShBX3SNd+5Mbs+HSbgMB9mfmfmv9im+YoQOQ+ 6 | RpbWYC3yp6XhvxGSUCHy3y9NJ+9C5N7LDcYVwp7srKYochoMPHDCcAt8ZY1re3ti 7 | hVk/19WuCGRHvcMEKccjHba4MdROTAGYh1QvXwIDAQABAoIBAE8NH0j9ozxA+t5s 8 | uVxpg/ldggp6tZ2hcQTewfXclgt9V0+Pr53lM3ppeLntc6r2oNdut0ytOToZmX+7 9 | 59kRVIjHhwQfCbYZg3VjzdK5yjLjp3xTtpKrYQlXWAoffjRUB165HLL7yqBtf/ld 10 | XwjHzOOJQG9WGMdJ105xMKcB19nJijgY19Eg/9svNQq6wwfDACKAYXusoz8ApGoz 11 | T6b8+o2nINQHaCgmf5qPbg44JfX3+k2er7bK+mk7cuoOIWOB5ItSfyq4GdbLM+XW 12 | /GIGkclXUOWRDmitudoLKtv5WtvYrsWe1iznHIrlIy7gI1i9Z+V9mEYb+aZPDtjB 13 | 4Kuy/gECgYEA7VBSov4Oph78yiIIwmJ6T+4+tdbJQ2lxV7GusSVjj76vmZASNJ7E 14 | gpR5woH1APEyHgnUkoRK8fGUHp+Lxms2rrYivcjXZ4DTvpZ/qQVJBNJ4pVyWn+iq 15 | 7gLro2wotiLDBzeG9oo46Czn0QCOk7tofxSbcEkhkO+Uuwc3ZSlEOfcCgYEAzEQf 16 | HQNFzBHqlvPVbfy9+IxnG+u33WEKSMr+1ek/hae43+D4vndBX1DPuhaWUapsU9Gg 17 | A5YIXOhCIrnx4MbMvcBn55CWIf7J+dEdPaUOGN+YtYbV29U45nX0dS4QXxBOVZwn 18 | qUBRDwptSFVaLHDUrOC4vnkST6iRHh/OJ5AuG9kCgYEAm8+SAiwWSCGuTbSc1au8 19 | rMA68j7sc9NGNJKXpP1sahODzapXGa9oTGfZrciPqSezhR9lLzGm10WKv7R3HDaG 20 | d51kIAE+1Fk0LT044itzLrRVvBSXXLRxjcXjGrBH5pXaQOHHPhWwmVfqeEIKWprA 21 | WDeaetW5MSTsHQP27fdzMS8CgYA4DXl8PKmqlkAJrF+lDvYSfnTM9KI/3aE02H+V 22 | s6v6wUu6I8IeghsuTL60Ef6t6lZPqfZ/BWzGEfYUEXKOe/8zEtlwcfzA12oVY4zi 23 | naiAqtr89UM6UAiNNVEf1sQnUhIs6+z2RO/5cKMMdl+IUm4KAqCvpAmiUl+AJLot 24 | oSMGAQKBgQCcWwsTbMnFJrBxHVe05C8Y/DeU1Cj7TlKKuYSidASus0KhJv7bkSIE 25 | N3x3RJFDVrkEW30AH/b+oMKHSjileWP7NLCT4Y/d/ZybhavhOziol408GsZNe6YV 26 | Tzk/USt/a4ZBK1K5RlHmTiFYNXvecXqzHnk77OzCMsJtteW/gr1ABQ== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /tests/test_ocsp_request_builder.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals, division, absolute_import, print_function 3 | 4 | import unittest 5 | import os 6 | 7 | import asn1crypto.x509 8 | from oscrypto import asymmetric 9 | from ocspbuilder import OCSPRequestBuilder 10 | 11 | from ._unittest_compat import patch 12 | 13 | patch() 14 | 15 | 16 | tests_root = os.path.dirname(__file__) 17 | fixtures_dir = os.path.join(tests_root, 'fixtures') 18 | 19 | 20 | class OCSPRequestBuilderTests(unittest.TestCase): 21 | 22 | def test_build_basic_request(self): 23 | issuer_cert = asymmetric.load_certificate(os.path.join(fixtures_dir, 'test.crt')) 24 | subject_cert = asymmetric.load_certificate(os.path.join(fixtures_dir, 'test-inter.crt')) 25 | 26 | builder = OCSPRequestBuilder(subject_cert, issuer_cert) 27 | ocsp_request = builder.build() 28 | der_bytes = ocsp_request.dump() 29 | 30 | new_request = asn1crypto.ocsp.OCSPRequest.load(der_bytes) 31 | tbs_request = new_request['tbs_request'] 32 | 33 | self.assertEqual(None, new_request['optional_signature'].native) 34 | self.assertEqual('v1', tbs_request['version'].native) 35 | self.assertEqual(None, tbs_request['requestor_name'].native) 36 | self.assertEqual(1, len(tbs_request['request_list'])) 37 | 38 | request = tbs_request['request_list'][0] 39 | self.assertEqual('sha1', request['req_cert']['hash_algorithm']['algorithm'].native) 40 | self.assertEqual(issuer_cert.asn1.subject.sha1, request['req_cert']['issuer_name_hash'].native) 41 | self.assertEqual(issuer_cert.asn1.public_key.sha1, request['req_cert']['issuer_key_hash'].native) 42 | self.assertEqual(subject_cert.asn1.serial_number, request['req_cert']['serial_number'].native) 43 | self.assertEqual(0, len(request['single_request_extensions'])) 44 | 45 | self.assertEqual(1, len(tbs_request['request_extensions'])) 46 | extn = tbs_request['request_extensions'][0] 47 | 48 | self.assertEqual('nonce', extn['extn_id'].native) 49 | self.assertEqual(16, len(extn['extn_value'].parsed.native)) 50 | 51 | def test_build_signed_request(self): 52 | issuer_cert = asymmetric.load_certificate(os.path.join(fixtures_dir, 'test.crt')) 53 | subject_cert = asymmetric.load_certificate(os.path.join(fixtures_dir, 'test-inter.crt')) 54 | 55 | requestor_cert = asymmetric.load_certificate(os.path.join(fixtures_dir, 'test-third.crt')) 56 | requestor_key = asymmetric.load_private_key(os.path.join(fixtures_dir, 'test-third.key')) 57 | 58 | builder = OCSPRequestBuilder(subject_cert, issuer_cert) 59 | ocsp_request = builder.build(requestor_key, requestor_cert, [subject_cert, issuer_cert]) 60 | der_bytes = ocsp_request.dump() 61 | 62 | new_request = asn1crypto.ocsp.OCSPRequest.load(der_bytes) 63 | tbs_request = new_request['tbs_request'] 64 | signature = new_request['optional_signature'] 65 | 66 | self.assertEqual('sha256', signature['signature_algorithm'].hash_algo) 67 | self.assertEqual('rsassa_pkcs1v15', signature['signature_algorithm'].signature_algo) 68 | self.assertEqual(3, len(signature['certs'])) 69 | self.assertEqual('v1', tbs_request['version'].native) 70 | self.assertEqual(requestor_cert.asn1.subject, tbs_request['requestor_name'].chosen) 71 | self.assertEqual(1, len(tbs_request['request_list'])) 72 | 73 | request = tbs_request['request_list'][0] 74 | self.assertEqual('sha1', request['req_cert']['hash_algorithm']['algorithm'].native) 75 | self.assertEqual(issuer_cert.asn1.subject.sha1, request['req_cert']['issuer_name_hash'].native) 76 | self.assertEqual(issuer_cert.asn1.public_key.sha1, request['req_cert']['issuer_key_hash'].native) 77 | self.assertEqual(subject_cert.asn1.serial_number, request['req_cert']['serial_number'].native) 78 | self.assertEqual(0, len(request['single_request_extensions'])) 79 | 80 | self.assertEqual(1, len(tbs_request['request_extensions'])) 81 | extn = tbs_request['request_extensions'][0] 82 | 83 | self.assertEqual('nonce', extn['extn_id'].native) 84 | self.assertEqual(16, len(extn['extn_value'].parsed.native)) 85 | -------------------------------------------------------------------------------- /tests/test_ocsp_response_builder.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals, division, absolute_import, print_function 3 | 4 | from datetime import datetime 5 | import unittest 6 | import os 7 | 8 | import asn1crypto.x509 9 | from oscrypto import asymmetric 10 | from asn1crypto.util import timezone 11 | from ocspbuilder import OCSPResponseBuilder 12 | 13 | from ._unittest_compat import patch 14 | 15 | patch() 16 | 17 | 18 | tests_root = os.path.dirname(__file__) 19 | fixtures_dir = os.path.join(tests_root, 'fixtures') 20 | 21 | 22 | class OCSPResponseBuilderTests(unittest.TestCase): 23 | 24 | def test_build_good_response(self): 25 | issuer_key = asymmetric.load_private_key(os.path.join(fixtures_dir, 'test.key')) 26 | issuer_cert = asymmetric.load_certificate(os.path.join(fixtures_dir, 'test.crt')) 27 | subject_cert = asymmetric.load_certificate(os.path.join(fixtures_dir, 'test-inter.crt')) 28 | 29 | builder = OCSPResponseBuilder('successful', subject_cert, 'good') 30 | ocsp_response = builder.build(issuer_key, issuer_cert) 31 | der_bytes = ocsp_response.dump() 32 | 33 | new_response = asn1crypto.ocsp.OCSPResponse.load(der_bytes) 34 | basic_response = new_response['response_bytes']['response'].parsed 35 | response_data = basic_response['tbs_response_data'] 36 | 37 | self.assertEqual('sha256', basic_response['signature_algorithm'].hash_algo) 38 | self.assertEqual('rsassa_pkcs1v15', basic_response['signature_algorithm'].signature_algo) 39 | self.assertEqual('v1', response_data['version'].native) 40 | self.assertEqual('by_key', response_data['responder_id'].name) 41 | self.assertEqual( 42 | issuer_cert.asn1.public_key.sha1, 43 | response_data['responder_id'].chosen.native 44 | ) 45 | self.assertGreaterEqual(datetime.now(timezone.utc), response_data['produced_at'].native) 46 | self.assertEqual(1, len(response_data['responses'])) 47 | self.assertEqual(0, len(response_data['response_extensions'])) 48 | 49 | cert_response = response_data['responses'][0] 50 | 51 | self.assertEqual('sha1', cert_response['cert_id']['hash_algorithm']['algorithm'].native) 52 | self.assertEqual(issuer_cert.asn1.subject.sha1, cert_response['cert_id']['issuer_name_hash'].native) 53 | self.assertEqual(issuer_cert.asn1.public_key.sha1, cert_response['cert_id']['issuer_key_hash'].native) 54 | self.assertEqual(subject_cert.asn1.serial_number, cert_response['cert_id']['serial_number'].native) 55 | 56 | self.assertEqual('good', cert_response['cert_status'].name) 57 | self.assertGreaterEqual(datetime.now(timezone.utc), cert_response['this_update'].native) 58 | self.assertGreaterEqual(set(), cert_response.critical_extensions) 59 | 60 | def test_build_no_certificate(self): 61 | issuer_key = asymmetric.load_private_key(os.path.join(fixtures_dir, 'test.key')) 62 | issuer_cert = asymmetric.load_certificate(os.path.join(fixtures_dir, 'test.crt')) 63 | subject_cert = asymmetric.load_certificate(os.path.join(fixtures_dir, 'test-inter.crt')) 64 | 65 | with self.assertRaisesRegex(ValueError, 'must be set if the response_status is "successful"'): 66 | builder = OCSPResponseBuilder('successful', subject_cert, 'good') 67 | builder.certificate = None 68 | builder.build(issuer_key, issuer_cert) 69 | 70 | with self.assertRaisesRegex(ValueError, 'must be set if the response_status is "successful"'): 71 | builder = OCSPResponseBuilder('successful', subject_cert, 'good') 72 | builder.certificate_status = None 73 | builder.build(issuer_key, issuer_cert) 74 | 75 | with self.assertRaisesRegex(ValueError, 'must be set if the response_status is "successful"'): 76 | builder = OCSPResponseBuilder('successful', subject_cert) 77 | builder.build(issuer_key, issuer_cert) 78 | 79 | with self.assertRaisesRegex(ValueError, 'must be set if the response_status is "successful"'): 80 | builder = OCSPResponseBuilder('successful', None, 'good') 81 | builder.build(issuer_key, issuer_cert) 82 | 83 | def test_build_revoked_response(self): 84 | issuer_key = asymmetric.load_private_key(os.path.join(fixtures_dir, 'test.key')) 85 | issuer_cert = asymmetric.load_certificate(os.path.join(fixtures_dir, 'test.crt')) 86 | subject_cert = asymmetric.load_certificate(os.path.join(fixtures_dir, 'test-inter.crt')) 87 | 88 | revoked_time = datetime(2015, 9, 1, 12, 0, 0, tzinfo=timezone.utc) 89 | builder = OCSPResponseBuilder('successful', subject_cert, 'key_compromise', revoked_time) 90 | ocsp_response = builder.build(issuer_key, issuer_cert) 91 | der_bytes = ocsp_response.dump() 92 | 93 | new_response = asn1crypto.ocsp.OCSPResponse.load(der_bytes) 94 | basic_response = new_response['response_bytes']['response'].parsed 95 | response_data = basic_response['tbs_response_data'] 96 | 97 | self.assertEqual('sha256', basic_response['signature_algorithm'].hash_algo) 98 | self.assertEqual('rsassa_pkcs1v15', basic_response['signature_algorithm'].signature_algo) 99 | self.assertEqual('v1', response_data['version'].native) 100 | self.assertEqual('by_key', response_data['responder_id'].name) 101 | self.assertEqual( 102 | issuer_cert.asn1.public_key.sha1, 103 | response_data['responder_id'].chosen.native 104 | ) 105 | self.assertGreaterEqual(datetime.now(timezone.utc), response_data['produced_at'].native) 106 | self.assertEqual(1, len(response_data['responses'])) 107 | self.assertEqual(0, len(response_data['response_extensions'])) 108 | 109 | cert_response = response_data['responses'][0] 110 | 111 | self.assertEqual('sha1', cert_response['cert_id']['hash_algorithm']['algorithm'].native) 112 | self.assertEqual(issuer_cert.asn1.subject.sha1, cert_response['cert_id']['issuer_name_hash'].native) 113 | self.assertEqual(issuer_cert.asn1.public_key.sha1, cert_response['cert_id']['issuer_key_hash'].native) 114 | self.assertEqual(subject_cert.asn1.serial_number, cert_response['cert_id']['serial_number'].native) 115 | 116 | self.assertEqual('revoked', cert_response['cert_status'].name) 117 | self.assertEqual(revoked_time, cert_response['cert_status'].chosen['revocation_time'].native) 118 | self.assertEqual('key_compromise', cert_response['cert_status'].chosen['revocation_reason'].native) 119 | self.assertGreaterEqual(datetime.now(timezone.utc), cert_response['this_update'].native) 120 | self.assertGreaterEqual(set(), cert_response.critical_extensions) 121 | 122 | def test_build_revoked_no_reason(self): 123 | issuer_key = asymmetric.load_private_key(os.path.join(fixtures_dir, 'test.key')) 124 | issuer_cert = asymmetric.load_certificate(os.path.join(fixtures_dir, 'test.crt')) 125 | subject_cert = asymmetric.load_certificate(os.path.join(fixtures_dir, 'test-inter.crt')) 126 | 127 | revoked_time = datetime(2015, 9, 1, 12, 0, 0, tzinfo=timezone.utc) 128 | builder = OCSPResponseBuilder('successful', subject_cert, 'revoked', revoked_time) 129 | ocsp_response = builder.build(issuer_key, issuer_cert) 130 | der_bytes = ocsp_response.dump() 131 | 132 | new_response = asn1crypto.ocsp.OCSPResponse.load(der_bytes) 133 | basic_response = new_response['response_bytes']['response'].parsed 134 | response_data = basic_response['tbs_response_data'] 135 | cert_response = response_data['responses'][0] 136 | 137 | self.assertEqual('revoked', cert_response['cert_status'].name) 138 | self.assertEqual(revoked_time, cert_response['cert_status'].chosen['revocation_time'].native) 139 | self.assertEqual('unspecified', cert_response['cert_status'].chosen['revocation_reason'].native) 140 | 141 | def test_build_delegated_good_response(self): 142 | responder_key = asymmetric.load_private_key(os.path.join(fixtures_dir, 'test-ocsp.key'), 'password') 143 | responder_cert = asymmetric.load_certificate(os.path.join(fixtures_dir, 'test-ocsp.crt')) 144 | issuer_cert = asymmetric.load_certificate(os.path.join(fixtures_dir, 'test.crt')) 145 | subject_cert = asymmetric.load_certificate(os.path.join(fixtures_dir, 'test-inter.crt')) 146 | 147 | builder = OCSPResponseBuilder('successful', subject_cert, 'good') 148 | builder.certificate_issuer = issuer_cert 149 | ocsp_response = builder.build(responder_key, responder_cert) 150 | der_bytes = ocsp_response.dump() 151 | 152 | new_response = asn1crypto.ocsp.OCSPResponse.load(der_bytes) 153 | basic_response = new_response['response_bytes']['response'].parsed 154 | response_data = basic_response['tbs_response_data'] 155 | 156 | self.assertEqual('sha256', basic_response['signature_algorithm'].hash_algo) 157 | self.assertEqual('rsassa_pkcs1v15', basic_response['signature_algorithm'].signature_algo) 158 | self.assertEqual('v1', response_data['version'].native) 159 | self.assertEqual('by_key', response_data['responder_id'].name) 160 | self.assertEqual( 161 | responder_cert.asn1.public_key.sha1, 162 | response_data['responder_id'].chosen.native 163 | ) 164 | self.assertGreaterEqual(datetime.now(timezone.utc), response_data['produced_at'].native) 165 | self.assertEqual(1, len(response_data['responses'])) 166 | self.assertEqual(0, len(response_data['response_extensions'])) 167 | 168 | cert_response = response_data['responses'][0] 169 | 170 | self.assertEqual('sha1', cert_response['cert_id']['hash_algorithm']['algorithm'].native) 171 | self.assertEqual(issuer_cert.asn1.subject.sha1, cert_response['cert_id']['issuer_name_hash'].native) 172 | self.assertEqual(issuer_cert.asn1.public_key.sha1, cert_response['cert_id']['issuer_key_hash'].native) 173 | self.assertEqual(subject_cert.asn1.serial_number, cert_response['cert_id']['serial_number'].native) 174 | 175 | self.assertEqual('good', cert_response['cert_status'].name) 176 | self.assertGreaterEqual(datetime.now(timezone.utc), cert_response['this_update'].native) 177 | self.assertGreaterEqual(set(), cert_response.critical_extensions) 178 | 179 | def test_build_unknown_response(self): 180 | issuer_key = asymmetric.load_private_key(os.path.join(fixtures_dir, 'test.key')) 181 | issuer_cert = asymmetric.load_certificate(os.path.join(fixtures_dir, 'test.crt')) 182 | subject_cert = asymmetric.load_certificate(os.path.join(fixtures_dir, 'test-inter.crt')) 183 | 184 | builder = OCSPResponseBuilder('successful', subject_cert, 'unknown') 185 | ocsp_response = builder.build(issuer_key, issuer_cert) 186 | der_bytes = ocsp_response.dump() 187 | 188 | new_response = asn1crypto.ocsp.OCSPResponse.load(der_bytes) 189 | basic_response = new_response['response_bytes']['response'].parsed 190 | response_data = basic_response['tbs_response_data'] 191 | 192 | self.assertEqual('sha256', basic_response['signature_algorithm'].hash_algo) 193 | self.assertEqual('rsassa_pkcs1v15', basic_response['signature_algorithm'].signature_algo) 194 | self.assertEqual('v1', response_data['version'].native) 195 | self.assertEqual('by_key', response_data['responder_id'].name) 196 | self.assertEqual( 197 | issuer_cert.asn1.public_key.sha1, 198 | response_data['responder_id'].chosen.native 199 | ) 200 | self.assertGreaterEqual(datetime.now(timezone.utc), response_data['produced_at'].native) 201 | self.assertEqual(1, len(response_data['responses'])) 202 | self.assertEqual(0, len(response_data['response_extensions'])) 203 | 204 | cert_response = response_data['responses'][0] 205 | 206 | self.assertEqual('sha1', cert_response['cert_id']['hash_algorithm']['algorithm'].native) 207 | self.assertEqual(issuer_cert.asn1.subject.sha1, cert_response['cert_id']['issuer_name_hash'].native) 208 | self.assertEqual(issuer_cert.asn1.public_key.sha1, cert_response['cert_id']['issuer_key_hash'].native) 209 | self.assertEqual(subject_cert.asn1.serial_number, cert_response['cert_id']['serial_number'].native) 210 | 211 | self.assertEqual('unknown', cert_response['cert_status'].name) 212 | self.assertGreaterEqual(datetime.now(timezone.utc), cert_response['this_update'].native) 213 | self.assertGreaterEqual(set(), cert_response.critical_extensions) 214 | 215 | def test_build_error_response(self): 216 | """ 217 | Build a response with error status. 218 | """ 219 | error_statuses = ['malformed_request', 'internal_error', 220 | 'try_later', 'sign_required', 'unauthorized'] 221 | 222 | for status in error_statuses: 223 | builder = OCSPResponseBuilder(status) 224 | ocsp_response = builder.build() 225 | der_bytes = ocsp_response.dump() 226 | 227 | new_response = asn1crypto.ocsp.OCSPResponse.load(der_bytes) 228 | assert dict(new_response.native) == { 229 | 'response_status': status, 230 | 'response_bytes': None, 231 | } 232 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26,py27,py32,py33,py34,py35,py36,py37,py38,py39,pypy 3 | 4 | [testenv] 5 | deps = -rrequires/ci 6 | commands = {envpython} run.py ci 7 | 8 | [pep8] 9 | max-line-length = 120 10 | 11 | [flake8] 12 | max-line-length = 120 13 | jobs = 1 14 | --------------------------------------------------------------------------------