diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..0f8cbd1 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,93 @@ +name: Deploy Repository + +on: + # Runs on pushes targeting the default branch + push: + branches: [$default-branch] + + # Runs dayly at 00:00 UTC + schedule: + - cron: "0 0 * * *" + + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + outputs: + updated: ${{ steps.resolve-repository.outputs.updated }} + steps: + - name: Set up python + id: setup-python + uses: actions/setup-python@v4 + with: + python-version: 3.11 + + - name: Checkout repository + id: checkout-repository + uses: actions/checkout@v4 + + - name: Restore cache + id: restore-cache + uses: actions/cache/restore@v3 + with: + path: |- + cache/ + _site/libraries.json.sha512 + blacklist.json + key: ${{ runner.os }}-state + restore-keys: | + ${{ runner.os }}-state + + - name: Resolve repository + id: resolve-repository + env: + GH_USER: ${{secrets.GH_APP_ID}} + GH_PASS: ${{secrets.GH_APP_TOKEN}} + run: python3 tasks crawl + + - name: Save cache + id: save-cache + uses: actions/cache/save@v3 + if: always() + with: + path: |- + cache/ + _site/libraries.json.sha512 + blacklist.json + key: ${{ runner.os }}-state-${{ hashFiles('cache/*') }} + + - name: Setup pages + id: setup-pages + if: steps.resolve-repository.outputs.updated == true + uses: actions/configure-pages@v3 + + - name: Upload pages + id: upload-pages + if: steps.resolve-repository.outputs.updated == true + uses: actions/upload-pages-artifact@v2 + with: + path: _site/ + + # Deploy job + deploy: + # Add a dependency to the build job + needs: build + if: needs.build.outputs.updated == true + + # Grant GITHUB_TOKEN the permissions required to make a Pages deployment + permissions: + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + + # Deploy to the github-pages environment + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + # Specify runner + deployment step + runs-on: ubuntu-latest + steps: + - name: Deploy pages + id: deployment + uses: actions/deploy-pages@v2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a405cdd --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# python cache +__pycache__/ +*.pyc + +.sublime/ diff --git a/_site/index.html b/_site/index.html new file mode 100644 index 0000000..914b18a --- /dev/null +++ b/_site/index.html @@ -0,0 +1,12 @@ + + + + + + Package Control Libraries + + +

Hello World

+ + + \ No newline at end of file diff --git a/repository.json b/repository.json new file mode 100644 index 0000000..d5f3f42 --- /dev/null +++ b/repository.json @@ -0,0 +1,1320 @@ +{ + "$schema": "sublime://packagecontrol.io/schemas/repository", + "schema_version": "4.0.0", + "packages": [], + "libraries": [ + { + "name": "anytree", + "description": "Python Anytree module - https://github.com/c0fec0de/anytree", + "author": "c0fec0de", + "issues": "https://github.com/c0fec0de/anytree/issues", + "releases": [ + { + "base": "https://github.com/nbeversl/anytree_sublime", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "arrow", + "description": "Python arrow module - https://github.com/crsmithdev/arrow", + "author": "douglas-vaz", + "issues": "https://github.com/douglas-vaz/arrow/issues", + "releases": [ + { + "base": "https://github.com/douglas-vaz/arrow", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "backports_lzma", + "description": "lzma modules for sublime text 2 and 3", + "author": "dvhh", + "issues": "https://github.com/dvhh/sublime-backports.lzma/issues", + "releases": [ + { + "base": "https://github.com/dvhh/sublime-backports.lzma", + "platforms": ["windows"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "backrefs", + "description": "Backrefs regular expression wrapper.", + "author": "facelessuser", + "issues": "https://github.com/facelessuser/sublime-backrefs/issues", + "releases": [ + { + "base": "https://github.com/facelessuser/sublime-backrefs", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "beautifulsoup4", + "description": "Beautiful Soup is a Python library for pulling data out of HTML and XML files - https://www.crummy.com/software/BeautifulSoup/", + "author": "jlegewie", + "issues": "https://github.com/jlegewie/sublime-beautifulsoup4/issues", + "releases": [ + { + "base": "https://github.com/jlegewie/sublime-beautifulsoup4", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "better_settings", + "description": "Easily implement OS, Host and OS/Host-level settings aside the normal user-level settings for any plugin.", + "author": "KuttKatrea", + "issues": "https://github.com/KuttKatrea/sublime-better-settings/issues", + "releases": [ + { + "base": "https://github.com/KuttKatrea/sublime-better-settings", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "boto3", + "description": "Python boto3 module for Amazon Web Services", + "author": "revmischa", + "issues": "https://github.com/revmischa/sublime-boto3/issues", + "releases": [ + { + "base": "https://github.com/revmischa/sublime-boto3", + "platforms": ["*"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "bracex", + "description": "Bracex creates arbitrary strings via brace expansion much like Bash's.", + "author": "facelessuser", + "issues": "https://github.com/facelessuser/sublime-bracex/issues", + "releases": [ + { + "base": "https://github.com/facelessuser/sublime-bracex", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "bson", + "description": "Independent BSON codec for Python that doesn’t depend on MongoDB - https://github.com/py-bson/bson", + "author": "idleberg", + "issues": "https://github.com/idleberg/sublime-bson/issues", + "releases": [ + { + "base": "https://github.com/idleberg/sublime-bson", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "bz2", + "description": "Python bz2 module", + "author": "wbond", + "issues": "https://github.com/codexns/sublime-bz2/issues", + "releases": [ + { + "base": "https://github.com/codexns/sublime-bz2", + "platforms": ["*"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "channelmanager", + "description": "Manages packages installed as git submodules", + "author": "evandrocoan", + "issues": "https://github.com/evandrocoan/channelmanager/issues", + "releases": [ + { + "base": "https://github.com/evandrocoan/channelmanager", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "chardet", + "description": "Universal encoding detector for Python 2 and 3 https://github.com/chardet/chardet", + "author": "Umoxfo", + "issues": "https://github.com/Umoxfo/sublime-chardet/issues", + "releases": [ + { + "base": "https://github.com/Umoxfo/sublime-chardet", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "coloraide", + "description": "A color library.", + "author": "facelessuser", + "issues": "https://github.com/facelessuser/sublime-coloraide/issues", + "releases": [ + { + "base": "https://github.com/facelessuser/sublime-coloraide", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "concurrentloghandler", + "description": "An additional concurrent file log handler for Python's standard logging package", + "author": "evandrocoan", + "issues": "https://github.com/evandroforks/concurrentloghandler/issues", + "releases": [ + { + "base": "https://github.com/evandroforks/concurrentloghandler", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "coverage", + "description": "coverage.py - http://coverage.readthedocs.org/en/latest/", + "author": "wbond", + "issues": "https://github.com/codexns/sublime-coverage/issues", + "releases": [ + { + "base": "https://github.com/codexns/sublime-coverage", + "platforms": ["*"], + "python_versions": ["3.3"], + "tags": true + }, + { + "base": "https://pypi.org/project/coverage", + "asset": "coverage-*-cp38-cp38-win_amd64.whl", + "tags": true, + "platforms": ["windows-x64"], + "python_versions": ["3.8"] + }, + { + "base": "https://pypi.org/project/coverage", + "asset": "coverage-*-cp38-cp38-win32.whl", + "tags": true, + "platforms": ["windows-x32"], + "python_versions": ["3.8"] + }, + { + "base": "https://pypi.org/project/coverage", + "asset": "coverage-*-cp38-cp38-manylinux_2_17_aarch64*.whl", + "tags": true, + "platforms": ["linux-arm64"], + "python_versions": ["3.8"] + }, + { + "base": "https://pypi.org/project/coverage", + "asset": "coverage-*-cp38-cp38-manylinux_2_5_x86_64*.whl", + "tags": true, + "platforms": ["linux-x64"], + "python_versions": ["3.8"] + }, + { + "base": "https://pypi.org/project/coverage", + "asset": "coverage-*-cp38-cp38-manylinux_2_5_i686*.whl", + "tags": true, + "platforms": ["linux-x32"], + "python_versions": ["3.8"] + }, + { + "base": "https://pypi.org/project/coverage", + "asset": "coverage-*-cp38-cp38-macosx_11_0_arm64.whl", + "tags": true, + "platforms": ["osx-arm64"], + "python_versions": ["3.8"] + }, { + "base": "https://pypi.org/project/coverage", + "asset": "coverage-*-cp38-cp38-macosx_10_9_x86_64.whl", + "tags": true, + "platforms": ["osx-x64"], + "python_versions": ["3.8"] + }, + { + "base": "https://pypi.org/project/coverage", + "asset": "coverage-*-cp33-cp33m-manylinux1_i686.whl", + "tags": true, + "platforms": ["linux-x32"], + "python_versions": ["3.3"] + }, + { + "base": "https://pypi.org/project/coverage", + "asset": "coverage-*-cp33-cp33m-manylinux1_x86_64.whl", + "tags": true, + "platforms": ["linux-x64"], + "python_versions": ["3.3"] + }, + { + "base": "https://pypi.org/project/coverage", + "asset": "coverage-*-cp33-cp33m-macosx_10_10_x86_64.whl", + "tags": true, + "platforms": ["osx-x64"], + "python_versions": ["3.3"] + }, + { + "base": "https://pypi.org/project/coverage", + "asset": "coverage-*-cp33-cp33m-win32.whl", + "tags": true, + "platforms": ["windows-x32"], + "python_versions": ["3.3"] + }, + { + "base": "https://pypi.org/project/coverage", + "asset": "coverage-*-cp33-cp33m-win_amd64.whl", + "tags": true, + "platforms": ["windows-x64"], + "python_versions": ["3.3"] + } + ] + }, + { + "name": "crypt", + "description": "Python crypt module", + "author": "adnanyaqoobvirk", + "issues": "https://github.com/adnanyaqoobvirk/sublime-crypt/issues", + "releases": [ + { + "base": "https://github.com/adnanyaqoobvirk/sublime-crypt", + "platforms": ["linux"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "cson", + "description": "A Coffescript Object Notation (CSON) parser for Python - https://github.com/avakar/pycson", + "author": "idleberg", + "issues": "https://github.com/idleberg/sublime-cson/issues", + "releases": [ + { + "base": "https://github.com/idleberg/sublime-cson", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "dateutil", + "description": "The dateutil module provides powerful extensions to the datetime module available in the Python standard library", + "author": "vovkkk", + "issues": "https://github.com/vovkkk/sublime-dateutil/issues", + "releases": [ + { + "base": "https://github.com/vovkkk/sublime-dateutil", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "debugtools", + "description": "Alternate simplified logging support and general utilities functions", + "author": "evandrocoan", + "issues": "https://github.com/evandrocoan/debugtools/issues", + "releases": [ + { + "base": "https://github.com/evandrocoan/debugtools", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "dicttoxml", + "description": "Converts a Python dictionary or other native data type into a valid XML string - https://github.com/idleberg/sublime-dicttoxml", + "author": "idleberg", + "issues": "https://github.com/idleberg/sublime-dicttoxml/issues", + "releases": [ + { + "base": "https://github.com/idleberg/sublime-dicttoxml", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "diffmatchpatch", + "description": "Autored by Neil Fraser and others, Diff Match Patch is a high-performance library in multiple languages that manipulates plain text", + "author": "evandrocoan", + "issues": "https://github.com/evandroforks/diffmatchpatch/issues", + "releases": [ + { + "base": "https://github.com/evandroforks/diffmatchpatch", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "docx", + "description": "Python DOCX Module", + "author": "WriteML", + "issues": "https://github.com/writeml/sublime-docx/issues", + "releases": [ + { + "base": "https://github.com/writeml/sublime-docx", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "emojitations", + "description": "A library for using Unicode emoji annotations - https://github.com/kcsaff/emojitations", + "author": "idleberg", + "issues": "https://github.com/idleberg/sublime-emojitations/issues", + "releases": [ + { + "base": "https://github.com/idleberg/sublime-emojitations", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "enum", + "description": "Python enum module", + "author": "FichteFoll", + "issues": "https://github.com/packagecontrol/enum/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/enum", + "platforms": ["*"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "gateone-terminal", + "description": "GateOne terminal", + "author": "randy3k", + "issues": "https://github.com/packagecontrol/gateone-terminal/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/gateone-terminal", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "gntp", + "description": "Growl Notification Transport Protocol library.", + "author": "facelessuser", + "issues": "https://github.com/facelessuser/sublime-gntp/issues", + "releases": [ + { + "base": "https://github.com/facelessuser/sublime-gntp", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "golangconfig", + "description": "A library for Go environment configuration", + "author": "Go Authors", + "issues": "https://github.com/golang/sublime-config/issues", + "releases": [ + { + "base": "https://github.com/golang/sublime-config", + "platforms": ["*"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "Jinja2", + "description": "Python Jinja2 module", + "author": "FichteFoll", + "issues": "https://github.com/packagecontrol/jinja2/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/jinja2", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "jsonschema", + "description": "An(other) implementation of JSON Schema for Python", + "author": "kylebebak", + "issues": "https://github.com/kylebebak/sublime-jsonschema/issues", + "releases": [ + { + "base": "https://github.com/kylebebak/sublime-jsonschema", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "lxml", + "description": "lxml", + "author": "eerohele", + "issues": "https://github.com/eerohele/sublime-lxml/issues", + "releases": [ + { + "base": "https://github.com/eerohele/sublime-lxml", + "platforms": ["osx-x64","linux","windows"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "lzma", + "description": "Python lzma module", + "author": "ehamiter", + "issues": "https://github.com/ehamiter/sublime-lzma/issues", + "releases": [ + { + "base": "https://github.com/ehamiter/sublime-lzma", + "platforms": ["osx-x64"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "Markdown", + "description": "Python Markdown module", + "author": "facelessuser", + "issues": "https://github.com/facelessuser/sublime-markdown/issues", + "releases": [ + { + "base": "https://github.com/facelessuser/sublime-markdown", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "markupsafe", + "description": "Python MarkupSafe module", + "author": "FichteFoll", + "issues": "https://github.com/packagecontrol/MarkupSafe/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/MarkupSafe", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "mdpopups", + "description": "Markdown Popups for Sublime", + "author": "facelessuser", + "issues": "https://github.com/facelessuser/sublime-markdown-popups/issues", + "releases": [ + { + "base": "https://github.com/facelessuser/sublime-markdown-popups", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "natsort", + "description": "Python natsort package", + "author": "alimony", + "issues": "https://github.com/alimony/sublime-natsort/issues", + "releases": [ + { + "base": "https://github.com/alimony/sublime-natsort", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "newterm", + "description": "Open a terminal to a specific folder and optionally set environment variables", + "author": "wbond", + "issues": "https://github.com/codexns/newterm/issues", + "releases": [ + { + "base": "https://github.com/codexns/newterm", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "numpy", + "description": "Python numpy module", + "author": "komsit37", + "issues": "https://github.com/komsit37/sublime-numpy/issues", + "releases": [ + { + "base": "https://github.com/komsit37/sublime-numpy", + "platforms": ["windows-x64","osx-x64","linux-x64"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "oauthlib", + "description": "Python oauthlib module", + "author": "csch0", + "issues": "https://github.com/packagecontrol/oauthlib/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/oauthlib", + "platforms": ["*"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "package_events", + "description": "Allows Sublime Text packages to emit and listen for events", + "author": "wbond", + "issues": "https://github.com/codexns/package_events/issues", + "releases": [ + { + "base": "https://github.com/codexns/package_events", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "package_setting_context", + "description": "Allow final user to disable key bindings with very few changes needed from the dev", + "author": "math2001", + "issues": "https://github.com/math2001/package_setting_context/issues", + "releases": [ + { + "base": "https://github.com/math2001/package_setting_context", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "paramiko", + "description": "Python implementation of the SSHv2 protocol - http://paramiko-www.readthedocs.org/en/latest/index.html", + "author": "jlegewie", + "issues": "https://github.com/jlegewie/sublime-paramiko/issues", + "releases": [ + { + "base": "https://github.com/jlegewie/sublime-paramiko", + "platforms": ["osx"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "pathlib", + "description": "Python pathlib module", + "author": "FichteFoll", + "issues": "https://github.com/packagecontrol/pathlib/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/pathlib", + "platforms": ["*"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "pathtools", + "description": "Path utilities for Python https://pypi.python.org/pypi/pathtools", + "author": "vovkkk", + "issues": "https://github.com/vovkkk/sublime-pathtools/issues", + "releases": [ + { + "base": "https://github.com/vovkkk/sublime-pathtools", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "pexpect", + "description": "Python pexpect module", + "author": "varp", + "issues": "https://github.com/varp/sublime-pexpect/issues", + "releases": [ + { + "base": "https://github.com/varp/sublime-pexpect", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "plantumlconnection", + "description": "Python interface to a plantuml web service instead of having to run Java locally", + "author": "evandrocoan", + "issues": "https://github.com/evandrocoan/plantumlconnection/issues", + "releases": [ + { + "base": "https://github.com/evandrocoan/plantumlconnection", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "portalockerfile", + "description": "An extended version of portalocker to lock files in Python using the with statement", + "author": "evandrocoan", + "issues": "https://github.com/evandroforks/portalockerfile/issues", + "releases": [ + { + "base": "https://github.com/evandroforks/portalockerfile", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "psutil", + "description": "Python psutil module", + "author": "randy3k", + "issues": "https://github.com/packagecontrol/psutil/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/psutil", + "platforms": ["osx-x64","windows","linux"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "ptyprocess", + "description": "Python ptyprocess module", + "author": "randy3k", + "issues": "https://github.com/packagecontrol/ptyprocess/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/ptyprocess", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "pushdownparser", + "description": "A modern parsing library for Python, implementing Earley & LALR(1) and an easy interface", + "author": "evandrocoan", + "issues": "https://github.com/evandrocoan/pushdownparser/issues", + "releases": [ + { + "base": "https://github.com/evandrocoan/pushdownparser", + "platforms": ["*"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "PyCrypto", + "description": "Python Cryptography Toolkit - https://www.dlitz.net/software/pycrypto/", + "author": "jlegewie", + "issues": "https://github.com/jlegewie/sublime-PyCrypto/issues", + "releases": [ + { + "base": "https://github.com/jlegewie/sublime-PyCrypto", + "platforms": ["osx-x64"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "pyfispip", + "description": " Python FIS MTM/PIP SQL/RPC Interface", + "author": "fopina", + "issues": "https://github.com/fopina/sublime-pyfispip/issues", + "releases": [ + { + "base": "https://github.com/fopina/sublime-pyfispip", + "platforms": ["*"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "pygments", + "description": "Python pygments module", + "author": "AndreasBackx", + "issues": "https://github.com/packagecontrol/pygments/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/pygments", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "pymdownx", + "description": "PyMdown Extensions for Python Markdown", + "author": "facelessuser", + "issues": "https://github.com/facelessuser/sublime-pymdownx/issues", + "releases": [ + { + "base": "https://github.com/facelessuser/sublime-pymdownx", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "pyserial", + "description": "Python serial port access library", + "author": "pyserial", + "issues": "https://github.com/bisguzar/sublime-serial/issues", + "releases": [ + { + "base": "https://github.com/bisguzar/sublime-serial", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "pyte", + "description": "Python pyte module", + "author": "randy3k", + "issues": "https://github.com/packagecontrol/pyte/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/pyte", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "pytz", + "description": "Python pytz module", + "author": "FichteFoll", + "issues": "https://github.com/packagecontrol/pytz/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/pytz", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "pywin32", + "description": "Pywin32 module", + "author": "randy3k", + "issues": "https://github.com/packagecontrol/pywin32/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/pywin32", + "platforms": ["windows"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "pywinpty", + "description": "Python winpty module", + "author": "randy3k", + "issues": "https://github.com/packagecontrol/pywinpty/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/pywinpty", + "platforms": ["windows"], + "python_versions": ["3.3"], + "tags": true + }, + { + "base": "https://pypi.org/project/pywinpty", + "asset": "pywinpty-*-cp38-*-win_amd64.whl", + "platforms": ["windows-x64"], + "python_versions": ["3.8"] + } + ] + }, + { + "name": "pyyaml", + "description": "Python PyYAML module", + "author": "FichteFoll", + "issues": "https://github.com/packagecontrol/pyyaml/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/pyyaml", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "pyzmq", + "description": "Python ZMQ module", + "author": "randy3k", + "issues": "https://github.com/packagecontrol/pyzmq/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/pyzmq", + "platforms": ["*"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "regex", + "description": "Regex Module", + "author": "facelessuser", + "issues": "https://github.com/facelessuser/sublime-regex/issues", + "releases": [ + { + "base": "https://github.com/facelessuser/sublime-regex", + "platforms": ["*"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "requests", + "description": "Python requests module", + "author": "FichteFoll", + "issues": "https://github.com/packagecontrol/requests/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/requests", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "requests-oauthlib", + "description": "Python requests-oauthlib module", + "author": "csch0", + "issues": "https://github.com/packagecontrol/requests-oauthlib/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/requests-oauthlib", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "resumeback", + "description": "Python resumeback module", + "author": "FichteFoll", + "issues": "https://github.com/packagecontrol/resumeback/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/resumeback", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "ruamel.yaml", + "description": "Python ruamel.yaml module", + "author": "Thomas Smith", + "issues": "https://github.com/Thom1729/sublime-ruamel/issues", + "releases": [ + { + "base": "https://github.com/Thom1729/sublime-ruamel", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "rx", + "description": "Reactive extensions for Python", + "author": "guillermooo", + "issues": "https://github.com/guillermooo/rxpyst/issues", + "releases": [ + { + "base": "https://github.com/guillermooo/rxpyst", + "platforms": ["*"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "sassc", + "description": "Sassc binaries", + "author": "blitzrk", + "issues": "https://github.com/blitzrk/sublime_sassc/issues", + "releases": [ + { + "base": "https://github.com/blitzrk/sublime_sassc", + "platforms": ["*"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "screeninfo", + "description": "Fetch location and size of physical screens - https://github.com/rr-/screeninfo", + "author": "ggets", + "issues": "https://github.com/ggets/sublime-screeninfo/issues", + "releases": [ + { + "base": "https://github.com/ggets/sublime-screeninfo", + "platforms": ["*"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "select-windows", + "description": "Python select module for Sublime Text 2 on Windows", + "author": "wbond", + "issues": "https://github.com/codexns/sublime-select-windows/issues", + "releases": [ + { + "base": "https://github.com/codexns/sublime-select-windows", + "platforms": ["*"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "shellenv", + "description": "Access a user's environmental variables as defined in their shell", + "author": "wbond", + "issues": "https://github.com/codexns/shellenv/issues", + "releases": [ + { + "base": "https://github.com/codexns/shellenv", + "platforms": ["*"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "six", + "description": "Python six library https://github.com/benjaminp/six", + "author": "jcoc611", + "issues": "https://github.com/packagecontrol/sublime-six/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/sublime-six", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "speg", + "description": "A PEG-based parser interpreter with memoization - https://github.com/avakar/speg", + "author": "idleberg", + "issues": "https://github.com/idleberg/sublime-speg/issues", + "releases": [ + { + "base": "https://github.com/idleberg/sublime-speg", + "platforms": ["*"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "sqlite3", + "description": "Python _sqlite3 module", + "author": "wbond", + "issues": "https://github.com/codexns/sublime-sqlite3/issues", + "releases": [ + { + "base": "https://github.com/codexns/sublime-sqlite3", + "platforms": ["windows","linux"], + "python_versions": ["3.3"], + "tags": true + }, + { + "base": "https://github.com/codexns/sublime-sqlite3", + "platforms": ["osx"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "StyledPopup", + "description": "Pyton module for Sublime Text to automatically style popups based on active color scheme.", + "author": "huot25", + "issues": "https://github.com/huot25/StyledPopup/issues", + "releases": [ + { + "base": "https://github.com/huot25/StyledPopup", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "sublime_lib", + "description": "Utility library for frequently used functionality in Sublime Text library and convenience functions or classes", + "author": "Thomas Smith", + "issues": "https://github.com/SublimeText/sublime_lib/issues", + "releases": [ + { + "base": "https://github.com/SublimeText/sublime_lib", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "SublimeP4Python", + "description": "Python module for the Perforce API, extracted from https://pypi.python.org/pypi/P4Python.", + "author": "mrelusive", + "issues": "https://github.com/claytonlemons/SublimeP4Python/issues", + "releases": [ + { + "base": "https://github.com/claytonlemons/SublimeP4Python", + "platforms": ["windows"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "tabulate", + "description": "Python tabulate module", + "author": "csch0", + "issues": "https://github.com/packagecontrol/tabulate/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/tabulate", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "TM1py", + "description": "Python module for interfacing with IBM TM1 Planning Analytics", + "author": "MariusWirtz", + "issues": "https://github.com/ajmyers/TM1py/issues", + "releases": [ + { + "base": "https://github.com/ajmyers/TM1py", + "platforms": ["*"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "toml", + "description": "Python lib for TOML", + "author": "Jimmy Girardet", + "issues": "https://github.com/jgirardet/sublime-toml/issues", + "releases": [ + { + "base": "https://github.com/jgirardet/sublime-toml", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "typing", + "description": "Python typing module", + "author": "FichteFoll", + "issues": "https://github.com/packagecontrol/typing/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/typing", + "platforms": ["*"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "watchdog", + "description": "Python library to monitor filesystem events http://packages.python.org/watchdog/", + "author": "vovkkk", + "issues": "https://github.com/vovkkk/sublime-watchdog/issues", + "releases": [ + { + "base": "https://github.com/vovkkk/sublime-watchdog", + "platforms": ["*"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "wcmatch", + "description": "Python wcmatch module which provides enhanced file globbing and matching", + "author": "facelessuser", + "issues": "https://github.com/facelessuser/sublime-wcmatch/issues", + "releases": [ + { + "base": "https://github.com/facelessuser/sublime-wcmatch", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "wcwidth", + "description": "Python wcwidth module", + "author": "randy3k", + "issues": "https://github.com/packagecontrol/wcwidth/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/wcwidth", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "websocket-client", + "description": "Python websocket client library https://github.com/websocket-client/websocket-client", + "author": "jcoc611", + "issues": "https://github.com/packagecontrol/sublime-websocket-client/issues", + "releases": [ + { + "base": "https://github.com/packagecontrol/sublime-websocket-client", + "platforms": ["*"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "xdotool", + "description": "Automation tool on Linux", + "author": "randy3k", + "issues": "https://github.com/randy3k/sublime-xdotool/issues", + "releases": [ + { + "base": "https://github.com/randy3k/sublime-xdotool", + "platforms": ["linux"], + "python_versions": ["3.3"], + "tags": true + } + ] + }, + { + "name": "xmltodict", + "description": "Makes working with XML feel like you are working with JSON - https://github.com/martinblech/xmltodict", + "author": "idleberg", + "issues": "https://github.com/idleberg/sublime-xmltodict/issues", + "releases": [ + { + "base": "https://github.com/idleberg/sublime-xmltodict", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + }, + { + "name": "yaml_macros_engine", + "description": "Engine for YAML Macros", + "author": "Thomas Smith", + "issues": "https://github.com/Thom1729/yaml-macros-engine/issues", + "releases": [ + { + "base": "https://github.com/Thom1729/yaml-macros-engine", + "platforms": ["*"], + "python_versions": ["3.3", "3.8"], + "tags": true + } + ] + } + ] +} diff --git a/tasks/__init__.py b/tasks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tasks/__main__.py b/tasks/__main__.py new file mode 100644 index 0000000..1a5cc82 --- /dev/null +++ b/tasks/__main__.py @@ -0,0 +1,53 @@ +import argparse +import os +import textwrap + +# tasks +import crawl + + +def main_parser() -> argparse.ArgumentParser: + """ + Construct the main parser. + """ + parser = argparse.ArgumentParser( + description=textwrap.indent( + textwrap.dedent( + ''' + Crawl Package Control Channel and Repositories + ''' + ).strip(), + ' ', + ), + formatter_class=argparse.RawTextHelpFormatter, + ) + parser.add_argument( + 'task', + type=str, + help='Task to execute', + ) + return parser + + +def main(): + parser = main_parser() + args = parser.parse_args() + + if args.task == 'crawl': + result = crawl.run() + try: + # write result to github action outputs + with open(os.environ['GITHUB_OUTPUT'], 'a') as fp: + print(f'updated={result}'.lower(), file=fp) + except (KeyError, OSError): + pass + + +if __name__ == '__main__': # pragma: no cover + main() + + +__all__ = [ + 'main', + 'main_parser', +] diff --git a/tasks/crawl.py b/tasks/crawl.py new file mode 100644 index 0000000..026afc8 --- /dev/null +++ b/tasks/crawl.py @@ -0,0 +1,237 @@ +import bz2 +import gzip +import datetime +import json +import os +import time +import hashlib + +from decimal import Decimal + +from pathlib import Path +from urllib.error import HTTPError + +from lib.package_control import sys_path +from lib.package_control.downloaders.rate_limit_exception import RateLimitException +from lib.package_control.providers import JsonRepositoryProvider + +settings = { + "debug": False, + + "max_releases": 1, + "min_api_calls": False, + + "http_cache": True, + "http_cache_length": 31536000, + + "http_basic_auth": { + "api.github.com": [os.environ.get("GH_USER"), os.environ.get("GH_PASS")] + }, + + "user_agent": "Package Control Crawler 4.0" +} + +class JsonDatetimeEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, datetime.datetime): + return obj.strftime('%Y-%m-%d %H:%M:%S') + + if isinstance(obj, Decimal): + return float(obj) + + return json.JSONEncoder.default(self, obj) + + +def store_asset(filename, content): + """ + Stores an asset uncompressed and as gzip, bzip2 archive. + + :param filename: + The filename + :param content: + The content + """ + filename = str(filename) + new_filename = filename + '-new' + new_filename_gz = filename + '.gz-new' + new_filename_bz2 = filename + '.bz2-new' + filename_gz = filename + '.gz' + filename_bz2 = filename + '.bz2' + filename_sha512 = filename + '.sha512' + + encoded_content = content.encode('utf-8') + content_hash = hashlib.sha512(encoded_content).hexdigest().encode('utf-8') + + # Abort, if content hasn't changed so http server continues to return 304 + # if clients already have a locally cached copy. + try: + with open(filename_sha512, 'rb') as f: + if f.read().strip() == content_hash: + return False + except FileNotFoundError: + pass + + with open(new_filename, 'wb') as f: + f.write(encoded_content) + try: + os.unlink(filename) + except FileNotFoundError: + pass + os.rename(new_filename, filename) + + with gzip.open(new_filename_gz, 'w') as f: + f.write(encoded_content) + try: + os.unlink(filename_gz) + except FileNotFoundError: + pass + os.rename(new_filename_gz, filename_gz) + + with bz2.open(new_filename_bz2, 'w') as f: + f.write(encoded_content) + try: + os.unlink(filename_bz2) + except FileNotFoundError: + pass + os.rename(new_filename_bz2, filename_bz2) + + with open(filename_sha512, 'wb') as f: + f.write(content_hash) + + return True + + +def run( + cache_dir=None, + dist_dir=None +): + # determine root path + root = Path().cwd() + + # adjust cache path + if not cache_dir: + cache_dir = root / "cache" + sys_path.set_cache_dir(cache_dir) + + # get list of blacklisted urls + try: + with open(root / "blacklist.json", encoding="utf-8") as fp: + blacklist = json.load(fp) + except FileNotFoundError: + blacklist = [] + + # # read settings + # with open(root / "settings.json", encoding="utf-8") as fp: + # settings = json.load(fp) + + repo_urls = ["repository.json"] + + broken_libraries = set() + broken_packages = set() + failed_sources = set() + + packages_cache = {} + libraries_cache = {} + + num_packages = 0 + num_libraries = 0 + + begin_time = time.time() + + if repo_urls: + for repo_url in repo_urls: + repo = JsonRepositoryProvider(repo_url, settings) + if not repo: + continue + + print(f"Fetching packages from {repo_url}...") + + try: + packages = [package for _, package in repo.get_packages(blacklist)] + except Exception as e: + print(f" Failed to fetch packages: {e}") + else: + packages_cache[repo_url] = packages + if packages: + print(f" Fetched {len(packages)} packages.") + num_packages += len(packages) + + try: + libraries = [library for _, library in repo.get_libraries(blacklist)] + except Exception as e: + print(f" Failed to fetch libraries: {e}") + else: + libraries_cache[repo_url] = libraries + if libraries: + print(f" Fetched {len(libraries)} libraries.") + num_libraries += len(libraries) + + broken_libraries |= repo.get_broken_libraries() + broken_packages |= repo.get_broken_packages() + failed_sources |= repo.get_failed_sources() + + if failed_sources: + url_to_review = set() + url_failed = set() + + for url, err in failed_sources: + if isinstance(err, RateLimitException): + pass + if isinstance(err, HTTPError) and err.code == 404: + url_to_review.add(url) + else: + url_failed.add((url, err)) + + if url_to_review: + print("Missing Sources (needs review):") + url_to_review = sorted(url_to_review) + for url in url_to_review: + print(f" {url}") + + blacklist = sorted(set(blacklist) | set(url_to_review)) + + with open(root / "blacklist.json", mode="w", encoding="utf-8") as fp: + json.dump(blacklist, fp, indent=4) + + if url_failed: + print("Failed Sources:") + for url, err in sorted(url_failed): + print(f" {url}: {err}") + + if broken_packages: + print("Broken Packages:") + for lib, err in sorted(broken_packages, key=lambda s: s[0].lower()): + print(f" {lib}: {err}") + + if broken_libraries: + print("Broken Libraries:") + for lib, err in sorted(broken_libraries, key=lambda s: s[0].lower()): + print(f" {lib}: {err}") + + duration = time.strftime("%H:%M:%S", time.gmtime(time.time() - begin_time)) + print(f"Fetched {num_packages} packages and {num_libraries} libraries in {duration}.") + + json_content = json.dumps( + { + "$schema": "sublime://packagecontrol.io/schemas/repository", + "schema_version": "4.0.0", + "packages": packages_cache, + "libraries": libraries_cache + }, + cls=JsonDatetimeEncoder, + check_circular=False, + sort_keys=True + ) + + # setup asset directory + if not dist_dir: + dist_dir = root / '_site' + dist_dir.mkdir(exist_ok=True) + + result = store_asset(dist_dir / 'libraries.json', json_content) + if result: + print("Stored resolved repository!") + else: + print("Repository unchanged, skipping!") + + return result diff --git a/tasks/lib/package_control/__init__.py b/tasks/lib/package_control/__init__.py new file mode 100644 index 0000000..1c7082d --- /dev/null +++ b/tasks/lib/package_control/__init__.py @@ -0,0 +1,2 @@ +__version__ = "4.0.0-beta9" +__version_info__ = (4, 0, 0, 'beta', 9) diff --git a/tasks/lib/package_control/ca_certs.py b/tasks/lib/package_control/ca_certs.py new file mode 100644 index 0000000..fe1f9ae --- /dev/null +++ b/tasks/lib/package_control/ca_certs.py @@ -0,0 +1,301 @@ +import os +import sys + +from . import sys_path +from .console_write import console_write +from .downloaders.downloader_exception import DownloaderException + +try: + import certifi +except ImportError: + certifi = None + +try: + from .deps.oscrypto import use_ctypes + use_ctypes() + from .deps.oscrypto import trust_list # noqa + from .deps.oscrypto.errors import CACertsError +except Exception as e: + trust_list = None + console_write('oscrypto trust lists unavailable - %s', e) + + +MIN_BUNDLE_SIZE = 100 +""" +The least required file size a CA bundle must have to be valid. + +The size is calculated from public key boundaries +and least amount of public key size. + +``MIN_BUNDLE_SIZE = begin (27) + end (25) + newlines (2) + key (?)`` + +``` +-----BEGIN CERTIFICATE----- + +-----END CERTIFICATE----- +``` +""" + + +def get_ca_bundle_path(settings): + """ + Return the path to the merged system and user ca bundles + + :param settings: + A dict to look in for the `debug` key + + :raises: + OSError or IOError if CA bundle creation fails + + :return: + The filesystem path to the merged ca bundle path + """ + + ca_bundle_dir = sys_path.pc_cache_dir() + if not ca_bundle_dir: + raise ValueError("Unknown Package Control cache directory") + + os.makedirs(ca_bundle_dir, exist_ok=True) + + system_ca_bundle_path = get_system_ca_bundle_path(settings, ca_bundle_dir) + user_ca_bundle_path = get_user_ca_bundle_path(settings) + merged_ca_bundle_path = os.path.join(ca_bundle_dir, 'merged-ca-bundle.crt') + merged_ca_bundle_size = 0 + + try: + # file exists and is not empty + system_ca_bundle_exists = system_ca_bundle_path \ + and os.path.getsize(system_ca_bundle_path) > MIN_BUNDLE_SIZE + except FileNotFoundError: + system_ca_bundle_exists = False + + try: + # file exists and is not empty + user_ca_bundle_exists = user_ca_bundle_path \ + and os.path.getsize(user_ca_bundle_path) > MIN_BUNDLE_SIZE + except FileNotFoundError: + user_ca_bundle_exists = False + + regenerate = system_ca_bundle_exists or user_ca_bundle_exists + if regenerate: + try: + stats = os.stat(merged_ca_bundle_path) + except FileNotFoundError: + pass + else: + merged_ca_bundle_size = stats.st_size + # regenerate if merged file is empty + regenerate = merged_ca_bundle_size < MIN_BUNDLE_SIZE + # regenerate if system CA file is newer + if system_ca_bundle_exists and not regenerate: + regenerate = os.path.getmtime(system_ca_bundle_path) > stats.st_mtime + # regenerate if user CA file is newer + if user_ca_bundle_exists and not regenerate: + regenerate = os.path.getmtime(user_ca_bundle_path) > stats.st_mtime + + if regenerate: + with open(merged_ca_bundle_path, 'w', encoding='utf-8') as merged: + if system_ca_bundle_exists: + with open(system_ca_bundle_path, 'r', encoding='utf-8') as system: + system_certs = system.read().strip() + merged.write(system_certs) + if len(system_certs) > 0: + merged.write('\n') + if user_ca_bundle_exists: + with open(user_ca_bundle_path, 'r', encoding='utf-8') as user: + user_certs = user.read().strip() + merged.write(user_certs) + if len(user_certs) > 0: + merged.write('\n') + + merged_ca_bundle_size = merged.tell() + + if merged_ca_bundle_size >= MIN_BUNDLE_SIZE and settings.get('debug'): + console_write( + ''' + Regenerated the merged CA bundle from the system and user CA bundles (%d kB) + ''', + merged_ca_bundle_size / 1024 + ) + + if merged_ca_bundle_size < MIN_BUNDLE_SIZE: + raise DownloaderException("No CA bundle available for HTTPS!") + + return merged_ca_bundle_path + + +def print_cert_subject(cert, reason): + """ + :param cert: + The asn1crypto.x509.Certificate object + + :param reason: + None if being exported, or a unicode string of the reason not being + exported + """ + + if reason is None: + console_write( + ''' + Exported certificate: %s + ''', + cert.subject.human_friendly + ) + else: + console_write( + ''' + Skipped certificate: %s - reason %s + ''', + (cert.subject.human_friendly, reason) + ) + + +def get_system_ca_bundle_path(settings, ca_bundle_dir): + """ + Get the filesystem path to the system CA bundle. On Linux it looks in a + number of predefined places, however on OS X it has to be programmatically + exported from the SystemRootCertificates.keychain. Windows does not ship + with a CA bundle, but also we use WinINet on Windows, so we don't need to + worry about CA certs. + + :param settings: + A dict to look in for the `debug` key + + :param ca_bundle_dir: + The filesystem path to the directory to store exported CA bundle in + + :return: + The full filesystem path to the .ca-bundle file, or False on error + """ + + hours_to_cache = 7 * 24 + + debug = settings.get('debug') + + ca_path = False + + if sys.platform == 'win32' or sys.platform == 'darwin': + if trust_list is not None: + ca_path, _ = trust_list._ca_path(ca_bundle_dir) + + if trust_list._cached_path_needs_update(ca_path, hours_to_cache): + cert_callback = None + if debug: + console_write( + ''' + Generating new CA bundle from system keychain + ''' + ) + cert_callback = print_cert_subject + + try: + trust_list.get_path(ca_bundle_dir, hours_to_cache, cert_callback) + if debug: + console_write( + ''' + Finished generating new CA bundle at %s (%d bytes) + ''', + (ca_path, os.stat(ca_path).st_size) + ) + + except (CACertsError, OSError) as e: + ca_path = False + if debug: + console_write( + ''' + Failed to generate new CA bundle. %s + ''', + e + ) + + elif debug: + console_write( + ''' + Found previously exported CA bundle at %s (%d bytes) + ''', + (ca_path, os.stat(ca_path).st_size) + ) + + elif debug: + console_write( + ''' + Unable to generate system CA bundle - oscrypto not available! + ''', + ) + + # Linux + else: + # Common CA cert paths + paths = [ + '/usr/lib/ssl/certs/ca-certificates.crt', + '/etc/ssl/certs/ca-certificates.crt', + '/etc/ssl/certs/ca-bundle.crt', + '/etc/pki/tls/certs/ca-bundle.crt', + '/etc/ssl/ca-bundle.pem', + '/usr/local/share/certs/ca-root-nss.crt', + '/etc/ssl/cert.pem' + ] + + # Prepend SSL_CERT_FILE only, if it doesn't match ST4's certifi CA bundle. + # Otherwise we'd never pick up any OS level CA bundle. + ssl_cert_file = os.environ.get('SSL_CERT_FILE') + if ssl_cert_file and not (certifi and os.path.samefile(ssl_cert_file, certifi.where())): + paths.insert(0, ssl_cert_file) + + for path in paths: + if os.path.isfile(path) and os.path.getsize(path) > MIN_BUNDLE_SIZE: + ca_path = path + break + + if debug: + if ca_path: + console_write( + ''' + Found system CA bundle at %s (%d bytes) + ''', + (ca_path, os.stat(ca_path).st_size) + ) + else: + console_write( + ''' + Failed to find system CA bundle. + ''' + ) + + if ca_path is False and certifi is not None: + ca_path = certifi.where() + if debug: + console_write( + ''' + Using CA bundle from "certifi %s" instead. + ''', + certifi.__version__ + ) + + return ca_path + + +def get_user_ca_bundle_path(settings): + """ + Return the path to the user CA bundle, ensuring the file exists + + :param settings: + A dict to look in for `debug` + + :return: + The full filesystem path to the .user-ca-bundle file, or False on error + """ + + user_ca_bundle = os.path.join(sys_path.user_config_dir(), 'Package Control.user-ca-bundle') + try: + open(user_ca_bundle, 'xb').close() + if settings.get('debug'): + console_write('Created blank user CA bundle') + except FileExistsError: + pass + except OSError as e: + user_ca_bundle = False + if settings.get('debug'): + console_write('Unable to create blank user CA bundle - %s', e) + + return user_ca_bundle diff --git a/tasks/lib/package_control/cache.py b/tasks/lib/package_control/cache.py new file mode 100644 index 0000000..1d84540 --- /dev/null +++ b/tasks/lib/package_control/cache.py @@ -0,0 +1,171 @@ +import time + + +# A cache of channel and repository info to allow users to install multiple +# packages without having to wait for the metadata to be downloaded more +# than once. The keys are managed locally by the utilizing code. +_channel_repository_cache = {} + + +def clear_cache(): + _channel_repository_cache.clear() + + +def get_cache(key, default=None): + """ + Gets an in-memory cache value + + :param key: + The string key + + :param default: + The value to return if the key has not been set, or the ttl expired + + :return: + The cached value, or default + """ + + struct = _channel_repository_cache.get(key, {}) + expires = struct.get('expires') + if expires and expires > time.time(): + return struct.get('data', default) + return default + + +def merge_cache_over_settings(destination, setting, key_prefix): + """ + Take the cached value of `key` and put it into the key `setting` of + the destination.settings dict. Merge the values by overlaying the + cached setting over the existing info. + + :param destination: + An object that has a `.settings` attribute that is a dict + + :param setting: + The dict key to use when pushing the value into the settings dict + + :param key_prefix: + The string to prefix to `setting` to make the cache key + """ + + existing = destination.settings.get(setting, {}) + value = get_cache(key_prefix + '.' + setting) + if value: + existing.update(value) + destination.settings[setting] = existing + + +def merge_cache_under_settings(destination, setting, key_prefix, list_=False): + """ + Take the cached value of `key` and put it into the key `setting` of + the destination.settings dict. Merge the values by overlaying the + existing setting value over the cached info. + + :param destination: + An object that has a `.settings` attribute that is a dict + + :param setting: + The dict key to use when pushing the value into the settings dict + + :param key_prefix: + The string to prefix to `setting` to make the cache key + + :param list_: + If a list should be used instead of a dict + """ + + value = get_cache(key_prefix + '.' + setting) + if value: + existing = destination.settings.get(setting) + if existing: + if list_: + # Prevent duplicate values + base = dict(zip(value, [None] * len(value))) + for val in existing: + if val in base: + continue + value.append(val) + else: + value.update(existing) + destination.settings[setting] = value + + +def set_cache(key, data, ttl=300): + """ + Sets an in-memory cache value + + :param key: + The string key + + :param data: + The data to cache + + :param ttl: + The integer number of second to cache the data for + """ + + _channel_repository_cache[key] = { + 'data': data, + 'expires': time.time() + ttl + } + + +def set_cache_over_settings(destination, setting, key_prefix, value, ttl): + """ + Take the value passed, and merge it over the current `setting`. Once + complete, take the value and set the cache `key` and destination.settings + `setting` to that value, using the `ttl` for set_cache(). + + :param destination: + An object that has a `.settings` attribute that is a dict + + :param setting: + The dict key to use when pushing the value into the settings dict + + :param key_prefix: + The string to prefix to `setting` to make the cache key + + :param value: + The value to set + + :param ttl: + The cache ttl to use + """ + + existing = destination.settings.get(setting, {}) + existing.update(value) + set_cache(key_prefix + '.' + setting, value, ttl) + destination.settings[setting] = value + + +def set_cache_under_settings(destination, setting, key_prefix, value, ttl, list_=False): + """ + Take the value passed, and merge the current `setting` over it. Once + complete, take the value and set the cache `key` and destination.settings + `setting` to that value, using the `ttl` for set_cache(). + + :param destination: + An object that has a `.settings` attribute that is a dict + + :param setting: + The dict key to use when pushing the value into the settings dict + + :param key_prefix: + The string to prefix to `setting` to make the cache key + + :param value: + The value to set + + :param ttl: + The cache ttl to use + """ + + if value: + if list_: + existing = destination.settings.get(setting, []) + value.extend(existing) + else: + existing = destination.settings.get(setting, {}) + value.update(existing) + set_cache(key_prefix + '.' + setting, value, ttl) + destination.settings[setting] = value diff --git a/tasks/lib/package_control/clients/__init__.py b/tasks/lib/package_control/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tasks/lib/package_control/clients/bitbucket_client.py b/tasks/lib/package_control/clients/bitbucket_client.py new file mode 100644 index 0000000..4d61f30 --- /dev/null +++ b/tasks/lib/package_control/clients/bitbucket_client.py @@ -0,0 +1,382 @@ +import re +from urllib.parse import urlencode, quote + +from ..downloaders.downloader_exception import DownloaderException +from ..package_version import version_match_prefix +from .json_api_client import JSONApiClient + + +# A predefined list of readme filenames to look for +_readme_filenames = [ + 'readme', + 'readme.txt', + 'readme.md', + 'readme.mkd', + 'readme.mdown', + 'readme.markdown', + 'readme.textile', + 'readme.creole', + 'readme.rst' +] + + +class BitBucketClient(JSONApiClient): + + @staticmethod + def user_repo_branch(url): + """ + Extract the username, repo and branch name from the URL + + :param url: + The URL to extract the info from, in one of the forms: + https://bitbucket.org/{user} + https://bitbucket.org/{user}/{repo} + https://bitbucket.org/{user}/{repo}.git + https://bitbucket.org/{user}/{repo}/src/{branch} + + :return: + A tuple of + (user name, repo name, branch name) or + (user name, repo name, None) or + (user name, None, None) or + (None, None, None) if no match. + """ + + match = re.match( + r'^https?://bitbucket\.org/([^/#?]+)(?:/([^/#?]+?)(?:\.git|/src/([^#?]*[^/#?])/?|/?)|/?)$', + url + ) + if match: + return match.groups() + + return (None, None, None) + + @staticmethod + def repo_url(user_name, repo_name): + """ + Generate the tags URL for a GitHub repo if the value passed is a GitHub + repository URL + + :param owener_name: + The repository owner name + + :param repo_name: + The repository name + + :return: + The repository URL of given owner and repo name + """ + + return 'https://bitbucket.com/%s/%s' % (quote(user_name), quote(repo_name)) + + def download_info(self, url, tag_prefix=None): + """ + Retrieve information about downloading a package + + :param url: + The URL of the repository, in one of the forms: + https://bitbucket.org/{user}/{repo} + https://bitbucket.org/{user}/{repo}/src/{branch} + https://bitbucket.org/{user}/{repo}/#tags + If the last option, grabs the info from the newest + tag that is a valid semver version. + + :param tag_prefix: + If the URL is a tags URL, only match tags that have this prefix. + If tag_prefix is None, match only tags without prefix. + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + None if no match, False if no commit, or a list of dicts with the + following keys: + `version` - the version number of the download + `url` - the download URL of a zip file of the package + `date` - the ISO-8601 timestamp string when the version was published + """ + + output = self.download_info_from_branch(url) + if output is None: + output = self.download_info_from_tags(url, tag_prefix) + return output + + def download_info_from_branch(self, url, default_branch=None): + """ + Retrieve information about downloading a package + + :param url: + The URL of the repository, in one of the forms: + https://bitbucket.org/{user}/{repo} + https://bitbucket.org/{user}/{repo}/src/{branch} + + :param default_branch: + The branch to use, in case url is a repo url + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + None if no match, False if no commit, or a list of dicts with the + following keys: + `version` - the version number of the download + `url` - the download URL of a zip file of the package + `date` - the ISO-8601 timestamp string when the version was published + """ + + user_name, repo_name, branch = self.user_repo_branch(url) + if not repo_name: + return None + + user_repo = "%s/%s" % (user_name, repo_name) + + if branch is None: + branch = default_branch + if branch is None: + repo_info = self.fetch_json(self._api_url(user_repo)) + branch = repo_info['mainbranch'].get('name', 'master') + + branch_url = self._api_url(user_repo, '/refs/branches/%s' % branch) + branch_info = self.fetch_json(branch_url) + + timestamp = branch_info['target']['date'][0:19].replace('T', ' ') + version = re.sub(r'[\-: ]', '.', timestamp) + + return [self._make_download_info(user_repo, branch, version, timestamp)] + + def download_info_from_releases(self, url, asset_templates, tag_prefix=None): + """ + BitBucket doesn't support releases in ways GitHub/Gitlab do. + + It supports download assets, but those are not bound to tags or releases. + + Version information could be extracted from file names, + but that's not how PC evaluates download assets, currently. + """ + + return None + + def download_info_from_tags(self, url, tag_prefix=None): + """ + Retrieve information about downloading a package + + :param url: + The URL of the repository, in one of the forms: + https://bitbucket.org/{user}/{repo} + https://bitbucket.org/{user}/{repo}/#tags + Grabs the info from the newest tag(s) that is a valid semver version. + + :param tag_prefix: + If the URL is a tags URL, only match tags that have this prefix. + If tag_prefix is None, match only tags without prefix. + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + None if no match, False if no commit, or a list of dicts with the + following keys: + `version` - the version number of the download + `url` - the download URL of a zip file of the package + `date` - the ISO-8601 timestamp string when the version was published + """ + + tags_match = re.match(r'https?://bitbucket\.org/([^/#?]+/[^/#?]+)/?(?:#tags)?$', url) + if not tags_match: + return None + + def _get_releases(user_repo, tag_prefix, page_size=100): + used_versions = set() + query_string = urlencode({'pagelen': page_size}) + tags_url = self._api_url(user_repo, '/refs/tags?%s' % query_string) + while tags_url: + tags_json = self.fetch_json(tags_url) + for tag in tags_json['values']: + version = version_match_prefix(tag['name'], tag_prefix) + if version and version not in used_versions: + used_versions.add(version) + yield ( + version, + tag['name'], + tag['target']['date'][0:19].replace('T', ' ') + ) + + tags_url = tags_json.get('next') + + user_repo = tags_match.group(1) + + max_releases = self.settings.get('max_releases', 0) + num_releases = 0 + + output = [] + for release in sorted(_get_releases(user_repo, tag_prefix), reverse=True): + version, tag, timestamp = release + + output.append(self._make_download_info(user_repo, tag, str(version), timestamp)) + + num_releases += version.is_final + if max_releases > 0 and num_releases >= max_releases: + break + + return output + + def repo_info(self, url): + """ + Retrieve general information about a repository + + :param url: + The URL to the repository, in one of the forms: + https://bitbucket.org/{user}/{repo} + https://bitbucket.org/{user}/{repo}/src/{branch} + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + None if no match, or a dict with the following keys: + `name` + `description` + `homepage` - URL of the homepage + `author` + `readme` - URL of the readme + `issues` - URL of bug tracker + `donate` - URL of a donate page + `default_branch` + """ + + user_name, repo_name, branch = self.user_repo_branch(url) + if not repo_name: + return None + + user_repo = "%s/%s" % (user_name, repo_name) + api_url = self._api_url(user_repo) + repo_info = self.fetch_json(api_url) + + if branch is None: + branch = repo_info['mainbranch'].get('name', 'master') + + issues_url = 'https://bitbucket.org/%s/issues' % user_repo + + author = repo_info['owner'].get('nickname') + if author is None: + author = repo_info['owner'].get('username') + + is_client = self.settings.get('min_api_calls', False) + readme_url = None if is_client else self._readme_url(user_repo, branch) + + return { + 'name': repo_info['name'], + 'description': repo_info['description'] or 'No description provided', + 'homepage': repo_info['website'] or url, + 'author': author, + 'donate': None, + 'readme': readme_url, + 'issues': issues_url if repo_info['has_issues'] else None, + 'default_branch': branch + } + + def user_info(self, url): + """ + For API compatibility with other clients. + + :param url: + The URL to the repository, in one of the forms: + https://bitbucket.org/{user} + + :return: + None + """ + return None + + def _make_download_info(self, user_repo, ref_name, version, timestamp): + """ + Generate a download_info record + + :param user_repo: + The user/repo of the repository + + :param ref_name: + The git reference (branch, commit, tag) + + :param version: + The prefixed version to add to the record + + :param timestamp: + The timestamp the revision was created + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + A dictionary with following keys: + `version` - the version number of the download + `url` - the download URL of a zip file of the package + `date` - the ISO-8601 timestamp string when the version was published + """ + + return { + 'url': 'https://bitbucket.org/%s/get/%s.zip' % (user_repo, ref_name), + 'version': version, + 'date': timestamp + } + + def _api_url(self, user_repo, suffix=''): + """ + Generate a URL for the BitBucket API + + :param user_repo: + The user/repo of the repository + + :param suffix: + The extra API path info to add to the URL + + :return: + The API URL + """ + + return 'https://api.bitbucket.org/2.0/repositories/%s%s' % (user_repo, suffix) + + def _readme_url(self, user_repo, branch, prefer_cached=False): + """ + Parse the root directory listing for the repo and return the URL + to any file that looks like a readme + + :param user_repo: + The user/repo string + + :param branch: + The branch to fetch the readme from + + :param prefer_cached: + If a cached directory listing should be used instead of a new HTTP request + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + The URL to the readme file, or None + """ + + listing_url = self._api_url(user_repo, '/src/%s/?pagelen=100' % branch) + + try: + while listing_url: + root_dir_info = self.fetch_json(listing_url, prefer_cached) + + for entry in root_dir_info['values']: + if entry['path'].lower() in _readme_filenames: + return 'https://bitbucket.org/%s/raw/%s/%s' % (user_repo, branch, entry['path']) + + listing_url = root_dir_info['next'] if 'next' in root_dir_info else None + + except (DownloaderException) as e: + if 'HTTP error 404' not in str(e): + raise + + return None diff --git a/tasks/lib/package_control/clients/client_exception.py b/tasks/lib/package_control/clients/client_exception.py new file mode 100644 index 0000000..a776f9d --- /dev/null +++ b/tasks/lib/package_control/clients/client_exception.py @@ -0,0 +1,6 @@ +class ClientException(Exception): + + """If a client could not fetch information""" + + def __bytes__(self): + return self.__str__().encode('utf-8') diff --git a/tasks/lib/package_control/clients/github_client.py b/tasks/lib/package_control/clients/github_client.py new file mode 100644 index 0000000..9f66bb7 --- /dev/null +++ b/tasks/lib/package_control/clients/github_client.py @@ -0,0 +1,563 @@ +import re +from urllib.parse import urlencode, quote + +from ..downloaders.downloader_exception import DownloaderException +from ..package_version import version_match_prefix +from .json_api_client import JSONApiClient + + +class GitHubClient(JSONApiClient): + + @staticmethod + def user_repo_branch(url): + """ + Extract the username, repo and branch name from the URL + + :param url: + The URL to extract the info from, in one of the forms: + https://github.com/{user} + https://github.com/{user}/{repo} + https://github.com/{user}/{repo}.git + https://github.com/{user}/{repo}/tree/{branch} + + :return: + A tuple of + (user name, repo name, branch name) or + (user name, repo name, None) or + (user name, None, None) or + (None, None, None) if no match. + """ + match = re.match( + r'^https?://github\.com/([^/#?]+)(?:/([^/#?]+?)(?:\.git|/tree/([^#?]*[^/#?])/?|/?)|/?)$', + url + ) + if match: + return match.groups() + + return (None, None, None) + + @staticmethod + def repo_url(user_name, repo_name): + """ + Generate the tags URL for a GitHub repo if the value passed is a GitHub + repository URL + + :param owener_name: + The repository owner name + + :param repo_name: + The repository name + + :return: + The repository URL of given owner and repo name + """ + + return 'https://github.com/%s/%s' % (quote(user_name), quote(repo_name)) + + def download_info(self, url, tag_prefix=None): + """ + Retrieve information about downloading a package + + :param url: + The URL of the repository, in one of the forms: + https://github.com/{user}/{repo} + https://github.com/{user}/{repo}/tree/{branch} + https://github.com/{user}/{repo}/tags + If the last option, grabs the info from the newest + tag that is a valid semver version. + + :param tag_prefix: + If the URL is a tags URL, only match tags that have this prefix. + If tag_prefix is None, match only tags without prefix. + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + None if no match, False if no commits, or a list of dicts with the + following keys: + `version` - the version number of the download + `url` - the download URL of a zip file of the package + `date` - the ISO-8601 timestamp string when the version was published + """ + + output = self.download_info_from_branch(url) + if output is None: + output = self.download_info_from_tags(url, tag_prefix) + return output + + def download_info_from_branch(self, url, default_branch=None): + """ + Retrieve information about downloading a package + + :param url: + The URL of the repository, in one of the forms: + https://github.com/{user}/{repo} + https://github.com/{user}/{repo}/tree/{branch} + + :param default_branch: + The branch to use, in case url is a repo url + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + None if no match, False if no commit, or a list of dicts with the + following keys: + `version` - the version number of the download + `url` - the download URL of a zip file of the package + `date` - the ISO-8601 timestamp string when the version was published + """ + + user_name, repo_name, branch = self.user_repo_branch(url) + if not repo_name: + return None + + user_repo = "%s/%s" % (user_name, repo_name) + + if branch is None: + branch = default_branch + if branch is None: + repo_info = self.fetch_json(self._api_url(user_repo)) + branch = repo_info.get('default_branch', 'master') + + branch_url = self._api_url(user_repo, '/branches/%s' % branch) + branch_info = self.fetch_json(branch_url) + + timestamp = branch_info['commit']['commit']['committer']['date'][0:19].replace('T', ' ') + version = re.sub(r'[\-: ]', '.', timestamp) + + return [self._make_download_info(user_repo, branch, version, timestamp)] + + def download_info_from_releases(self, url, asset_templates, tag_prefix=None): + """ + Retrieve information about downloading a package + + :param url: + The URL of the repository, in one of the forms: + https://github.com/{user}/{repo} + https://github.com/{user}/{repo}/releases + Grabs the info from the newest tag(s) that is a valid semver version. + + :param tag_prefix: + If the URL is a tags URL, only match tags that have this prefix. + If tag_prefix is None, match only tags without prefix. + + :param asset_templates: + A list of tuples of asset template and download_info. + + [ + ( + "Name-${version}-st${st_build}-*-x??.sublime", + { + "platforms": ["windows-x64"], + "python_versions": ["3.3", "3.8"], + "sublime_text": ">=4107" + } + ) + ] + + Supported globs: + + * : any number of characters + ? : single character placeholder + + Supported variables are: + + ${platform} + A platform-arch string as given in "platforms" list. + A separate explicit release is evaluated for each platform. + If "platforms": ['*'] is specified, variable is set to "any". + + ${py_version} + Major and minor part of required python version without period. + One of "33", "38" or any other valid python version supported by ST. + + ${st_build} + Value of "st_specifier" stripped by leading operator + "*" => "any" + ">=4107" => "4107" + "<4107" => "4107" + "4107 - 4126" => "4107" + + ${version} + Resolved semver without tag prefix + (e.g.: tag st4107-1.0.5 => version 1.0.5) + + Note: is not replaced by this method, but by the ``ClientProvider``. + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + ``None`` if no match, ``False`` if no commit, or a list of dicts with the + following keys: + + - `version` - the version number of the download + - `url` - the download URL of a zip file of the package + - `date` - the ISO-8601 timestamp string when the version was published + - `platforms` - list of unicode strings with compatible platforms + - `python_versions` - list of compatible python versions + - `sublime_text` - sublime text version specifier + + Example: + + ```py + [ + { + "url": "https://server.com/file.zip", + "version": "1.0.0", + "date": "2023-10-21 12:00:00", + "platforms": ["windows-x64"], + "python_versions": ["3.8"], + "sublime_text": ">=4107" + }, + ... + ] + ``` + """ + + match = re.match(r'https?://github\.com/([^/#?]+/[^/#?]+)(?:/releases)?/?$', url) + if not match: + return None + + def _get_releases(user_repo, tag_prefix=None, page_size=1000): + used_versions = set() + for page in range(10): + query_string = urlencode({'page': page * page_size, 'per_page': page_size}) + api_url = self._api_url(user_repo, '/releases?%s' % query_string) + releases = self.fetch_json(api_url) + + for release in releases: + if release['draft']: + continue + version = version_match_prefix(release['tag_name'], tag_prefix) + if not version or version in used_versions: + continue + + used_versions.add(version) + + yield ( + version, + release['published_at'][0:19].replace('T', ' '), + [ + ((a['label'], a['browser_download_url'])) + for a in release['assets'] + if a['state'] == 'uploaded' + ] + ) + + if len(releases) < page_size: + return + + asset_templates = self._expand_asset_variables(asset_templates) + + user_repo = match.group(1) + max_releases = self.settings.get('max_releases', 0) + num_releases = [0] * len(asset_templates) + + output = [] + + for release in _get_releases(user_repo, tag_prefix): + version, timestamp, assets = release + + version_string = str(version) + + for idx, (pattern, selectors) in enumerate(asset_templates): + if max_releases > 0 and num_releases[idx] >= max_releases: + continue + + pattern = pattern.replace('${version}', version_string) + pattern = pattern.replace('.', r'\.') + pattern = pattern.replace('?', r'.') + pattern = pattern.replace('*', r'.*?') + regex = re.compile(pattern) + + for asset_name, asset_url in assets: + if not regex.match(asset_name): + continue + + info = {'url': asset_url, 'version': version_string, 'date': timestamp} + info.update(selectors) + output.append(info) + num_releases[idx] += version.is_final + break + + if max_releases > 0 and min(num_releases) >= max_releases: + break + + return output + + def download_info_from_tags(self, url, tag_prefix=None): + """ + Retrieve information about downloading a package + + :param url: + The URL of the repository, in one of the forms: + https://github.com/{user}/{repo} + https://github.com/{user}/{repo}/tags + Grabs the info from the newest tag(s) that is a valid semver version. + + :param tag_prefix: + If the URL is a tags URL, only match tags that have this prefix. + If tag_prefix is None, match only tags without prefix. + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + None if no match, False if no commit, or a list of dicts with the + following keys: + `version` - the version number of the download + `url` - the download URL of a zip file of the package + `date` - the ISO-8601 timestamp string when the version was published + """ + + tags_match = re.match(r'https?://github\.com/([^/#?]+/[^/#?]+)(?:/tags)?/?$', url) + if not tags_match: + return None + + def _get_releases(user_repo, tag_prefix=None, page_size=1000): + used_versions = set() + for page in range(10): + query_string = urlencode({'page': page * page_size, 'per_page': page_size}) + tags_url = self._api_url(user_repo, '/tags?%s' % query_string) + tags_json = self.fetch_json(tags_url) + + for tag in tags_json: + version = version_match_prefix(tag['name'], tag_prefix) + if version and version not in used_versions: + used_versions.add(version) + yield (version, tag['name'], tag['commit']['url']) + + if len(tags_json) < page_size: + return + + user_repo = tags_match.group(1) + is_client = self.settings.get('min_api_calls', False) + max_releases = self.settings.get('max_releases', 0) + num_releases = 0 + + output = [] + for release in sorted(_get_releases(user_repo, tag_prefix), reverse=True): + version, tag, tag_url = release + + if is_client: + timestamp = '1970-01-01 00:00:00' + else: + tag_info = self.fetch_json(tag_url) + timestamp = tag_info['commit']['committer']['date'][0:19].replace('T', ' ') + + output.append(self._make_download_info(user_repo, tag, str(version), timestamp)) + + num_releases += version.is_final + if max_releases > 0 and num_releases >= max_releases: + break + + return output + + def repo_info(self, url): + """ + Retrieve general information about a repository + + :param url: + The URL to the repository, in one of the forms: + https://github.com/{user}/{repo} + https://github.com/{user}/{repo}/tree/{branch} + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + None if no match, or a dict with the following keys: + `name` + `description` + `homepage` - URL of the homepage + `author` + `readme` - URL of the readme + `issues` - URL of bug tracker + `donate` - URL of a donate page + `default_branch` + """ + + user_name, repo_name, branch = self.user_repo_branch(url) + if not repo_name: + return None + + user_repo = "%s/%s" % (user_name, repo_name) + api_url = self._api_url(user_repo) + repo_info = self.fetch_json(api_url) + + if branch is None: + branch = repo_info.get('default_branch', 'master') + + return self._extract_repo_info(branch, repo_info) + + def user_info(self, url): + """ + Retrieve general information about all repositories that are + part of a user/organization. + + :param url: + The URL to the user/organization, in the following form: + https://github.com/{user} + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + None if no match, or am list of dicts with the following keys: + `name` + `description` + `homepage` - URL of the homepage + `author` + `readme` - URL of the readme + `issues` - URL of bug tracker + `donate` - URL of a donate page + `default_branch` + """ + + user_match = re.match(r'https?://github\.com/([^/#?]+)/?$', url) + if user_match is None: + return None + + user = user_match.group(1) + api_url = 'https://api.github.com/users/%s/repos' % user + + repos_info = self.fetch_json(api_url) + + return [ + self._extract_repo_info(info.get('default_branch', 'master'), info) + for info in repos_info + ] + + def _extract_repo_info(self, branch, result): + """ + Extracts information about a repository from the API result + + :param branch: + The branch to return data from + + :param result: + A dict representing the data returned from the GitHub API + + :return: + A dict with the following keys: + `name` + `description` + `homepage` - URL of the homepage + `author` + `readme` - URL of the homepage + `issues` - URL of bug tracker + `donate` - URL of a donate page + `default_branch` + """ + + user_name = result['owner']['login'] + repo_name = result['name'] + user_repo = '%s/%s' % (user_name, repo_name) + + issues_url = None + if result['has_issues']: + issues_url = 'https://github.com/%s/issues' % user_repo + + return { + 'name': repo_name, + 'description': result['description'] or 'No description provided', + 'homepage': result['homepage'] or result['html_url'], + 'author': user_name, + 'readme': self._readme_url(user_repo, branch), + 'issues': issues_url, + 'donate': None, + 'default_branch': branch + } + + def _make_download_info(self, user_repo, ref_name, version, timestamp): + """ + Generate a download_info record + + :param user_repo: + The user/repo of the repository + + :param ref_name: + The git reference (branch, commit, tag) + + :param version: + The prefixed version to add to the record + + :param timestamp: + The timestamp the revision was created + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + A dictionary with following keys: + `version` - the version number of the download + `url` - the download URL of a zip file of the package + `date` - the ISO-8601 timestamp string when the version was published + """ + + return { + 'url': 'https://codeload.github.com/%s/zip/%s' % (user_repo, ref_name), + 'version': version, + 'date': timestamp + } + + def _api_url(self, user_repo, suffix=''): + """ + Generate a URL for the BitBucket API + + :param user_repo: + The user/repo of the repository + + :param suffix: + The extra API path info to add to the URL + + :return: + The API URL + """ + + return 'https://api.github.com/repos/%s%s' % (user_repo, suffix) + + def _readme_url(self, user_repo, branch, prefer_cached=False): + """ + Fetches the raw GitHub API information about a readme + + :param user_repo: + The user/repo of the repository + + :param branch: + The branch to pull the readme from + + :param prefer_cached: + If a cached version of the info should be returned instead of making a new HTTP request + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + A dict containing all of the info from the GitHub API, or None if no readme exists + """ + + query_string = urlencode({'ref': branch}) + readme_url = self._api_url(user_repo, '/readme?%s' % query_string) + + try: + readme_file = self.fetch_json(readme_url, prefer_cached).get('path') + if readme_file: + return 'https://raw.githubusercontent.com/%s/%s/%s' % (user_repo, branch, readme_file) + + except (DownloaderException) as e: + if 'HTTP error 404' not in str(e): + raise + + return None diff --git a/tasks/lib/package_control/clients/gitlab_client.py b/tasks/lib/package_control/clients/gitlab_client.py new file mode 100644 index 0000000..5f289e7 --- /dev/null +++ b/tasks/lib/package_control/clients/gitlab_client.py @@ -0,0 +1,585 @@ +import re +from urllib.parse import urlencode, quote + +from ..downloaders.downloader_exception import DownloaderException +from ..package_version import version_match_prefix +from .json_api_client import JSONApiClient + + +class GitLabClient(JSONApiClient): + + @staticmethod + def user_repo_branch(url): + """ + Extract the username, repo and branch name from the URL + + :param url: + The URL to extract the info from, in one of the forms: + https://gitlab.com/{user} + https://gitlab.com/{user}/{repo} + https://gitlab.com/{user}/{repo}.git + https://gitlab.com/{user}/{repo}/-/tree/{branch} + + :return: + A tuple of + (user name, repo name, branch name) or + (user name, repo name, None) or + (user name, None, None) or + (None, None, None) if no match. + + The branch name may be a branch name or a commit + """ + + match = re.match( + r'^https?://gitlab\.com/([^/#?]+)(?:/([^/#?]+?)(?:\.git|/-/tree/([^#?]*[^/#?])/?|/?)|/?)$', + url + ) + if match: + return match.groups() + + return (None, None, None) + + @staticmethod + def repo_url(user_name, repo_name): + """ + Generate the tags URL for a GitLab repo if the value passed is a GitLab + repository URL + + :param owener_name: + The repository owner name + + :param repo_name: + The repository name + + :return: + The repository URL of given owner and repo name + """ + + return 'https://gitlab.com/%s/%s' % (quote(user_name), quote(repo_name)) + + def download_info(self, url, tag_prefix=None): + """ + Retrieve information about downloading a package + + :param url: + The URL of the repository, in one of the forms: + https://gitlab.com/{user}/{repo} + https://gitlab.com/{user}/{repo}/-/tree/{branch} + https://gitlab.com/{user}/{repo}/-/tags + If the last option, grabs the info from the newest + tag that is a valid semver version. + + :param tag_prefix: + If the URL is a tags URL, only match tags that have this prefix. + If tag_prefix is None, match only tags without prefix. + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + None if no match, False if no commits, or a list of dicts with the + following keys: + `version` - the version number of the download + `url` - the download URL of a zip file of the package + `date` - the ISO-8601 timestamp string when the version was published + """ + + output = self.download_info_from_branch(url) + if output is None: + output = self.download_info_from_tags(url, tag_prefix) + return output + + def download_info_from_branch(self, url, default_branch=None): + """ + Retrieve information about downloading a package + + :param url: + The URL of the repository, in one of the forms: + https://gitlab.com/{user}/{repo} + https://gitlab.com/{user}/{repo}/-/tree/{branch} + + :param default_branch: + The branch to use, in case url is a repo url + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + None if no match, False if no commit, or a list of dicts with the + following keys: + `version` - the version number of the download + `url` - the download URL of a zip file of the package + `date` - the ISO-8601 timestamp string when the version was published + """ + + user_name, repo_name, branch = self.user_repo_branch(url) + if not repo_name: + return None + + repo_id = '%s%%2F%s' % (user_name, repo_name) + + if branch is None: + branch = default_branch + if branch is None: + repo_info = self.fetch_json(self._api_url(repo_id)) + branch = repo_info.get('default_branch', 'master') + + branch_url = self._api_url(repo_id, '/repository/branches/%s' % branch) + branch_info = self.fetch_json(branch_url) + + timestamp = branch_info['commit']['committed_date'][0:19].replace('T', ' ') + version = re.sub(r'[\-: ]', '.', timestamp) + + return [self._make_download_info(user_name, repo_name, branch, version, timestamp)] + + def download_info_from_releases(self, url, asset_templates, tag_prefix=None): + """ + Retrieve information about downloading a package + + :param url: + The URL of the repository, in one of the forms: + https://gitlab.com/{user}/{repo} + https://gitlab.com/{user}/{repo}/-/releases + Grabs the info from the newest tag(s) that is a valid semver version. + + :param tag_prefix: + If the URL is a tags URL, only match tags that have this prefix. + If tag_prefix is None, match only tags without prefix. + + :param asset_templates: + A list of tuples of asset template and download_info. + + [ + ( + "Name-${version}-st${st_build}-*-x??.sublime", + { + "platforms": ["windows-x64"], + "python_versions": ["3.3", "3.8"], + "sublime_text": ">=4107" + } + ) + ] + + Supported globs: + + * : any number of characters + ? : single character placeholder + + Supported variables are: + + ${platform} + A platform-arch string as given in "platforms" list. + A separate explicit release is evaluated for each platform. + If "platforms": ['*'] is specified, variable is set to "any". + + ${py_version} + Major and minor part of required python version without period. + One of "33", "38" or any other valid python version supported by ST. + + ${st_build} + Value of "st_specifier" stripped by leading operator + "*" => "any" + ">=4107" => "4107" + "<4107" => "4107" + "4107 - 4126" => "4107" + + ${version} + Resolved semver without tag prefix + (e.g.: tag st4107-1.0.5 => version 1.0.5) + + Note: is not replaced by this method, but by the ``ClientProvider``. + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + ``None`` if no match, ``False`` if no commit, or a list of dicts with the + following keys: + + - `version` - the version number of the download + - `url` - the download URL of a zip file of the package + - `date` - the ISO-8601 timestamp string when the version was published + - `platforms` - list of unicode strings with compatible platforms + - `python_versions` - list of compatible python versions + - `sublime_text` - sublime text version specifier + + Example: + + ```py + [ + { + "url": "https://server.com/file.zip", + "version": "1.0.0", + "date": "2023-10-21 12:00:00", + "platforms": ["windows-x64"], + "python_versions": ["3.8"], + "sublime_text": ">=4107" + }, + ... + ] + ``` + """ + + match = re.match(r'https?://gitlab\.com/([^/#?]+)/([^/#?]+)(?:/-/releases)?/?$', url) + if not match: + return None + + def _get_releases(user_repo, tag_prefix=None, page_size=1000): + used_versions = set() + for page in range(10): + query_string = urlencode({'page': page * page_size, 'per_page': page_size}) + api_url = self._api_url(user_repo, '/releases?%s' % query_string) + releases = self.fetch_json(api_url) + + for release in releases: + version = version_match_prefix(release['tag_name'], tag_prefix) + if not version or version in used_versions: + continue + + used_versions.add(version) + + yield ( + version, + release['released_at'][0:19].replace('T', ' '), + [ + ((a['name'], a['direct_asset_url'])) + for a in release['assets']['links'] + ] + ) + + if len(releases) < page_size: + return + + user_name, repo_name = match.groups() + repo_id = '%s%%2F%s' % (user_name, repo_name) + + asset_templates = self._expand_asset_variables(asset_templates) + + max_releases = self.settings.get('max_releases', 0) + num_releases = [0] * len(asset_templates) + + output = [] + + for release in _get_releases(repo_id, tag_prefix): + version, timestamp, assets = release + + version_string = str(version) + + for idx, (pattern, selectors) in enumerate(asset_templates): + if max_releases > 0 and num_releases[idx] >= max_releases: + continue + + pattern = pattern.replace('${version}', version_string) + pattern = pattern.replace('.', r'\.') + pattern = pattern.replace('?', r'.') + pattern = pattern.replace('*', r'.*?') + regex = re.compile(pattern) + + for asset_name, asset_url in assets: + if not regex.match(asset_name): + continue + + info = {'url': asset_url, 'version': version_string, 'date': timestamp} + info.update(selectors) + output.append(info) + num_releases[idx] += version.is_final + break + + if max_releases > 0 and min(num_releases) >= max_releases: + break + + return output + + def download_info_from_tags(self, url, tag_prefix=None): + """ + Retrieve information about downloading a package + + :param url: + The URL of the repository, in one of the forms: + https://gitlab.com/{user}/{repo} + https://gitlab.com/{user}/{repo}/-/tags + Grabs the info from the newest tag(s) that is a valid semver version. + + :param tag_prefix: + If the URL is a tags URL, only match tags that have this prefix. + If tag_prefix is None, match only tags without prefix. + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + None if no match, False if no commit, or a list of dicts with the + following keys: + `version` - the version number of the download + `url` - the download URL of a zip file of the package + `date` - the ISO-8601 timestamp string when the version was published + """ + + tags_match = re.match(r'https?://gitlab\.com/([^/#?]+)/([^/#?]+)(?:/-/tags)?/?$', url) + if not tags_match: + return None + + def _get_releases(repo_id, tag_prefix=None, page_size=1000): + used_versions = set() + for page in range(10): + query_string = urlencode({'page': page * page_size, 'per_page': page_size}) + tags_url = self._api_url(repo_id, '/repository/tags?%s' % query_string) + tags_json = self.fetch_json(tags_url) + + for tag in tags_json: + version = version_match_prefix(tag['name'], tag_prefix) + if version and version not in used_versions: + used_versions.add(version) + yield ( + version, + tag['name'], + tag['commit']['committed_date'][0:19].replace('T', ' ') + ) + + if len(tags_json) < page_size: + return + + user_name, repo_name = tags_match.groups() + repo_id = '%s%%2F%s' % (user_name, repo_name) + + max_releases = self.settings.get('max_releases', 0) + num_releases = 0 + + output = [] + for release in sorted(_get_releases(repo_id, tag_prefix), reverse=True): + version, tag, timestamp = release + + output.append(self._make_download_info(user_name, repo_name, tag, str(version), timestamp)) + + num_releases += version.is_final + if max_releases > 0 and num_releases >= max_releases: + break + + return output + + def repo_info(self, url): + """ + Retrieve general information about a repository + :param url: + The URL to the repository, in one of the forms: + https://gitlab.com/{user}/{repo} + https://gitlab.com/{user}/{repo}/-/tree/{branch} + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + None if no match, or a dict with the following keys: + `name` + `description` + `homepage` - URL of the homepage + `author` + `readme` - URL of the readme + `issues` - URL of bug tracker + `donate` - URL of a donate page + `default_branch` + """ + + user_name, repo_name, branch = self.user_repo_branch(url) + if not user_name or not repo_name: + return None + + repo_id = '%s%%2F%s' % (user_name, repo_name) + repo_url = self._api_url(repo_id) + repo_info = self.fetch_json(repo_url) + + if not branch: + branch = repo_info.get('default_branch', 'master') + + return self._extract_repo_info(branch, repo_info) + + def user_info(self, url): + """ + Retrieve general information about all repositories that are + part of a user/organization. + + :param url: + The URL to the user/organization, in the following form: + https://gitlab.com/{user} + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + None if no match, or am list of dicts with the following keys: + `name` + `description` + `homepage` - URL of the homepage + `author` + `readme` - URL of the readme + `issues` - URL of bug tracker + `donate` - URL of a donate page + `default_branch` + """ + + user_match = re.match(r'https?://gitlab\.com/([^/#?]+)/?$', url) + if user_match is None: + return None + + user = user_match.group(1) + user_id, user_repo_type = self._extract_user_id(user) + + api_url = 'https://gitlab.com/api/v4/%s/%s/projects' % ( + 'users' if user_repo_type else 'groups', user_id) + + repos_info = self.fetch_json(api_url) + + return [ + self._extract_repo_info(info.get('default_branch', 'master'), info) + for info in repos_info + ] + + def _extract_repo_info(self, branch, result): + """ + Extracts information about a repository from the API result + + :param branch: + The branch to return data from + + :param result: + A dict representing the data returned from the GitLab API + + :return: + A dict with the following keys: + `name` + `description` + `homepage` - URL of the homepage + `author` + `readme` - URL of the homepage + `issues` - URL of bug tracker + `donate` - URL of a donate page + `default_branch` + """ + + user_name = result['owner']['username'] if result.get('owner') else result['namespace']['name'] + repo_name = result['name'] + user_repo = '%s/%s' % (user_name, repo_name) + + readme_url = None + if result['readme_url']: + readme_url = 'https://gitlab.com/%s/-/raw/%s/%s' % ( + user_repo, branch, result['readme_url'].split('/')[-1] + ) + + return { + 'name': repo_name, + 'description': result['description'] or 'No description provided', + 'homepage': result['web_url'] or None, + 'author': user_name, + 'readme': readme_url, + 'issues': result.get('issues', None) if result.get('_links') else None, + 'donate': None, + 'default_branch': branch + } + + def _make_download_info(self, user_name, repo_name, ref_name, version, timestamp): + """ + Generate a download_info record + + :param user_name: + The owner of the repository + + :param repo_name: + The name of the repository + + :param ref_name: + The git reference (branch, commit, tag) + + :param version: + The prefixed version to add to the record + + :param timestamp: + The timestamp the revision was created + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + A dictionary with following keys: + `version` - the version number of the download + `url` - the download URL of a zip file of the package + `date` - the ISO-8601 timestamp string when the version was published + """ + + return { + 'url': 'https://gitlab.com/%s/%s/-/archive/%s/%s-%s.zip' % ( + user_name, repo_name, ref_name, repo_name, ref_name), + 'version': version, + 'date': timestamp + } + + def _api_url(self, project_id, suffix=''): + """ + Generate a URL for the GitLab API + + :param user_repo: + The user/repo of the repository + + :param suffix: + The extra API path info to add to the URL + + :return: + The API URL + """ + + return 'https://gitlab.com/api/v4/projects/%s%s' % (project_id, suffix) + + def _extract_user_id(self, username): + """ + Extract the user id from the repo results + + :param username: + The username to extract the user_id from + + :return: + A user_id or None if no match + """ + + user_url = 'https://gitlab.com/api/v4/users?username=%s' % username + try: + repos_info = self.fetch_json(user_url) + except (DownloaderException) as e: + if str(e).find('HTTP error 404') != -1: + return self._extract_group_id(username) + raise + + if not repos_info: + return self._extract_group_id(username) + + return (repos_info[0]['id'], True) + + def _extract_group_id(self, group_name): + """ + Extract the group id from the repo results + + :param group: + The group to extract the user_id from + + :return: + A group_id or (None, None) if no match + """ + + group_url = 'https://gitlab.com/api/v4/groups?search=%s' % group_name + try: + repos_info = self.fetch_json(group_url) + except (DownloaderException) as e: + if str(e).find('HTTP error 404') != -1: + return (None, None) + raise + + if not repos_info: + return (None, None) + + return (repos_info[0]['id'], False) diff --git a/tasks/lib/package_control/clients/json_api_client.py b/tasks/lib/package_control/clients/json_api_client.py new file mode 100644 index 0000000..72da376 --- /dev/null +++ b/tasks/lib/package_control/clients/json_api_client.py @@ -0,0 +1,169 @@ +import json +from urllib.parse import urlencode, urlparse + +from .client_exception import ClientException +from ..download_manager import http_get + + +class JSONApiClient: + + def __init__(self, settings): + self.settings = settings + + def fetch(self, url, prefer_cached=False): + """ + Retrieves the contents of a URL + + :param url: + The URL to download the content from + + :param prefer_cached: + If a cached copy of the content is preferred + + :raises: + DownloaderException: when there is an error downloading + + :return: + The bytes/string + """ + + # If there are extra params for the domain name, add them + extra_params = self.settings.get('query_string_params') + domain_name = urlparse(url).netloc + if extra_params and domain_name in extra_params: + params = urlencode(extra_params[domain_name]) + joiner = '?%s' if url.find('?') == -1 else '&%s' + url += joiner % params + + return http_get(url, self.settings, 'Error downloading repository.', prefer_cached) + + def fetch_json(self, url, prefer_cached=False): + """ + Retrieves and parses the JSON from a URL + + :param url: + The URL to download the JSON from + + :param prefer_cached: + If a cached copy of the JSON is preferred + + :raises: + ClientException: when there is an error parsing the response + + :return: + A dict or list from the JSON + """ + + repository_json = self.fetch(url, prefer_cached) + + try: + return json.loads(repository_json.decode('utf-8')) + except (ValueError): + error_string = 'Error parsing JSON from URL %s.' % url + raise ClientException(error_string) + + @staticmethod + def _expand_asset_variables(asset_templates): + """ + Expands the asset variables. + + Note: ``${version}`` is not replaced. + + :param asset_templates: + A list of tuples of asset template and download_info. + + ```py + [ + ( + "Name-${version}-py${py_version}-*-x??.whl", + { + "platforms": ["windows-x64"], + "python_versions": ["3.3", "3.8"], + "sublime_text": ">=4107" + } + ) + ] + ``` + + Supported variables are: + + ``${platform}`` + A platform-arch string as given in "platforms" list. + A separate explicit release is evaluated for each platform. + If "platforms": ['*'] is specified, variable is set to "any". + + ``${py_version}`` + Major and minor part of required python version without period. + One of "33", "38" or any other valid python version supported by ST. + + ``${st_build}`` + Value of "st_specifier" stripped by leading operator + "*" => "any" + ">=4107" => "4107" + "<4107" => "4107" + "4107 - 4126" => "4107" + + :returns: + A list of asset templates with all variables (except ``${version}``) resolved. + + ```py + [ + ( + "Name-${version}-py33-*-x??.whl", + { + "platforms": ["windows-x64"], + "python_versions": ["3.3"], + "sublime_text": ">=4107" + } + ), + ( + "Name-${version}-py33-*-x??.whl", + { + "platforms": ["windows-x64"], + "python_versions": ["3.8"], + "sublime_text": ">=4107" + } + ) + ] + ``` + """ + + output = [] + var = '${st_build}' + for pattern, selectors in asset_templates: + # resolve ${st_build} + if var in pattern: + # convert st_specifier version specifier to build number + st_specifier = selectors['sublime_text'] + if st_specifier == '*': + st_build = 'any' + elif st_specifier[0].isdigit(): + # 4107, 4107 - 4126 + st_build = st_specifier[:4] + elif st_specifier[1].isdigit(): + # <4107, >4107 + st_build = st_specifier[1:] + else: + # ==4107, <=4107, >=4107 + st_build = st_specifier[2:] + + pattern = pattern.replace(var, st_build) + + output.append((pattern, selectors)) + + def resolve(templates, var, key): + for pattern, selectors in templates: + if var not in pattern: + yield (pattern, selectors) + continue + + for platform in selectors[key]: + new_selectors = selectors.copy() + new_selectors[key] = [platform] + yield (pattern.replace(var, platform), new_selectors) + + return None + + output = resolve(output, '${platform}', 'platforms') + output = resolve(output, '${py_version}', 'python_versions') + return list(output) diff --git a/tasks/lib/package_control/clients/pypi_client.py b/tasks/lib/package_control/clients/pypi_client.py new file mode 100644 index 0000000..289e3eb --- /dev/null +++ b/tasks/lib/package_control/clients/pypi_client.py @@ -0,0 +1,276 @@ +import re + +from ..pep440 import PEP440Version +from ..pep440 import PEP440VersionSpecifier + +from .json_api_client import JSONApiClient + + +class PyPiClient(JSONApiClient): + @staticmethod + def name_and_version(url): + match = re.match( + r"^https?://pypi\.org/project/([^/#?]+)(?:/([^/#?]+?)|/?)$", url + ) + if match: + return match.groups() + + return (None, None) + + def repo_info(self, url): + name, _ = self.name_and_version(url) + if not name: + return None + + pypi_url = "https://pypi.org/pypi/{}/json".format(name) + info = self.fetch_json(pypi_url) + + return { + "name": name, + "description": info["summary"], + "homepage": info["home_page"] + or info.get("project_urls", {}).get("Homepage"), + "author": info["author"], + "issues": info["bugtrack_url"] + or info.get("project_urls", {}).get("Issues"), + } + + def download_info(self, url, tag_prefix=None): + """Branch or tag based releases are not supported.""" + return None + + def download_info_from_branch(self, url, default_branch=None): + """Branch or tag based releases are not supported.""" + return None + + def download_info_from_tags(self, url, tag_prefix=None): + """Branch or tag based releases are not supported.""" + return None + + def download_info_from_releases(self, url, asset_templates, tag_prefix=None): + """ + Retrieve information about package + + :param url: + The URL of the repository, in one of the forms: + https://pypi.org/projects/{library_name} + https://pypi.org/projects/{library_name}/{version} + Grabs the info from the newest compatible release(s). + + :param tag_prefix: + unused, present for API compatibility. + + :param asset_templates: + A list of tuples of asset template and download_info. + + [ + ( + "coverage-${version}-cp33-*-win_amd64*.whl", + { + "platforms": ["windows-x64"], + "python_versions": ["3.3"] + } + ) + ] + + Supported globs: + + * : any number of characters + ? : single character placeholder + + Supported variables are: + + ${platform} + A platform-arch string as given in "platforms" list. + A separate explicit release is evaluated for each platform. + If "platforms": ["*"] is specified, variable is set to "any". + + ${py_version} + Major and minor part of required python version without period. + One of "33", "38" or any other valid python version supported by ST. + + ${st_build} + Value of "st_specifier" stripped by leading operator + "*" => "any" + ">=4107" => "4107" + "<4107" => "4107" + "4107 - 4126" => "4107" + + ${version} + Resolved semver without tag prefix + (e.g.: tag st4107-1.0.5 => version 1.0.5) + + Note: is not replaced by this method, but by the ``ClientProvider``. + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + ``None`` if no match, ``False`` if no commit, or a list of dicts with the + following keys: + + - `version` - the version number of the download + - `url` - the download URL of a zip file of the package + - `date` - the ISO-8601 timestamp string when the version was published + - `platforms` - list of unicode strings with compatible platforms + - `python_versions` - list of compatible python versions + - `sublime_text` - sublime text version specifier + + Example: + + ```py + [ + { + "url": "https://files.pythonhosted.org/packages/.../coverage-4.2-cp33-cp33m-win_amd64.whl", + "version": "4.2", + "date": "2016-07-26 21:09:17", + "sha256": "bd4eba631f07cae8cdb9c55c144f165649e6701b962f9d604b4e00cf8802406c", + "platforms": ["windows-x64"], + "python_versions": ["3.3"] + }, + ... + ] + ``` + """ + + name, version = self.name_and_version(url) + if not name: + return None + + if version: + return self._download_info_from_fixed_version( + name, version, asset_templates + ) + + return self._download_info_from_latest_version(name, asset_templates) + + def _download_info_from_fixed_version(self, name, version, asset_templates): + """ + Build download information from fixed version. + + :param name: + The package name + :param version: + The package version + :param asset_templates: + A list of tuples of asset template and download_info. + + :return: + ``None`` if no match, ``False`` if no commit, or a list of dicts with the + following keys: + """ + + pypi_url = "https://pypi.org/pypi/{}/{}/json".format(name, version) + info = self.fetch_json(pypi_url) + + asset_templates = self._expand_asset_variables(asset_templates) + assets = info["urls"] + + output = [] + for pattern, selectors in asset_templates: + info = self._make_download_info(pattern, selectors, version, assets) + if info: + output.append(info) + + return output + + def _download_info_from_latest_version(self, name, asset_templates): + """ + Build download information from latest compatible versions of each asset template. + + :param name: + The package name + :param version: + The package version + :param asset_templates: + A list of tuples of asset template and download_info. + + :return: + ``None`` if no match, ``False`` if no commit, or a list of dicts with the + following keys: + """ + + pypi_url = "https://pypi.org/pypi/{}/json".format(name) + info = self.fetch_json(pypi_url) + + asset_templates = self._expand_asset_variables(asset_templates) + + max_releases = self.settings.get("max_releases", 0) + num_releases = [0] * len(asset_templates) + + output = [] + + # get latest compatible release for each asset template + for version, assets in reversed(info["releases"].items()): + # we don"t want beta releases! + if not PEP440Version(version).is_final: + continue + + for idx, (pattern, selectors) in enumerate(asset_templates): + if max_releases > 0 and num_releases[idx] >= max_releases: + continue + info = self._make_download_info(pattern, selectors, version, assets) + if not info: + continue + output.append(info) + num_releases[idx] += 1 + if max_releases > 0 and min(num_releases) >= max_releases: + break + + return output + + @staticmethod + def _make_download_info(pattern, selectors, version, assets): + """ + Build download information for given asset template. + + :param pattern: + The glob pattern of a given asset template + :param selectors: + The dictionary of release specification of given asset template from repository.json + :param version: + The package version + :param assets: + A list of dictionaries of asset information downloaded from PyPI. + + :return: + ``None`` if no match, ``False`` if no commit, or a list of dicts with the + following keys: + """ + + pattern = pattern.replace("${version}", version) + pattern = pattern.replace(".", r"\.") + pattern = pattern.replace("?", r".") + pattern = pattern.replace("*", r".*?") + regex = re.compile(pattern) + + python_versions = (PEP440Version(ver) for ver in selectors["python_versions"]) + + for asset in assets: + if asset["packagetype"] != "bdist_wheel": + continue + if asset["yanked"]: + continue + if not regex.match(asset["filename"]): + continue + + specs = asset["requires_python"] + if specs: + specs = ( + PEP440VersionSpecifier(spec) + for spec in asset["requires_python"].split(",") + ) + if not all(ver in spec for spec in specs for ver in python_versions): + continue + + info = { + "url": asset["url"], + "version": version, + "date": asset["upload_time"][0:19].replace("T", " "), + "sha256": asset["digests"]["sha256"], + } + info.update(selectors) + return info + + return None diff --git a/tasks/lib/package_control/clients/readme_client.py b/tasks/lib/package_control/clients/readme_client.py new file mode 100644 index 0000000..d3f09dc --- /dev/null +++ b/tasks/lib/package_control/clients/readme_client.py @@ -0,0 +1,81 @@ +import re +import os +import base64 +from urllib.parse import urlencode + +from .json_api_client import JSONApiClient + + +# Used to map file extensions to formats +_readme_formats = { + '.md': 'markdown', + '.mkd': 'markdown', + '.mdown': 'markdown', + '.markdown': 'markdown', + '.textile': 'textile', + '.creole': 'creole', + '.rst': 'rst' +} + + +class ReadmeClient(JSONApiClient): + + def readme_info(self, url): + """ + Retrieve the readme and info about it + + :param url: + The URL of the readme file + + :raises: + DownloaderException: if there is an error downloading the readme + ClientException: if there is an error parsing the response + + :return: + A dict with the following keys: + `filename` + `format` - `markdown`, `textile`, `creole`, `rst` or `txt` + `contents` - contents of the readme as str/unicode + """ + + contents = None + + # Try to grab the contents of a GitHub-based readme by grabbing the cached + # content of the readme API call + github_match = re.match( + r'https://raw\.github(?:usercontent)?\.com/([^/#?]+/[^/#?]+)/([^/#?]+)/' + r'readme(\.(md|mkd|mdown|markdown|textile|creole|rst|txt))?$', + url, + re.I + ) + if github_match: + user_repo = github_match.group(1) + branch = github_match.group(2) + + query_string = urlencode({'ref': branch}) + readme_json_url = 'https://api.github.com/repos/%s/readme?%s' % (user_repo, query_string) + try: + info = self.fetch_json(readme_json_url, prefer_cached=True) + contents = base64.b64decode(info['content']) + except (ValueError): + pass + + if not contents: + contents = self.fetch(url) + + _, ext = os.path.splitext(url) + format = 'txt' + ext = ext.lower() + if ext in _readme_formats: + format = _readme_formats[ext] + + try: + contents = contents.decode('utf-8') + except (UnicodeDecodeError): + contents = contents.decode('cp1252', errors='replace') + + return { + 'filename': os.path.basename(url), + 'format': format, + 'contents': contents + } diff --git a/tasks/lib/package_control/cmd.py b/tasks/lib/package_control/cmd.py new file mode 100644 index 0000000..ae576fe --- /dev/null +++ b/tasks/lib/package_control/cmd.py @@ -0,0 +1,299 @@ +import os +import subprocess +import re + +from .console_write import console_write +from .show_error import show_error +from . import text + +if os.name == 'nt': + from ctypes import windll, create_unicode_buffer + +try: + # Allow using this file on the website where the sublime + # module is unavailable + import sublime +except (ImportError): + sublime = None + + +def create_cmd(args, basename_binary=False): + """ + Takes an array of strings to be passed to subprocess.Popen and creates + a string that can be pasted into a terminal + + :param args: + The array containing the binary name/path and all arguments + + :param basename_binary: + If only the basename of the binary should be disabled instead of the full path + + :return: + The command string + """ + + if basename_binary: + args[0] = os.path.basename(args[0]) + + if os.name == 'nt': + return subprocess.list2cmdline(args) + else: + escaped_args = [] + for arg in args: + if re.search('^[a-zA-Z0-9/_^\\-\\.:=]+$', arg) is None: + arg = "'" + arg.replace("'", "'\\''") + "'" + escaped_args.append(arg) + return ' '.join(escaped_args) + + +class Cli: + + """ + Base class for running command line apps + + :param binary_locations: + The full filesystem path to the executable for the version control + system. May be set to None to allow the code to try and find it. May + also be a list of locations to attempt. This allows settings to be + shared across operating systems. + """ + + # Prevent duplicate lookups + binary_paths = {} + + cli_name = None + + ok_returncodes = set([0]) + + def __init__(self, binary_locations, debug): + self.binary_locations = binary_locations + self.debug = debug + + def execute(self, args, cwd, input=None, encoding='utf-8', meaningful_output=False, ignore_errors=None): + """ + Creates a subprocess with the executable/args + + :param args: + A list of the executable path and all arguments to it + + :param cwd: + The directory in which to run the executable + + :param input: + The input text to send to the program + + :param meaningful_output: + If the output from the command is possibly meaningful and should + be displayed if in debug mode + + :param ignore_errors: + A regex of errors to ignore + + :return: + A string of the executable output or False on error + """ + + orig_cwd = cwd + + startupinfo = None + if os.name == 'nt': + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + + # Make sure the cwd is ascii + try: + cwd.encode('mbcs') + except UnicodeEncodeError: + buf = create_unicode_buffer(512) + if windll.kernel32.GetShortPathNameW(cwd, buf, len(buf)): + cwd = buf.value + + if self.debug: + console_write( + ''' + Executing %s [%s] + ''', + (create_cmd(args), cwd) + ) + + try: + proc = subprocess.Popen( + args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + startupinfo=startupinfo, + cwd=cwd, + env=os.environ + ) + + if input and isinstance(input, str): + input = input.encode(encoding) + + stuck = True + + binary_name = os.path.basename(args[0]) + if re.search('git', binary_name): + is_vcs = True + elif re.search('hg', binary_name): + is_vcs = True + else: + is_vcs = False + + if sublime: + def kill_proc(): + if not stuck: + return + # This doesn't actually work! + proc.kill() + + message = text.format( + ''' + The process %s seems to have gotten stuck. + + Command: %s + + Working directory: %s + ''', + (binary_name, create_cmd(args), orig_cwd) + ) + if is_vcs: + message += text.format( + ''' + + This is likely due to a password or passphrase + prompt. Please ensure %s works without a prompt, or + change the "ignore_vcs_packages" Package Control + setting to true. + ''', + binary_name + ) + show_error(message) + sublime.set_timeout(kill_proc, 60000) + + output, error = proc.communicate(input) + + stuck = False + + output = output.decode(encoding) + output = output.replace('\r\n', '\n').rstrip(' \n\r') + + if proc.returncode not in self.ok_returncodes: + if error: + error = error.decode(encoding) + error = error.replace('\r\n', '\n').rstrip(' \n\r') + if not ignore_errors or re.search(ignore_errors, error or output) is None: + message = text.format( + ''' + Error executing: %s + + Working directory: %s + + %s + ''', + (create_cmd(args), orig_cwd, error or output) + ).rstrip() + if is_vcs: + message += text.format( + ''' + + VCS-based packages can be ignored by changing the + "ignore_vcs_packages" setting to true. + ''' + ) + console_write(message) + return False + + if meaningful_output and self.debug and len(output) > 0: + console_write(output, indent=' ', prefix=False) + + return output + + except (OSError) as e: + show_error( + ''' + Error executing: %s + + %s + + Try checking your "%s_binary" setting? + ''', + (create_cmd(args), str(e), self.cli_name) + ) + return False + + def find_binary(self, name): + """ + Locates the executable by looking in the PATH and well-known directories + + :param name: + The string filename of the executable + + :return: + The filesystem path to the executable, or None if not found + """ + + # Use the cached path + if self.cli_name in Cli.binary_paths: + return Cli.binary_paths[self.cli_name] + + check_binaries = [] + + # Use the settings first + if self.binary_locations: + if not isinstance(self.binary_locations, list): + self.binary_locations = [self.binary_locations] + check_binaries.extend(self.binary_locations) + + # Next check the PATH + for dir_ in os.environ['PATH'].split(os.pathsep): + check_binaries.append(os.path.join(dir_, name)) + + # Finally look in common locations that may not be in the PATH + if os.name == 'nt': + dirs = [ + 'C:\\Program Files\\Git\\bin', + 'C:\\Program Files (x86)\\Git\\bin', + 'C:\\Program Files\\TortoiseGit\\bin', + 'C:\\Program Files\\Mercurial', + 'C:\\Program Files (x86)\\Mercurial', + 'C:\\Program Files (x86)\\TortoiseHg', + 'C:\\Program Files\\TortoiseHg', + 'C:\\cygwin\\bin' + ] + else: + # ST seems to launch with a minimal set of environmental variables + # on OS X, so we add some common paths for it + dirs = ['/usr/local/git/bin', '/usr/local/bin'] + + for dir_ in dirs: + check_binaries.append(os.path.join(dir_, name)) + + if self.debug: + console_write( + ''' + Looking for %s at: "%s" + ''', + (self.cli_name, '", "'.join(check_binaries)) + ) + + for path in check_binaries: + if os.path.exists(path) and not os.path.isdir(path) and os.access(path, os.X_OK): + if self.debug: + console_write( + ''' + Found %s at "%s" + ''', + (self.cli_name, path) + ) + Cli.binary_paths[self.cli_name] = path + return path + + if self.debug: + console_write( + ''' + Could not find %s on your machine + ''', + self.cli_name + ) + return None diff --git a/tasks/lib/package_control/console_write.py b/tasks/lib/package_control/console_write.py new file mode 100644 index 0000000..0f29e0b --- /dev/null +++ b/tasks/lib/package_control/console_write.py @@ -0,0 +1,32 @@ +import sys + +from . import text + + +def console_write(string, params=None, strip=True, indent=None, prefix=True): + """ + Writes a value to the Sublime Text console, formatting it for output via + text.format() and then encoding unicode to utf-8 + + :param string: + The value to write + + :param params: + Params to interpolate into the string using the % operator + + :param strip: + If a single trailing newline should be stripped + + :param indent: + If all lines should be indented by a set indent after being dedented + + :param prefix: + If the string "Package Control: " should be prefixed to the string + """ + + string = text.format(str(string), params, strip=strip, indent=indent) + + if prefix: + sys.stdout.write('Package Control: ') + + print(string) diff --git a/tasks/lib/package_control/deps/__init__.py b/tasks/lib/package_control/deps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tasks/lib/package_control/deps/asn1crypto/__init__.py b/tasks/lib/package_control/deps/asn1crypto/__init__.py new file mode 100644 index 0000000..2c93f00 --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/__init__.py @@ -0,0 +1,47 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from .version import __version__, __version_info__ + +__all__ = [ + '__version__', + '__version_info__', + 'load_order', +] + + +def load_order(): + """ + Returns a list of the module and sub-module names for asn1crypto in + dependency load order, for the sake of live reloading code + + :return: + A list of unicode strings of module names, as they would appear in + sys.modules, ordered by which module should be reloaded first + """ + + return [ + 'asn1crypto._errors', + 'asn1crypto._int', + 'asn1crypto._ordereddict', + 'asn1crypto._teletex_codec', + 'asn1crypto._types', + 'asn1crypto._inet', + 'asn1crypto._iri', + 'asn1crypto.version', + 'asn1crypto.pem', + 'asn1crypto.util', + 'asn1crypto.parser', + 'asn1crypto.core', + 'asn1crypto.algos', + 'asn1crypto.keys', + 'asn1crypto.x509', + 'asn1crypto.crl', + 'asn1crypto.csr', + 'asn1crypto.ocsp', + 'asn1crypto.cms', + 'asn1crypto.pdf', + 'asn1crypto.pkcs12', + 'asn1crypto.tsp', + 'asn1crypto', + ] diff --git a/tasks/lib/package_control/deps/asn1crypto/_errors.py b/tasks/lib/package_control/deps/asn1crypto/_errors.py new file mode 100644 index 0000000..d8797a2 --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/_errors.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" +Exports the following items: + + - unwrap() + - APIException() +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +import re +import textwrap + + +class APIException(Exception): + """ + An exception indicating an API has been removed from asn1crypto + """ + + pass + + +def unwrap(string, *params): + """ + Takes a multi-line string and does the following: + + - dedents + - converts newlines with text before and after into a single line + - strips leading and trailing whitespace + + :param string: + The string to format + + :param *params: + Params to interpolate into the string + + :return: + The formatted string + """ + + output = textwrap.dedent(string) + + # Unwrap lines, taking into account bulleted lists, ordered lists and + # underlines consisting of = signs + if output.find('\n') != -1: + output = re.sub('(?<=\\S)\n(?=[^ \n\t\\d\\*\\-=])', ' ', output) + + if params: + output = output % params + + output = output.strip() + + return output diff --git a/tasks/lib/package_control/deps/asn1crypto/_inet.py b/tasks/lib/package_control/deps/asn1crypto/_inet.py new file mode 100644 index 0000000..045ba56 --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/_inet.py @@ -0,0 +1,170 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import socket +import struct + +from ._errors import unwrap +from ._types import byte_cls, bytes_to_list, str_cls, type_name + + +def inet_ntop(address_family, packed_ip): + """ + Windows compatibility shim for socket.inet_ntop(). + + :param address_family: + socket.AF_INET for IPv4 or socket.AF_INET6 for IPv6 + + :param packed_ip: + A byte string of the network form of an IP address + + :return: + A unicode string of the IP address + """ + + if address_family not in set([socket.AF_INET, socket.AF_INET6]): + raise ValueError(unwrap( + ''' + address_family must be socket.AF_INET (%s) or socket.AF_INET6 (%s), + not %s + ''', + repr(socket.AF_INET), + repr(socket.AF_INET6), + repr(address_family) + )) + + if not isinstance(packed_ip, byte_cls): + raise TypeError(unwrap( + ''' + packed_ip must be a byte string, not %s + ''', + type_name(packed_ip) + )) + + required_len = 4 if address_family == socket.AF_INET else 16 + if len(packed_ip) != required_len: + raise ValueError(unwrap( + ''' + packed_ip must be %d bytes long - is %d + ''', + required_len, + len(packed_ip) + )) + + if address_family == socket.AF_INET: + return '%d.%d.%d.%d' % tuple(bytes_to_list(packed_ip)) + + octets = struct.unpack(b'!HHHHHHHH', packed_ip) + + runs_of_zero = {} + longest_run = 0 + zero_index = None + for i, octet in enumerate(octets + (-1,)): + if octet != 0: + if zero_index is not None: + length = i - zero_index + if length not in runs_of_zero: + runs_of_zero[length] = zero_index + longest_run = max(longest_run, length) + zero_index = None + elif zero_index is None: + zero_index = i + + hexed = [hex(o)[2:] for o in octets] + + if longest_run < 2: + return ':'.join(hexed) + + zero_start = runs_of_zero[longest_run] + zero_end = zero_start + longest_run + + return ':'.join(hexed[:zero_start]) + '::' + ':'.join(hexed[zero_end:]) + + +def inet_pton(address_family, ip_string): + """ + Windows compatibility shim for socket.inet_ntop(). + + :param address_family: + socket.AF_INET for IPv4 or socket.AF_INET6 for IPv6 + + :param ip_string: + A unicode string of an IP address + + :return: + A byte string of the network form of the IP address + """ + + if address_family not in set([socket.AF_INET, socket.AF_INET6]): + raise ValueError(unwrap( + ''' + address_family must be socket.AF_INET (%s) or socket.AF_INET6 (%s), + not %s + ''', + repr(socket.AF_INET), + repr(socket.AF_INET6), + repr(address_family) + )) + + if not isinstance(ip_string, str_cls): + raise TypeError(unwrap( + ''' + ip_string must be a unicode string, not %s + ''', + type_name(ip_string) + )) + + if address_family == socket.AF_INET: + octets = ip_string.split('.') + error = len(octets) != 4 + if not error: + ints = [] + for o in octets: + o = int(o) + if o > 255 or o < 0: + error = True + break + ints.append(o) + + if error: + raise ValueError(unwrap( + ''' + ip_string must be a dotted string with four integers in the + range of 0 to 255, got %s + ''', + repr(ip_string) + )) + + return struct.pack(b'!BBBB', *ints) + + error = False + omitted = ip_string.count('::') + if omitted > 1: + error = True + elif omitted == 0: + octets = ip_string.split(':') + error = len(octets) != 8 + else: + begin, end = ip_string.split('::') + begin_octets = begin.split(':') + end_octets = end.split(':') + missing = 8 - len(begin_octets) - len(end_octets) + octets = begin_octets + (['0'] * missing) + end_octets + + if not error: + ints = [] + for o in octets: + o = int(o, 16) + if o > 65535 or o < 0: + error = True + break + ints.append(o) + + return struct.pack(b'!HHHHHHHH', *ints) + + raise ValueError(unwrap( + ''' + ip_string must be a valid ipv6 string, got %s + ''', + repr(ip_string) + )) diff --git a/tasks/lib/package_control/deps/asn1crypto/_int.py b/tasks/lib/package_control/deps/asn1crypto/_int.py new file mode 100644 index 0000000..094fc95 --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/_int.py @@ -0,0 +1,22 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + + +def fill_width(bytes_, width): + """ + Ensure a byte string representing a positive integer is a specific width + (in bytes) + + :param bytes_: + The integer byte string + + :param width: + The desired width as an integer + + :return: + A byte string of the width specified + """ + + while len(bytes_) < width: + bytes_ = b'\x00' + bytes_ + return bytes_ diff --git a/tasks/lib/package_control/deps/asn1crypto/_iri.py b/tasks/lib/package_control/deps/asn1crypto/_iri.py new file mode 100644 index 0000000..7394b4d --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/_iri.py @@ -0,0 +1,291 @@ +# coding: utf-8 + +""" +Functions to convert unicode IRIs into ASCII byte string URIs and back. Exports +the following items: + + - iri_to_uri() + - uri_to_iri() +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +from encodings import idna # noqa +import codecs +import re +import sys + +from ._errors import unwrap +from ._types import byte_cls, str_cls, type_name, bytes_to_list, int_types + +if sys.version_info < (3,): + from urlparse import urlsplit, urlunsplit + from urllib import ( + quote as urlquote, + unquote as unquote_to_bytes, + ) + +else: + from urllib.parse import ( + quote as urlquote, + unquote_to_bytes, + urlsplit, + urlunsplit, + ) + + +def iri_to_uri(value, normalize=False): + """ + Encodes a unicode IRI into an ASCII byte string URI + + :param value: + A unicode string of an IRI + + :param normalize: + A bool that controls URI normalization + + :return: + A byte string of the ASCII-encoded URI + """ + + if not isinstance(value, str_cls): + raise TypeError(unwrap( + ''' + value must be a unicode string, not %s + ''', + type_name(value) + )) + + scheme = None + # Python 2.6 doesn't split properly is the URL doesn't start with http:// or https:// + if sys.version_info < (2, 7) and not value.startswith('http://') and not value.startswith('https://'): + real_prefix = None + prefix_match = re.match('^[^:]*://', value) + if prefix_match: + real_prefix = prefix_match.group(0) + value = 'http://' + value[len(real_prefix):] + parsed = urlsplit(value) + if real_prefix: + value = real_prefix + value[7:] + scheme = _urlquote(real_prefix[:-3]) + else: + parsed = urlsplit(value) + + if scheme is None: + scheme = _urlquote(parsed.scheme) + hostname = parsed.hostname + if hostname is not None: + hostname = hostname.encode('idna') + # RFC 3986 allows userinfo to contain sub-delims + username = _urlquote(parsed.username, safe='!$&\'()*+,;=') + password = _urlquote(parsed.password, safe='!$&\'()*+,;=') + port = parsed.port + if port is not None: + port = str_cls(port).encode('ascii') + + netloc = b'' + if username is not None: + netloc += username + if password: + netloc += b':' + password + netloc += b'@' + if hostname is not None: + netloc += hostname + if port is not None: + default_http = scheme == b'http' and port == b'80' + default_https = scheme == b'https' and port == b'443' + if not normalize or (not default_http and not default_https): + netloc += b':' + port + + # RFC 3986 allows a path to contain sub-delims, plus "@" and ":" + path = _urlquote(parsed.path, safe='/!$&\'()*+,;=@:') + # RFC 3986 allows the query to contain sub-delims, plus "@", ":" , "/" and "?" + query = _urlquote(parsed.query, safe='/?!$&\'()*+,;=@:') + # RFC 3986 allows the fragment to contain sub-delims, plus "@", ":" , "/" and "?" + fragment = _urlquote(parsed.fragment, safe='/?!$&\'()*+,;=@:') + + if normalize and query is None and fragment is None and path == b'/': + path = None + + # Python 2.7 compat + if path is None: + path = '' + + output = urlunsplit((scheme, netloc, path, query, fragment)) + if isinstance(output, str_cls): + output = output.encode('latin1') + return output + + +def uri_to_iri(value): + """ + Converts an ASCII URI byte string into a unicode IRI + + :param value: + An ASCII-encoded byte string of the URI + + :return: + A unicode string of the IRI + """ + + if not isinstance(value, byte_cls): + raise TypeError(unwrap( + ''' + value must be a byte string, not %s + ''', + type_name(value) + )) + + parsed = urlsplit(value) + + scheme = parsed.scheme + if scheme is not None: + scheme = scheme.decode('ascii') + + username = _urlunquote(parsed.username, remap=[':', '@']) + password = _urlunquote(parsed.password, remap=[':', '@']) + hostname = parsed.hostname + if hostname: + hostname = hostname.decode('idna') + port = parsed.port + if port and not isinstance(port, int_types): + port = port.decode('ascii') + + netloc = '' + if username is not None: + netloc += username + if password: + netloc += ':' + password + netloc += '@' + if hostname is not None: + netloc += hostname + if port is not None: + netloc += ':' + str_cls(port) + + path = _urlunquote(parsed.path, remap=['/'], preserve=True) + query = _urlunquote(parsed.query, remap=['&', '='], preserve=True) + fragment = _urlunquote(parsed.fragment) + + return urlunsplit((scheme, netloc, path, query, fragment)) + + +def _iri_utf8_errors_handler(exc): + """ + Error handler for decoding UTF-8 parts of a URI into an IRI. Leaves byte + sequences encoded in %XX format, but as part of a unicode string. + + :param exc: + The UnicodeDecodeError exception + + :return: + A 2-element tuple of (replacement unicode string, integer index to + resume at) + """ + + bytes_as_ints = bytes_to_list(exc.object[exc.start:exc.end]) + replacements = ['%%%02x' % num for num in bytes_as_ints] + return (''.join(replacements), exc.end) + + +codecs.register_error('iriutf8', _iri_utf8_errors_handler) + + +def _urlquote(string, safe=''): + """ + Quotes a unicode string for use in a URL + + :param string: + A unicode string + + :param safe: + A unicode string of character to not encode + + :return: + None (if string is None) or an ASCII byte string of the quoted string + """ + + if string is None or string == '': + return None + + # Anything already hex quoted is pulled out of the URL and unquoted if + # possible + escapes = [] + if re.search('%[0-9a-fA-F]{2}', string): + # Try to unquote any percent values, restoring them if they are not + # valid UTF-8. Also, requote any safe chars since encoded versions of + # those are functionally different than the unquoted ones. + def _try_unescape(match): + byte_string = unquote_to_bytes(match.group(0)) + unicode_string = byte_string.decode('utf-8', 'iriutf8') + for safe_char in list(safe): + unicode_string = unicode_string.replace(safe_char, '%%%02x' % ord(safe_char)) + return unicode_string + string = re.sub('(?:%[0-9a-fA-F]{2})+', _try_unescape, string) + + # Once we have the minimal set of hex quoted values, removed them from + # the string so that they are not double quoted + def _extract_escape(match): + escapes.append(match.group(0).encode('ascii')) + return '\x00' + string = re.sub('%[0-9a-fA-F]{2}', _extract_escape, string) + + output = urlquote(string.encode('utf-8'), safe=safe.encode('utf-8')) + if not isinstance(output, byte_cls): + output = output.encode('ascii') + + # Restore the existing quoted values that we extracted + if len(escapes) > 0: + def _return_escape(_): + return escapes.pop(0) + output = re.sub(b'%00', _return_escape, output) + + return output + + +def _urlunquote(byte_string, remap=None, preserve=None): + """ + Unquotes a URI portion from a byte string into unicode using UTF-8 + + :param byte_string: + A byte string of the data to unquote + + :param remap: + A list of characters (as unicode) that should be re-mapped to a + %XX encoding. This is used when characters are not valid in part of a + URL. + + :param preserve: + A bool - indicates that the chars to be remapped if they occur in + non-hex form, should be preserved. E.g. / for URL path. + + :return: + A unicode string + """ + + if byte_string is None: + return byte_string + + if byte_string == b'': + return '' + + if preserve: + replacements = ['\x1A', '\x1C', '\x1D', '\x1E', '\x1F'] + preserve_unmap = {} + for char in remap: + replacement = replacements.pop(0) + preserve_unmap[replacement] = char + byte_string = byte_string.replace(char.encode('ascii'), replacement.encode('ascii')) + + byte_string = unquote_to_bytes(byte_string) + + if remap: + for char in remap: + byte_string = byte_string.replace(char.encode('ascii'), ('%%%02x' % ord(char)).encode('ascii')) + + output = byte_string.decode('utf-8', 'iriutf8') + + if preserve: + for replacement, original in preserve_unmap.items(): + output = output.replace(replacement, original) + + return output diff --git a/tasks/lib/package_control/deps/asn1crypto/_ordereddict.py b/tasks/lib/package_control/deps/asn1crypto/_ordereddict.py new file mode 100644 index 0000000..2f18ab5 --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/_ordereddict.py @@ -0,0 +1,135 @@ +# Copyright (c) 2009 Raymond Hettinger +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +import sys + +if not sys.version_info < (2, 7): + + from collections import OrderedDict + +else: + + from UserDict import DictMixin + + class OrderedDict(dict, DictMixin): + + def __init__(self, *args, **kwds): + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__end + except AttributeError: + self.clear() + self.update(*args, **kwds) + + def clear(self): + self.__end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.__map = {} # key --> [key, prev, next] + dict.clear(self) + + def __setitem__(self, key, value): + if key not in self: + end = self.__end + curr = end[1] + curr[2] = end[1] = self.__map[key] = [key, curr, end] + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + dict.__delitem__(self, key) + key, prev, next_ = self.__map.pop(key) + prev[2] = next_ + next_[1] = prev + + def __iter__(self): + end = self.__end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): + end = self.__end + curr = end[1] + while curr is not end: + yield curr[0] + curr = curr[1] + + def popitem(self, last=True): + if not self: + raise KeyError('dictionary is empty') + if last: + key = reversed(self).next() + else: + key = iter(self).next() + value = self.pop(key) + return key, value + + def __reduce__(self): + items = [[k, self[k]] for k in self] + tmp = self.__map, self.__end + del self.__map, self.__end + inst_dict = vars(self).copy() + self.__map, self.__end = tmp + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def keys(self): + return list(self) + + setdefault = DictMixin.setdefault + update = DictMixin.update + pop = DictMixin.pop + values = DictMixin.values + items = DictMixin.items + iterkeys = DictMixin.iterkeys + itervalues = DictMixin.itervalues + iteritems = DictMixin.iteritems + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) + + def copy(self): + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + d = cls() + for key in iterable: + d[key] = value + return d + + def __eq__(self, other): + if isinstance(other, OrderedDict): + if len(self) != len(other): + return False + for p, q in zip(self.items(), other.items()): + if p != q: + return False + return True + return dict.__eq__(self, other) + + def __ne__(self, other): + return not self == other diff --git a/tasks/lib/package_control/deps/asn1crypto/_teletex_codec.py b/tasks/lib/package_control/deps/asn1crypto/_teletex_codec.py new file mode 100644 index 0000000..b5991aa --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/_teletex_codec.py @@ -0,0 +1,331 @@ +# coding: utf-8 + +""" +Implementation of the teletex T.61 codec. Exports the following items: + + - register() +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +import codecs + + +class TeletexCodec(codecs.Codec): + + def encode(self, input_, errors='strict'): + return codecs.charmap_encode(input_, errors, ENCODING_TABLE) + + def decode(self, input_, errors='strict'): + return codecs.charmap_decode(input_, errors, DECODING_TABLE) + + +class TeletexIncrementalEncoder(codecs.IncrementalEncoder): + + def encode(self, input_, final=False): + return codecs.charmap_encode(input_, self.errors, ENCODING_TABLE)[0] + + +class TeletexIncrementalDecoder(codecs.IncrementalDecoder): + + def decode(self, input_, final=False): + return codecs.charmap_decode(input_, self.errors, DECODING_TABLE)[0] + + +class TeletexStreamWriter(TeletexCodec, codecs.StreamWriter): + + pass + + +class TeletexStreamReader(TeletexCodec, codecs.StreamReader): + + pass + + +def teletex_search_function(name): + """ + Search function for teletex codec that is passed to codecs.register() + """ + + if name != 'teletex': + return None + + return codecs.CodecInfo( + name='teletex', + encode=TeletexCodec().encode, + decode=TeletexCodec().decode, + incrementalencoder=TeletexIncrementalEncoder, + incrementaldecoder=TeletexIncrementalDecoder, + streamreader=TeletexStreamReader, + streamwriter=TeletexStreamWriter, + ) + + +def register(): + """ + Registers the teletex codec + """ + + codecs.register(teletex_search_function) + + +# http://en.wikipedia.org/wiki/ITU_T.61 +DECODING_TABLE = ( + '\u0000' + '\u0001' + '\u0002' + '\u0003' + '\u0004' + '\u0005' + '\u0006' + '\u0007' + '\u0008' + '\u0009' + '\u000A' + '\u000B' + '\u000C' + '\u000D' + '\u000E' + '\u000F' + '\u0010' + '\u0011' + '\u0012' + '\u0013' + '\u0014' + '\u0015' + '\u0016' + '\u0017' + '\u0018' + '\u0019' + '\u001A' + '\u001B' + '\u001C' + '\u001D' + '\u001E' + '\u001F' + '\u0020' + '\u0021' + '\u0022' + '\ufffe' + '\ufffe' + '\u0025' + '\u0026' + '\u0027' + '\u0028' + '\u0029' + '\u002A' + '\u002B' + '\u002C' + '\u002D' + '\u002E' + '\u002F' + '\u0030' + '\u0031' + '\u0032' + '\u0033' + '\u0034' + '\u0035' + '\u0036' + '\u0037' + '\u0038' + '\u0039' + '\u003A' + '\u003B' + '\u003C' + '\u003D' + '\u003E' + '\u003F' + '\u0040' + '\u0041' + '\u0042' + '\u0043' + '\u0044' + '\u0045' + '\u0046' + '\u0047' + '\u0048' + '\u0049' + '\u004A' + '\u004B' + '\u004C' + '\u004D' + '\u004E' + '\u004F' + '\u0050' + '\u0051' + '\u0052' + '\u0053' + '\u0054' + '\u0055' + '\u0056' + '\u0057' + '\u0058' + '\u0059' + '\u005A' + '\u005B' + '\ufffe' + '\u005D' + '\ufffe' + '\u005F' + '\ufffe' + '\u0061' + '\u0062' + '\u0063' + '\u0064' + '\u0065' + '\u0066' + '\u0067' + '\u0068' + '\u0069' + '\u006A' + '\u006B' + '\u006C' + '\u006D' + '\u006E' + '\u006F' + '\u0070' + '\u0071' + '\u0072' + '\u0073' + '\u0074' + '\u0075' + '\u0076' + '\u0077' + '\u0078' + '\u0079' + '\u007A' + '\ufffe' + '\u007C' + '\ufffe' + '\ufffe' + '\u007F' + '\u0080' + '\u0081' + '\u0082' + '\u0083' + '\u0084' + '\u0085' + '\u0086' + '\u0087' + '\u0088' + '\u0089' + '\u008A' + '\u008B' + '\u008C' + '\u008D' + '\u008E' + '\u008F' + '\u0090' + '\u0091' + '\u0092' + '\u0093' + '\u0094' + '\u0095' + '\u0096' + '\u0097' + '\u0098' + '\u0099' + '\u009A' + '\u009B' + '\u009C' + '\u009D' + '\u009E' + '\u009F' + '\u00A0' + '\u00A1' + '\u00A2' + '\u00A3' + '\u0024' + '\u00A5' + '\u0023' + '\u00A7' + '\u00A4' + '\ufffe' + '\ufffe' + '\u00AB' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\u00B0' + '\u00B1' + '\u00B2' + '\u00B3' + '\u00D7' + '\u00B5' + '\u00B6' + '\u00B7' + '\u00F7' + '\ufffe' + '\ufffe' + '\u00BB' + '\u00BC' + '\u00BD' + '\u00BE' + '\u00BF' + '\ufffe' + '\u0300' + '\u0301' + '\u0302' + '\u0303' + '\u0304' + '\u0306' + '\u0307' + '\u0308' + '\ufffe' + '\u030A' + '\u0327' + '\u0332' + '\u030B' + '\u0328' + '\u030C' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\u2126' + '\u00C6' + '\u00D0' + '\u00AA' + '\u0126' + '\ufffe' + '\u0132' + '\u013F' + '\u0141' + '\u00D8' + '\u0152' + '\u00BA' + '\u00DE' + '\u0166' + '\u014A' + '\u0149' + '\u0138' + '\u00E6' + '\u0111' + '\u00F0' + '\u0127' + '\u0131' + '\u0133' + '\u0140' + '\u0142' + '\u00F8' + '\u0153' + '\u00DF' + '\u00FE' + '\u0167' + '\u014B' + '\ufffe' +) +ENCODING_TABLE = codecs.charmap_build(DECODING_TABLE) diff --git a/tasks/lib/package_control/deps/asn1crypto/_types.py b/tasks/lib/package_control/deps/asn1crypto/_types.py new file mode 100644 index 0000000..b9ca8cc --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/_types.py @@ -0,0 +1,46 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import inspect +import sys + + +if sys.version_info < (3,): + str_cls = unicode # noqa + byte_cls = str + int_types = (int, long) # noqa + + def bytes_to_list(byte_string): + return [ord(b) for b in byte_string] + + chr_cls = chr + +else: + str_cls = str + byte_cls = bytes + int_types = int + + bytes_to_list = list + + def chr_cls(num): + return bytes([num]) + + +def type_name(value): + """ + Returns a user-readable name for the type of an object + + :param value: + A value to get the type name of + + :return: + A unicode string of the object's type name + """ + + if inspect.isclass(value): + cls = value + else: + cls = value.__class__ + if cls.__module__ in set(['builtins', '__builtin__']): + return cls.__name__ + return '%s.%s' % (cls.__module__, cls.__name__) diff --git a/tasks/lib/package_control/deps/asn1crypto/algos.py b/tasks/lib/package_control/deps/asn1crypto/algos.py new file mode 100644 index 0000000..fc25e4d --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/algos.py @@ -0,0 +1,1206 @@ +# coding: utf-8 + +""" +ASN.1 type classes for various algorithms using in various aspects of public +key cryptography. Exports the following items: + + - AlgorithmIdentifier() + - AnyAlgorithmIdentifier() + - DigestAlgorithm() + - DigestInfo() + - DSASignature() + - EncryptionAlgorithm() + - HmacAlgorithm() + - KdfAlgorithm() + - Pkcs5MacAlgorithm() + - SignedDigestAlgorithm() + +Other type classes are defined that help compose the types listed above. +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +from ._errors import unwrap +from ._int import fill_width +from .util import int_from_bytes, int_to_bytes +from .core import ( + Any, + Choice, + Integer, + Null, + ObjectIdentifier, + OctetString, + Sequence, + Void, +) + + +# Structures and OIDs in this file are pulled from +# https://tools.ietf.org/html/rfc3279, https://tools.ietf.org/html/rfc4055, +# https://tools.ietf.org/html/rfc5758, https://tools.ietf.org/html/rfc7292, +# http://www.emc.com/collateral/white-papers/h11302-pkcs5v2-1-password-based-cryptography-standard-wp.pdf + +class AlgorithmIdentifier(Sequence): + _fields = [ + ('algorithm', ObjectIdentifier), + ('parameters', Any, {'optional': True}), + ] + + +class _ForceNullParameters(object): + """ + Various structures based on AlgorithmIdentifier require that the parameters + field be core.Null() for certain OIDs. This mixin ensures that happens. + """ + + # The following attribute, plus the parameters spec callback and custom + # __setitem__ are all to handle a situation where parameters should not be + # optional and must be Null for certain OIDs. More info at + # https://tools.ietf.org/html/rfc4055#page-15 and + # https://tools.ietf.org/html/rfc4055#section-2.1 + _null_algos = set([ + '1.2.840.113549.1.1.1', # rsassa_pkcs1v15 / rsaes_pkcs1v15 / rsa + '1.2.840.113549.1.1.11', # sha256_rsa + '1.2.840.113549.1.1.12', # sha384_rsa + '1.2.840.113549.1.1.13', # sha512_rsa + '1.2.840.113549.1.1.14', # sha224_rsa + '1.3.14.3.2.26', # sha1 + '2.16.840.1.101.3.4.2.4', # sha224 + '2.16.840.1.101.3.4.2.1', # sha256 + '2.16.840.1.101.3.4.2.2', # sha384 + '2.16.840.1.101.3.4.2.3', # sha512 + ]) + + def _parameters_spec(self): + if self._oid_pair == ('algorithm', 'parameters'): + algo = self['algorithm'].native + if algo in self._oid_specs: + return self._oid_specs[algo] + + if self['algorithm'].dotted in self._null_algos: + return Null + + return None + + _spec_callbacks = { + 'parameters': _parameters_spec + } + + # We have to override this since the spec callback uses the value of + # algorithm to determine the parameter spec, however default values are + # assigned before setting a field, so a default value can't be based on + # another field value (unless it is a default also). Thus we have to + # manually check to see if the algorithm was set and parameters is unset, + # and then fix the value as appropriate. + def __setitem__(self, key, value): + res = super(_ForceNullParameters, self).__setitem__(key, value) + if key != 'algorithm': + return res + if self['algorithm'].dotted not in self._null_algos: + return res + if self['parameters'].__class__ != Void: + return res + self['parameters'] = Null() + return res + + +class HmacAlgorithmId(ObjectIdentifier): + _map = { + '1.3.14.3.2.10': 'des_mac', + '1.2.840.113549.2.7': 'sha1', + '1.2.840.113549.2.8': 'sha224', + '1.2.840.113549.2.9': 'sha256', + '1.2.840.113549.2.10': 'sha384', + '1.2.840.113549.2.11': 'sha512', + '1.2.840.113549.2.12': 'sha512_224', + '1.2.840.113549.2.13': 'sha512_256', + '2.16.840.1.101.3.4.2.13': 'sha3_224', + '2.16.840.1.101.3.4.2.14': 'sha3_256', + '2.16.840.1.101.3.4.2.15': 'sha3_384', + '2.16.840.1.101.3.4.2.16': 'sha3_512', + } + + +class HmacAlgorithm(Sequence): + _fields = [ + ('algorithm', HmacAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + + +class DigestAlgorithmId(ObjectIdentifier): + _map = { + '1.2.840.113549.2.2': 'md2', + '1.2.840.113549.2.5': 'md5', + '1.3.14.3.2.26': 'sha1', + '2.16.840.1.101.3.4.2.4': 'sha224', + '2.16.840.1.101.3.4.2.1': 'sha256', + '2.16.840.1.101.3.4.2.2': 'sha384', + '2.16.840.1.101.3.4.2.3': 'sha512', + '2.16.840.1.101.3.4.2.5': 'sha512_224', + '2.16.840.1.101.3.4.2.6': 'sha512_256', + '2.16.840.1.101.3.4.2.7': 'sha3_224', + '2.16.840.1.101.3.4.2.8': 'sha3_256', + '2.16.840.1.101.3.4.2.9': 'sha3_384', + '2.16.840.1.101.3.4.2.10': 'sha3_512', + '2.16.840.1.101.3.4.2.11': 'shake128', + '2.16.840.1.101.3.4.2.12': 'shake256', + '2.16.840.1.101.3.4.2.17': 'shake128_len', + '2.16.840.1.101.3.4.2.18': 'shake256_len', + } + + +class DigestAlgorithm(_ForceNullParameters, Sequence): + _fields = [ + ('algorithm', DigestAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + + +# This structure is what is signed with a SignedDigestAlgorithm +class DigestInfo(Sequence): + _fields = [ + ('digest_algorithm', DigestAlgorithm), + ('digest', OctetString), + ] + + +class MaskGenAlgorithmId(ObjectIdentifier): + _map = { + '1.2.840.113549.1.1.8': 'mgf1', + } + + +class MaskGenAlgorithm(Sequence): + _fields = [ + ('algorithm', MaskGenAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'mgf1': DigestAlgorithm + } + + +class TrailerField(Integer): + _map = { + 1: 'trailer_field_bc', + } + + +class RSASSAPSSParams(Sequence): + _fields = [ + ( + 'hash_algorithm', + DigestAlgorithm, + { + 'explicit': 0, + 'default': {'algorithm': 'sha1'}, + } + ), + ( + 'mask_gen_algorithm', + MaskGenAlgorithm, + { + 'explicit': 1, + 'default': { + 'algorithm': 'mgf1', + 'parameters': {'algorithm': 'sha1'}, + }, + } + ), + ( + 'salt_length', + Integer, + { + 'explicit': 2, + 'default': 20, + } + ), + ( + 'trailer_field', + TrailerField, + { + 'explicit': 3, + 'default': 'trailer_field_bc', + } + ), + ] + + +class SignedDigestAlgorithmId(ObjectIdentifier): + _map = { + '1.3.14.3.2.3': 'md5_rsa', + '1.3.14.3.2.29': 'sha1_rsa', + '1.3.14.7.2.3.1': 'md2_rsa', + '1.2.840.113549.1.1.2': 'md2_rsa', + '1.2.840.113549.1.1.4': 'md5_rsa', + '1.2.840.113549.1.1.5': 'sha1_rsa', + '1.2.840.113549.1.1.14': 'sha224_rsa', + '1.2.840.113549.1.1.11': 'sha256_rsa', + '1.2.840.113549.1.1.12': 'sha384_rsa', + '1.2.840.113549.1.1.13': 'sha512_rsa', + '1.2.840.113549.1.1.10': 'rsassa_pss', + '1.2.840.10040.4.3': 'sha1_dsa', + '1.3.14.3.2.13': 'sha1_dsa', + '1.3.14.3.2.27': 'sha1_dsa', + '2.16.840.1.101.3.4.3.1': 'sha224_dsa', + '2.16.840.1.101.3.4.3.2': 'sha256_dsa', + '1.2.840.10045.4.1': 'sha1_ecdsa', + '1.2.840.10045.4.3.1': 'sha224_ecdsa', + '1.2.840.10045.4.3.2': 'sha256_ecdsa', + '1.2.840.10045.4.3.3': 'sha384_ecdsa', + '1.2.840.10045.4.3.4': 'sha512_ecdsa', + '2.16.840.1.101.3.4.3.9': 'sha3_224_ecdsa', + '2.16.840.1.101.3.4.3.10': 'sha3_256_ecdsa', + '2.16.840.1.101.3.4.3.11': 'sha3_384_ecdsa', + '2.16.840.1.101.3.4.3.12': 'sha3_512_ecdsa', + # For when the digest is specified elsewhere in a Sequence + '1.2.840.113549.1.1.1': 'rsassa_pkcs1v15', + '1.2.840.10040.4.1': 'dsa', + '1.2.840.10045.4': 'ecdsa', + # RFC 8410 -- https://tools.ietf.org/html/rfc8410 + '1.3.101.112': 'ed25519', + '1.3.101.113': 'ed448', + } + + _reverse_map = { + 'dsa': '1.2.840.10040.4.1', + 'ecdsa': '1.2.840.10045.4', + 'md2_rsa': '1.2.840.113549.1.1.2', + 'md5_rsa': '1.2.840.113549.1.1.4', + 'rsassa_pkcs1v15': '1.2.840.113549.1.1.1', + 'rsassa_pss': '1.2.840.113549.1.1.10', + 'sha1_dsa': '1.2.840.10040.4.3', + 'sha1_ecdsa': '1.2.840.10045.4.1', + 'sha1_rsa': '1.2.840.113549.1.1.5', + 'sha224_dsa': '2.16.840.1.101.3.4.3.1', + 'sha224_ecdsa': '1.2.840.10045.4.3.1', + 'sha224_rsa': '1.2.840.113549.1.1.14', + 'sha256_dsa': '2.16.840.1.101.3.4.3.2', + 'sha256_ecdsa': '1.2.840.10045.4.3.2', + 'sha256_rsa': '1.2.840.113549.1.1.11', + 'sha384_ecdsa': '1.2.840.10045.4.3.3', + 'sha384_rsa': '1.2.840.113549.1.1.12', + 'sha512_ecdsa': '1.2.840.10045.4.3.4', + 'sha512_rsa': '1.2.840.113549.1.1.13', + 'sha3_224_ecdsa': '2.16.840.1.101.3.4.3.9', + 'sha3_256_ecdsa': '2.16.840.1.101.3.4.3.10', + 'sha3_384_ecdsa': '2.16.840.1.101.3.4.3.11', + 'sha3_512_ecdsa': '2.16.840.1.101.3.4.3.12', + 'ed25519': '1.3.101.112', + 'ed448': '1.3.101.113', + } + + +class SignedDigestAlgorithm(_ForceNullParameters, Sequence): + _fields = [ + ('algorithm', SignedDigestAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'rsassa_pss': RSASSAPSSParams, + } + + @property + def signature_algo(self): + """ + :return: + A unicode string of "rsassa_pkcs1v15", "rsassa_pss", "dsa", + "ecdsa", "ed25519" or "ed448" + """ + + algorithm = self['algorithm'].native + + algo_map = { + 'md2_rsa': 'rsassa_pkcs1v15', + 'md5_rsa': 'rsassa_pkcs1v15', + 'sha1_rsa': 'rsassa_pkcs1v15', + 'sha224_rsa': 'rsassa_pkcs1v15', + 'sha256_rsa': 'rsassa_pkcs1v15', + 'sha384_rsa': 'rsassa_pkcs1v15', + 'sha512_rsa': 'rsassa_pkcs1v15', + 'rsassa_pkcs1v15': 'rsassa_pkcs1v15', + 'rsassa_pss': 'rsassa_pss', + 'sha1_dsa': 'dsa', + 'sha224_dsa': 'dsa', + 'sha256_dsa': 'dsa', + 'dsa': 'dsa', + 'sha1_ecdsa': 'ecdsa', + 'sha224_ecdsa': 'ecdsa', + 'sha256_ecdsa': 'ecdsa', + 'sha384_ecdsa': 'ecdsa', + 'sha512_ecdsa': 'ecdsa', + 'sha3_224_ecdsa': 'ecdsa', + 'sha3_256_ecdsa': 'ecdsa', + 'sha3_384_ecdsa': 'ecdsa', + 'sha3_512_ecdsa': 'ecdsa', + 'ecdsa': 'ecdsa', + 'ed25519': 'ed25519', + 'ed448': 'ed448', + } + if algorithm in algo_map: + return algo_map[algorithm] + + raise ValueError(unwrap( + ''' + Signature algorithm not known for %s + ''', + algorithm + )) + + @property + def hash_algo(self): + """ + :return: + A unicode string of "md2", "md5", "sha1", "sha224", "sha256", + "sha384", "sha512", "sha512_224", "sha512_256" or "shake256" + """ + + algorithm = self['algorithm'].native + + algo_map = { + 'md2_rsa': 'md2', + 'md5_rsa': 'md5', + 'sha1_rsa': 'sha1', + 'sha224_rsa': 'sha224', + 'sha256_rsa': 'sha256', + 'sha384_rsa': 'sha384', + 'sha512_rsa': 'sha512', + 'sha1_dsa': 'sha1', + 'sha224_dsa': 'sha224', + 'sha256_dsa': 'sha256', + 'sha1_ecdsa': 'sha1', + 'sha224_ecdsa': 'sha224', + 'sha256_ecdsa': 'sha256', + 'sha384_ecdsa': 'sha384', + 'sha512_ecdsa': 'sha512', + 'ed25519': 'sha512', + 'ed448': 'shake256', + } + if algorithm in algo_map: + return algo_map[algorithm] + + if algorithm == 'rsassa_pss': + return self['parameters']['hash_algorithm']['algorithm'].native + + raise ValueError(unwrap( + ''' + Hash algorithm not known for %s + ''', + algorithm + )) + + +class Pbkdf2Salt(Choice): + _alternatives = [ + ('specified', OctetString), + ('other_source', AlgorithmIdentifier), + ] + + +class Pbkdf2Params(Sequence): + _fields = [ + ('salt', Pbkdf2Salt), + ('iteration_count', Integer), + ('key_length', Integer, {'optional': True}), + ('prf', HmacAlgorithm, {'default': {'algorithm': 'sha1'}}), + ] + + +class ScryptParams(Sequence): + # https://tools.ietf.org/html/rfc7914#section-7 + _fields = [ + ('salt', OctetString), + ('cost_parameter', Integer), + ('block_size', Integer), + ('parallelization_parameter', Integer), + ('key_length', Integer, {'optional': True}), + ] + + +class KdfAlgorithmId(ObjectIdentifier): + _map = { + '1.2.840.113549.1.5.12': 'pbkdf2', + '1.3.6.1.4.1.11591.4.11': 'scrypt', + } + + +class KdfAlgorithm(Sequence): + _fields = [ + ('algorithm', KdfAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'pbkdf2': Pbkdf2Params, + 'scrypt': ScryptParams, + } + + +class DHParameters(Sequence): + """ + Original Name: DHParameter + Source: ftp://ftp.rsasecurity.com/pub/pkcs/ascii/pkcs-3.asc section 9 + """ + + _fields = [ + ('p', Integer), + ('g', Integer), + ('private_value_length', Integer, {'optional': True}), + ] + + +class KeyExchangeAlgorithmId(ObjectIdentifier): + _map = { + '1.2.840.113549.1.3.1': 'dh', + } + + +class KeyExchangeAlgorithm(Sequence): + _fields = [ + ('algorithm', KeyExchangeAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'dh': DHParameters, + } + + +class Rc2Params(Sequence): + _fields = [ + ('rc2_parameter_version', Integer, {'optional': True}), + ('iv', OctetString), + ] + + +class Rc5ParamVersion(Integer): + _map = { + 16: 'v1-0' + } + + +class Rc5Params(Sequence): + _fields = [ + ('version', Rc5ParamVersion), + ('rounds', Integer), + ('block_size_in_bits', Integer), + ('iv', OctetString, {'optional': True}), + ] + + +class Pbes1Params(Sequence): + _fields = [ + ('salt', OctetString), + ('iterations', Integer), + ] + + +class CcmParams(Sequence): + # https://tools.ietf.org/html/rfc5084 + # aes_ICVlen: 4 | 6 | 8 | 10 | 12 | 14 | 16 + _fields = [ + ('aes_nonce', OctetString), + ('aes_icvlen', Integer), + ] + + +class PSourceAlgorithmId(ObjectIdentifier): + _map = { + '1.2.840.113549.1.1.9': 'p_specified', + } + + +class PSourceAlgorithm(Sequence): + _fields = [ + ('algorithm', PSourceAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'p_specified': OctetString + } + + +class RSAESOAEPParams(Sequence): + _fields = [ + ( + 'hash_algorithm', + DigestAlgorithm, + { + 'explicit': 0, + 'default': {'algorithm': 'sha1'} + } + ), + ( + 'mask_gen_algorithm', + MaskGenAlgorithm, + { + 'explicit': 1, + 'default': { + 'algorithm': 'mgf1', + 'parameters': {'algorithm': 'sha1'} + } + } + ), + ( + 'p_source_algorithm', + PSourceAlgorithm, + { + 'explicit': 2, + 'default': { + 'algorithm': 'p_specified', + 'parameters': b'' + } + } + ), + ] + + +class DSASignature(Sequence): + """ + An ASN.1 class for translating between the OS crypto library's + representation of an (EC)DSA signature and the ASN.1 structure that is part + of various RFCs. + + Original Name: DSS-Sig-Value + Source: https://tools.ietf.org/html/rfc3279#section-2.2.2 + """ + + _fields = [ + ('r', Integer), + ('s', Integer), + ] + + @classmethod + def from_p1363(cls, data): + """ + Reads a signature from a byte string encoding accordint to IEEE P1363, + which is used by Microsoft's BCryptSignHash() function. + + :param data: + A byte string from BCryptSignHash() + + :return: + A DSASignature object + """ + + r = int_from_bytes(data[0:len(data) // 2]) + s = int_from_bytes(data[len(data) // 2:]) + return cls({'r': r, 's': s}) + + def to_p1363(self): + """ + Dumps a signature to a byte string compatible with Microsoft's + BCryptVerifySignature() function. + + :return: + A byte string compatible with BCryptVerifySignature() + """ + + r_bytes = int_to_bytes(self['r'].native) + s_bytes = int_to_bytes(self['s'].native) + + int_byte_length = max(len(r_bytes), len(s_bytes)) + r_bytes = fill_width(r_bytes, int_byte_length) + s_bytes = fill_width(s_bytes, int_byte_length) + + return r_bytes + s_bytes + + +class EncryptionAlgorithmId(ObjectIdentifier): + _map = { + '1.3.14.3.2.7': 'des', + '1.2.840.113549.3.7': 'tripledes_3key', + '1.2.840.113549.3.2': 'rc2', + '1.2.840.113549.3.4': 'rc4', + '1.2.840.113549.3.9': 'rc5', + # From http://csrc.nist.gov/groups/ST/crypto_apps_infra/csor/algorithms.html#AES + '2.16.840.1.101.3.4.1.1': 'aes128_ecb', + '2.16.840.1.101.3.4.1.2': 'aes128_cbc', + '2.16.840.1.101.3.4.1.3': 'aes128_ofb', + '2.16.840.1.101.3.4.1.4': 'aes128_cfb', + '2.16.840.1.101.3.4.1.5': 'aes128_wrap', + '2.16.840.1.101.3.4.1.6': 'aes128_gcm', + '2.16.840.1.101.3.4.1.7': 'aes128_ccm', + '2.16.840.1.101.3.4.1.8': 'aes128_wrap_pad', + '2.16.840.1.101.3.4.1.21': 'aes192_ecb', + '2.16.840.1.101.3.4.1.22': 'aes192_cbc', + '2.16.840.1.101.3.4.1.23': 'aes192_ofb', + '2.16.840.1.101.3.4.1.24': 'aes192_cfb', + '2.16.840.1.101.3.4.1.25': 'aes192_wrap', + '2.16.840.1.101.3.4.1.26': 'aes192_gcm', + '2.16.840.1.101.3.4.1.27': 'aes192_ccm', + '2.16.840.1.101.3.4.1.28': 'aes192_wrap_pad', + '2.16.840.1.101.3.4.1.41': 'aes256_ecb', + '2.16.840.1.101.3.4.1.42': 'aes256_cbc', + '2.16.840.1.101.3.4.1.43': 'aes256_ofb', + '2.16.840.1.101.3.4.1.44': 'aes256_cfb', + '2.16.840.1.101.3.4.1.45': 'aes256_wrap', + '2.16.840.1.101.3.4.1.46': 'aes256_gcm', + '2.16.840.1.101.3.4.1.47': 'aes256_ccm', + '2.16.840.1.101.3.4.1.48': 'aes256_wrap_pad', + # From PKCS#5 + '1.2.840.113549.1.5.13': 'pbes2', + '1.2.840.113549.1.5.1': 'pbes1_md2_des', + '1.2.840.113549.1.5.3': 'pbes1_md5_des', + '1.2.840.113549.1.5.4': 'pbes1_md2_rc2', + '1.2.840.113549.1.5.6': 'pbes1_md5_rc2', + '1.2.840.113549.1.5.10': 'pbes1_sha1_des', + '1.2.840.113549.1.5.11': 'pbes1_sha1_rc2', + # From PKCS#12 + '1.2.840.113549.1.12.1.1': 'pkcs12_sha1_rc4_128', + '1.2.840.113549.1.12.1.2': 'pkcs12_sha1_rc4_40', + '1.2.840.113549.1.12.1.3': 'pkcs12_sha1_tripledes_3key', + '1.2.840.113549.1.12.1.4': 'pkcs12_sha1_tripledes_2key', + '1.2.840.113549.1.12.1.5': 'pkcs12_sha1_rc2_128', + '1.2.840.113549.1.12.1.6': 'pkcs12_sha1_rc2_40', + # PKCS#1 v2.2 + '1.2.840.113549.1.1.1': 'rsaes_pkcs1v15', + '1.2.840.113549.1.1.7': 'rsaes_oaep', + } + + +class EncryptionAlgorithm(_ForceNullParameters, Sequence): + _fields = [ + ('algorithm', EncryptionAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'des': OctetString, + 'tripledes_3key': OctetString, + 'rc2': Rc2Params, + 'rc5': Rc5Params, + 'aes128_cbc': OctetString, + 'aes192_cbc': OctetString, + 'aes256_cbc': OctetString, + 'aes128_ofb': OctetString, + 'aes192_ofb': OctetString, + 'aes256_ofb': OctetString, + # From RFC5084 + 'aes128_ccm': CcmParams, + 'aes192_ccm': CcmParams, + 'aes256_ccm': CcmParams, + # From PKCS#5 + 'pbes1_md2_des': Pbes1Params, + 'pbes1_md5_des': Pbes1Params, + 'pbes1_md2_rc2': Pbes1Params, + 'pbes1_md5_rc2': Pbes1Params, + 'pbes1_sha1_des': Pbes1Params, + 'pbes1_sha1_rc2': Pbes1Params, + # From PKCS#12 + 'pkcs12_sha1_rc4_128': Pbes1Params, + 'pkcs12_sha1_rc4_40': Pbes1Params, + 'pkcs12_sha1_tripledes_3key': Pbes1Params, + 'pkcs12_sha1_tripledes_2key': Pbes1Params, + 'pkcs12_sha1_rc2_128': Pbes1Params, + 'pkcs12_sha1_rc2_40': Pbes1Params, + # PKCS#1 v2.2 + 'rsaes_oaep': RSAESOAEPParams, + } + + @property + def kdf(self): + """ + Returns the name of the key derivation function to use. + + :return: + A unicode from of one of the following: "pbkdf1", "pbkdf2", + "pkcs12_kdf" + """ + + encryption_algo = self['algorithm'].native + + if encryption_algo == 'pbes2': + return self['parameters']['key_derivation_func']['algorithm'].native + + if encryption_algo.find('.') == -1: + if encryption_algo.find('_') != -1: + encryption_algo, _ = encryption_algo.split('_', 1) + + if encryption_algo == 'pbes1': + return 'pbkdf1' + + if encryption_algo == 'pkcs12': + return 'pkcs12_kdf' + + raise ValueError(unwrap( + ''' + Encryption algorithm "%s" does not have a registered key + derivation function + ''', + encryption_algo + )) + + raise ValueError(unwrap( + ''' + Unrecognized encryption algorithm "%s", can not determine key + derivation function + ''', + encryption_algo + )) + + @property + def kdf_hmac(self): + """ + Returns the HMAC algorithm to use with the KDF. + + :return: + A unicode string of one of the following: "md2", "md5", "sha1", + "sha224", "sha256", "sha384", "sha512" + """ + + encryption_algo = self['algorithm'].native + + if encryption_algo == 'pbes2': + if self.kdf == 'scrypt': + return None + return self['parameters']['key_derivation_func']['parameters']['prf']['algorithm'].native + + if encryption_algo.find('.') == -1: + if encryption_algo.find('_') != -1: + _, hmac_algo, _ = encryption_algo.split('_', 2) + return hmac_algo + + raise ValueError(unwrap( + ''' + Encryption algorithm "%s" does not have a registered key + derivation function + ''', + encryption_algo + )) + + raise ValueError(unwrap( + ''' + Unrecognized encryption algorithm "%s", can not determine key + derivation hmac algorithm + ''', + encryption_algo + )) + + @property + def kdf_salt(self): + """ + Returns the byte string to use as the salt for the KDF. + + :return: + A byte string + """ + + encryption_algo = self['algorithm'].native + + if encryption_algo == 'pbes2': + salt = self['parameters']['key_derivation_func']['parameters']['salt'] + + if salt.name == 'other_source': + raise ValueError(unwrap( + ''' + Can not determine key derivation salt - the + reserved-for-future-use other source salt choice was + specified in the PBKDF2 params structure + ''' + )) + + return salt.native + + if encryption_algo.find('.') == -1: + if encryption_algo.find('_') != -1: + return self['parameters']['salt'].native + + raise ValueError(unwrap( + ''' + Encryption algorithm "%s" does not have a registered key + derivation function + ''', + encryption_algo + )) + + raise ValueError(unwrap( + ''' + Unrecognized encryption algorithm "%s", can not determine key + derivation salt + ''', + encryption_algo + )) + + @property + def kdf_iterations(self): + """ + Returns the number of iterations that should be run via the KDF. + + :return: + An integer + """ + + encryption_algo = self['algorithm'].native + + if encryption_algo == 'pbes2': + if self.kdf == 'scrypt': + return None + return self['parameters']['key_derivation_func']['parameters']['iteration_count'].native + + if encryption_algo.find('.') == -1: + if encryption_algo.find('_') != -1: + return self['parameters']['iterations'].native + + raise ValueError(unwrap( + ''' + Encryption algorithm "%s" does not have a registered key + derivation function + ''', + encryption_algo + )) + + raise ValueError(unwrap( + ''' + Unrecognized encryption algorithm "%s", can not determine key + derivation iterations + ''', + encryption_algo + )) + + @property + def key_length(self): + """ + Returns the key length to pass to the cipher/kdf. The PKCS#5 spec does + not specify a way to store the RC5 key length, however this tends not + to be a problem since OpenSSL does not support RC5 in PKCS#8 and OS X + does not provide an RC5 cipher for use in the Security Transforms + library. + + :raises: + ValueError - when the key length can not be determined + + :return: + An integer representing the length in bytes + """ + + encryption_algo = self['algorithm'].native + + if encryption_algo[0:3] == 'aes': + return { + 'aes128_': 16, + 'aes192_': 24, + 'aes256_': 32, + }[encryption_algo[0:7]] + + cipher_lengths = { + 'des': 8, + 'tripledes_3key': 24, + } + + if encryption_algo in cipher_lengths: + return cipher_lengths[encryption_algo] + + if encryption_algo == 'rc2': + rc2_parameter_version = self['parameters']['rc2_parameter_version'].native + + # See page 24 of + # http://www.emc.com/collateral/white-papers/h11302-pkcs5v2-1-password-based-cryptography-standard-wp.pdf + encoded_key_bits_map = { + 160: 5, # 40-bit + 120: 8, # 64-bit + 58: 16, # 128-bit + } + + if rc2_parameter_version in encoded_key_bits_map: + return encoded_key_bits_map[rc2_parameter_version] + + if rc2_parameter_version >= 256: + return rc2_parameter_version + + if rc2_parameter_version is None: + return 4 # 32-bit default + + raise ValueError(unwrap( + ''' + Invalid RC2 parameter version found in EncryptionAlgorithm + parameters + ''' + )) + + if encryption_algo == 'pbes2': + key_length = self['parameters']['key_derivation_func']['parameters']['key_length'].native + if key_length is not None: + return key_length + + # If the KDF params don't specify the key size, we can infer it from + # the encryption scheme for all schemes except for RC5. However, in + # practical terms, neither OpenSSL or OS X support RC5 for PKCS#8 + # so it is unlikely to be an issue that is run into. + + return self['parameters']['encryption_scheme'].key_length + + if encryption_algo.find('.') == -1: + return { + 'pbes1_md2_des': 8, + 'pbes1_md5_des': 8, + 'pbes1_md2_rc2': 8, + 'pbes1_md5_rc2': 8, + 'pbes1_sha1_des': 8, + 'pbes1_sha1_rc2': 8, + 'pkcs12_sha1_rc4_128': 16, + 'pkcs12_sha1_rc4_40': 5, + 'pkcs12_sha1_tripledes_3key': 24, + 'pkcs12_sha1_tripledes_2key': 16, + 'pkcs12_sha1_rc2_128': 16, + 'pkcs12_sha1_rc2_40': 5, + }[encryption_algo] + + raise ValueError(unwrap( + ''' + Unrecognized encryption algorithm "%s" + ''', + encryption_algo + )) + + @property + def encryption_mode(self): + """ + Returns the name of the encryption mode to use. + + :return: + A unicode string from one of the following: "cbc", "ecb", "ofb", + "cfb", "wrap", "gcm", "ccm", "wrap_pad" + """ + + encryption_algo = self['algorithm'].native + + if encryption_algo[0:7] in set(['aes128_', 'aes192_', 'aes256_']): + return encryption_algo[7:] + + if encryption_algo[0:6] == 'pbes1_': + return 'cbc' + + if encryption_algo[0:7] == 'pkcs12_': + return 'cbc' + + if encryption_algo in set(['des', 'tripledes_3key', 'rc2', 'rc5']): + return 'cbc' + + if encryption_algo == 'pbes2': + return self['parameters']['encryption_scheme'].encryption_mode + + raise ValueError(unwrap( + ''' + Unrecognized encryption algorithm "%s" + ''', + encryption_algo + )) + + @property + def encryption_cipher(self): + """ + Returns the name of the symmetric encryption cipher to use. The key + length can be retrieved via the .key_length property to disabiguate + between different variations of TripleDES, AES, and the RC* ciphers. + + :return: + A unicode string from one of the following: "rc2", "rc5", "des", + "tripledes", "aes" + """ + + encryption_algo = self['algorithm'].native + + if encryption_algo[0:7] in set(['aes128_', 'aes192_', 'aes256_']): + return 'aes' + + if encryption_algo in set(['des', 'rc2', 'rc5']): + return encryption_algo + + if encryption_algo == 'tripledes_3key': + return 'tripledes' + + if encryption_algo == 'pbes2': + return self['parameters']['encryption_scheme'].encryption_cipher + + if encryption_algo.find('.') == -1: + return { + 'pbes1_md2_des': 'des', + 'pbes1_md5_des': 'des', + 'pbes1_md2_rc2': 'rc2', + 'pbes1_md5_rc2': 'rc2', + 'pbes1_sha1_des': 'des', + 'pbes1_sha1_rc2': 'rc2', + 'pkcs12_sha1_rc4_128': 'rc4', + 'pkcs12_sha1_rc4_40': 'rc4', + 'pkcs12_sha1_tripledes_3key': 'tripledes', + 'pkcs12_sha1_tripledes_2key': 'tripledes', + 'pkcs12_sha1_rc2_128': 'rc2', + 'pkcs12_sha1_rc2_40': 'rc2', + }[encryption_algo] + + raise ValueError(unwrap( + ''' + Unrecognized encryption algorithm "%s" + ''', + encryption_algo + )) + + @property + def encryption_block_size(self): + """ + Returns the block size of the encryption cipher, in bytes. + + :return: + An integer that is the block size in bytes + """ + + encryption_algo = self['algorithm'].native + + if encryption_algo[0:7] in set(['aes128_', 'aes192_', 'aes256_']): + return 16 + + cipher_map = { + 'des': 8, + 'tripledes_3key': 8, + 'rc2': 8, + } + if encryption_algo in cipher_map: + return cipher_map[encryption_algo] + + if encryption_algo == 'rc5': + return self['parameters']['block_size_in_bits'].native // 8 + + if encryption_algo == 'pbes2': + return self['parameters']['encryption_scheme'].encryption_block_size + + if encryption_algo.find('.') == -1: + return { + 'pbes1_md2_des': 8, + 'pbes1_md5_des': 8, + 'pbes1_md2_rc2': 8, + 'pbes1_md5_rc2': 8, + 'pbes1_sha1_des': 8, + 'pbes1_sha1_rc2': 8, + 'pkcs12_sha1_rc4_128': 0, + 'pkcs12_sha1_rc4_40': 0, + 'pkcs12_sha1_tripledes_3key': 8, + 'pkcs12_sha1_tripledes_2key': 8, + 'pkcs12_sha1_rc2_128': 8, + 'pkcs12_sha1_rc2_40': 8, + }[encryption_algo] + + raise ValueError(unwrap( + ''' + Unrecognized encryption algorithm "%s" + ''', + encryption_algo + )) + + @property + def encryption_iv(self): + """ + Returns the byte string of the initialization vector for the encryption + scheme. Only the PBES2 stores the IV in the params. For PBES1, the IV + is derived from the KDF and this property will return None. + + :return: + A byte string or None + """ + + encryption_algo = self['algorithm'].native + + if encryption_algo in set(['rc2', 'rc5']): + return self['parameters']['iv'].native + + # For DES/Triple DES and AES the IV is the entirety of the parameters + octet_string_iv_oids = set([ + 'des', + 'tripledes_3key', + 'aes128_cbc', + 'aes192_cbc', + 'aes256_cbc', + 'aes128_ofb', + 'aes192_ofb', + 'aes256_ofb', + ]) + if encryption_algo in octet_string_iv_oids: + return self['parameters'].native + + if encryption_algo == 'pbes2': + return self['parameters']['encryption_scheme'].encryption_iv + + # All of the PBES1 algos use their KDF to create the IV. For the pbkdf1, + # the KDF is told to generate a key that is an extra 8 bytes long, and + # that is used for the IV. For the PKCS#12 KDF, it is called with an id + # of 2 to generate the IV. In either case, we can't return the IV + # without knowing the user's password. + if encryption_algo.find('.') == -1: + return None + + raise ValueError(unwrap( + ''' + Unrecognized encryption algorithm "%s" + ''', + encryption_algo + )) + + +class Pbes2Params(Sequence): + _fields = [ + ('key_derivation_func', KdfAlgorithm), + ('encryption_scheme', EncryptionAlgorithm), + ] + + +class Pbmac1Params(Sequence): + _fields = [ + ('key_derivation_func', KdfAlgorithm), + ('message_auth_scheme', HmacAlgorithm), + ] + + +class Pkcs5MacId(ObjectIdentifier): + _map = { + '1.2.840.113549.1.5.14': 'pbmac1', + } + + +class Pkcs5MacAlgorithm(Sequence): + _fields = [ + ('algorithm', Pkcs5MacId), + ('parameters', Any), + ] + + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'pbmac1': Pbmac1Params, + } + + +EncryptionAlgorithm._oid_specs['pbes2'] = Pbes2Params + + +class AnyAlgorithmId(ObjectIdentifier): + _map = {} + + def _setup(self): + _map = self.__class__._map + for other_cls in (EncryptionAlgorithmId, SignedDigestAlgorithmId, DigestAlgorithmId): + for oid, name in other_cls._map.items(): + _map[oid] = name + + +class AnyAlgorithmIdentifier(_ForceNullParameters, Sequence): + _fields = [ + ('algorithm', AnyAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + + _oid_pair = ('algorithm', 'parameters') + _oid_specs = {} + + def _setup(self): + Sequence._setup(self) + specs = self.__class__._oid_specs + for other_cls in (EncryptionAlgorithm, SignedDigestAlgorithm): + for oid, spec in other_cls._oid_specs.items(): + specs[oid] = spec diff --git a/tasks/lib/package_control/deps/asn1crypto/cms.py b/tasks/lib/package_control/deps/asn1crypto/cms.py new file mode 100644 index 0000000..c395b22 --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/cms.py @@ -0,0 +1,1003 @@ +# coding: utf-8 + +""" +ASN.1 type classes for cryptographic message syntax (CMS). Structures are also +compatible with PKCS#7. Exports the following items: + + - AuthenticatedData() + - AuthEnvelopedData() + - CompressedData() + - ContentInfo() + - DigestedData() + - EncryptedData() + - EnvelopedData() + - SignedAndEnvelopedData() + - SignedData() + +Other type classes are defined that help compose the types listed above. + +Most CMS structures in the wild are formatted as ContentInfo encapsulating one of the other types. +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +try: + import zlib +except (ImportError): + zlib = None + +from .algos import ( + _ForceNullParameters, + DigestAlgorithm, + EncryptionAlgorithm, + EncryptionAlgorithmId, + HmacAlgorithm, + KdfAlgorithm, + RSAESOAEPParams, + SignedDigestAlgorithm, +) +from .core import ( + Any, + BitString, + Choice, + Enumerated, + GeneralizedTime, + Integer, + ObjectIdentifier, + OctetBitString, + OctetString, + ParsableOctetString, + Sequence, + SequenceOf, + SetOf, + UTCTime, + UTF8String, +) +from .crl import CertificateList +from .keys import PublicKeyInfo +from .ocsp import OCSPResponse +from .x509 import Attributes, Certificate, Extensions, GeneralName, GeneralNames, Name + + +# These structures are taken from +# ftp://ftp.rsasecurity.com/pub/pkcs/ascii/pkcs-6.asc + +class ExtendedCertificateInfo(Sequence): + _fields = [ + ('version', Integer), + ('certificate', Certificate), + ('attributes', Attributes), + ] + + +class ExtendedCertificate(Sequence): + _fields = [ + ('extended_certificate_info', ExtendedCertificateInfo), + ('signature_algorithm', SignedDigestAlgorithm), + ('signature', OctetBitString), + ] + + +# These structures are taken from https://tools.ietf.org/html/rfc5652, +# https://tools.ietf.org/html/rfc5083, http://tools.ietf.org/html/rfc2315, +# https://tools.ietf.org/html/rfc5940, https://tools.ietf.org/html/rfc3274, +# https://tools.ietf.org/html/rfc3281 + + +class CMSVersion(Integer): + _map = { + 0: 'v0', + 1: 'v1', + 2: 'v2', + 3: 'v3', + 4: 'v4', + 5: 'v5', + } + + +class CMSAttributeType(ObjectIdentifier): + _map = { + '1.2.840.113549.1.9.3': 'content_type', + '1.2.840.113549.1.9.4': 'message_digest', + '1.2.840.113549.1.9.5': 'signing_time', + '1.2.840.113549.1.9.6': 'counter_signature', + # https://datatracker.ietf.org/doc/html/rfc2633#section-2.5.2 + '1.2.840.113549.1.9.15': 'smime_capabilities', + # https://tools.ietf.org/html/rfc2633#page-26 + '1.2.840.113549.1.9.16.2.11': 'encrypt_key_pref', + # https://tools.ietf.org/html/rfc3161#page-20 + '1.2.840.113549.1.9.16.2.14': 'signature_time_stamp_token', + # https://tools.ietf.org/html/rfc6211#page-5 + '1.2.840.113549.1.9.52': 'cms_algorithm_protection', + # https://docs.microsoft.com/en-us/previous-versions/hh968145(v%3Dvs.85) + '1.3.6.1.4.1.311.2.4.1': 'microsoft_nested_signature', + # Some places refer to this as SPC_RFC3161_OBJID, others szOID_RFC3161_counterSign. + # https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/ns-wincrypt-crypt_algorithm_identifier + # refers to szOID_RFC3161_counterSign as "1.2.840.113549.1.9.16.1.4", + # but that OID is also called szOID_TIMESTAMP_TOKEN. Because of there being + # no canonical source for this OID, we give it our own name + '1.3.6.1.4.1.311.3.3.1': 'microsoft_time_stamp_token', + } + + +class Time(Choice): + _alternatives = [ + ('utc_time', UTCTime), + ('generalized_time', GeneralizedTime), + ] + + +class ContentType(ObjectIdentifier): + _map = { + '1.2.840.113549.1.7.1': 'data', + '1.2.840.113549.1.7.2': 'signed_data', + '1.2.840.113549.1.7.3': 'enveloped_data', + '1.2.840.113549.1.7.4': 'signed_and_enveloped_data', + '1.2.840.113549.1.7.5': 'digested_data', + '1.2.840.113549.1.7.6': 'encrypted_data', + '1.2.840.113549.1.9.16.1.2': 'authenticated_data', + '1.2.840.113549.1.9.16.1.9': 'compressed_data', + '1.2.840.113549.1.9.16.1.23': 'authenticated_enveloped_data', + } + + +class CMSAlgorithmProtection(Sequence): + _fields = [ + ('digest_algorithm', DigestAlgorithm), + ('signature_algorithm', SignedDigestAlgorithm, {'implicit': 1, 'optional': True}), + ('mac_algorithm', HmacAlgorithm, {'implicit': 2, 'optional': True}), + ] + + +class SetOfContentType(SetOf): + _child_spec = ContentType + + +class SetOfOctetString(SetOf): + _child_spec = OctetString + + +class SetOfTime(SetOf): + _child_spec = Time + + +class SetOfAny(SetOf): + _child_spec = Any + + +class SetOfCMSAlgorithmProtection(SetOf): + _child_spec = CMSAlgorithmProtection + + +class CMSAttribute(Sequence): + _fields = [ + ('type', CMSAttributeType), + ('values', None), + ] + + _oid_specs = {} + + def _values_spec(self): + return self._oid_specs.get(self['type'].native, SetOfAny) + + _spec_callbacks = { + 'values': _values_spec + } + + +class CMSAttributes(SetOf): + _child_spec = CMSAttribute + + +class IssuerSerial(Sequence): + _fields = [ + ('issuer', GeneralNames), + ('serial', Integer), + ('issuer_uid', OctetBitString, {'optional': True}), + ] + + +class AttCertVersion(Integer): + _map = { + 0: 'v1', + 1: 'v2', + } + + +class AttCertSubject(Choice): + _alternatives = [ + ('base_certificate_id', IssuerSerial, {'explicit': 0}), + ('subject_name', GeneralNames, {'explicit': 1}), + ] + + +class AttCertValidityPeriod(Sequence): + _fields = [ + ('not_before_time', GeneralizedTime), + ('not_after_time', GeneralizedTime), + ] + + +class AttributeCertificateInfoV1(Sequence): + _fields = [ + ('version', AttCertVersion, {'default': 'v1'}), + ('subject', AttCertSubject), + ('issuer', GeneralNames), + ('signature', SignedDigestAlgorithm), + ('serial_number', Integer), + ('att_cert_validity_period', AttCertValidityPeriod), + ('attributes', Attributes), + ('issuer_unique_id', OctetBitString, {'optional': True}), + ('extensions', Extensions, {'optional': True}), + ] + + +class AttributeCertificateV1(Sequence): + _fields = [ + ('ac_info', AttributeCertificateInfoV1), + ('signature_algorithm', SignedDigestAlgorithm), + ('signature', OctetBitString), + ] + + +class DigestedObjectType(Enumerated): + _map = { + 0: 'public_key', + 1: 'public_key_cert', + 2: 'other_objy_types', + } + + +class ObjectDigestInfo(Sequence): + _fields = [ + ('digested_object_type', DigestedObjectType), + ('other_object_type_id', ObjectIdentifier, {'optional': True}), + ('digest_algorithm', DigestAlgorithm), + ('object_digest', OctetBitString), + ] + + +class Holder(Sequence): + _fields = [ + ('base_certificate_id', IssuerSerial, {'implicit': 0, 'optional': True}), + ('entity_name', GeneralNames, {'implicit': 1, 'optional': True}), + ('object_digest_info', ObjectDigestInfo, {'implicit': 2, 'optional': True}), + ] + + +class V2Form(Sequence): + _fields = [ + ('issuer_name', GeneralNames, {'optional': True}), + ('base_certificate_id', IssuerSerial, {'explicit': 0, 'optional': True}), + ('object_digest_info', ObjectDigestInfo, {'explicit': 1, 'optional': True}), + ] + + +class AttCertIssuer(Choice): + _alternatives = [ + ('v1_form', GeneralNames), + ('v2_form', V2Form, {'implicit': 0}), + ] + + +class IetfAttrValue(Choice): + _alternatives = [ + ('octets', OctetString), + ('oid', ObjectIdentifier), + ('string', UTF8String), + ] + + +class IetfAttrValues(SequenceOf): + _child_spec = IetfAttrValue + + +class IetfAttrSyntax(Sequence): + _fields = [ + ('policy_authority', GeneralNames, {'implicit': 0, 'optional': True}), + ('values', IetfAttrValues), + ] + + +class SetOfIetfAttrSyntax(SetOf): + _child_spec = IetfAttrSyntax + + +class SvceAuthInfo(Sequence): + _fields = [ + ('service', GeneralName), + ('ident', GeneralName), + ('auth_info', OctetString, {'optional': True}), + ] + + +class SetOfSvceAuthInfo(SetOf): + _child_spec = SvceAuthInfo + + +class RoleSyntax(Sequence): + _fields = [ + ('role_authority', GeneralNames, {'implicit': 0, 'optional': True}), + ('role_name', GeneralName, {'explicit': 1}), + ] + + +class SetOfRoleSyntax(SetOf): + _child_spec = RoleSyntax + + +class ClassList(BitString): + _map = { + 0: 'unmarked', + 1: 'unclassified', + 2: 'restricted', + 3: 'confidential', + 4: 'secret', + 5: 'top_secret', + } + + +class SecurityCategory(Sequence): + _fields = [ + ('type', ObjectIdentifier, {'implicit': 0}), + ('value', Any, {'explicit': 1}), + ] + + +class SetOfSecurityCategory(SetOf): + _child_spec = SecurityCategory + + +class Clearance(Sequence): + _fields = [ + ('policy_id', ObjectIdentifier), + ('class_list', ClassList, {'default': set(['unclassified'])}), + ('security_categories', SetOfSecurityCategory, {'optional': True}), + ] + + +class SetOfClearance(SetOf): + _child_spec = Clearance + + +class BigTime(Sequence): + _fields = [ + ('major', Integer), + ('fractional_seconds', Integer), + ('sign', Integer, {'optional': True}), + ] + + +class LeapData(Sequence): + _fields = [ + ('leap_time', BigTime), + ('action', Integer), + ] + + +class SetOfLeapData(SetOf): + _child_spec = LeapData + + +class TimingMetrics(Sequence): + _fields = [ + ('ntp_time', BigTime), + ('offset', BigTime), + ('delay', BigTime), + ('expiration', BigTime), + ('leap_event', SetOfLeapData, {'optional': True}), + ] + + +class SetOfTimingMetrics(SetOf): + _child_spec = TimingMetrics + + +class TimingPolicy(Sequence): + _fields = [ + ('policy_id', SequenceOf, {'spec': ObjectIdentifier}), + ('max_offset', BigTime, {'explicit': 0, 'optional': True}), + ('max_delay', BigTime, {'explicit': 1, 'optional': True}), + ] + + +class SetOfTimingPolicy(SetOf): + _child_spec = TimingPolicy + + +class AttCertAttributeType(ObjectIdentifier): + _map = { + '1.3.6.1.5.5.7.10.1': 'authentication_info', + '1.3.6.1.5.5.7.10.2': 'access_identity', + '1.3.6.1.5.5.7.10.3': 'charging_identity', + '1.3.6.1.5.5.7.10.4': 'group', + '2.5.4.72': 'role', + '2.5.4.55': 'clearance', + '1.3.6.1.4.1.601.10.4.1': 'timing_metrics', + '1.3.6.1.4.1.601.10.4.2': 'timing_policy', + } + + +class AttCertAttribute(Sequence): + _fields = [ + ('type', AttCertAttributeType), + ('values', None), + ] + + _oid_specs = { + 'authentication_info': SetOfSvceAuthInfo, + 'access_identity': SetOfSvceAuthInfo, + 'charging_identity': SetOfIetfAttrSyntax, + 'group': SetOfIetfAttrSyntax, + 'role': SetOfRoleSyntax, + 'clearance': SetOfClearance, + 'timing_metrics': SetOfTimingMetrics, + 'timing_policy': SetOfTimingPolicy, + } + + def _values_spec(self): + return self._oid_specs.get(self['type'].native, SetOfAny) + + _spec_callbacks = { + 'values': _values_spec + } + + +class AttCertAttributes(SequenceOf): + _child_spec = AttCertAttribute + + +class AttributeCertificateInfoV2(Sequence): + _fields = [ + ('version', AttCertVersion), + ('holder', Holder), + ('issuer', AttCertIssuer), + ('signature', SignedDigestAlgorithm), + ('serial_number', Integer), + ('att_cert_validity_period', AttCertValidityPeriod), + ('attributes', AttCertAttributes), + ('issuer_unique_id', OctetBitString, {'optional': True}), + ('extensions', Extensions, {'optional': True}), + ] + + +class AttributeCertificateV2(Sequence): + # Handle the situation where a V2 cert is encoded as V1 + _bad_tag = 1 + + _fields = [ + ('ac_info', AttributeCertificateInfoV2), + ('signature_algorithm', SignedDigestAlgorithm), + ('signature', OctetBitString), + ] + + +class OtherCertificateFormat(Sequence): + _fields = [ + ('other_cert_format', ObjectIdentifier), + ('other_cert', Any), + ] + + +class CertificateChoices(Choice): + _alternatives = [ + ('certificate', Certificate), + ('extended_certificate', ExtendedCertificate, {'implicit': 0}), + ('v1_attr_cert', AttributeCertificateV1, {'implicit': 1}), + ('v2_attr_cert', AttributeCertificateV2, {'implicit': 2}), + ('other', OtherCertificateFormat, {'implicit': 3}), + ] + + def validate(self, class_, tag, contents): + """ + Ensures that the class and tag specified exist as an alternative. This + custom version fixes parsing broken encodings there a V2 attribute + # certificate is encoded as a V1 + + :param class_: + The integer class_ from the encoded value header + + :param tag: + The integer tag from the encoded value header + + :param contents: + A byte string of the contents of the value - used when the object + is explicitly tagged + + :raises: + ValueError - when value is not a valid alternative + """ + + super(CertificateChoices, self).validate(class_, tag, contents) + if self._choice == 2: + if AttCertVersion.load(Sequence.load(contents)[0].dump()).native == 'v2': + self._choice = 3 + + +class CertificateSet(SetOf): + _child_spec = CertificateChoices + + +class ContentInfo(Sequence): + _fields = [ + ('content_type', ContentType), + ('content', Any, {'explicit': 0, 'optional': True}), + ] + + _oid_pair = ('content_type', 'content') + _oid_specs = {} + + +class SetOfContentInfo(SetOf): + _child_spec = ContentInfo + + +class EncapsulatedContentInfo(Sequence): + _fields = [ + ('content_type', ContentType), + ('content', ParsableOctetString, {'explicit': 0, 'optional': True}), + ] + + _oid_pair = ('content_type', 'content') + _oid_specs = {} + + +class IssuerAndSerialNumber(Sequence): + _fields = [ + ('issuer', Name), + ('serial_number', Integer), + ] + + +class SignerIdentifier(Choice): + _alternatives = [ + ('issuer_and_serial_number', IssuerAndSerialNumber), + ('subject_key_identifier', OctetString, {'implicit': 0}), + ] + + +class DigestAlgorithms(SetOf): + _child_spec = DigestAlgorithm + + +class CertificateRevocationLists(SetOf): + _child_spec = CertificateList + + +class SCVPReqRes(Sequence): + _fields = [ + ('request', ContentInfo, {'explicit': 0, 'optional': True}), + ('response', ContentInfo), + ] + + +class OtherRevInfoFormatId(ObjectIdentifier): + _map = { + '1.3.6.1.5.5.7.16.2': 'ocsp_response', + '1.3.6.1.5.5.7.16.4': 'scvp', + } + + +class OtherRevocationInfoFormat(Sequence): + _fields = [ + ('other_rev_info_format', OtherRevInfoFormatId), + ('other_rev_info', Any), + ] + + _oid_pair = ('other_rev_info_format', 'other_rev_info') + _oid_specs = { + 'ocsp_response': OCSPResponse, + 'scvp': SCVPReqRes, + } + + +class RevocationInfoChoice(Choice): + _alternatives = [ + ('crl', CertificateList), + ('other', OtherRevocationInfoFormat, {'implicit': 1}), + ] + + +class RevocationInfoChoices(SetOf): + _child_spec = RevocationInfoChoice + + +class SignerInfo(Sequence): + _fields = [ + ('version', CMSVersion), + ('sid', SignerIdentifier), + ('digest_algorithm', DigestAlgorithm), + ('signed_attrs', CMSAttributes, {'implicit': 0, 'optional': True}), + ('signature_algorithm', SignedDigestAlgorithm), + ('signature', OctetString), + ('unsigned_attrs', CMSAttributes, {'implicit': 1, 'optional': True}), + ] + + +class SignerInfos(SetOf): + _child_spec = SignerInfo + + +class SignedData(Sequence): + _fields = [ + ('version', CMSVersion), + ('digest_algorithms', DigestAlgorithms), + ('encap_content_info', None), + ('certificates', CertificateSet, {'implicit': 0, 'optional': True}), + ('crls', RevocationInfoChoices, {'implicit': 1, 'optional': True}), + ('signer_infos', SignerInfos), + ] + + def _encap_content_info_spec(self): + # If the encap_content_info is version v1, then this could be a PKCS#7 + # structure, or a CMS structure. CMS wraps the encoded value in an + # Octet String tag. + + # If the version is greater than 1, it is definite CMS + if self['version'].native != 'v1': + return EncapsulatedContentInfo + + # Otherwise, the ContentInfo spec from PKCS#7 will be compatible with + # CMS v1 (which only allows Data, an Octet String) and PKCS#7, which + # allows Any + return ContentInfo + + _spec_callbacks = { + 'encap_content_info': _encap_content_info_spec + } + + +class OriginatorInfo(Sequence): + _fields = [ + ('certs', CertificateSet, {'implicit': 0, 'optional': True}), + ('crls', RevocationInfoChoices, {'implicit': 1, 'optional': True}), + ] + + +class RecipientIdentifier(Choice): + _alternatives = [ + ('issuer_and_serial_number', IssuerAndSerialNumber), + ('subject_key_identifier', OctetString, {'implicit': 0}), + ] + + +class KeyEncryptionAlgorithmId(ObjectIdentifier): + _map = { + '1.2.840.113549.1.1.1': 'rsaes_pkcs1v15', + '1.2.840.113549.1.1.7': 'rsaes_oaep', + '2.16.840.1.101.3.4.1.5': 'aes128_wrap', + '2.16.840.1.101.3.4.1.8': 'aes128_wrap_pad', + '2.16.840.1.101.3.4.1.25': 'aes192_wrap', + '2.16.840.1.101.3.4.1.28': 'aes192_wrap_pad', + '2.16.840.1.101.3.4.1.45': 'aes256_wrap', + '2.16.840.1.101.3.4.1.48': 'aes256_wrap_pad', + } + + _reverse_map = { + 'rsa': '1.2.840.113549.1.1.1', + 'rsaes_pkcs1v15': '1.2.840.113549.1.1.1', + 'rsaes_oaep': '1.2.840.113549.1.1.7', + 'aes128_wrap': '2.16.840.1.101.3.4.1.5', + 'aes128_wrap_pad': '2.16.840.1.101.3.4.1.8', + 'aes192_wrap': '2.16.840.1.101.3.4.1.25', + 'aes192_wrap_pad': '2.16.840.1.101.3.4.1.28', + 'aes256_wrap': '2.16.840.1.101.3.4.1.45', + 'aes256_wrap_pad': '2.16.840.1.101.3.4.1.48', + } + + +class KeyEncryptionAlgorithm(_ForceNullParameters, Sequence): + _fields = [ + ('algorithm', KeyEncryptionAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'rsaes_oaep': RSAESOAEPParams, + } + + +class KeyTransRecipientInfo(Sequence): + _fields = [ + ('version', CMSVersion), + ('rid', RecipientIdentifier), + ('key_encryption_algorithm', KeyEncryptionAlgorithm), + ('encrypted_key', OctetString), + ] + + +class OriginatorIdentifierOrKey(Choice): + _alternatives = [ + ('issuer_and_serial_number', IssuerAndSerialNumber), + ('subject_key_identifier', OctetString, {'implicit': 0}), + ('originator_key', PublicKeyInfo, {'implicit': 1}), + ] + + +class OtherKeyAttribute(Sequence): + _fields = [ + ('key_attr_id', ObjectIdentifier), + ('key_attr', Any), + ] + + +class RecipientKeyIdentifier(Sequence): + _fields = [ + ('subject_key_identifier', OctetString), + ('date', GeneralizedTime, {'optional': True}), + ('other', OtherKeyAttribute, {'optional': True}), + ] + + +class KeyAgreementRecipientIdentifier(Choice): + _alternatives = [ + ('issuer_and_serial_number', IssuerAndSerialNumber), + ('r_key_id', RecipientKeyIdentifier, {'implicit': 0}), + ] + + +class RecipientEncryptedKey(Sequence): + _fields = [ + ('rid', KeyAgreementRecipientIdentifier), + ('encrypted_key', OctetString), + ] + + +class RecipientEncryptedKeys(SequenceOf): + _child_spec = RecipientEncryptedKey + + +class KeyAgreeRecipientInfo(Sequence): + _fields = [ + ('version', CMSVersion), + ('originator', OriginatorIdentifierOrKey, {'explicit': 0}), + ('ukm', OctetString, {'explicit': 1, 'optional': True}), + ('key_encryption_algorithm', KeyEncryptionAlgorithm), + ('recipient_encrypted_keys', RecipientEncryptedKeys), + ] + + +class KEKIdentifier(Sequence): + _fields = [ + ('key_identifier', OctetString), + ('date', GeneralizedTime, {'optional': True}), + ('other', OtherKeyAttribute, {'optional': True}), + ] + + +class KEKRecipientInfo(Sequence): + _fields = [ + ('version', CMSVersion), + ('kekid', KEKIdentifier), + ('key_encryption_algorithm', KeyEncryptionAlgorithm), + ('encrypted_key', OctetString), + ] + + +class PasswordRecipientInfo(Sequence): + _fields = [ + ('version', CMSVersion), + ('key_derivation_algorithm', KdfAlgorithm, {'implicit': 0, 'optional': True}), + ('key_encryption_algorithm', KeyEncryptionAlgorithm), + ('encrypted_key', OctetString), + ] + + +class OtherRecipientInfo(Sequence): + _fields = [ + ('ori_type', ObjectIdentifier), + ('ori_value', Any), + ] + + +class RecipientInfo(Choice): + _alternatives = [ + ('ktri', KeyTransRecipientInfo), + ('kari', KeyAgreeRecipientInfo, {'implicit': 1}), + ('kekri', KEKRecipientInfo, {'implicit': 2}), + ('pwri', PasswordRecipientInfo, {'implicit': 3}), + ('ori', OtherRecipientInfo, {'implicit': 4}), + ] + + +class RecipientInfos(SetOf): + _child_spec = RecipientInfo + + +class EncryptedContentInfo(Sequence): + _fields = [ + ('content_type', ContentType), + ('content_encryption_algorithm', EncryptionAlgorithm), + ('encrypted_content', OctetString, {'implicit': 0, 'optional': True}), + ] + + +class EnvelopedData(Sequence): + _fields = [ + ('version', CMSVersion), + ('originator_info', OriginatorInfo, {'implicit': 0, 'optional': True}), + ('recipient_infos', RecipientInfos), + ('encrypted_content_info', EncryptedContentInfo), + ('unprotected_attrs', CMSAttributes, {'implicit': 1, 'optional': True}), + ] + + +class SignedAndEnvelopedData(Sequence): + _fields = [ + ('version', CMSVersion), + ('recipient_infos', RecipientInfos), + ('digest_algorithms', DigestAlgorithms), + ('encrypted_content_info', EncryptedContentInfo), + ('certificates', CertificateSet, {'implicit': 0, 'optional': True}), + ('crls', CertificateRevocationLists, {'implicit': 1, 'optional': True}), + ('signer_infos', SignerInfos), + ] + + +class DigestedData(Sequence): + _fields = [ + ('version', CMSVersion), + ('digest_algorithm', DigestAlgorithm), + ('encap_content_info', None), + ('digest', OctetString), + ] + + def _encap_content_info_spec(self): + # If the encap_content_info is version v1, then this could be a PKCS#7 + # structure, or a CMS structure. CMS wraps the encoded value in an + # Octet String tag. + + # If the version is greater than 1, it is definite CMS + if self['version'].native != 'v1': + return EncapsulatedContentInfo + + # Otherwise, the ContentInfo spec from PKCS#7 will be compatible with + # CMS v1 (which only allows Data, an Octet String) and PKCS#7, which + # allows Any + return ContentInfo + + _spec_callbacks = { + 'encap_content_info': _encap_content_info_spec + } + + +class EncryptedData(Sequence): + _fields = [ + ('version', CMSVersion), + ('encrypted_content_info', EncryptedContentInfo), + ('unprotected_attrs', CMSAttributes, {'implicit': 1, 'optional': True}), + ] + + +class AuthenticatedData(Sequence): + _fields = [ + ('version', CMSVersion), + ('originator_info', OriginatorInfo, {'implicit': 0, 'optional': True}), + ('recipient_infos', RecipientInfos), + ('mac_algorithm', HmacAlgorithm), + ('digest_algorithm', DigestAlgorithm, {'implicit': 1, 'optional': True}), + # This does not require the _spec_callbacks approach of SignedData and + # DigestedData since AuthenticatedData was not part of PKCS#7 + ('encap_content_info', EncapsulatedContentInfo), + ('auth_attrs', CMSAttributes, {'implicit': 2, 'optional': True}), + ('mac', OctetString), + ('unauth_attrs', CMSAttributes, {'implicit': 3, 'optional': True}), + ] + + +class AuthEnvelopedData(Sequence): + _fields = [ + ('version', CMSVersion), + ('originator_info', OriginatorInfo, {'implicit': 0, 'optional': True}), + ('recipient_infos', RecipientInfos), + ('auth_encrypted_content_info', EncryptedContentInfo), + ('auth_attrs', CMSAttributes, {'implicit': 1, 'optional': True}), + ('mac', OctetString), + ('unauth_attrs', CMSAttributes, {'implicit': 2, 'optional': True}), + ] + + +class CompressionAlgorithmId(ObjectIdentifier): + _map = { + '1.2.840.113549.1.9.16.3.8': 'zlib', + } + + +class CompressionAlgorithm(Sequence): + _fields = [ + ('algorithm', CompressionAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + + +class CompressedData(Sequence): + _fields = [ + ('version', CMSVersion), + ('compression_algorithm', CompressionAlgorithm), + ('encap_content_info', EncapsulatedContentInfo), + ] + + _decompressed = None + + @property + def decompressed(self): + if self._decompressed is None: + if zlib is None: + raise SystemError('The zlib module is not available') + self._decompressed = zlib.decompress(self['encap_content_info']['content'].native) + return self._decompressed + + +class RecipientKeyIdentifier(Sequence): + _fields = [ + ('subjectKeyIdentifier', OctetString), + ('date', GeneralizedTime, {'optional': True}), + ('other', OtherKeyAttribute, {'optional': True}), + ] + + +class SMIMEEncryptionKeyPreference(Choice): + _alternatives = [ + ('issuer_and_serial_number', IssuerAndSerialNumber, {'implicit': 0}), + ('recipientKeyId', RecipientKeyIdentifier, {'implicit': 1}), + ('subjectAltKeyIdentifier', PublicKeyInfo, {'implicit': 2}), + ] + + +class SMIMEEncryptionKeyPreferences(SetOf): + _child_spec = SMIMEEncryptionKeyPreference + + +class SMIMECapabilityIdentifier(Sequence): + _fields = [ + ('capability_id', EncryptionAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + + +class SMIMECapabilites(SequenceOf): + _child_spec = SMIMECapabilityIdentifier + + +class SetOfSMIMECapabilites(SetOf): + _child_spec = SMIMECapabilites + + +ContentInfo._oid_specs = { + 'data': OctetString, + 'signed_data': SignedData, + 'enveloped_data': EnvelopedData, + 'signed_and_enveloped_data': SignedAndEnvelopedData, + 'digested_data': DigestedData, + 'encrypted_data': EncryptedData, + 'authenticated_data': AuthenticatedData, + 'compressed_data': CompressedData, + 'authenticated_enveloped_data': AuthEnvelopedData, +} + + +EncapsulatedContentInfo._oid_specs = { + 'signed_data': SignedData, + 'enveloped_data': EnvelopedData, + 'signed_and_enveloped_data': SignedAndEnvelopedData, + 'digested_data': DigestedData, + 'encrypted_data': EncryptedData, + 'authenticated_data': AuthenticatedData, + 'compressed_data': CompressedData, + 'authenticated_enveloped_data': AuthEnvelopedData, +} + + +CMSAttribute._oid_specs = { + 'content_type': SetOfContentType, + 'message_digest': SetOfOctetString, + 'signing_time': SetOfTime, + 'counter_signature': SignerInfos, + 'signature_time_stamp_token': SetOfContentInfo, + 'cms_algorithm_protection': SetOfCMSAlgorithmProtection, + 'microsoft_nested_signature': SetOfContentInfo, + 'microsoft_time_stamp_token': SetOfContentInfo, + 'encrypt_key_pref': SMIMEEncryptionKeyPreferences, + 'smime_capabilities': SetOfSMIMECapabilites, +} diff --git a/tasks/lib/package_control/deps/asn1crypto/core.py b/tasks/lib/package_control/deps/asn1crypto/core.py new file mode 100644 index 0000000..364c6b5 --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/core.py @@ -0,0 +1,5676 @@ +# coding: utf-8 + +""" +ASN.1 type classes for universal types. Exports the following items: + + - load() + - Any() + - Asn1Value() + - BitString() + - BMPString() + - Boolean() + - CharacterString() + - Choice() + - EmbeddedPdv() + - Enumerated() + - GeneralizedTime() + - GeneralString() + - GraphicString() + - IA5String() + - InstanceOf() + - Integer() + - IntegerBitString() + - IntegerOctetString() + - Null() + - NumericString() + - ObjectDescriptor() + - ObjectIdentifier() + - OctetBitString() + - OctetString() + - PrintableString() + - Real() + - RelativeOid() + - Sequence() + - SequenceOf() + - Set() + - SetOf() + - TeletexString() + - UniversalString() + - UTCTime() + - UTF8String() + - VideotexString() + - VisibleString() + - VOID + - Void() + +Other type classes are defined that help compose the types listed above. +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +from datetime import datetime, timedelta +from fractions import Fraction +import binascii +import copy +import math +import re +import sys + +from . import _teletex_codec +from ._errors import unwrap +from ._ordereddict import OrderedDict +from ._types import type_name, str_cls, byte_cls, int_types, chr_cls +from .parser import _parse, _dump_header +from .util import int_to_bytes, int_from_bytes, timezone, extended_datetime, create_timezone, utc_with_dst + +if sys.version_info <= (3,): + from cStringIO import StringIO as BytesIO + + range = xrange # noqa + _PY2 = True + +else: + from io import BytesIO + + _PY2 = False + + +_teletex_codec.register() + + +CLASS_NUM_TO_NAME_MAP = { + 0: 'universal', + 1: 'application', + 2: 'context', + 3: 'private', +} + +CLASS_NAME_TO_NUM_MAP = { + 'universal': 0, + 'application': 1, + 'context': 2, + 'private': 3, + 0: 0, + 1: 1, + 2: 2, + 3: 3, +} + +METHOD_NUM_TO_NAME_MAP = { + 0: 'primitive', + 1: 'constructed', +} + + +_OID_RE = re.compile(r'^\d+(\.\d+)*$') + + +# A global tracker to ensure that _setup() is called for every class, even +# if is has been called for a parent class. This allows different _fields +# definitions for child classes. Without such a construct, the child classes +# would just see the parent class attributes and would use them. +_SETUP_CLASSES = {} + + +def load(encoded_data, strict=False): + """ + Loads a BER/DER-encoded byte string and construct a universal object based + on the tag value: + + - 1: Boolean + - 2: Integer + - 3: BitString + - 4: OctetString + - 5: Null + - 6: ObjectIdentifier + - 7: ObjectDescriptor + - 8: InstanceOf + - 9: Real + - 10: Enumerated + - 11: EmbeddedPdv + - 12: UTF8String + - 13: RelativeOid + - 16: Sequence, + - 17: Set + - 18: NumericString + - 19: PrintableString + - 20: TeletexString + - 21: VideotexString + - 22: IA5String + - 23: UTCTime + - 24: GeneralizedTime + - 25: GraphicString + - 26: VisibleString + - 27: GeneralString + - 28: UniversalString + - 29: CharacterString + - 30: BMPString + + :param encoded_data: + A byte string of BER or DER-encoded data + + :param strict: + A boolean indicating if trailing data should be forbidden - if so, a + ValueError will be raised when trailing data exists + + :raises: + ValueError - when strict is True and trailing data is present + ValueError - when the encoded value tag a tag other than listed above + ValueError - when the ASN.1 header length is longer than the data + TypeError - when encoded_data is not a byte string + + :return: + An instance of the one of the universal classes + """ + + return Asn1Value.load(encoded_data, strict=strict) + + +class Asn1Value(object): + """ + The basis of all ASN.1 values + """ + + # The integer 0 for primitive, 1 for constructed + method = None + + # An integer 0 through 3 - see CLASS_NUM_TO_NAME_MAP for value + class_ = None + + # An integer 1 or greater indicating the tag number + tag = None + + # An alternate tag allowed for this type - used for handling broken + # structures where a string value is encoded using an incorrect tag + _bad_tag = None + + # If the value has been implicitly tagged + implicit = False + + # If explicitly tagged, a tuple of 2-element tuples containing the + # class int and tag int, from innermost to outermost + explicit = None + + # The BER/DER header bytes + _header = None + + # Raw encoded value bytes not including class, method, tag, length header + contents = None + + # The BER/DER trailer bytes + _trailer = b'' + + # The native python representation of the value - this is not used by + # some classes since they utilize _bytes or _unicode + _native = None + + @classmethod + def load(cls, encoded_data, strict=False, **kwargs): + """ + Loads a BER/DER-encoded byte string using the current class as the spec + + :param encoded_data: + A byte string of BER or DER-encoded data + + :param strict: + A boolean indicating if trailing data should be forbidden - if so, a + ValueError will be raised when trailing data exists + + :return: + An instance of the current class + """ + + if not isinstance(encoded_data, byte_cls): + raise TypeError('encoded_data must be a byte string, not %s' % type_name(encoded_data)) + + spec = None + if cls.tag is not None: + spec = cls + + value, _ = _parse_build(encoded_data, spec=spec, spec_params=kwargs, strict=strict) + return value + + def __init__(self, explicit=None, implicit=None, no_explicit=False, tag_type=None, class_=None, tag=None, + optional=None, default=None, contents=None, method=None): + """ + The optional parameter is not used, but rather included so we don't + have to delete it from the parameter dictionary when passing as keyword + args + + :param explicit: + An int tag number for explicit tagging, or a 2-element tuple of + class and tag. + + :param implicit: + An int tag number for implicit tagging, or a 2-element tuple of + class and tag. + + :param no_explicit: + If explicit tagging info should be removed from this instance. + Used internally to allow contructing the underlying value that + has been wrapped in an explicit tag. + + :param tag_type: + None for normal values, or one of "implicit", "explicit" for tagged + values. Deprecated in favor of explicit and implicit params. + + :param class_: + The class for the value - defaults to "universal" if tag_type is + None, otherwise defaults to "context". Valid values include: + - "universal" + - "application" + - "context" + - "private" + Deprecated in favor of explicit and implicit params. + + :param tag: + The integer tag to override - usually this is used with tag_type or + class_. Deprecated in favor of explicit and implicit params. + + :param optional: + Dummy parameter that allows "optional" key in spec param dicts + + :param default: + The default value to use if the value is currently None + + :param contents: + A byte string of the encoded contents of the value + + :param method: + The method for the value - no default value since this is + normally set on a class. Valid values include: + - "primitive" or 0 + - "constructed" or 1 + + :raises: + ValueError - when implicit, explicit, tag_type, class_ or tag are invalid values + """ + + try: + if self.__class__ not in _SETUP_CLASSES: + cls = self.__class__ + # Allow explicit to be specified as a simple 2-element tuple + # instead of requiring the user make a nested tuple + if cls.explicit is not None and isinstance(cls.explicit[0], int_types): + cls.explicit = (cls.explicit, ) + if hasattr(cls, '_setup'): + self._setup() + _SETUP_CLASSES[cls] = True + + # Normalize tagging values + if explicit is not None: + if isinstance(explicit, int_types): + if class_ is None: + class_ = 'context' + explicit = (class_, explicit) + # Prevent both explicit and tag_type == 'explicit' + if tag_type == 'explicit': + tag_type = None + tag = None + + if implicit is not None: + if isinstance(implicit, int_types): + if class_ is None: + class_ = 'context' + implicit = (class_, implicit) + # Prevent both implicit and tag_type == 'implicit' + if tag_type == 'implicit': + tag_type = None + tag = None + + # Convert old tag_type API to explicit/implicit params + if tag_type is not None: + if class_ is None: + class_ = 'context' + if tag_type == 'explicit': + explicit = (class_, tag) + elif tag_type == 'implicit': + implicit = (class_, tag) + else: + raise ValueError(unwrap( + ''' + tag_type must be one of "implicit", "explicit", not %s + ''', + repr(tag_type) + )) + + if explicit is not None: + # Ensure we have a tuple of 2-element tuples + if len(explicit) == 2 and isinstance(explicit[1], int_types): + explicit = (explicit, ) + for class_, tag in explicit: + invalid_class = None + if isinstance(class_, int_types): + if class_ not in CLASS_NUM_TO_NAME_MAP: + invalid_class = class_ + else: + if class_ not in CLASS_NAME_TO_NUM_MAP: + invalid_class = class_ + class_ = CLASS_NAME_TO_NUM_MAP[class_] + if invalid_class is not None: + raise ValueError(unwrap( + ''' + explicit class must be one of "universal", "application", + "context", "private", not %s + ''', + repr(invalid_class) + )) + if tag is not None: + if not isinstance(tag, int_types): + raise TypeError(unwrap( + ''' + explicit tag must be an integer, not %s + ''', + type_name(tag) + )) + if self.explicit is None: + self.explicit = ((class_, tag), ) + else: + self.explicit = self.explicit + ((class_, tag), ) + + elif implicit is not None: + class_, tag = implicit + if class_ not in CLASS_NAME_TO_NUM_MAP: + raise ValueError(unwrap( + ''' + implicit class must be one of "universal", "application", + "context", "private", not %s + ''', + repr(class_) + )) + if tag is not None: + if not isinstance(tag, int_types): + raise TypeError(unwrap( + ''' + implicit tag must be an integer, not %s + ''', + type_name(tag) + )) + self.class_ = CLASS_NAME_TO_NUM_MAP[class_] + self.tag = tag + self.implicit = True + else: + if class_ is not None: + if class_ not in CLASS_NAME_TO_NUM_MAP: + raise ValueError(unwrap( + ''' + class_ must be one of "universal", "application", + "context", "private", not %s + ''', + repr(class_) + )) + self.class_ = CLASS_NAME_TO_NUM_MAP[class_] + + if self.class_ is None: + self.class_ = 0 + + if tag is not None: + self.tag = tag + + if method is not None: + if method not in set(["primitive", 0, "constructed", 1]): + raise ValueError(unwrap( + ''' + method must be one of "primitive" or "constructed", + not %s + ''', + repr(method) + )) + if method == "primitive": + method = 0 + elif method == "constructed": + method = 1 + self.method = method + + if no_explicit: + self.explicit = None + + if contents is not None: + self.contents = contents + + elif default is not None: + self.set(default) + + except (ValueError, TypeError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args + raise e + + def __str__(self): + """ + Since str is different in Python 2 and 3, this calls the appropriate + method, __unicode__() or __bytes__() + + :return: + A unicode string + """ + + if _PY2: + return self.__bytes__() + else: + return self.__unicode__() + + def __repr__(self): + """ + :return: + A unicode string + """ + + if _PY2: + return '<%s %s b%s>' % (type_name(self), id(self), repr(self.dump())) + else: + return '<%s %s %s>' % (type_name(self), id(self), repr(self.dump())) + + def __bytes__(self): + """ + A fall-back method for print() in Python 2 + + :return: + A byte string of the output of repr() + """ + + return self.__repr__().encode('utf-8') + + def __unicode__(self): + """ + A fall-back method for print() in Python 3 + + :return: + A unicode string of the output of repr() + """ + + return self.__repr__() + + def _new_instance(self): + """ + Constructs a new copy of the current object, preserving any tagging + + :return: + An Asn1Value object + """ + + new_obj = self.__class__() + new_obj.class_ = self.class_ + new_obj.tag = self.tag + new_obj.implicit = self.implicit + new_obj.explicit = self.explicit + return new_obj + + def __copy__(self): + """ + Implements the copy.copy() interface + + :return: + A new shallow copy of the current Asn1Value object + """ + + new_obj = self._new_instance() + new_obj._copy(self, copy.copy) + return new_obj + + def __deepcopy__(self, memo): + """ + Implements the copy.deepcopy() interface + + :param memo: + A dict for memoization + + :return: + A new deep copy of the current Asn1Value object + """ + + new_obj = self._new_instance() + memo[id(self)] = new_obj + new_obj._copy(self, copy.deepcopy) + return new_obj + + def copy(self): + """ + Copies the object, preserving any special tagging from it + + :return: + An Asn1Value object + """ + + return copy.deepcopy(self) + + def retag(self, tagging, tag=None): + """ + Copies the object, applying a new tagging to it + + :param tagging: + A dict containing the keys "explicit" and "implicit". Legacy + API allows a unicode string of "implicit" or "explicit". + + :param tag: + A integer tag number. Only used when tagging is a unicode string. + + :return: + An Asn1Value object + """ + + # This is required to preserve the old API + if not isinstance(tagging, dict): + tagging = {tagging: tag} + new_obj = self.__class__(explicit=tagging.get('explicit'), implicit=tagging.get('implicit')) + new_obj._copy(self, copy.deepcopy) + return new_obj + + def untag(self): + """ + Copies the object, removing any special tagging from it + + :return: + An Asn1Value object + """ + + new_obj = self.__class__() + new_obj._copy(self, copy.deepcopy) + return new_obj + + def _copy(self, other, copy_func): + """ + Copies the contents of another Asn1Value object to itself + + :param object: + Another instance of the same class + + :param copy_func: + An reference of copy.copy() or copy.deepcopy() to use when copying + lists, dicts and objects + """ + + if self.__class__ != other.__class__: + raise TypeError(unwrap( + ''' + Can not copy values from %s object to %s object + ''', + type_name(other), + type_name(self) + )) + + self.contents = other.contents + self._native = copy_func(other._native) + + def debug(self, nest_level=1): + """ + Show the binary data and parsed data in a tree structure + """ + + prefix = ' ' * nest_level + + # This interacts with Any and moves the tag, implicit, explicit, _header, + # contents, _footer to the parsed value so duplicate data isn't present + has_parsed = hasattr(self, 'parsed') + + _basic_debug(prefix, self) + if has_parsed: + self.parsed.debug(nest_level + 2) + elif hasattr(self, 'chosen'): + self.chosen.debug(nest_level + 2) + else: + if _PY2 and isinstance(self.native, byte_cls): + print('%s Native: b%s' % (prefix, repr(self.native))) + else: + print('%s Native: %s' % (prefix, self.native)) + + def dump(self, force=False): + """ + Encodes the value using DER + + :param force: + If the encoded contents already exist, clear them and regenerate + to ensure they are in DER format instead of BER format + + :return: + A byte string of the DER-encoded value + """ + + contents = self.contents + + # If the length is indefinite, force the re-encoding + if self._header is not None and self._header[-1:] == b'\x80': + force = True + + if self._header is None or force: + if isinstance(self, Constructable) and self._indefinite: + self.method = 0 + + header = _dump_header(self.class_, self.method, self.tag, self.contents) + + if self.explicit is not None: + for class_, tag in self.explicit: + header = _dump_header(class_, 1, tag, header + self.contents) + header + + self._header = header + self._trailer = b'' + + return self._header + contents + self._trailer + + +class ValueMap(): + """ + Basic functionality that allows for mapping values from ints or OIDs to + python unicode strings + """ + + # A dict from primitive value (int or OID) to unicode string. This needs + # to be defined in the source code + _map = None + + # A dict from unicode string to int/OID. This is automatically generated + # from _map the first time it is needed + _reverse_map = None + + def _setup(self): + """ + Generates _reverse_map from _map + """ + + cls = self.__class__ + if cls._map is None or cls._reverse_map is not None: + return + cls._reverse_map = {} + for key, value in cls._map.items(): + cls._reverse_map[value] = key + + +class Castable(object): + """ + A mixin to handle converting an object between different classes that + represent the same encoded value, but with different rules for converting + to and from native Python values + """ + + def cast(self, other_class): + """ + Converts the current object into an object of a different class. The + new class must use the ASN.1 encoding for the value. + + :param other_class: + The class to instantiate the new object from + + :return: + An instance of the type other_class + """ + + if other_class.tag != self.__class__.tag: + raise TypeError(unwrap( + ''' + Can not covert a value from %s object to %s object since they + use different tags: %d versus %d + ''', + type_name(other_class), + type_name(self), + other_class.tag, + self.__class__.tag + )) + + new_obj = other_class() + new_obj.class_ = self.class_ + new_obj.implicit = self.implicit + new_obj.explicit = self.explicit + new_obj._header = self._header + new_obj.contents = self.contents + new_obj._trailer = self._trailer + if isinstance(self, Constructable): + new_obj.method = self.method + new_obj._indefinite = self._indefinite + return new_obj + + +class Constructable(object): + """ + A mixin to handle string types that may be constructed from chunks + contained within an indefinite length BER-encoded container + """ + + # Instance attribute indicating if an object was indefinite + # length when parsed - affects parsing and dumping + _indefinite = False + + def _merge_chunks(self): + """ + :return: + A concatenation of the native values of the contained chunks + """ + + if not self._indefinite: + return self._as_chunk() + + pointer = 0 + contents_len = len(self.contents) + output = None + + while pointer < contents_len: + # We pass the current class as the spec so content semantics are preserved + sub_value, pointer = _parse_build(self.contents, pointer, spec=self.__class__) + if output is None: + output = sub_value._merge_chunks() + else: + output += sub_value._merge_chunks() + + if output is None: + return self._as_chunk() + + return output + + def _as_chunk(self): + """ + A method to return a chunk of data that can be combined for + constructed method values + + :return: + A native Python value that can be added together. Examples include + byte strings, unicode strings or tuples. + """ + + return self.contents + + def _setable_native(self): + """ + Returns a native value that can be round-tripped into .set(), to + result in a DER encoding. This differs from .native in that .native + is designed for the end use, and may account for the fact that the + merged value is further parsed as ASN.1, such as in the case of + ParsableOctetString() and ParsableOctetBitString(). + + :return: + A python value that is valid to pass to .set() + """ + + return self.native + + def _copy(self, other, copy_func): + """ + Copies the contents of another Constructable object to itself + + :param object: + Another instance of the same class + + :param copy_func: + An reference of copy.copy() or copy.deepcopy() to use when copying + lists, dicts and objects + """ + + super(Constructable, self)._copy(other, copy_func) + # We really don't want to dump BER encodings, so if we see an + # indefinite encoding, let's re-encode it + if other._indefinite: + self.set(other._setable_native()) + + +class Void(Asn1Value): + """ + A representation of an optional value that is not present. Has .native + property and .dump() method to be compatible with other value classes. + """ + + contents = b'' + + def __eq__(self, other): + """ + :param other: + The other Primitive to compare to + + :return: + A boolean + """ + + return other.__class__ == self.__class__ + + def __nonzero__(self): + return False + + def __len__(self): + return 0 + + def __iter__(self): + return iter(()) + + @property + def native(self): + """ + The native Python datatype representation of this value + + :return: + None + """ + + return None + + def dump(self, force=False): + """ + Encodes the value using DER + + :param force: + If the encoded contents already exist, clear them and regenerate + to ensure they are in DER format instead of BER format + + :return: + A byte string of the DER-encoded value + """ + + return b'' + + +VOID = Void() + + +class Any(Asn1Value): + """ + A value class that can contain any value, and allows for easy parsing of + the underlying encoded value using a spec. This is normally contained in + a Structure that has an ObjectIdentifier field and _oid_pair and _oid_specs + defined. + """ + + # The parsed value object + _parsed = None + + def __init__(self, value=None, **kwargs): + """ + Sets the value of the object before passing to Asn1Value.__init__() + + :param value: + An Asn1Value object that will be set as the parsed value + """ + + Asn1Value.__init__(self, **kwargs) + + try: + if value is not None: + if not isinstance(value, Asn1Value): + raise TypeError(unwrap( + ''' + value must be an instance of Asn1Value, not %s + ''', + type_name(value) + )) + + self._parsed = (value, value.__class__, None) + self.contents = value.dump() + + except (ValueError, TypeError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args + raise e + + @property + def native(self): + """ + The native Python datatype representation of this value + + :return: + The .native value from the parsed value object + """ + + if self._parsed is None: + self.parse() + + return self._parsed[0].native + + @property + def parsed(self): + """ + Returns the parsed object from .parse() + + :return: + The object returned by .parse() + """ + + if self._parsed is None: + self.parse() + + return self._parsed[0] + + def parse(self, spec=None, spec_params=None): + """ + Parses the contents generically, or using a spec with optional params + + :param spec: + A class derived from Asn1Value that defines what class_ and tag the + value should have, and the semantics of the encoded value. The + return value will be of this type. If omitted, the encoded value + will be decoded using the standard universal tag based on the + encoded tag number. + + :param spec_params: + A dict of params to pass to the spec object + + :return: + An object of the type spec, or if not present, a child of Asn1Value + """ + + if self._parsed is None or self._parsed[1:3] != (spec, spec_params): + try: + passed_params = spec_params or {} + _tag_type_to_explicit_implicit(passed_params) + if self.explicit is not None: + if 'explicit' in passed_params: + passed_params['explicit'] = self.explicit + passed_params['explicit'] + else: + passed_params['explicit'] = self.explicit + contents = self._header + self.contents + self._trailer + parsed_value, _ = _parse_build( + contents, + spec=spec, + spec_params=passed_params + ) + self._parsed = (parsed_value, spec, spec_params) + + # Once we've parsed the Any value, clear any attributes from this object + # since they are now duplicate + self.tag = None + self.explicit = None + self.implicit = False + self._header = b'' + self.contents = contents + self._trailer = b'' + + except (ValueError, TypeError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args + raise e + return self._parsed[0] + + def _copy(self, other, copy_func): + """ + Copies the contents of another Any object to itself + + :param object: + Another instance of the same class + + :param copy_func: + An reference of copy.copy() or copy.deepcopy() to use when copying + lists, dicts and objects + """ + + super(Any, self)._copy(other, copy_func) + self._parsed = copy_func(other._parsed) + + def dump(self, force=False): + """ + Encodes the value using DER + + :param force: + If the encoded contents already exist, clear them and regenerate + to ensure they are in DER format instead of BER format + + :return: + A byte string of the DER-encoded value + """ + + if self._parsed is None: + self.parse() + + return self._parsed[0].dump(force=force) + + +class Choice(Asn1Value): + """ + A class to handle when a value may be one of several options + """ + + # The index in _alternatives of the validated alternative + _choice = None + + # The name of the chosen alternative + _name = None + + # The Asn1Value object for the chosen alternative + _parsed = None + + # Choice overrides .contents to be a property so that the code expecting + # the .contents attribute will get the .contents of the chosen alternative + _contents = None + + # A list of tuples in one of the following forms. + # + # Option 1, a unicode string field name and a value class + # + # ("name", Asn1ValueClass) + # + # Option 2, same as Option 1, but with a dict of class params + # + # ("name", Asn1ValueClass, {'explicit': 5}) + _alternatives = None + + # A dict that maps tuples of (class_, tag) to an index in _alternatives + _id_map = None + + # A dict that maps alternative names to an index in _alternatives + _name_map = None + + @classmethod + def load(cls, encoded_data, strict=False, **kwargs): + """ + Loads a BER/DER-encoded byte string using the current class as the spec + + :param encoded_data: + A byte string of BER or DER encoded data + + :param strict: + A boolean indicating if trailing data should be forbidden - if so, a + ValueError will be raised when trailing data exists + + :return: + A instance of the current class + """ + + if not isinstance(encoded_data, byte_cls): + raise TypeError('encoded_data must be a byte string, not %s' % type_name(encoded_data)) + + value, _ = _parse_build(encoded_data, spec=cls, spec_params=kwargs, strict=strict) + return value + + def _setup(self): + """ + Generates _id_map from _alternatives to allow validating contents + """ + + cls = self.__class__ + cls._id_map = {} + cls._name_map = {} + for index, info in enumerate(cls._alternatives): + if len(info) < 3: + info = info + ({},) + cls._alternatives[index] = info + id_ = _build_id_tuple(info[2], info[1]) + cls._id_map[id_] = index + cls._name_map[info[0]] = index + + def __init__(self, name=None, value=None, **kwargs): + """ + Checks to ensure implicit tagging is not being used since it is + incompatible with Choice, then forwards on to Asn1Value.__init__() + + :param name: + The name of the alternative to be set - used with value. + Alternatively this may be a dict with a single key being the name + and the value being the value, or a two-element tuple of the name + and the value. + + :param value: + The alternative value to set - used with name + + :raises: + ValueError - when implicit param is passed (or legacy tag_type param is "implicit") + """ + + _tag_type_to_explicit_implicit(kwargs) + + Asn1Value.__init__(self, **kwargs) + + try: + if kwargs.get('implicit') is not None: + raise ValueError(unwrap( + ''' + The Choice type can not be implicitly tagged even if in an + implicit module - due to its nature any tagging must be + explicit + ''' + )) + + if name is not None: + if isinstance(name, dict): + if len(name) != 1: + raise ValueError(unwrap( + ''' + When passing a dict as the "name" argument to %s, + it must have a single key/value - however %d were + present + ''', + type_name(self), + len(name) + )) + name, value = list(name.items())[0] + + if isinstance(name, tuple): + if len(name) != 2: + raise ValueError(unwrap( + ''' + When passing a tuple as the "name" argument to %s, + it must have two elements, the name and value - + however %d were present + ''', + type_name(self), + len(name) + )) + value = name[1] + name = name[0] + + if name not in self._name_map: + raise ValueError(unwrap( + ''' + The name specified, "%s", is not a valid alternative + for %s + ''', + name, + type_name(self) + )) + + self._choice = self._name_map[name] + _, spec, params = self._alternatives[self._choice] + + if not isinstance(value, spec): + value = spec(value, **params) + else: + value = _fix_tagging(value, params) + self._parsed = value + + except (ValueError, TypeError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args + raise e + + @property + def contents(self): + """ + :return: + A byte string of the DER-encoded contents of the chosen alternative + """ + + if self._parsed is not None: + return self._parsed.contents + + return self._contents + + @contents.setter + def contents(self, value): + """ + :param value: + A byte string of the DER-encoded contents of the chosen alternative + """ + + self._contents = value + + @property + def name(self): + """ + :return: + A unicode string of the field name of the chosen alternative + """ + if not self._name: + self._name = self._alternatives[self._choice][0] + return self._name + + def parse(self): + """ + Parses the detected alternative + + :return: + An Asn1Value object of the chosen alternative + """ + + if self._parsed is None: + try: + _, spec, params = self._alternatives[self._choice] + self._parsed, _ = _parse_build(self._contents, spec=spec, spec_params=params) + except (ValueError, TypeError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args + raise e + return self._parsed + + @property + def chosen(self): + """ + :return: + An Asn1Value object of the chosen alternative + """ + + return self.parse() + + @property + def native(self): + """ + The native Python datatype representation of this value + + :return: + The .native value from the contained value object + """ + + return self.chosen.native + + def validate(self, class_, tag, contents): + """ + Ensures that the class and tag specified exist as an alternative + + :param class_: + The integer class_ from the encoded value header + + :param tag: + The integer tag from the encoded value header + + :param contents: + A byte string of the contents of the value - used when the object + is explicitly tagged + + :raises: + ValueError - when value is not a valid alternative + """ + + id_ = (class_, tag) + + if self.explicit is not None: + if self.explicit[-1] != id_: + raise ValueError(unwrap( + ''' + %s was explicitly tagged, but the value provided does not + match the class and tag + ''', + type_name(self) + )) + + ((class_, _, tag, _, _, _), _) = _parse(contents, len(contents)) + id_ = (class_, tag) + + if id_ in self._id_map: + self._choice = self._id_map[id_] + return + + # This means the Choice was implicitly tagged + if self.class_ is not None and self.tag is not None: + if len(self._alternatives) > 1: + raise ValueError(unwrap( + ''' + %s was implicitly tagged, but more than one alternative + exists + ''', + type_name(self) + )) + if id_ == (self.class_, self.tag): + self._choice = 0 + return + + asn1 = self._format_class_tag(class_, tag) + asn1s = [self._format_class_tag(pair[0], pair[1]) for pair in self._id_map] + + raise ValueError(unwrap( + ''' + Value %s did not match the class and tag of any of the alternatives + in %s: %s + ''', + asn1, + type_name(self), + ', '.join(asn1s) + )) + + def _format_class_tag(self, class_, tag): + """ + :return: + A unicode string of a human-friendly representation of the class and tag + """ + + return '[%s %s]' % (CLASS_NUM_TO_NAME_MAP[class_].upper(), tag) + + def _copy(self, other, copy_func): + """ + Copies the contents of another Choice object to itself + + :param object: + Another instance of the same class + + :param copy_func: + An reference of copy.copy() or copy.deepcopy() to use when copying + lists, dicts and objects + """ + + super(Choice, self)._copy(other, copy_func) + self._choice = other._choice + self._name = other._name + self._parsed = copy_func(other._parsed) + + def dump(self, force=False): + """ + Encodes the value using DER + + :param force: + If the encoded contents already exist, clear them and regenerate + to ensure they are in DER format instead of BER format + + :return: + A byte string of the DER-encoded value + """ + + # If the length is indefinite, force the re-encoding + if self._header is not None and self._header[-1:] == b'\x80': + force = True + + self._contents = self.chosen.dump(force=force) + if self._header is None or force: + self._header = b'' + if self.explicit is not None: + for class_, tag in self.explicit: + self._header = _dump_header(class_, 1, tag, self._header + self._contents) + self._header + return self._header + self._contents + + +class Concat(object): + """ + A class that contains two or more encoded child values concatentated + together. THIS IS NOT PART OF THE ASN.1 SPECIFICATION! This exists to handle + the x509.TrustedCertificate() class for OpenSSL certificates containing + extra information. + """ + + # A list of the specs of the concatenated values + _child_specs = None + + _children = None + + @classmethod + def load(cls, encoded_data, strict=False): + """ + Loads a BER/DER-encoded byte string using the current class as the spec + + :param encoded_data: + A byte string of BER or DER encoded data + + :param strict: + A boolean indicating if trailing data should be forbidden - if so, a + ValueError will be raised when trailing data exists + + :return: + A Concat object + """ + + return cls(contents=encoded_data, strict=strict) + + def __init__(self, value=None, contents=None, strict=False): + """ + :param value: + A native Python datatype to initialize the object value with + + :param contents: + A byte string of the encoded contents of the value + + :param strict: + A boolean indicating if trailing data should be forbidden - if so, a + ValueError will be raised when trailing data exists in contents + + :raises: + ValueError - when an error occurs with one of the children + TypeError - when an error occurs with one of the children + """ + + if contents is not None: + try: + contents_len = len(contents) + self._children = [] + + offset = 0 + for spec in self._child_specs: + if offset < contents_len: + child_value, offset = _parse_build(contents, pointer=offset, spec=spec) + else: + child_value = spec() + self._children.append(child_value) + + if strict and offset != contents_len: + extra_bytes = contents_len - offset + raise ValueError('Extra data - %d bytes of trailing data were provided' % extra_bytes) + + except (ValueError, TypeError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args + raise e + + if value is not None: + if self._children is None: + self._children = [None] * len(self._child_specs) + for index, data in enumerate(value): + self.__setitem__(index, data) + + def __str__(self): + """ + Since str is different in Python 2 and 3, this calls the appropriate + method, __unicode__() or __bytes__() + + :return: + A unicode string + """ + + if _PY2: + return self.__bytes__() + else: + return self.__unicode__() + + def __bytes__(self): + """ + A byte string of the DER-encoded contents + """ + + return self.dump() + + def __unicode__(self): + """ + :return: + A unicode string + """ + + return repr(self) + + def __repr__(self): + """ + :return: + A unicode string + """ + + return '<%s %s %s>' % (type_name(self), id(self), repr(self.dump())) + + def __copy__(self): + """ + Implements the copy.copy() interface + + :return: + A new shallow copy of the Concat object + """ + + new_obj = self.__class__() + new_obj._copy(self, copy.copy) + return new_obj + + def __deepcopy__(self, memo): + """ + Implements the copy.deepcopy() interface + + :param memo: + A dict for memoization + + :return: + A new deep copy of the Concat object and all child objects + """ + + new_obj = self.__class__() + memo[id(self)] = new_obj + new_obj._copy(self, copy.deepcopy) + return new_obj + + def copy(self): + """ + Copies the object + + :return: + A Concat object + """ + + return copy.deepcopy(self) + + def _copy(self, other, copy_func): + """ + Copies the contents of another Concat object to itself + + :param object: + Another instance of the same class + + :param copy_func: + An reference of copy.copy() or copy.deepcopy() to use when copying + lists, dicts and objects + """ + + if self.__class__ != other.__class__: + raise TypeError(unwrap( + ''' + Can not copy values from %s object to %s object + ''', + type_name(other), + type_name(self) + )) + + self._children = copy_func(other._children) + + def debug(self, nest_level=1): + """ + Show the binary data and parsed data in a tree structure + """ + + prefix = ' ' * nest_level + print('%s%s Object #%s' % (prefix, type_name(self), id(self))) + print('%s Children:' % (prefix,)) + for child in self._children: + child.debug(nest_level + 2) + + def dump(self, force=False): + """ + Encodes the value using DER + + :param force: + If the encoded contents already exist, clear them and regenerate + to ensure they are in DER format instead of BER format + + :return: + A byte string of the DER-encoded value + """ + + contents = b'' + for child in self._children: + contents += child.dump(force=force) + return contents + + @property + def contents(self): + """ + :return: + A byte string of the DER-encoded contents of the children + """ + + return self.dump() + + def __len__(self): + """ + :return: + Integer + """ + + return len(self._children) + + def __getitem__(self, key): + """ + Allows accessing children by index + + :param key: + An integer of the child index + + :raises: + KeyError - when an index is invalid + + :return: + The Asn1Value object of the child specified + """ + + if key > len(self._child_specs) - 1 or key < 0: + raise KeyError(unwrap( + ''' + No child is definition for position %d of %s + ''', + key, + type_name(self) + )) + + return self._children[key] + + def __setitem__(self, key, value): + """ + Allows settings children by index + + :param key: + An integer of the child index + + :param value: + An Asn1Value object to set the child to + + :raises: + KeyError - when an index is invalid + ValueError - when the value is not an instance of Asn1Value + """ + + if key > len(self._child_specs) - 1 or key < 0: + raise KeyError(unwrap( + ''' + No child is defined for position %d of %s + ''', + key, + type_name(self) + )) + + if not isinstance(value, Asn1Value): + raise ValueError(unwrap( + ''' + Value for child %s of %s is not an instance of + asn1crypto.core.Asn1Value + ''', + key, + type_name(self) + )) + + self._children[key] = value + + def __iter__(self): + """ + :return: + An iterator of child values + """ + + return iter(self._children) + + +class Primitive(Asn1Value): + """ + Sets the class_ and method attributes for primitive, universal values + """ + + class_ = 0 + + method = 0 + + def __init__(self, value=None, default=None, contents=None, **kwargs): + """ + Sets the value of the object before passing to Asn1Value.__init__() + + :param value: + A native Python datatype to initialize the object value with + + :param default: + The default value if no value is specified + + :param contents: + A byte string of the encoded contents of the value + """ + + Asn1Value.__init__(self, **kwargs) + + try: + if contents is not None: + self.contents = contents + + elif value is not None: + self.set(value) + + elif default is not None: + self.set(default) + + except (ValueError, TypeError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args + raise e + + def set(self, value): + """ + Sets the value of the object + + :param value: + A byte string + """ + + if not isinstance(value, byte_cls): + raise TypeError(unwrap( + ''' + %s value must be a byte string, not %s + ''', + type_name(self), + type_name(value) + )) + + self._native = value + self.contents = value + self._header = None + if self._trailer != b'': + self._trailer = b'' + + def dump(self, force=False): + """ + Encodes the value using DER + + :param force: + If the encoded contents already exist, clear them and regenerate + to ensure they are in DER format instead of BER format + + :return: + A byte string of the DER-encoded value + """ + + # If the length is indefinite, force the re-encoding + if self._header is not None and self._header[-1:] == b'\x80': + force = True + + if force: + native = self.native + self.contents = None + self.set(native) + + return Asn1Value.dump(self) + + def __ne__(self, other): + return not self == other + + def __eq__(self, other): + """ + :param other: + The other Primitive to compare to + + :return: + A boolean + """ + + if not isinstance(other, Primitive): + return False + + if self.contents != other.contents: + return False + + # We compare class tag numbers since object tag numbers could be + # different due to implicit or explicit tagging + if self.__class__.tag != other.__class__.tag: + return False + + if self.__class__ == other.__class__ and self.contents == other.contents: + return True + + # If the objects share a common base class that is not too low-level + # then we can compare the contents + self_bases = (set(self.__class__.__bases__) | set([self.__class__])) - set([Asn1Value, Primitive, ValueMap]) + other_bases = (set(other.__class__.__bases__) | set([other.__class__])) - set([Asn1Value, Primitive, ValueMap]) + if self_bases | other_bases: + return self.contents == other.contents + + # When tagging is going on, do the extra work of constructing new + # objects to see if the dumped representation are the same + if self.implicit or self.explicit or other.implicit or other.explicit: + return self.untag().dump() == other.untag().dump() + + return self.dump() == other.dump() + + +class AbstractString(Constructable, Primitive): + """ + A base class for all strings that have a known encoding. In general, we do + not worry ourselves with confirming that the decoded values match a specific + set of characters, only that they are decoded into a Python unicode string + """ + + # The Python encoding name to use when decoding or encoded the contents + _encoding = 'latin1' + + # Instance attribute of (possibly-merged) unicode string + _unicode = None + + def set(self, value): + """ + Sets the value of the string + + :param value: + A unicode string + """ + + if not isinstance(value, str_cls): + raise TypeError(unwrap( + ''' + %s value must be a unicode string, not %s + ''', + type_name(self), + type_name(value) + )) + + self._unicode = value + self.contents = value.encode(self._encoding) + self._header = None + if self._indefinite: + self._indefinite = False + self.method = 0 + if self._trailer != b'': + self._trailer = b'' + + def __unicode__(self): + """ + :return: + A unicode string + """ + + if self.contents is None: + return '' + if self._unicode is None: + self._unicode = self._merge_chunks().decode(self._encoding) + return self._unicode + + def _copy(self, other, copy_func): + """ + Copies the contents of another AbstractString object to itself + + :param object: + Another instance of the same class + + :param copy_func: + An reference of copy.copy() or copy.deepcopy() to use when copying + lists, dicts and objects + """ + + super(AbstractString, self)._copy(other, copy_func) + self._unicode = other._unicode + + @property + def native(self): + """ + The native Python datatype representation of this value + + :return: + A unicode string or None + """ + + if self.contents is None: + return None + + return self.__unicode__() + + +class Boolean(Primitive): + """ + Represents a boolean in both ASN.1 and Python + """ + + tag = 1 + + def set(self, value): + """ + Sets the value of the object + + :param value: + True, False or another value that works with bool() + """ + + self._native = bool(value) + self.contents = b'\x00' if not value else b'\xff' + self._header = None + if self._trailer != b'': + self._trailer = b'' + + # Python 2 + def __nonzero__(self): + """ + :return: + True or False + """ + return self.__bool__() + + def __bool__(self): + """ + :return: + True or False + """ + return self.contents != b'\x00' + + @property + def native(self): + """ + The native Python datatype representation of this value + + :return: + True, False or None + """ + + if self.contents is None: + return None + + if self._native is None: + self._native = self.__bool__() + return self._native + + +class Integer(Primitive, ValueMap): + """ + Represents an integer in both ASN.1 and Python + """ + + tag = 2 + + def set(self, value): + """ + Sets the value of the object + + :param value: + An integer, or a unicode string if _map is set + + :raises: + ValueError - when an invalid value is passed + """ + + if isinstance(value, str_cls): + if self._map is None: + raise ValueError(unwrap( + ''' + %s value is a unicode string, but no _map provided + ''', + type_name(self) + )) + + if value not in self._reverse_map: + raise ValueError(unwrap( + ''' + %s value, %s, is not present in the _map + ''', + type_name(self), + value + )) + + value = self._reverse_map[value] + + elif not isinstance(value, int_types): + raise TypeError(unwrap( + ''' + %s value must be an integer or unicode string when a name_map + is provided, not %s + ''', + type_name(self), + type_name(value) + )) + + self._native = self._map[value] if self._map and value in self._map else value + + self.contents = int_to_bytes(value, signed=True) + self._header = None + if self._trailer != b'': + self._trailer = b'' + + def __int__(self): + """ + :return: + An integer + """ + return int_from_bytes(self.contents, signed=True) + + @property + def native(self): + """ + The native Python datatype representation of this value + + :return: + An integer or None + """ + + if self.contents is None: + return None + + if self._native is None: + self._native = self.__int__() + if self._map is not None and self._native in self._map: + self._native = self._map[self._native] + return self._native + + +class _IntegerBitString(object): + """ + A mixin for IntegerBitString and BitString to parse the contents as an integer. + """ + + # Tuple of 1s and 0s; set through native + _unused_bits = () + + def _as_chunk(self): + """ + Parse the contents of a primitive BitString encoding as an integer value. + Allows reconstructing indefinite length values. + + :raises: + ValueError - when an invalid value is passed + + :return: + A list with one tuple (value, bits, unused_bits) where value is an integer + with the value of the BitString, bits is the bit count of value and + unused_bits is a tuple of 1s and 0s. + """ + + if self._indefinite: + # return an empty chunk, for cases like \x23\x80\x00\x00 + return [] + + unused_bits_len = ord(self.contents[0]) if _PY2 else self.contents[0] + value = int_from_bytes(self.contents[1:]) + bits = (len(self.contents) - 1) * 8 + + if not unused_bits_len: + return [(value, bits, ())] + + if len(self.contents) == 1: + # Disallowed by X.690 §8.6.2.3 + raise ValueError('Empty bit string has {0} unused bits'.format(unused_bits_len)) + + if unused_bits_len > 7: + # Disallowed by X.690 §8.6.2.2 + raise ValueError('Bit string has {0} unused bits'.format(unused_bits_len)) + + unused_bits = _int_to_bit_tuple(value & ((1 << unused_bits_len) - 1), unused_bits_len) + value >>= unused_bits_len + bits -= unused_bits_len + + return [(value, bits, unused_bits)] + + def _chunks_to_int(self): + """ + Combines the chunks into a single value. + + :raises: + ValueError - when an invalid value is passed + + :return: + A tuple (value, bits, unused_bits) where value is an integer with the + value of the BitString, bits is the bit count of value and unused_bits + is a tuple of 1s and 0s. + """ + + if not self._indefinite: + # Fast path + return self._as_chunk()[0] + + value = 0 + total_bits = 0 + unused_bits = () + + # X.690 §8.6.3 allows empty indefinite encodings + for chunk, bits, unused_bits in self._merge_chunks(): + if total_bits & 7: + # Disallowed by X.690 §8.6.4 + raise ValueError('Only last chunk in a bit string may have unused bits') + total_bits += bits + value = (value << bits) | chunk + + return value, total_bits, unused_bits + + def _copy(self, other, copy_func): + """ + Copies the contents of another _IntegerBitString object to itself + + :param object: + Another instance of the same class + + :param copy_func: + An reference of copy.copy() or copy.deepcopy() to use when copying + lists, dicts and objects + """ + + super(_IntegerBitString, self)._copy(other, copy_func) + self._unused_bits = other._unused_bits + + @property + def unused_bits(self): + """ + The unused bits of the bit string encoding. + + :return: + A tuple of 1s and 0s + """ + + # call native to set _unused_bits + self.native + + return self._unused_bits + + +class BitString(_IntegerBitString, Constructable, Castable, Primitive, ValueMap): + """ + Represents a bit string from ASN.1 as a Python tuple of 1s and 0s + """ + + tag = 3 + + _size = None + + def _setup(self): + """ + Generates _reverse_map from _map + """ + + ValueMap._setup(self) + + cls = self.__class__ + if cls._map is not None: + cls._size = max(self._map.keys()) + 1 + + def set(self, value): + """ + Sets the value of the object + + :param value: + An integer or a tuple of integers 0 and 1 + + :raises: + ValueError - when an invalid value is passed + """ + + if isinstance(value, set): + if self._map is None: + raise ValueError(unwrap( + ''' + %s._map has not been defined + ''', + type_name(self) + )) + + bits = [0] * self._size + self._native = value + for index in range(0, self._size): + key = self._map.get(index) + if key is None: + continue + if key in value: + bits[index] = 1 + + value = ''.join(map(str_cls, bits)) + + elif value.__class__ == tuple: + if self._map is None: + self._native = value + else: + self._native = set() + for index, bit in enumerate(value): + if bit: + name = self._map.get(index, index) + self._native.add(name) + value = ''.join(map(str_cls, value)) + + else: + raise TypeError(unwrap( + ''' + %s value must be a tuple of ones and zeros or a set of unicode + strings, not %s + ''', + type_name(self), + type_name(value) + )) + + if self._map is not None: + if len(value) > self._size: + raise ValueError(unwrap( + ''' + %s value must be at most %s bits long, specified was %s long + ''', + type_name(self), + self._size, + len(value) + )) + # A NamedBitList must have trailing zero bit truncated. See + # https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf + # section 11.2, + # https://tools.ietf.org/html/rfc5280#page-134 and + # https://www.ietf.org/mail-archive/web/pkix/current/msg10443.html + value = value.rstrip('0') + size = len(value) + + size_mod = size % 8 + extra_bits = 0 + if size_mod != 0: + extra_bits = 8 - size_mod + value += '0' * extra_bits + + size_in_bytes = int(math.ceil(size / 8)) + + if extra_bits: + extra_bits_byte = int_to_bytes(extra_bits) + else: + extra_bits_byte = b'\x00' + + if value == '': + value_bytes = b'' + else: + value_bytes = int_to_bytes(int(value, 2)) + if len(value_bytes) != size_in_bytes: + value_bytes = (b'\x00' * (size_in_bytes - len(value_bytes))) + value_bytes + + self.contents = extra_bits_byte + value_bytes + self._unused_bits = (0,) * extra_bits + self._header = None + if self._indefinite: + self._indefinite = False + self.method = 0 + if self._trailer != b'': + self._trailer = b'' + + def __getitem__(self, key): + """ + Retrieves a boolean version of one of the bits based on a name from the + _map + + :param key: + The unicode string of one of the bit names + + :raises: + ValueError - when _map is not set or the key name is invalid + + :return: + A boolean if the bit is set + """ + + is_int = isinstance(key, int_types) + if not is_int: + if not isinstance(self._map, dict): + raise ValueError(unwrap( + ''' + %s._map has not been defined + ''', + type_name(self) + )) + + if key not in self._reverse_map: + raise ValueError(unwrap( + ''' + %s._map does not contain an entry for "%s" + ''', + type_name(self), + key + )) + + if self._native is None: + self.native + + if self._map is None: + if len(self._native) >= key + 1: + return bool(self._native[key]) + return False + + if is_int: + key = self._map.get(key, key) + + return key in self._native + + def __setitem__(self, key, value): + """ + Sets one of the bits based on a name from the _map + + :param key: + The unicode string of one of the bit names + + :param value: + A boolean value + + :raises: + ValueError - when _map is not set or the key name is invalid + """ + + is_int = isinstance(key, int_types) + if not is_int: + if self._map is None: + raise ValueError(unwrap( + ''' + %s._map has not been defined + ''', + type_name(self) + )) + + if key not in self._reverse_map: + raise ValueError(unwrap( + ''' + %s._map does not contain an entry for "%s" + ''', + type_name(self), + key + )) + + if self._native is None: + self.native + + if self._map is None: + new_native = list(self._native) + max_key = len(new_native) - 1 + if key > max_key: + new_native.extend([0] * (key - max_key)) + new_native[key] = 1 if value else 0 + self._native = tuple(new_native) + + else: + if is_int: + key = self._map.get(key, key) + + if value: + if key not in self._native: + self._native.add(key) + else: + if key in self._native: + self._native.remove(key) + + self.set(self._native) + + @property + def native(self): + """ + The native Python datatype representation of this value + + :return: + If a _map is set, a set of names, or if no _map is set, a tuple of + integers 1 and 0. None if no value. + """ + + # For BitString we default the value to be all zeros + if self.contents is None: + if self._map is None: + self.set(()) + else: + self.set(set()) + + if self._native is None: + int_value, bit_count, self._unused_bits = self._chunks_to_int() + bits = _int_to_bit_tuple(int_value, bit_count) + + if self._map: + self._native = set() + for index, bit in enumerate(bits): + if bit: + name = self._map.get(index, index) + self._native.add(name) + else: + self._native = bits + return self._native + + +class OctetBitString(Constructable, Castable, Primitive): + """ + Represents a bit string in ASN.1 as a Python byte string + """ + + tag = 3 + + # Instance attribute of (possibly-merged) byte string + _bytes = None + + # Tuple of 1s and 0s; set through native + _unused_bits = () + + def set(self, value): + """ + Sets the value of the object + + :param value: + A byte string + + :raises: + ValueError - when an invalid value is passed + """ + + if not isinstance(value, byte_cls): + raise TypeError(unwrap( + ''' + %s value must be a byte string, not %s + ''', + type_name(self), + type_name(value) + )) + + self._bytes = value + # Set the unused bits to 0 + self.contents = b'\x00' + value + self._unused_bits = () + self._header = None + if self._indefinite: + self._indefinite = False + self.method = 0 + if self._trailer != b'': + self._trailer = b'' + + def __bytes__(self): + """ + :return: + A byte string + """ + + if self.contents is None: + return b'' + if self._bytes is None: + if not self._indefinite: + self._bytes, self._unused_bits = self._as_chunk()[0] + else: + chunks = self._merge_chunks() + self._unused_bits = () + for chunk in chunks: + if self._unused_bits: + # Disallowed by X.690 §8.6.4 + raise ValueError('Only last chunk in a bit string may have unused bits') + self._unused_bits = chunk[1] + self._bytes = b''.join(chunk[0] for chunk in chunks) + + return self._bytes + + def _copy(self, other, copy_func): + """ + Copies the contents of another OctetBitString object to itself + + :param object: + Another instance of the same class + + :param copy_func: + An reference of copy.copy() or copy.deepcopy() to use when copying + lists, dicts and objects + """ + + super(OctetBitString, self)._copy(other, copy_func) + self._bytes = other._bytes + self._unused_bits = other._unused_bits + + def _as_chunk(self): + """ + Allows reconstructing indefinite length values + + :raises: + ValueError - when an invalid value is passed + + :return: + List with one tuple, consisting of a byte string and an integer (unused bits) + """ + + unused_bits_len = ord(self.contents[0]) if _PY2 else self.contents[0] + if not unused_bits_len: + return [(self.contents[1:], ())] + + if len(self.contents) == 1: + # Disallowed by X.690 §8.6.2.3 + raise ValueError('Empty bit string has {0} unused bits'.format(unused_bits_len)) + + if unused_bits_len > 7: + # Disallowed by X.690 §8.6.2.2 + raise ValueError('Bit string has {0} unused bits'.format(unused_bits_len)) + + mask = (1 << unused_bits_len) - 1 + last_byte = ord(self.contents[-1]) if _PY2 else self.contents[-1] + + # zero out the unused bits in the last byte. + zeroed_byte = last_byte & ~mask + value = self.contents[1:-1] + (chr(zeroed_byte) if _PY2 else bytes((zeroed_byte,))) + + unused_bits = _int_to_bit_tuple(last_byte & mask, unused_bits_len) + + return [(value, unused_bits)] + + @property + def native(self): + """ + The native Python datatype representation of this value + + :return: + A byte string or None + """ + + if self.contents is None: + return None + + return self.__bytes__() + + @property + def unused_bits(self): + """ + The unused bits of the bit string encoding. + + :return: + A tuple of 1s and 0s + """ + + # call native to set _unused_bits + self.native + + return self._unused_bits + + +class IntegerBitString(_IntegerBitString, Constructable, Castable, Primitive): + """ + Represents a bit string in ASN.1 as a Python integer + """ + + tag = 3 + + def set(self, value): + """ + Sets the value of the object + + :param value: + An integer + + :raises: + ValueError - when an invalid value is passed + """ + + if not isinstance(value, int_types): + raise TypeError(unwrap( + ''' + %s value must be a positive integer, not %s + ''', + type_name(self), + type_name(value) + )) + + if value < 0: + raise ValueError(unwrap( + ''' + %s value must be a positive integer, not %d + ''', + type_name(self), + value + )) + + self._native = value + # Set the unused bits to 0 + self.contents = b'\x00' + int_to_bytes(value, signed=True) + self._unused_bits = () + self._header = None + if self._indefinite: + self._indefinite = False + self.method = 0 + if self._trailer != b'': + self._trailer = b'' + + @property + def native(self): + """ + The native Python datatype representation of this value + + :return: + An integer or None + """ + + if self.contents is None: + return None + + if self._native is None: + self._native, __, self._unused_bits = self._chunks_to_int() + + return self._native + + +class OctetString(Constructable, Castable, Primitive): + """ + Represents a byte string in both ASN.1 and Python + """ + + tag = 4 + + # Instance attribute of (possibly-merged) byte string + _bytes = None + + def set(self, value): + """ + Sets the value of the object + + :param value: + A byte string + """ + + if not isinstance(value, byte_cls): + raise TypeError(unwrap( + ''' + %s value must be a byte string, not %s + ''', + type_name(self), + type_name(value) + )) + + self._bytes = value + self.contents = value + self._header = None + if self._indefinite: + self._indefinite = False + self.method = 0 + if self._trailer != b'': + self._trailer = b'' + + def __bytes__(self): + """ + :return: + A byte string + """ + + if self.contents is None: + return b'' + if self._bytes is None: + self._bytes = self._merge_chunks() + return self._bytes + + def _copy(self, other, copy_func): + """ + Copies the contents of another OctetString object to itself + + :param object: + Another instance of the same class + + :param copy_func: + An reference of copy.copy() or copy.deepcopy() to use when copying + lists, dicts and objects + """ + + super(OctetString, self)._copy(other, copy_func) + self._bytes = other._bytes + + @property + def native(self): + """ + The native Python datatype representation of this value + + :return: + A byte string or None + """ + + if self.contents is None: + return None + + return self.__bytes__() + + +class IntegerOctetString(Constructable, Castable, Primitive): + """ + Represents a byte string in ASN.1 as a Python integer + """ + + tag = 4 + + # An explicit length in bytes the integer should be encoded to. This should + # generally not be used since DER defines a canonical encoding, however some + # use of this, such as when storing elliptic curve private keys, requires an + # exact number of bytes, even if the leading bytes are null. + _encoded_width = None + + def set(self, value): + """ + Sets the value of the object + + :param value: + An integer + + :raises: + ValueError - when an invalid value is passed + """ + + if not isinstance(value, int_types): + raise TypeError(unwrap( + ''' + %s value must be a positive integer, not %s + ''', + type_name(self), + type_name(value) + )) + + if value < 0: + raise ValueError(unwrap( + ''' + %s value must be a positive integer, not %d + ''', + type_name(self), + value + )) + + self._native = value + self.contents = int_to_bytes(value, signed=False, width=self._encoded_width) + self._header = None + if self._indefinite: + self._indefinite = False + self.method = 0 + if self._trailer != b'': + self._trailer = b'' + + @property + def native(self): + """ + The native Python datatype representation of this value + + :return: + An integer or None + """ + + if self.contents is None: + return None + + if self._native is None: + self._native = int_from_bytes(self._merge_chunks()) + return self._native + + def set_encoded_width(self, width): + """ + Set the explicit enoding width for the integer + + :param width: + An integer byte width to encode the integer to + """ + + self._encoded_width = width + # Make sure the encoded value is up-to-date with the proper width + if self.contents is not None and len(self.contents) != width: + self.set(self.native) + + +class ParsableOctetString(Constructable, Castable, Primitive): + + tag = 4 + + _parsed = None + + # Instance attribute of (possibly-merged) byte string + _bytes = None + + def __init__(self, value=None, parsed=None, **kwargs): + """ + Allows providing a parsed object that will be serialized to get the + byte string value + + :param value: + A native Python datatype to initialize the object value with + + :param parsed: + If value is None and this is an Asn1Value object, this will be + set as the parsed value, and the value will be obtained by calling + .dump() on this object. + """ + + set_parsed = False + if value is None and parsed is not None and isinstance(parsed, Asn1Value): + value = parsed.dump() + set_parsed = True + + Primitive.__init__(self, value=value, **kwargs) + + if set_parsed: + self._parsed = (parsed, parsed.__class__, None) + + def set(self, value): + """ + Sets the value of the object + + :param value: + A byte string + """ + + if not isinstance(value, byte_cls): + raise TypeError(unwrap( + ''' + %s value must be a byte string, not %s + ''', + type_name(self), + type_name(value) + )) + + self._bytes = value + self.contents = value + self._header = None + if self._indefinite: + self._indefinite = False + self.method = 0 + if self._trailer != b'': + self._trailer = b'' + + def parse(self, spec=None, spec_params=None): + """ + Parses the contents generically, or using a spec with optional params + + :param spec: + A class derived from Asn1Value that defines what class_ and tag the + value should have, and the semantics of the encoded value. The + return value will be of this type. If omitted, the encoded value + will be decoded using the standard universal tag based on the + encoded tag number. + + :param spec_params: + A dict of params to pass to the spec object + + :return: + An object of the type spec, or if not present, a child of Asn1Value + """ + + if self._parsed is None or self._parsed[1:3] != (spec, spec_params): + parsed_value, _ = _parse_build(self.__bytes__(), spec=spec, spec_params=spec_params) + self._parsed = (parsed_value, spec, spec_params) + return self._parsed[0] + + def __bytes__(self): + """ + :return: + A byte string + """ + + if self.contents is None: + return b'' + if self._bytes is None: + self._bytes = self._merge_chunks() + return self._bytes + + def _setable_native(self): + """ + Returns a byte string that can be passed into .set() + + :return: + A python value that is valid to pass to .set() + """ + + return self.__bytes__() + + def _copy(self, other, copy_func): + """ + Copies the contents of another ParsableOctetString object to itself + + :param object: + Another instance of the same class + + :param copy_func: + An reference of copy.copy() or copy.deepcopy() to use when copying + lists, dicts and objects + """ + + super(ParsableOctetString, self)._copy(other, copy_func) + self._bytes = other._bytes + self._parsed = copy_func(other._parsed) + + @property + def native(self): + """ + The native Python datatype representation of this value + + :return: + A byte string or None + """ + + if self.contents is None: + return None + + if self._parsed is not None: + return self._parsed[0].native + else: + return self.__bytes__() + + @property + def parsed(self): + """ + Returns the parsed object from .parse() + + :return: + The object returned by .parse() + """ + + if self._parsed is None: + self.parse() + + return self._parsed[0] + + def dump(self, force=False): + """ + Encodes the value using DER + + :param force: + If the encoded contents already exist, clear them and regenerate + to ensure they are in DER format instead of BER format + + :return: + A byte string of the DER-encoded value + """ + + # If the length is indefinite, force the re-encoding + if self._indefinite: + force = True + + if force: + if self._parsed is not None: + native = self.parsed.dump(force=force) + else: + native = self.native + self.contents = None + self.set(native) + + return Asn1Value.dump(self) + + +class ParsableOctetBitString(ParsableOctetString): + + tag = 3 + + def set(self, value): + """ + Sets the value of the object + + :param value: + A byte string + + :raises: + ValueError - when an invalid value is passed + """ + + if not isinstance(value, byte_cls): + raise TypeError(unwrap( + ''' + %s value must be a byte string, not %s + ''', + type_name(self), + type_name(value) + )) + + self._bytes = value + # Set the unused bits to 0 + self.contents = b'\x00' + value + self._header = None + if self._indefinite: + self._indefinite = False + self.method = 0 + if self._trailer != b'': + self._trailer = b'' + + def _as_chunk(self): + """ + Allows reconstructing indefinite length values + + :raises: + ValueError - when an invalid value is passed + + :return: + A byte string + """ + + unused_bits_len = ord(self.contents[0]) if _PY2 else self.contents[0] + if unused_bits_len: + raise ValueError('ParsableOctetBitString should have no unused bits') + + return self.contents[1:] + + +class Null(Primitive): + """ + Represents a null value in ASN.1 as None in Python + """ + + tag = 5 + + contents = b'' + + def set(self, value): + """ + Sets the value of the object + + :param value: + None + """ + + self.contents = b'' + + @property + def native(self): + """ + The native Python datatype representation of this value + + :return: + None + """ + + return None + + +class ObjectIdentifier(Primitive, ValueMap): + """ + Represents an object identifier in ASN.1 as a Python unicode dotted + integer string + """ + + tag = 6 + + # A unicode string of the dotted form of the object identifier + _dotted = None + + @classmethod + def map(cls, value): + """ + Converts a dotted unicode string OID into a mapped unicode string + + :param value: + A dotted unicode string OID + + :raises: + ValueError - when no _map dict has been defined on the class + TypeError - when value is not a unicode string + + :return: + A mapped unicode string + """ + + if cls._map is None: + raise ValueError(unwrap( + ''' + %s._map has not been defined + ''', + type_name(cls) + )) + + if not isinstance(value, str_cls): + raise TypeError(unwrap( + ''' + value must be a unicode string, not %s + ''', + type_name(value) + )) + + return cls._map.get(value, value) + + @classmethod + def unmap(cls, value): + """ + Converts a mapped unicode string value into a dotted unicode string OID + + :param value: + A mapped unicode string OR dotted unicode string OID + + :raises: + ValueError - when no _map dict has been defined on the class or the value can't be unmapped + TypeError - when value is not a unicode string + + :return: + A dotted unicode string OID + """ + + if cls not in _SETUP_CLASSES: + cls()._setup() + _SETUP_CLASSES[cls] = True + + if cls._map is None: + raise ValueError(unwrap( + ''' + %s._map has not been defined + ''', + type_name(cls) + )) + + if not isinstance(value, str_cls): + raise TypeError(unwrap( + ''' + value must be a unicode string, not %s + ''', + type_name(value) + )) + + if value in cls._reverse_map: + return cls._reverse_map[value] + + if not _OID_RE.match(value): + raise ValueError(unwrap( + ''' + %s._map does not contain an entry for "%s" + ''', + type_name(cls), + value + )) + + return value + + def set(self, value): + """ + Sets the value of the object + + :param value: + A unicode string. May be a dotted integer string, or if _map is + provided, one of the mapped values. + + :raises: + ValueError - when an invalid value is passed + """ + + if not isinstance(value, str_cls): + raise TypeError(unwrap( + ''' + %s value must be a unicode string, not %s + ''', + type_name(self), + type_name(value) + )) + + self._native = value + + if self._map is not None: + if value in self._reverse_map: + value = self._reverse_map[value] + + self.contents = b'' + first = None + for index, part in enumerate(value.split('.')): + part = int(part) + + # The first two parts are merged into a single byte + if index == 0: + first = part + continue + elif index == 1: + if first > 2: + raise ValueError(unwrap( + ''' + First arc must be one of 0, 1 or 2, not %s + ''', + repr(first) + )) + elif first < 2 and part >= 40: + raise ValueError(unwrap( + ''' + Second arc must be less than 40 if first arc is 0 or + 1, not %s + ''', + repr(part) + )) + part = (first * 40) + part + + encoded_part = chr_cls(0x7F & part) + part = part >> 7 + while part > 0: + encoded_part = chr_cls(0x80 | (0x7F & part)) + encoded_part + part = part >> 7 + self.contents += encoded_part + + self._header = None + if self._trailer != b'': + self._trailer = b'' + + def __unicode__(self): + """ + :return: + A unicode string + """ + + return self.dotted + + @property + def dotted(self): + """ + :return: + A unicode string of the object identifier in dotted notation, thus + ignoring any mapped value + """ + + if self._dotted is None: + output = [] + + part = 0 + for byte in self.contents: + if _PY2: + byte = ord(byte) + part = part * 128 + part += byte & 127 + # Last byte in subidentifier has the eighth bit set to 0 + if byte & 0x80 == 0: + if len(output) == 0: + if part >= 80: + output.append(str_cls(2)) + output.append(str_cls(part - 80)) + elif part >= 40: + output.append(str_cls(1)) + output.append(str_cls(part - 40)) + else: + output.append(str_cls(0)) + output.append(str_cls(part)) + else: + output.append(str_cls(part)) + part = 0 + + self._dotted = '.'.join(output) + return self._dotted + + @property + def native(self): + """ + The native Python datatype representation of this value + + :return: + A unicode string or None. If _map is not defined, the unicode string + is a string of dotted integers. If _map is defined and the dotted + string is present in the _map, the mapped value is returned. + """ + + if self.contents is None: + return None + + if self._native is None: + self._native = self.dotted + if self._map is not None and self._native in self._map: + self._native = self._map[self._native] + return self._native + + +class ObjectDescriptor(Primitive): + """ + Represents an object descriptor from ASN.1 - no Python implementation + """ + + tag = 7 + + +class InstanceOf(Primitive): + """ + Represents an instance from ASN.1 - no Python implementation + """ + + tag = 8 + + +class Real(Primitive): + """ + Represents a real number from ASN.1 - no Python implementation + """ + + tag = 9 + + +class Enumerated(Integer): + """ + Represents a enumerated list of integers from ASN.1 as a Python + unicode string + """ + + tag = 10 + + def set(self, value): + """ + Sets the value of the object + + :param value: + An integer or a unicode string from _map + + :raises: + ValueError - when an invalid value is passed + """ + + if not isinstance(value, int_types) and not isinstance(value, str_cls): + raise TypeError(unwrap( + ''' + %s value must be an integer or a unicode string, not %s + ''', + type_name(self), + type_name(value) + )) + + if isinstance(value, str_cls): + if value not in self._reverse_map: + raise ValueError(unwrap( + ''' + %s value "%s" is not a valid value + ''', + type_name(self), + value + )) + + value = self._reverse_map[value] + + elif value not in self._map: + raise ValueError(unwrap( + ''' + %s value %s is not a valid value + ''', + type_name(self), + value + )) + + Integer.set(self, value) + + @property + def native(self): + """ + The native Python datatype representation of this value + + :return: + A unicode string or None + """ + + if self.contents is None: + return None + + if self._native is None: + self._native = self._map[self.__int__()] + return self._native + + +class UTF8String(AbstractString): + """ + Represents a UTF-8 string from ASN.1 as a Python unicode string + """ + + tag = 12 + _encoding = 'utf-8' + + +class RelativeOid(ObjectIdentifier): + """ + Represents an object identifier in ASN.1 as a Python unicode dotted + integer string + """ + + tag = 13 + + +class Sequence(Asn1Value): + """ + Represents a sequence of fields from ASN.1 as a Python object with a + dict-like interface + """ + + tag = 16 + + class_ = 0 + method = 1 + + # A list of child objects, in order of _fields + children = None + + # Sequence overrides .contents to be a property so that the mutated state + # of child objects can be checked to ensure everything is up-to-date + _contents = None + + # Variable to track if the object has been mutated + _mutated = False + + # A list of tuples in one of the following forms. + # + # Option 1, a unicode string field name and a value class + # + # ("name", Asn1ValueClass) + # + # Option 2, same as Option 1, but with a dict of class params + # + # ("name", Asn1ValueClass, {'explicit': 5}) + _fields = [] + + # A dict with keys being the name of a field and the value being a unicode + # string of the method name on self to call to get the spec for that field + _spec_callbacks = None + + # A dict that maps unicode string field names to an index in _fields + _field_map = None + + # A list in the same order as _fields that has tuples in the form (class_, tag) + _field_ids = None + + # An optional 2-element tuple that defines the field names of an OID field + # and the field that the OID should be used to help decode. Works with the + # _oid_specs attribute. + _oid_pair = None + + # A dict with keys that are unicode string OID values and values that are + # Asn1Value classes to use for decoding a variable-type field. + _oid_specs = None + + # A 2-element tuple of the indexes in _fields of the OID and value fields + _oid_nums = None + + # Predetermined field specs to optimize away calls to _determine_spec() + _precomputed_specs = None + + def __init__(self, value=None, default=None, **kwargs): + """ + Allows setting field values before passing everything else along to + Asn1Value.__init__() + + :param value: + A native Python datatype to initialize the object value with + + :param default: + The default value if no value is specified + """ + + Asn1Value.__init__(self, **kwargs) + + check_existing = False + if value is None and default is not None: + check_existing = True + if self.children is None: + if self.contents is None: + check_existing = False + else: + self._parse_children() + value = default + + if value is not None: + try: + # Fields are iterated in definition order to allow things like + # OID-based specs. Otherwise sometimes the value would be processed + # before the OID field, resulting in invalid value object creation. + if self._fields: + keys = [info[0] for info in self._fields] + unused_keys = set(value.keys()) + else: + keys = value.keys() + unused_keys = set(keys) + + for key in keys: + # If we are setting defaults, but a real value has already + # been set for the field, then skip it + if check_existing: + index = self._field_map[key] + if index < len(self.children) and self.children[index] is not VOID: + if key in unused_keys: + unused_keys.remove(key) + continue + + if key in value: + self.__setitem__(key, value[key]) + unused_keys.remove(key) + + if len(unused_keys): + raise ValueError(unwrap( + ''' + One or more unknown fields was passed to the constructor + of %s: %s + ''', + type_name(self), + ', '.join(sorted(list(unused_keys))) + )) + + except (ValueError, TypeError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args + raise e + + @property + def contents(self): + """ + :return: + A byte string of the DER-encoded contents of the sequence + """ + + if self.children is None: + return self._contents + + if self._is_mutated(): + self._set_contents() + + return self._contents + + @contents.setter + def contents(self, value): + """ + :param value: + A byte string of the DER-encoded contents of the sequence + """ + + self._contents = value + + def _is_mutated(self): + """ + :return: + A boolean - if the sequence or any children (recursively) have been + mutated + """ + + mutated = self._mutated + if self.children is not None: + for child in self.children: + if isinstance(child, Sequence) or isinstance(child, SequenceOf): + mutated = mutated or child._is_mutated() + + return mutated + + def _lazy_child(self, index): + """ + Builds a child object if the child has only been parsed into a tuple so far + """ + + child = self.children[index] + if child.__class__ == tuple: + child = self.children[index] = _build(*child) + return child + + def __len__(self): + """ + :return: + Integer + """ + # We inline this check to prevent method invocation each time + if self.children is None: + self._parse_children() + + return len(self.children) + + def __getitem__(self, key): + """ + Allows accessing fields by name or index + + :param key: + A unicode string of the field name, or an integer of the field index + + :raises: + KeyError - when a field name or index is invalid + + :return: + The Asn1Value object of the field specified + """ + + # We inline this check to prevent method invocation each time + if self.children is None: + self._parse_children() + + if not isinstance(key, int_types): + if key not in self._field_map: + raise KeyError(unwrap( + ''' + No field named "%s" defined for %s + ''', + key, + type_name(self) + )) + key = self._field_map[key] + + if key >= len(self.children): + raise KeyError(unwrap( + ''' + No field numbered %s is present in this %s + ''', + key, + type_name(self) + )) + + try: + return self._lazy_child(key) + + except (ValueError, TypeError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args + raise e + + def __setitem__(self, key, value): + """ + Allows settings fields by name or index + + :param key: + A unicode string of the field name, or an integer of the field index + + :param value: + A native Python datatype to set the field value to. This method will + construct the appropriate Asn1Value object from _fields. + + :raises: + ValueError - when a field name or index is invalid + """ + + # We inline this check to prevent method invocation each time + if self.children is None: + self._parse_children() + + if not isinstance(key, int_types): + if key not in self._field_map: + raise KeyError(unwrap( + ''' + No field named "%s" defined for %s + ''', + key, + type_name(self) + )) + key = self._field_map[key] + + field_name, field_spec, value_spec, field_params, _ = self._determine_spec(key) + + new_value = self._make_value(field_name, field_spec, value_spec, field_params, value) + + invalid_value = False + if isinstance(new_value, Any): + invalid_value = new_value.parsed is None + else: + invalid_value = new_value.contents is None + + if invalid_value: + raise ValueError(unwrap( + ''' + Value for field "%s" of %s is not set + ''', + field_name, + type_name(self) + )) + + self.children[key] = new_value + + if self._native is not None: + self._native[self._fields[key][0]] = self.children[key].native + self._mutated = True + + def __delitem__(self, key): + """ + Allows deleting optional or default fields by name or index + + :param key: + A unicode string of the field name, or an integer of the field index + + :raises: + ValueError - when a field name or index is invalid, or the field is not optional or defaulted + """ + + # We inline this check to prevent method invocation each time + if self.children is None: + self._parse_children() + + if not isinstance(key, int_types): + if key not in self._field_map: + raise KeyError(unwrap( + ''' + No field named "%s" defined for %s + ''', + key, + type_name(self) + )) + key = self._field_map[key] + + name, _, params = self._fields[key] + if not params or ('default' not in params and 'optional' not in params): + raise ValueError(unwrap( + ''' + Can not delete the value for the field "%s" of %s since it is + not optional or defaulted + ''', + name, + type_name(self) + )) + + if 'optional' in params: + self.children[key] = VOID + if self._native is not None: + self._native[name] = None + else: + self.__setitem__(key, None) + self._mutated = True + + def __iter__(self): + """ + :return: + An iterator of field key names + """ + + for info in self._fields: + yield info[0] + + def _set_contents(self, force=False): + """ + Updates the .contents attribute of the value with the encoded value of + all of the child objects + + :param force: + Ensure all contents are in DER format instead of possibly using + cached BER-encoded data + """ + + if self.children is None: + self._parse_children() + + contents = BytesIO() + for index, info in enumerate(self._fields): + child = self.children[index] + if child is None: + child_dump = b'' + elif child.__class__ == tuple: + if force: + child_dump = self._lazy_child(index).dump(force=force) + else: + child_dump = child[3] + child[4] + child[5] + else: + child_dump = child.dump(force=force) + # Skip values that are the same as the default + if info[2] and 'default' in info[2]: + default_value = info[1](**info[2]) + if default_value.dump() == child_dump: + continue + contents.write(child_dump) + self._contents = contents.getvalue() + + self._header = None + if self._trailer != b'': + self._trailer = b'' + + def _setup(self): + """ + Generates _field_map, _field_ids and _oid_nums for use in parsing + """ + + cls = self.__class__ + cls._field_map = {} + cls._field_ids = [] + cls._precomputed_specs = [] + for index, field in enumerate(cls._fields): + if len(field) < 3: + field = field + ({},) + cls._fields[index] = field + cls._field_map[field[0]] = index + cls._field_ids.append(_build_id_tuple(field[2], field[1])) + + if cls._oid_pair is not None: + cls._oid_nums = (cls._field_map[cls._oid_pair[0]], cls._field_map[cls._oid_pair[1]]) + + for index, field in enumerate(cls._fields): + has_callback = cls._spec_callbacks is not None and field[0] in cls._spec_callbacks + is_mapped_oid = cls._oid_nums is not None and cls._oid_nums[1] == index + if has_callback or is_mapped_oid: + cls._precomputed_specs.append(None) + else: + cls._precomputed_specs.append((field[0], field[1], field[1], field[2], None)) + + def _determine_spec(self, index): + """ + Determine how a value for a field should be constructed + + :param index: + The field number + + :return: + A tuple containing the following elements: + - unicode string of the field name + - Asn1Value class of the field spec + - Asn1Value class of the value spec + - None or dict of params to pass to the field spec + - None or Asn1Value class indicating the value spec was derived from an OID or a spec callback + """ + + name, field_spec, field_params = self._fields[index] + value_spec = field_spec + spec_override = None + + if self._spec_callbacks is not None and name in self._spec_callbacks: + callback = self._spec_callbacks[name] + spec_override = callback(self) + if spec_override: + # Allow a spec callback to specify both the base spec and + # the override, for situations such as OctetString and parse_as + if spec_override.__class__ == tuple and len(spec_override) == 2: + field_spec, value_spec = spec_override + if value_spec is None: + value_spec = field_spec + spec_override = None + # When no field spec is specified, use a single return value as that + elif field_spec is None: + field_spec = spec_override + value_spec = field_spec + spec_override = None + else: + value_spec = spec_override + + elif self._oid_nums is not None and self._oid_nums[1] == index: + oid = self._lazy_child(self._oid_nums[0]).native + if oid in self._oid_specs: + spec_override = self._oid_specs[oid] + value_spec = spec_override + + return (name, field_spec, value_spec, field_params, spec_override) + + def _make_value(self, field_name, field_spec, value_spec, field_params, value): + """ + Contructs an appropriate Asn1Value object for a field + + :param field_name: + A unicode string of the field name + + :param field_spec: + An Asn1Value class that is the field spec + + :param value_spec: + An Asn1Value class that is the vaue spec + + :param field_params: + None or a dict of params for the field spec + + :param value: + The value to construct an Asn1Value object from + + :return: + An instance of a child class of Asn1Value + """ + + if value is None and 'optional' in field_params: + return VOID + + specs_different = field_spec != value_spec + is_any = issubclass(field_spec, Any) + + if issubclass(value_spec, Choice): + is_asn1value = isinstance(value, Asn1Value) + is_tuple = isinstance(value, tuple) and len(value) == 2 + is_dict = isinstance(value, dict) and len(value) == 1 + if not is_asn1value and not is_tuple and not is_dict: + raise ValueError(unwrap( + ''' + Can not set a native python value to %s, which has the + choice type of %s - value must be an instance of Asn1Value + ''', + field_name, + type_name(value_spec) + )) + if is_tuple or is_dict: + value = value_spec(value) + if not isinstance(value, value_spec): + wrapper = value_spec() + wrapper.validate(value.class_, value.tag, value.contents) + wrapper._parsed = value + new_value = wrapper + else: + new_value = value + + elif isinstance(value, field_spec): + new_value = value + if specs_different: + new_value.parse(value_spec) + + elif (not specs_different or is_any) and not isinstance(value, value_spec): + if (not is_any or specs_different) and isinstance(value, Asn1Value): + raise TypeError(unwrap( + ''' + %s value must be %s, not %s + ''', + field_name, + type_name(value_spec), + type_name(value) + )) + new_value = value_spec(value, **field_params) + + else: + if isinstance(value, value_spec): + new_value = value + else: + if isinstance(value, Asn1Value): + raise TypeError(unwrap( + ''' + %s value must be %s, not %s + ''', + field_name, + type_name(value_spec), + type_name(value) + )) + new_value = value_spec(value) + + # For when the field is OctetString or OctetBitString with embedded + # values we need to wrap the value in the field spec to get the + # appropriate encoded value. + if specs_different and not is_any: + wrapper = field_spec(value=new_value.dump(), **field_params) + wrapper._parsed = (new_value, new_value.__class__, None) + new_value = wrapper + + new_value = _fix_tagging(new_value, field_params) + + return new_value + + def _parse_children(self, recurse=False): + """ + Parses the contents and generates Asn1Value objects based on the + definitions from _fields. + + :param recurse: + If child objects that are Sequence or SequenceOf objects should + be recursively parsed + + :raises: + ValueError - when an error occurs parsing child objects + """ + + cls = self.__class__ + if self._contents is None: + if self._fields: + self.children = [VOID] * len(self._fields) + for index, (_, _, params) in enumerate(self._fields): + if 'default' in params: + if cls._precomputed_specs[index]: + field_name, field_spec, value_spec, field_params, _ = cls._precomputed_specs[index] + else: + field_name, field_spec, value_spec, field_params, _ = self._determine_spec(index) + self.children[index] = self._make_value(field_name, field_spec, value_spec, field_params, None) + return + + try: + self.children = [] + contents_length = len(self._contents) + child_pointer = 0 + field = 0 + field_len = len(self._fields) + parts = None + again = child_pointer < contents_length + while again: + if parts is None: + parts, child_pointer = _parse(self._contents, contents_length, pointer=child_pointer) + again = child_pointer < contents_length + + if field < field_len: + _, field_spec, value_spec, field_params, spec_override = ( + cls._precomputed_specs[field] or self._determine_spec(field)) + + # If the next value is optional or default, allow it to be absent + if field_params and ('optional' in field_params or 'default' in field_params): + if self._field_ids[field] != (parts[0], parts[2]) and field_spec != Any: + + # See if the value is a valid choice before assuming + # that we have a missing optional or default value + choice_match = False + if issubclass(field_spec, Choice): + try: + tester = field_spec(**field_params) + tester.validate(parts[0], parts[2], parts[4]) + choice_match = True + except (ValueError): + pass + + if not choice_match: + if 'optional' in field_params: + self.children.append(VOID) + else: + self.children.append(field_spec(**field_params)) + field += 1 + again = True + continue + + if field_spec is None or (spec_override and issubclass(field_spec, Any)): + field_spec = value_spec + spec_override = None + + if spec_override: + child = parts + (field_spec, field_params, value_spec) + else: + child = parts + (field_spec, field_params) + + # Handle situations where an optional or defaulted field definition is incorrect + elif field_len > 0 and field + 1 <= field_len: + missed_fields = [] + prev_field = field - 1 + while prev_field >= 0: + prev_field_info = self._fields[prev_field] + if len(prev_field_info) < 3: + break + if 'optional' in prev_field_info[2] or 'default' in prev_field_info[2]: + missed_fields.append(prev_field_info[0]) + prev_field -= 1 + plural = 's' if len(missed_fields) > 1 else '' + missed_field_names = ', '.join(missed_fields) + raise ValueError(unwrap( + ''' + Data for field %s (%s class, %s method, tag %s) does + not match the field definition%s of %s + ''', + field + 1, + CLASS_NUM_TO_NAME_MAP.get(parts[0]), + METHOD_NUM_TO_NAME_MAP.get(parts[1]), + parts[2], + plural, + missed_field_names + )) + + else: + child = parts + + if recurse: + child = _build(*child) + if isinstance(child, (Sequence, SequenceOf)): + child._parse_children(recurse=True) + + self.children.append(child) + field += 1 + parts = None + + index = len(self.children) + while index < field_len: + name, field_spec, field_params = self._fields[index] + if 'default' in field_params: + self.children.append(field_spec(**field_params)) + elif 'optional' in field_params: + self.children.append(VOID) + else: + raise ValueError(unwrap( + ''' + Field "%s" is missing from structure + ''', + name + )) + index += 1 + + except (ValueError, TypeError) as e: + self.children = None + args = e.args[1:] + e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args + raise e + + def spec(self, field_name): + """ + Determines the spec to use for the field specified. Depending on how + the spec is determined (_oid_pair or _spec_callbacks), it may be + necessary to set preceding field values before calling this. Usually + specs, if dynamic, are controlled by a preceding ObjectIdentifier + field. + + :param field_name: + A unicode string of the field name to get the spec for + + :return: + A child class of asn1crypto.core.Asn1Value that the field must be + encoded using + """ + + if not isinstance(field_name, str_cls): + raise TypeError(unwrap( + ''' + field_name must be a unicode string, not %s + ''', + type_name(field_name) + )) + + if self._fields is None: + raise ValueError(unwrap( + ''' + Unable to retrieve spec for field %s in the class %s because + _fields has not been set + ''', + repr(field_name), + type_name(self) + )) + + index = self._field_map[field_name] + info = self._determine_spec(index) + + return info[2] + + @property + def native(self): + """ + The native Python datatype representation of this value + + :return: + An OrderedDict or None. If an OrderedDict, all child values are + recursively converted to native representation also. + """ + + if self.contents is None: + return None + + if self._native is None: + if self.children is None: + self._parse_children(recurse=True) + try: + self._native = OrderedDict() + for index, child in enumerate(self.children): + if child.__class__ == tuple: + child = _build(*child) + self.children[index] = child + try: + name = self._fields[index][0] + except (IndexError): + name = str_cls(index) + self._native[name] = child.native + except (ValueError, TypeError) as e: + self._native = None + args = e.args[1:] + e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args + raise e + return self._native + + def _copy(self, other, copy_func): + """ + Copies the contents of another Sequence object to itself + + :param object: + Another instance of the same class + + :param copy_func: + An reference of copy.copy() or copy.deepcopy() to use when copying + lists, dicts and objects + """ + + super(Sequence, self)._copy(other, copy_func) + if self.children is not None: + self.children = [] + for child in other.children: + if child.__class__ == tuple: + self.children.append(child) + else: + self.children.append(child.copy()) + + def debug(self, nest_level=1): + """ + Show the binary data and parsed data in a tree structure + """ + + if self.children is None: + self._parse_children() + + prefix = ' ' * nest_level + _basic_debug(prefix, self) + for field_name in self: + child = self._lazy_child(self._field_map[field_name]) + if child is not VOID: + print('%s Field "%s"' % (prefix, field_name)) + child.debug(nest_level + 3) + + def dump(self, force=False): + """ + Encodes the value using DER + + :param force: + If the encoded contents already exist, clear them and regenerate + to ensure they are in DER format instead of BER format + + :return: + A byte string of the DER-encoded value + """ + + # If the length is indefinite, force the re-encoding + if self._header is not None and self._header[-1:] == b'\x80': + force = True + + # We can't force encoding if we don't have a spec + if force and self._fields == [] and self.__class__ is Sequence: + force = False + + if force: + self._set_contents(force=force) + + if self._fields and self.children is not None: + for index, (field_name, _, params) in enumerate(self._fields): + if self.children[index] is not VOID: + continue + if 'default' in params or 'optional' in params: + continue + raise ValueError(unwrap( + ''' + Field "%s" is missing from structure + ''', + field_name + )) + + return Asn1Value.dump(self) + + +class SequenceOf(Asn1Value): + """ + Represents a sequence (ordered) of a single type of values from ASN.1 as a + Python object with a list-like interface + """ + + tag = 16 + + class_ = 0 + method = 1 + + # A list of child objects + children = None + + # SequenceOf overrides .contents to be a property so that the mutated state + # of child objects can be checked to ensure everything is up-to-date + _contents = None + + # Variable to track if the object has been mutated + _mutated = False + + # An Asn1Value class to use when parsing children + _child_spec = None + + def __init__(self, value=None, default=None, contents=None, spec=None, **kwargs): + """ + Allows setting child objects and the _child_spec via the spec parameter + before passing everything else along to Asn1Value.__init__() + + :param value: + A native Python datatype to initialize the object value with + + :param default: + The default value if no value is specified + + :param contents: + A byte string of the encoded contents of the value + + :param spec: + A class derived from Asn1Value to use to parse children + """ + + if spec: + self._child_spec = spec + + Asn1Value.__init__(self, **kwargs) + + try: + if contents is not None: + self.contents = contents + else: + if value is None and default is not None: + value = default + + if value is not None: + for index, child in enumerate(value): + self.__setitem__(index, child) + + # Make sure a blank list is serialized + if self.contents is None: + self._set_contents() + + except (ValueError, TypeError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args + raise e + + @property + def contents(self): + """ + :return: + A byte string of the DER-encoded contents of the sequence + """ + + if self.children is None: + return self._contents + + if self._is_mutated(): + self._set_contents() + + return self._contents + + @contents.setter + def contents(self, value): + """ + :param value: + A byte string of the DER-encoded contents of the sequence + """ + + self._contents = value + + def _is_mutated(self): + """ + :return: + A boolean - if the sequence or any children (recursively) have been + mutated + """ + + mutated = self._mutated + if self.children is not None: + for child in self.children: + if isinstance(child, Sequence) or isinstance(child, SequenceOf): + mutated = mutated or child._is_mutated() + + return mutated + + def _lazy_child(self, index): + """ + Builds a child object if the child has only been parsed into a tuple so far + """ + + child = self.children[index] + if child.__class__ == tuple: + child = _build(*child) + self.children[index] = child + return child + + def _make_value(self, value): + """ + Constructs a _child_spec value from a native Python data type, or + an appropriate Asn1Value object + + :param value: + A native Python value, or some child of Asn1Value + + :return: + An object of type _child_spec + """ + + if isinstance(value, self._child_spec): + new_value = value + + elif issubclass(self._child_spec, Any): + if isinstance(value, Asn1Value): + new_value = value + else: + raise ValueError(unwrap( + ''' + Can not set a native python value to %s where the + _child_spec is Any - value must be an instance of Asn1Value + ''', + type_name(self) + )) + + elif issubclass(self._child_spec, Choice): + if not isinstance(value, Asn1Value): + raise ValueError(unwrap( + ''' + Can not set a native python value to %s where the + _child_spec is the choice type %s - value must be an + instance of Asn1Value + ''', + type_name(self), + self._child_spec.__name__ + )) + if not isinstance(value, self._child_spec): + wrapper = self._child_spec() + wrapper.validate(value.class_, value.tag, value.contents) + wrapper._parsed = value + value = wrapper + new_value = value + + else: + return self._child_spec(value=value) + + params = {} + if self._child_spec.explicit: + params['explicit'] = self._child_spec.explicit + if self._child_spec.implicit: + params['implicit'] = (self._child_spec.class_, self._child_spec.tag) + return _fix_tagging(new_value, params) + + def __len__(self): + """ + :return: + An integer + """ + # We inline this checks to prevent method invocation each time + if self.children is None: + self._parse_children() + + return len(self.children) + + def __getitem__(self, key): + """ + Allows accessing children via index + + :param key: + Integer index of child + """ + + # We inline this checks to prevent method invocation each time + if self.children is None: + self._parse_children() + + return self._lazy_child(key) + + def __setitem__(self, key, value): + """ + Allows overriding a child via index + + :param key: + Integer index of child + + :param value: + Native python datatype that will be passed to _child_spec to create + new child object + """ + + # We inline this checks to prevent method invocation each time + if self.children is None: + self._parse_children() + + new_value = self._make_value(value) + + # If adding at the end, create a space for the new value + if key == len(self.children): + self.children.append(None) + if self._native is not None: + self._native.append(None) + + self.children[key] = new_value + + if self._native is not None: + self._native[key] = self.children[key].native + + self._mutated = True + + def __delitem__(self, key): + """ + Allows removing a child via index + + :param key: + Integer index of child + """ + + # We inline this checks to prevent method invocation each time + if self.children is None: + self._parse_children() + + self.children.pop(key) + if self._native is not None: + self._native.pop(key) + + self._mutated = True + + def __iter__(self): + """ + :return: + An iter() of child objects + """ + + # We inline this checks to prevent method invocation each time + if self.children is None: + self._parse_children() + + for index in range(0, len(self.children)): + yield self._lazy_child(index) + + def __contains__(self, item): + """ + :param item: + An object of the type cls._child_spec + + :return: + A boolean if the item is contained in this SequenceOf + """ + + if item is None or item is VOID: + return False + + if not isinstance(item, self._child_spec): + raise TypeError(unwrap( + ''' + Checking membership in %s is only available for instances of + %s, not %s + ''', + type_name(self), + type_name(self._child_spec), + type_name(item) + )) + + for child in self: + if child == item: + return True + + return False + + def append(self, value): + """ + Allows adding a child to the end of the sequence + + :param value: + Native python datatype that will be passed to _child_spec to create + new child object + """ + + # We inline this checks to prevent method invocation each time + if self.children is None: + self._parse_children() + + self.children.append(self._make_value(value)) + + if self._native is not None: + self._native.append(self.children[-1].native) + + self._mutated = True + + def _set_contents(self, force=False): + """ + Encodes all child objects into the contents for this object + + :param force: + Ensure all contents are in DER format instead of possibly using + cached BER-encoded data + """ + + if self.children is None: + self._parse_children() + + contents = BytesIO() + for child in self: + contents.write(child.dump(force=force)) + self._contents = contents.getvalue() + self._header = None + if self._trailer != b'': + self._trailer = b'' + + def _parse_children(self, recurse=False): + """ + Parses the contents and generates Asn1Value objects based on the + definitions from _child_spec. + + :param recurse: + If child objects that are Sequence or SequenceOf objects should + be recursively parsed + + :raises: + ValueError - when an error occurs parsing child objects + """ + + try: + self.children = [] + if self._contents is None: + return + contents_length = len(self._contents) + child_pointer = 0 + while child_pointer < contents_length: + parts, child_pointer = _parse(self._contents, contents_length, pointer=child_pointer) + if self._child_spec: + child = parts + (self._child_spec,) + else: + child = parts + if recurse: + child = _build(*child) + if isinstance(child, (Sequence, SequenceOf)): + child._parse_children(recurse=True) + self.children.append(child) + except (ValueError, TypeError) as e: + self.children = None + args = e.args[1:] + e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args + raise e + + def spec(self): + """ + Determines the spec to use for child values. + + :return: + A child class of asn1crypto.core.Asn1Value that child values must be + encoded using + """ + + return self._child_spec + + @property + def native(self): + """ + The native Python datatype representation of this value + + :return: + A list or None. If a list, all child values are recursively + converted to native representation also. + """ + + if self.contents is None: + return None + + if self._native is None: + if self.children is None: + self._parse_children(recurse=True) + try: + self._native = [child.native for child in self] + except (ValueError, TypeError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args + raise e + return self._native + + def _copy(self, other, copy_func): + """ + Copies the contents of another SequenceOf object to itself + + :param object: + Another instance of the same class + + :param copy_func: + An reference of copy.copy() or copy.deepcopy() to use when copying + lists, dicts and objects + """ + + super(SequenceOf, self)._copy(other, copy_func) + if self.children is not None: + self.children = [] + for child in other.children: + if child.__class__ == tuple: + self.children.append(child) + else: + self.children.append(child.copy()) + + def debug(self, nest_level=1): + """ + Show the binary data and parsed data in a tree structure + """ + + if self.children is None: + self._parse_children() + + prefix = ' ' * nest_level + _basic_debug(prefix, self) + for child in self: + child.debug(nest_level + 1) + + def dump(self, force=False): + """ + Encodes the value using DER + + :param force: + If the encoded contents already exist, clear them and regenerate + to ensure they are in DER format instead of BER format + + :return: + A byte string of the DER-encoded value + """ + + # If the length is indefinite, force the re-encoding + if self._header is not None and self._header[-1:] == b'\x80': + force = True + + if force: + self._set_contents(force=force) + + return Asn1Value.dump(self) + + +class Set(Sequence): + """ + Represents a set of fields (unordered) from ASN.1 as a Python object with a + dict-like interface + """ + + method = 1 + class_ = 0 + tag = 17 + + # A dict of 2-element tuples in the form (class_, tag) as keys and integers + # as values that are the index of the field in _fields + _field_ids = None + + def _setup(self): + """ + Generates _field_map, _field_ids and _oid_nums for use in parsing + """ + + cls = self.__class__ + cls._field_map = {} + cls._field_ids = {} + cls._precomputed_specs = [] + for index, field in enumerate(cls._fields): + if len(field) < 3: + field = field + ({},) + cls._fields[index] = field + cls._field_map[field[0]] = index + cls._field_ids[_build_id_tuple(field[2], field[1])] = index + + if cls._oid_pair is not None: + cls._oid_nums = (cls._field_map[cls._oid_pair[0]], cls._field_map[cls._oid_pair[1]]) + + for index, field in enumerate(cls._fields): + has_callback = cls._spec_callbacks is not None and field[0] in cls._spec_callbacks + is_mapped_oid = cls._oid_nums is not None and cls._oid_nums[1] == index + if has_callback or is_mapped_oid: + cls._precomputed_specs.append(None) + else: + cls._precomputed_specs.append((field[0], field[1], field[1], field[2], None)) + + def _parse_children(self, recurse=False): + """ + Parses the contents and generates Asn1Value objects based on the + definitions from _fields. + + :param recurse: + If child objects that are Sequence or SequenceOf objects should + be recursively parsed + + :raises: + ValueError - when an error occurs parsing child objects + """ + + cls = self.__class__ + if self._contents is None: + if self._fields: + self.children = [VOID] * len(self._fields) + for index, (_, _, params) in enumerate(self._fields): + if 'default' in params: + if cls._precomputed_specs[index]: + field_name, field_spec, value_spec, field_params, _ = cls._precomputed_specs[index] + else: + field_name, field_spec, value_spec, field_params, _ = self._determine_spec(index) + self.children[index] = self._make_value(field_name, field_spec, value_spec, field_params, None) + return + + try: + child_map = {} + contents_length = len(self.contents) + child_pointer = 0 + seen_field = 0 + while child_pointer < contents_length: + parts, child_pointer = _parse(self.contents, contents_length, pointer=child_pointer) + + id_ = (parts[0], parts[2]) + + field = self._field_ids.get(id_) + if field is None: + raise ValueError(unwrap( + ''' + Data for field %s (%s class, %s method, tag %s) does + not match any of the field definitions + ''', + seen_field, + CLASS_NUM_TO_NAME_MAP.get(parts[0]), + METHOD_NUM_TO_NAME_MAP.get(parts[1]), + parts[2], + )) + + _, field_spec, value_spec, field_params, spec_override = ( + cls._precomputed_specs[field] or self._determine_spec(field)) + + if field_spec is None or (spec_override and issubclass(field_spec, Any)): + field_spec = value_spec + spec_override = None + + if spec_override: + child = parts + (field_spec, field_params, value_spec) + else: + child = parts + (field_spec, field_params) + + if recurse: + child = _build(*child) + if isinstance(child, (Sequence, SequenceOf)): + child._parse_children(recurse=True) + + child_map[field] = child + seen_field += 1 + + total_fields = len(self._fields) + + for index in range(0, total_fields): + if index in child_map: + continue + + name, field_spec, value_spec, field_params, spec_override = ( + cls._precomputed_specs[index] or self._determine_spec(index)) + + if field_spec is None or (spec_override and issubclass(field_spec, Any)): + field_spec = value_spec + spec_override = None + + missing = False + + if not field_params: + missing = True + elif 'optional' not in field_params and 'default' not in field_params: + missing = True + elif 'optional' in field_params: + child_map[index] = VOID + elif 'default' in field_params: + child_map[index] = field_spec(**field_params) + + if missing: + raise ValueError(unwrap( + ''' + Missing required field "%s" from %s + ''', + name, + type_name(self) + )) + + self.children = [] + for index in range(0, total_fields): + self.children.append(child_map[index]) + + except (ValueError, TypeError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args + raise e + + def _set_contents(self, force=False): + """ + Encodes all child objects into the contents for this object. + + This method is overridden because a Set needs to be encoded by + removing defaulted fields and then sorting the fields by tag. + + :param force: + Ensure all contents are in DER format instead of possibly using + cached BER-encoded data + """ + + if self.children is None: + self._parse_children() + + child_tag_encodings = [] + for index, child in enumerate(self.children): + child_encoding = child.dump(force=force) + + # Skip encoding defaulted children + name, spec, field_params = self._fields[index] + if 'default' in field_params: + if spec(**field_params).dump() == child_encoding: + continue + + child_tag_encodings.append((child.tag, child_encoding)) + child_tag_encodings.sort(key=lambda ct: ct[0]) + + self._contents = b''.join([ct[1] for ct in child_tag_encodings]) + self._header = None + if self._trailer != b'': + self._trailer = b'' + + +class SetOf(SequenceOf): + """ + Represents a set (unordered) of a single type of values from ASN.1 as a + Python object with a list-like interface + """ + + tag = 17 + + def _set_contents(self, force=False): + """ + Encodes all child objects into the contents for this object. + + This method is overridden because a SetOf needs to be encoded by + sorting the child encodings. + + :param force: + Ensure all contents are in DER format instead of possibly using + cached BER-encoded data + """ + + if self.children is None: + self._parse_children() + + child_encodings = [] + for child in self: + child_encodings.append(child.dump(force=force)) + + self._contents = b''.join(sorted(child_encodings)) + self._header = None + if self._trailer != b'': + self._trailer = b'' + + +class EmbeddedPdv(Sequence): + """ + A sequence structure + """ + + tag = 11 + + +class NumericString(AbstractString): + """ + Represents a numeric string from ASN.1 as a Python unicode string + """ + + tag = 18 + _encoding = 'latin1' + + +class PrintableString(AbstractString): + """ + Represents a printable string from ASN.1 as a Python unicode string + """ + + tag = 19 + _encoding = 'latin1' + + +class TeletexString(AbstractString): + """ + Represents a teletex string from ASN.1 as a Python unicode string + """ + + tag = 20 + _encoding = 'teletex' + + +class VideotexString(OctetString): + """ + Represents a videotex string from ASN.1 as a Python byte string + """ + + tag = 21 + + +class IA5String(AbstractString): + """ + Represents an IA5 string from ASN.1 as a Python unicode string + """ + + tag = 22 + _encoding = 'ascii' + + +class AbstractTime(AbstractString): + """ + Represents a time from ASN.1 as a Python datetime.datetime object + """ + + @property + def _parsed_time(self): + """ + The parsed datetime string. + + :raises: + ValueError - when an invalid value is passed + + :return: + A dict with the parsed values + """ + + string = str_cls(self) + + m = self._TIMESTRING_RE.match(string) + if not m: + raise ValueError(unwrap( + ''' + Error parsing %s to a %s + ''', + string, + type_name(self), + )) + + groups = m.groupdict() + + tz = None + if groups['zulu']: + tz = timezone.utc + elif groups['dsign']: + sign = 1 if groups['dsign'] == '+' else -1 + tz = create_timezone(sign * timedelta( + hours=int(groups['dhour']), + minutes=int(groups['dminute'] or 0) + )) + + if groups['fraction']: + # Compute fraction in microseconds + fract = Fraction( + int(groups['fraction']), + 10 ** len(groups['fraction']) + ) * 1000000 + + if groups['minute'] is None: + fract *= 3600 + elif groups['second'] is None: + fract *= 60 + + fract_usec = int(fract.limit_denominator(1)) + + else: + fract_usec = 0 + + return { + 'year': int(groups['year']), + 'month': int(groups['month']), + 'day': int(groups['day']), + 'hour': int(groups['hour']), + 'minute': int(groups['minute'] or 0), + 'second': int(groups['second'] or 0), + 'tzinfo': tz, + 'fraction': fract_usec, + } + + @property + def native(self): + """ + The native Python datatype representation of this value + + :return: + A datetime.datetime object, asn1crypto.util.extended_datetime object or + None. The datetime object is usually timezone aware. If it's naive, then + it's in the sender's local time; see X.680 sect. 42.3 + """ + + if self.contents is None: + return None + + if self._native is None: + parsed = self._parsed_time + + fraction = parsed.pop('fraction', 0) + + value = self._get_datetime(parsed) + + if fraction: + value += timedelta(microseconds=fraction) + + self._native = value + + return self._native + + +class UTCTime(AbstractTime): + """ + Represents a UTC time from ASN.1 as a timezone aware Python datetime.datetime object + """ + + tag = 23 + + # Regular expression for UTCTime as described in X.680 sect. 43 and ISO 8601 + _TIMESTRING_RE = re.compile(r''' + ^ + # YYMMDD + (?P\d{2}) + (?P\d{2}) + (?P\d{2}) + + # hhmm or hhmmss + (?P\d{2}) + (?P\d{2}) + (?P\d{2})? + + # Matches nothing, needed because GeneralizedTime uses this. + (?P) + + # Z or [-+]hhmm + (?: + (?PZ) + | + (?: + (?P[-+]) + (?P\d{2}) + (?P\d{2}) + ) + ) + $ + ''', re.X) + + def set(self, value): + """ + Sets the value of the object + + :param value: + A unicode string or a datetime.datetime object + + :raises: + ValueError - when an invalid value is passed + """ + + if isinstance(value, datetime): + if not value.tzinfo: + raise ValueError('Must be timezone aware') + + # Convert value to UTC. + value = value.astimezone(utc_with_dst) + + if not 1950 <= value.year <= 2049: + raise ValueError('Year of the UTCTime is not in range [1950, 2049], use GeneralizedTime instead') + + value = value.strftime('%y%m%d%H%M%SZ') + if _PY2: + value = value.decode('ascii') + + AbstractString.set(self, value) + # Set it to None and let the class take care of converting the next + # time that .native is called + self._native = None + + def _get_datetime(self, parsed): + """ + Create a datetime object from the parsed time. + + :return: + An aware datetime.datetime object + """ + + # X.680 only specifies that UTCTime is not using a century. + # So "18" could as well mean 2118 or 1318. + # X.509 and CMS specify to use UTCTime for years earlier than 2050. + # Assume that UTCTime is only used for years [1950, 2049]. + if parsed['year'] < 50: + parsed['year'] += 2000 + else: + parsed['year'] += 1900 + + return datetime(**parsed) + + +class GeneralizedTime(AbstractTime): + """ + Represents a generalized time from ASN.1 as a Python datetime.datetime + object or asn1crypto.util.extended_datetime object in UTC + """ + + tag = 24 + + # Regular expression for GeneralizedTime as described in X.680 sect. 42 and ISO 8601 + _TIMESTRING_RE = re.compile(r''' + ^ + # YYYYMMDD + (?P\d{4}) + (?P\d{2}) + (?P\d{2}) + + # hh or hhmm or hhmmss + (?P\d{2}) + (?: + (?P\d{2}) + (?P\d{2})? + )? + + # Optional fraction; [.,]dddd (one or more decimals) + # If Seconds are given, it's fractions of Seconds. + # Else if Minutes are given, it's fractions of Minutes. + # Else it's fractions of Hours. + (?: + [,.] + (?P\d+) + )? + + # Optional timezone. If left out, the time is in local time. + # Z or [-+]hh or [-+]hhmm + (?: + (?PZ) + | + (?: + (?P[-+]) + (?P\d{2}) + (?P\d{2})? + ) + )? + $ + ''', re.X) + + def set(self, value): + """ + Sets the value of the object + + :param value: + A unicode string, a datetime.datetime object or an + asn1crypto.util.extended_datetime object + + :raises: + ValueError - when an invalid value is passed + """ + + if isinstance(value, (datetime, extended_datetime)): + if not value.tzinfo: + raise ValueError('Must be timezone aware') + + # Convert value to UTC. + value = value.astimezone(utc_with_dst) + + if value.microsecond: + fraction = '.' + str(value.microsecond).zfill(6).rstrip('0') + else: + fraction = '' + + value = value.strftime('%Y%m%d%H%M%S') + fraction + 'Z' + if _PY2: + value = value.decode('ascii') + + AbstractString.set(self, value) + # Set it to None and let the class take care of converting the next + # time that .native is called + self._native = None + + def _get_datetime(self, parsed): + """ + Create a datetime object from the parsed time. + + :return: + A datetime.datetime object or asn1crypto.util.extended_datetime object. + It may or may not be aware. + """ + + if parsed['year'] == 0: + # datetime does not support year 0. Use extended_datetime instead. + return extended_datetime(**parsed) + else: + return datetime(**parsed) + + +class GraphicString(AbstractString): + """ + Represents a graphic string from ASN.1 as a Python unicode string + """ + + tag = 25 + # This is technically not correct since this type can contain any charset + _encoding = 'latin1' + + +class VisibleString(AbstractString): + """ + Represents a visible string from ASN.1 as a Python unicode string + """ + + tag = 26 + _encoding = 'latin1' + + +class GeneralString(AbstractString): + """ + Represents a general string from ASN.1 as a Python unicode string + """ + + tag = 27 + # This is technically not correct since this type can contain any charset + _encoding = 'latin1' + + +class UniversalString(AbstractString): + """ + Represents a universal string from ASN.1 as a Python unicode string + """ + + tag = 28 + _encoding = 'utf-32-be' + + +class CharacterString(AbstractString): + """ + Represents a character string from ASN.1 as a Python unicode string + """ + + tag = 29 + # This is technically not correct since this type can contain any charset + _encoding = 'latin1' + + +class BMPString(AbstractString): + """ + Represents a BMP string from ASN.1 as a Python unicode string + """ + + tag = 30 + _encoding = 'utf-16-be' + + +def _basic_debug(prefix, self): + """ + Prints out basic information about an Asn1Value object. Extracted for reuse + among different classes that customize the debug information. + + :param prefix: + A unicode string of spaces to prefix output line with + + :param self: + The object to print the debugging information about + """ + + print('%s%s Object #%s' % (prefix, type_name(self), id(self))) + if self._header: + print('%s Header: 0x%s' % (prefix, binascii.hexlify(self._header or b'').decode('utf-8'))) + + has_header = self.method is not None and self.class_ is not None and self.tag is not None + if has_header: + method_name = METHOD_NUM_TO_NAME_MAP.get(self.method) + class_name = CLASS_NUM_TO_NAME_MAP.get(self.class_) + + if self.explicit is not None: + for class_, tag in self.explicit: + print( + '%s %s tag %s (explicitly tagged)' % + ( + prefix, + CLASS_NUM_TO_NAME_MAP.get(class_), + tag + ) + ) + if has_header: + print('%s %s %s %s' % (prefix, method_name, class_name, self.tag)) + + elif self.implicit: + if has_header: + print('%s %s %s tag %s (implicitly tagged)' % (prefix, method_name, class_name, self.tag)) + + elif has_header: + print('%s %s %s tag %s' % (prefix, method_name, class_name, self.tag)) + + if self._trailer: + print('%s Trailer: 0x%s' % (prefix, binascii.hexlify(self._trailer or b'').decode('utf-8'))) + + print('%s Data: 0x%s' % (prefix, binascii.hexlify(self.contents or b'').decode('utf-8'))) + + +def _tag_type_to_explicit_implicit(params): + """ + Converts old-style "tag_type" and "tag" params to "explicit" and "implicit" + + :param params: + A dict of parameters to convert from tag_type/tag to explicit/implicit + """ + + if 'tag_type' in params: + if params['tag_type'] == 'explicit': + params['explicit'] = (params.get('class', 2), params['tag']) + elif params['tag_type'] == 'implicit': + params['implicit'] = (params.get('class', 2), params['tag']) + del params['tag_type'] + del params['tag'] + if 'class' in params: + del params['class'] + + +def _fix_tagging(value, params): + """ + Checks if a value is properly tagged based on the spec, and re/untags as + necessary + + :param value: + An Asn1Value object + + :param params: + A dict of spec params + + :return: + An Asn1Value that is properly tagged + """ + + _tag_type_to_explicit_implicit(params) + + retag = False + if 'implicit' not in params: + if value.implicit is not False: + retag = True + else: + if isinstance(params['implicit'], tuple): + class_, tag = params['implicit'] + else: + tag = params['implicit'] + class_ = 'context' + if value.implicit is False: + retag = True + elif value.class_ != CLASS_NAME_TO_NUM_MAP[class_] or value.tag != tag: + retag = True + + if params.get('explicit') != value.explicit: + retag = True + + if retag: + return value.retag(params) + return value + + +def _build_id_tuple(params, spec): + """ + Builds a 2-element tuple used to identify fields by grabbing the class_ + and tag from an Asn1Value class and the params dict being passed to it + + :param params: + A dict of params to pass to spec + + :param spec: + An Asn1Value class + + :return: + A 2-element integer tuple in the form (class_, tag) + """ + + # Handle situations where the spec is not known at setup time + if spec is None: + return (None, None) + + required_class = spec.class_ + required_tag = spec.tag + + _tag_type_to_explicit_implicit(params) + + if 'explicit' in params: + if isinstance(params['explicit'], tuple): + required_class, required_tag = params['explicit'] + else: + required_class = 2 + required_tag = params['explicit'] + elif 'implicit' in params: + if isinstance(params['implicit'], tuple): + required_class, required_tag = params['implicit'] + else: + required_class = 2 + required_tag = params['implicit'] + if required_class is not None and not isinstance(required_class, int_types): + required_class = CLASS_NAME_TO_NUM_MAP[required_class] + + required_class = params.get('class_', required_class) + required_tag = params.get('tag', required_tag) + + return (required_class, required_tag) + + +def _int_to_bit_tuple(value, bits): + """ + Format value as a tuple of 1s and 0s. + + :param value: + A non-negative integer to format + + :param bits: + Number of bits in the output + + :return: + A tuple of 1s and 0s with bits members. + """ + + if not value and not bits: + return () + + result = tuple(map(int, format(value, '0{0}b'.format(bits)))) + if len(result) != bits: + raise ValueError('Result too large: {0} > {1}'.format(len(result), bits)) + + return result + + +_UNIVERSAL_SPECS = { + 1: Boolean, + 2: Integer, + 3: BitString, + 4: OctetString, + 5: Null, + 6: ObjectIdentifier, + 7: ObjectDescriptor, + 8: InstanceOf, + 9: Real, + 10: Enumerated, + 11: EmbeddedPdv, + 12: UTF8String, + 13: RelativeOid, + 16: Sequence, + 17: Set, + 18: NumericString, + 19: PrintableString, + 20: TeletexString, + 21: VideotexString, + 22: IA5String, + 23: UTCTime, + 24: GeneralizedTime, + 25: GraphicString, + 26: VisibleString, + 27: GeneralString, + 28: UniversalString, + 29: CharacterString, + 30: BMPString +} + + +def _build(class_, method, tag, header, contents, trailer, spec=None, spec_params=None, nested_spec=None): + """ + Builds an Asn1Value object generically, or using a spec with optional params + + :param class_: + An integer representing the ASN.1 class + + :param method: + An integer representing the ASN.1 method + + :param tag: + An integer representing the ASN.1 tag + + :param header: + A byte string of the ASN.1 header (class, method, tag, length) + + :param contents: + A byte string of the ASN.1 value + + :param trailer: + A byte string of any ASN.1 trailer (only used by indefinite length encodings) + + :param spec: + A class derived from Asn1Value that defines what class_ and tag the + value should have, and the semantics of the encoded value. The + return value will be of this type. If omitted, the encoded value + will be decoded using the standard universal tag based on the + encoded tag number. + + :param spec_params: + A dict of params to pass to the spec object + + :param nested_spec: + For certain Asn1Value classes (such as OctetString and BitString), the + contents can be further parsed and interpreted as another Asn1Value. + This parameter controls the spec for that sub-parsing. + + :return: + An object of the type spec, or if not specified, a child of Asn1Value + """ + + if spec_params is not None: + _tag_type_to_explicit_implicit(spec_params) + + if header is None: + return VOID + + header_set = False + + # If an explicit specification was passed in, make sure it matches + if spec is not None: + # If there is explicit tagging and contents, we have to split + # the header and trailer off before we do the parsing + no_explicit = spec_params and 'no_explicit' in spec_params + if not no_explicit and (spec.explicit or (spec_params and 'explicit' in spec_params)): + if spec_params: + value = spec(**spec_params) + else: + value = spec() + original_explicit = value.explicit + explicit_info = reversed(original_explicit) + parsed_class = class_ + parsed_method = method + parsed_tag = tag + to_parse = contents + explicit_header = header + explicit_trailer = trailer or b'' + for expected_class, expected_tag in explicit_info: + if parsed_class != expected_class: + raise ValueError(unwrap( + ''' + Error parsing %s - explicitly-tagged class should have been + %s, but %s was found + ''', + type_name(value), + CLASS_NUM_TO_NAME_MAP.get(expected_class), + CLASS_NUM_TO_NAME_MAP.get(parsed_class, parsed_class) + )) + if parsed_method != 1: + raise ValueError(unwrap( + ''' + Error parsing %s - explicitly-tagged method should have + been %s, but %s was found + ''', + type_name(value), + METHOD_NUM_TO_NAME_MAP.get(1), + METHOD_NUM_TO_NAME_MAP.get(parsed_method, parsed_method) + )) + if parsed_tag != expected_tag: + raise ValueError(unwrap( + ''' + Error parsing %s - explicitly-tagged tag should have been + %s, but %s was found + ''', + type_name(value), + expected_tag, + parsed_tag + )) + info, _ = _parse(to_parse, len(to_parse)) + parsed_class, parsed_method, parsed_tag, parsed_header, to_parse, parsed_trailer = info + + if not isinstance(value, Choice): + explicit_header += parsed_header + explicit_trailer = parsed_trailer + explicit_trailer + + value = _build(*info, spec=spec, spec_params={'no_explicit': True}) + value._header = explicit_header + value._trailer = explicit_trailer + value.explicit = original_explicit + header_set = True + else: + if spec_params: + value = spec(contents=contents, **spec_params) + else: + value = spec(contents=contents) + + if spec is Any: + pass + + elif isinstance(value, Choice): + value.validate(class_, tag, contents) + try: + # Force parsing the Choice now + value.contents = header + value.contents + header = b'' + value.parse() + except (ValueError, TypeError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while parsing %s' % type_name(value),) + args + raise e + + else: + if class_ != value.class_: + raise ValueError(unwrap( + ''' + Error parsing %s - class should have been %s, but %s was + found + ''', + type_name(value), + CLASS_NUM_TO_NAME_MAP.get(value.class_), + CLASS_NUM_TO_NAME_MAP.get(class_, class_) + )) + if method != value.method: + # Allow parsing a primitive method as constructed if the value + # is indefinite length. This is to allow parsing BER. + ber_indef = method == 1 and value.method == 0 and trailer == b'\x00\x00' + if not ber_indef or not isinstance(value, Constructable): + raise ValueError(unwrap( + ''' + Error parsing %s - method should have been %s, but %s was found + ''', + type_name(value), + METHOD_NUM_TO_NAME_MAP.get(value.method), + METHOD_NUM_TO_NAME_MAP.get(method, method) + )) + else: + value.method = method + value._indefinite = True + if tag != value.tag: + if isinstance(value._bad_tag, tuple): + is_bad_tag = tag in value._bad_tag + else: + is_bad_tag = tag == value._bad_tag + if not is_bad_tag: + raise ValueError(unwrap( + ''' + Error parsing %s - tag should have been %s, but %s was found + ''', + type_name(value), + value.tag, + tag + )) + + # For explicitly tagged, un-speced parsings, we use a generic container + # since we will be parsing the contents and discarding the outer object + # anyway a little further on + elif spec_params and 'explicit' in spec_params: + original_value = Asn1Value(contents=contents, **spec_params) + original_explicit = original_value.explicit + + to_parse = contents + explicit_header = header + explicit_trailer = trailer or b'' + for expected_class, expected_tag in reversed(original_explicit): + info, _ = _parse(to_parse, len(to_parse)) + _, _, _, parsed_header, to_parse, parsed_trailer = info + explicit_header += parsed_header + explicit_trailer = parsed_trailer + explicit_trailer + value = _build(*info, spec=spec, spec_params={'no_explicit': True}) + value._header = header + value._header + value._trailer += trailer or b'' + value.explicit = original_explicit + header_set = True + + # If no spec was specified, allow anything and just process what + # is in the input data + else: + if tag not in _UNIVERSAL_SPECS: + raise ValueError(unwrap( + ''' + Unknown element - %s class, %s method, tag %s + ''', + CLASS_NUM_TO_NAME_MAP.get(class_), + METHOD_NUM_TO_NAME_MAP.get(method), + tag + )) + + spec = _UNIVERSAL_SPECS[tag] + + value = spec(contents=contents, class_=class_) + ber_indef = method == 1 and value.method == 0 and trailer == b'\x00\x00' + if ber_indef and isinstance(value, Constructable): + value._indefinite = True + value.method = method + + if not header_set: + value._header = header + value._trailer = trailer or b'' + + # Destroy any default value that our contents have overwritten + value._native = None + + if nested_spec: + try: + value.parse(nested_spec) + except (ValueError, TypeError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while parsing %s' % type_name(value),) + args + raise e + + return value + + +def _parse_build(encoded_data, pointer=0, spec=None, spec_params=None, strict=False): + """ + Parses a byte string generically, or using a spec with optional params + + :param encoded_data: + A byte string that contains BER-encoded data + + :param pointer: + The index in the byte string to parse from + + :param spec: + A class derived from Asn1Value that defines what class_ and tag the + value should have, and the semantics of the encoded value. The + return value will be of this type. If omitted, the encoded value + will be decoded using the standard universal tag based on the + encoded tag number. + + :param spec_params: + A dict of params to pass to the spec object + + :param strict: + A boolean indicating if trailing data should be forbidden - if so, a + ValueError will be raised when trailing data exists + + :return: + A 2-element tuple: + - 0: An object of the type spec, or if not specified, a child of Asn1Value + - 1: An integer indicating how many bytes were consumed + """ + + encoded_len = len(encoded_data) + info, new_pointer = _parse(encoded_data, encoded_len, pointer) + if strict and new_pointer != pointer + encoded_len: + extra_bytes = pointer + encoded_len - new_pointer + raise ValueError('Extra data - %d bytes of trailing data were provided' % extra_bytes) + return (_build(*info, spec=spec, spec_params=spec_params), new_pointer) diff --git a/tasks/lib/package_control/deps/asn1crypto/crl.py b/tasks/lib/package_control/deps/asn1crypto/crl.py new file mode 100644 index 0000000..84cb168 --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/crl.py @@ -0,0 +1,536 @@ +# coding: utf-8 + +""" +ASN.1 type classes for certificate revocation lists (CRL). Exports the +following items: + + - CertificateList() + +Other type classes are defined that help compose the types listed above. +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +import hashlib + +from .algos import SignedDigestAlgorithm +from .core import ( + Boolean, + Enumerated, + GeneralizedTime, + Integer, + ObjectIdentifier, + OctetBitString, + ParsableOctetString, + Sequence, + SequenceOf, +) +from .x509 import ( + AuthorityInfoAccessSyntax, + AuthorityKeyIdentifier, + CRLDistributionPoints, + DistributionPointName, + GeneralNames, + Name, + ReasonFlags, + Time, +) + + +# The structures in this file are taken from https://tools.ietf.org/html/rfc5280 + + +class Version(Integer): + _map = { + 0: 'v1', + 1: 'v2', + 2: 'v3', + } + + +class IssuingDistributionPoint(Sequence): + _fields = [ + ('distribution_point', DistributionPointName, {'explicit': 0, 'optional': True}), + ('only_contains_user_certs', Boolean, {'implicit': 1, 'default': False}), + ('only_contains_ca_certs', Boolean, {'implicit': 2, 'default': False}), + ('only_some_reasons', ReasonFlags, {'implicit': 3, 'optional': True}), + ('indirect_crl', Boolean, {'implicit': 4, 'default': False}), + ('only_contains_attribute_certs', Boolean, {'implicit': 5, 'default': False}), + ] + + +class TBSCertListExtensionId(ObjectIdentifier): + _map = { + '2.5.29.18': 'issuer_alt_name', + '2.5.29.20': 'crl_number', + '2.5.29.27': 'delta_crl_indicator', + '2.5.29.28': 'issuing_distribution_point', + '2.5.29.35': 'authority_key_identifier', + '2.5.29.46': 'freshest_crl', + '1.3.6.1.5.5.7.1.1': 'authority_information_access', + } + + +class TBSCertListExtension(Sequence): + _fields = [ + ('extn_id', TBSCertListExtensionId), + ('critical', Boolean, {'default': False}), + ('extn_value', ParsableOctetString), + ] + + _oid_pair = ('extn_id', 'extn_value') + _oid_specs = { + 'issuer_alt_name': GeneralNames, + 'crl_number': Integer, + 'delta_crl_indicator': Integer, + 'issuing_distribution_point': IssuingDistributionPoint, + 'authority_key_identifier': AuthorityKeyIdentifier, + 'freshest_crl': CRLDistributionPoints, + 'authority_information_access': AuthorityInfoAccessSyntax, + } + + +class TBSCertListExtensions(SequenceOf): + _child_spec = TBSCertListExtension + + +class CRLReason(Enumerated): + _map = { + 0: 'unspecified', + 1: 'key_compromise', + 2: 'ca_compromise', + 3: 'affiliation_changed', + 4: 'superseded', + 5: 'cessation_of_operation', + 6: 'certificate_hold', + 8: 'remove_from_crl', + 9: 'privilege_withdrawn', + 10: 'aa_compromise', + } + + @property + def human_friendly(self): + """ + :return: + A unicode string with revocation description that is suitable to + show to end-users. Starts with a lower case letter and phrased in + such a way that it makes sense after the phrase "because of" or + "due to". + """ + + return { + 'unspecified': 'an unspecified reason', + 'key_compromise': 'a compromised key', + 'ca_compromise': 'the CA being compromised', + 'affiliation_changed': 'an affiliation change', + 'superseded': 'certificate supersession', + 'cessation_of_operation': 'a cessation of operation', + 'certificate_hold': 'a certificate hold', + 'remove_from_crl': 'removal from the CRL', + 'privilege_withdrawn': 'privilege withdrawl', + 'aa_compromise': 'the AA being compromised', + }[self.native] + + +class CRLEntryExtensionId(ObjectIdentifier): + _map = { + '2.5.29.21': 'crl_reason', + '2.5.29.23': 'hold_instruction_code', + '2.5.29.24': 'invalidity_date', + '2.5.29.29': 'certificate_issuer', + } + + +class CRLEntryExtension(Sequence): + _fields = [ + ('extn_id', CRLEntryExtensionId), + ('critical', Boolean, {'default': False}), + ('extn_value', ParsableOctetString), + ] + + _oid_pair = ('extn_id', 'extn_value') + _oid_specs = { + 'crl_reason': CRLReason, + 'hold_instruction_code': ObjectIdentifier, + 'invalidity_date': GeneralizedTime, + 'certificate_issuer': GeneralNames, + } + + +class CRLEntryExtensions(SequenceOf): + _child_spec = CRLEntryExtension + + +class RevokedCertificate(Sequence): + _fields = [ + ('user_certificate', Integer), + ('revocation_date', Time), + ('crl_entry_extensions', CRLEntryExtensions, {'optional': True}), + ] + + _processed_extensions = False + _critical_extensions = None + _crl_reason_value = None + _invalidity_date_value = None + _certificate_issuer_value = None + _issuer_name = False + + def _set_extensions(self): + """ + Sets common named extensions to private attributes and creates a list + of critical extensions + """ + + self._critical_extensions = set() + + for extension in self['crl_entry_extensions']: + name = extension['extn_id'].native + attribute_name = '_%s_value' % name + if hasattr(self, attribute_name): + setattr(self, attribute_name, extension['extn_value'].parsed) + if extension['critical'].native: + self._critical_extensions.add(name) + + self._processed_extensions = True + + @property + def critical_extensions(self): + """ + Returns a set of the names (or OID if not a known extension) of the + extensions marked as critical + + :return: + A set of unicode strings + """ + + if not self._processed_extensions: + self._set_extensions() + return self._critical_extensions + + @property + def crl_reason_value(self): + """ + This extension indicates the reason that a certificate was revoked. + + :return: + None or a CRLReason object + """ + + if self._processed_extensions is False: + self._set_extensions() + return self._crl_reason_value + + @property + def invalidity_date_value(self): + """ + This extension indicates the suspected date/time the private key was + compromised or the certificate became invalid. This would usually be + before the revocation date, which is when the CA processed the + revocation. + + :return: + None or a GeneralizedTime object + """ + + if self._processed_extensions is False: + self._set_extensions() + return self._invalidity_date_value + + @property + def certificate_issuer_value(self): + """ + This extension indicates the issuer of the certificate in question, + and is used in indirect CRLs. CRL entries without this extension are + for certificates issued from the last seen issuer. + + :return: + None or an x509.GeneralNames object + """ + + if self._processed_extensions is False: + self._set_extensions() + return self._certificate_issuer_value + + @property + def issuer_name(self): + """ + :return: + None, or an asn1crypto.x509.Name object for the issuer of the cert + """ + + if self._issuer_name is False: + self._issuer_name = None + if self.certificate_issuer_value: + for general_name in self.certificate_issuer_value: + if general_name.name == 'directory_name': + self._issuer_name = general_name.chosen + break + return self._issuer_name + + +class RevokedCertificates(SequenceOf): + _child_spec = RevokedCertificate + + +class TbsCertList(Sequence): + _fields = [ + ('version', Version, {'optional': True}), + ('signature', SignedDigestAlgorithm), + ('issuer', Name), + ('this_update', Time), + ('next_update', Time, {'optional': True}), + ('revoked_certificates', RevokedCertificates, {'optional': True}), + ('crl_extensions', TBSCertListExtensions, {'explicit': 0, 'optional': True}), + ] + + +class CertificateList(Sequence): + _fields = [ + ('tbs_cert_list', TbsCertList), + ('signature_algorithm', SignedDigestAlgorithm), + ('signature', OctetBitString), + ] + + _processed_extensions = False + _critical_extensions = None + _issuer_alt_name_value = None + _crl_number_value = None + _delta_crl_indicator_value = None + _issuing_distribution_point_value = None + _authority_key_identifier_value = None + _freshest_crl_value = None + _authority_information_access_value = None + _issuer_cert_urls = None + _delta_crl_distribution_points = None + _sha1 = None + _sha256 = None + + def _set_extensions(self): + """ + Sets common named extensions to private attributes and creates a list + of critical extensions + """ + + self._critical_extensions = set() + + for extension in self['tbs_cert_list']['crl_extensions']: + name = extension['extn_id'].native + attribute_name = '_%s_value' % name + if hasattr(self, attribute_name): + setattr(self, attribute_name, extension['extn_value'].parsed) + if extension['critical'].native: + self._critical_extensions.add(name) + + self._processed_extensions = True + + @property + def critical_extensions(self): + """ + Returns a set of the names (or OID if not a known extension) of the + extensions marked as critical + + :return: + A set of unicode strings + """ + + if not self._processed_extensions: + self._set_extensions() + return self._critical_extensions + + @property + def issuer_alt_name_value(self): + """ + This extension allows associating one or more alternative names with + the issuer of the CRL. + + :return: + None or an x509.GeneralNames object + """ + + if self._processed_extensions is False: + self._set_extensions() + return self._issuer_alt_name_value + + @property + def crl_number_value(self): + """ + This extension adds a monotonically increasing number to the CRL and is + used to distinguish different versions of the CRL. + + :return: + None or an Integer object + """ + + if self._processed_extensions is False: + self._set_extensions() + return self._crl_number_value + + @property + def delta_crl_indicator_value(self): + """ + This extension indicates a CRL is a delta CRL, and contains the CRL + number of the base CRL that it is a delta from. + + :return: + None or an Integer object + """ + + if self._processed_extensions is False: + self._set_extensions() + return self._delta_crl_indicator_value + + @property + def issuing_distribution_point_value(self): + """ + This extension includes information about what types of revocations + and certificates are part of the CRL. + + :return: + None or an IssuingDistributionPoint object + """ + + if self._processed_extensions is False: + self._set_extensions() + return self._issuing_distribution_point_value + + @property + def authority_key_identifier_value(self): + """ + This extension helps in identifying the public key with which to + validate the authenticity of the CRL. + + :return: + None or an AuthorityKeyIdentifier object + """ + + if self._processed_extensions is False: + self._set_extensions() + return self._authority_key_identifier_value + + @property + def freshest_crl_value(self): + """ + This extension is used in complete CRLs to indicate where a delta CRL + may be located. + + :return: + None or a CRLDistributionPoints object + """ + + if self._processed_extensions is False: + self._set_extensions() + return self._freshest_crl_value + + @property + def authority_information_access_value(self): + """ + This extension is used to provide a URL with which to download the + certificate used to sign this CRL. + + :return: + None or an AuthorityInfoAccessSyntax object + """ + + if self._processed_extensions is False: + self._set_extensions() + return self._authority_information_access_value + + @property + def issuer(self): + """ + :return: + An asn1crypto.x509.Name object for the issuer of the CRL + """ + + return self['tbs_cert_list']['issuer'] + + @property + def authority_key_identifier(self): + """ + :return: + None or a byte string of the key_identifier from the authority key + identifier extension + """ + + if not self.authority_key_identifier_value: + return None + + return self.authority_key_identifier_value['key_identifier'].native + + @property + def issuer_cert_urls(self): + """ + :return: + A list of unicode strings that are URLs that should contain either + an individual DER-encoded X.509 certificate, or a DER-encoded CMS + message containing multiple certificates + """ + + if self._issuer_cert_urls is None: + self._issuer_cert_urls = [] + if self.authority_information_access_value: + for entry in self.authority_information_access_value: + if entry['access_method'].native == 'ca_issuers': + location = entry['access_location'] + if location.name != 'uniform_resource_identifier': + continue + url = location.native + if url.lower()[0:7] == 'http://': + self._issuer_cert_urls.append(url) + return self._issuer_cert_urls + + @property + def delta_crl_distribution_points(self): + """ + Returns delta CRL URLs - only applies to complete CRLs + + :return: + A list of zero or more DistributionPoint objects + """ + + if self._delta_crl_distribution_points is None: + self._delta_crl_distribution_points = [] + + if self.freshest_crl_value is not None: + for distribution_point in self.freshest_crl_value: + distribution_point_name = distribution_point['distribution_point'] + # RFC 5280 indicates conforming CA should not use the relative form + if distribution_point_name.name == 'name_relative_to_crl_issuer': + continue + # This library is currently only concerned with HTTP-based CRLs + for general_name in distribution_point_name.chosen: + if general_name.name == 'uniform_resource_identifier': + self._delta_crl_distribution_points.append(distribution_point) + + return self._delta_crl_distribution_points + + @property + def signature(self): + """ + :return: + A byte string of the signature + """ + + return self['signature'].native + + @property + def sha1(self): + """ + :return: + The SHA1 hash of the DER-encoded bytes of this certificate list + """ + + if self._sha1 is None: + self._sha1 = hashlib.sha1(self.dump()).digest() + return self._sha1 + + @property + def sha256(self): + """ + :return: + The SHA-256 hash of the DER-encoded bytes of this certificate list + """ + + if self._sha256 is None: + self._sha256 = hashlib.sha256(self.dump()).digest() + return self._sha256 diff --git a/tasks/lib/package_control/deps/asn1crypto/csr.py b/tasks/lib/package_control/deps/asn1crypto/csr.py new file mode 100644 index 0000000..7d5ba44 --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/csr.py @@ -0,0 +1,133 @@ +# coding: utf-8 + +""" +ASN.1 type classes for certificate signing requests (CSR). Exports the +following items: + + - CertificationRequest() + +Other type classes are defined that help compose the types listed above. +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +from .algos import SignedDigestAlgorithm +from .core import ( + Any, + BitString, + BMPString, + Integer, + ObjectIdentifier, + OctetBitString, + Sequence, + SetOf, + UTF8String +) +from .keys import PublicKeyInfo +from .x509 import DirectoryString, Extensions, Name + + +# The structures in this file are taken from https://tools.ietf.org/html/rfc2986 +# and https://tools.ietf.org/html/rfc2985 + + +class Version(Integer): + _map = { + 0: 'v1', + } + + +class CSRAttributeType(ObjectIdentifier): + _map = { + '1.2.840.113549.1.9.7': 'challenge_password', + '1.2.840.113549.1.9.9': 'extended_certificate_attributes', + '1.2.840.113549.1.9.14': 'extension_request', + # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wcce/a5eaae36-e9f3-4dc5-a687-bfa7115954f1 + '1.3.6.1.4.1.311.13.2.2': 'microsoft_enrollment_csp_provider', + # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wcce/7c677cba-030d-48be-ba2b-01e407705f34 + '1.3.6.1.4.1.311.13.2.3': 'microsoft_os_version', + # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wcce/64e5ff6d-c6dd-4578-92f7-b3d895f9b9c7 + '1.3.6.1.4.1.311.21.20': 'microsoft_request_client_info', + } + + +class SetOfDirectoryString(SetOf): + _child_spec = DirectoryString + + +class Attribute(Sequence): + _fields = [ + ('type', ObjectIdentifier), + ('values', SetOf, {'spec': Any}), + ] + + +class SetOfAttributes(SetOf): + _child_spec = Attribute + + +class SetOfExtensions(SetOf): + _child_spec = Extensions + + +class MicrosoftEnrollmentCSProvider(Sequence): + _fields = [ + ('keyspec', Integer), + ('cspname', BMPString), # cryptographic service provider name + ('signature', BitString), + ] + + +class SetOfMicrosoftEnrollmentCSProvider(SetOf): + _child_spec = MicrosoftEnrollmentCSProvider + + +class MicrosoftRequestClientInfo(Sequence): + _fields = [ + ('clientid', Integer), + ('machinename', UTF8String), + ('username', UTF8String), + ('processname', UTF8String), + ] + + +class SetOfMicrosoftRequestClientInfo(SetOf): + _child_spec = MicrosoftRequestClientInfo + + +class CRIAttribute(Sequence): + _fields = [ + ('type', CSRAttributeType), + ('values', Any), + ] + + _oid_pair = ('type', 'values') + _oid_specs = { + 'challenge_password': SetOfDirectoryString, + 'extended_certificate_attributes': SetOfAttributes, + 'extension_request': SetOfExtensions, + 'microsoft_enrollment_csp_provider': SetOfMicrosoftEnrollmentCSProvider, + 'microsoft_os_version': SetOfDirectoryString, + 'microsoft_request_client_info': SetOfMicrosoftRequestClientInfo, + } + + +class CRIAttributes(SetOf): + _child_spec = CRIAttribute + + +class CertificationRequestInfo(Sequence): + _fields = [ + ('version', Version), + ('subject', Name), + ('subject_pk_info', PublicKeyInfo), + ('attributes', CRIAttributes, {'implicit': 0, 'optional': True}), + ] + + +class CertificationRequest(Sequence): + _fields = [ + ('certification_request_info', CertificationRequestInfo), + ('signature_algorithm', SignedDigestAlgorithm), + ('signature', OctetBitString), + ] diff --git a/tasks/lib/package_control/deps/asn1crypto/keys.py b/tasks/lib/package_control/deps/asn1crypto/keys.py new file mode 100644 index 0000000..b4a87ae --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/keys.py @@ -0,0 +1,1301 @@ +# coding: utf-8 + +""" +ASN.1 type classes for public and private keys. Exports the following items: + + - DSAPrivateKey() + - ECPrivateKey() + - EncryptedPrivateKeyInfo() + - PrivateKeyInfo() + - PublicKeyInfo() + - RSAPrivateKey() + - RSAPublicKey() + +Other type classes are defined that help compose the types listed above. +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +import hashlib +import math + +from ._errors import unwrap, APIException +from ._types import type_name, byte_cls +from .algos import _ForceNullParameters, DigestAlgorithm, EncryptionAlgorithm, RSAESOAEPParams, RSASSAPSSParams +from .core import ( + Any, + Asn1Value, + BitString, + Choice, + Integer, + IntegerOctetString, + Null, + ObjectIdentifier, + OctetBitString, + OctetString, + ParsableOctetString, + ParsableOctetBitString, + Sequence, + SequenceOf, + SetOf, +) +from .util import int_from_bytes, int_to_bytes + + +class OtherPrimeInfo(Sequence): + """ + Source: https://tools.ietf.org/html/rfc3447#page-46 + """ + + _fields = [ + ('prime', Integer), + ('exponent', Integer), + ('coefficient', Integer), + ] + + +class OtherPrimeInfos(SequenceOf): + """ + Source: https://tools.ietf.org/html/rfc3447#page-46 + """ + + _child_spec = OtherPrimeInfo + + +class RSAPrivateKeyVersion(Integer): + """ + Original Name: Version + Source: https://tools.ietf.org/html/rfc3447#page-45 + """ + + _map = { + 0: 'two-prime', + 1: 'multi', + } + + +class RSAPrivateKey(Sequence): + """ + Source: https://tools.ietf.org/html/rfc3447#page-45 + """ + + _fields = [ + ('version', RSAPrivateKeyVersion), + ('modulus', Integer), + ('public_exponent', Integer), + ('private_exponent', Integer), + ('prime1', Integer), + ('prime2', Integer), + ('exponent1', Integer), + ('exponent2', Integer), + ('coefficient', Integer), + ('other_prime_infos', OtherPrimeInfos, {'optional': True}) + ] + + +class RSAPublicKey(Sequence): + """ + Source: https://tools.ietf.org/html/rfc3447#page-44 + """ + + _fields = [ + ('modulus', Integer), + ('public_exponent', Integer) + ] + + +class DSAPrivateKey(Sequence): + """ + The ASN.1 structure that OpenSSL uses to store a DSA private key that is + not part of a PKCS#8 structure. Reversed engineered from english-language + description on linked OpenSSL documentation page. + + Original Name: None + Source: https://www.openssl.org/docs/apps/dsa.html + """ + + _fields = [ + ('version', Integer), + ('p', Integer), + ('q', Integer), + ('g', Integer), + ('public_key', Integer), + ('private_key', Integer), + ] + + +class _ECPoint(): + """ + In both PublicKeyInfo and PrivateKeyInfo, the EC public key is a byte + string that is encoded as a bit string. This class adds convenience + methods for converting to and from the byte string to a pair of integers + that are the X and Y coordinates. + """ + + @classmethod + def from_coords(cls, x, y): + """ + Creates an ECPoint object from the X and Y integer coordinates of the + point + + :param x: + The X coordinate, as an integer + + :param y: + The Y coordinate, as an integer + + :return: + An ECPoint object + """ + + x_bytes = int(math.ceil(math.log(x, 2) / 8.0)) + y_bytes = int(math.ceil(math.log(y, 2) / 8.0)) + + num_bytes = max(x_bytes, y_bytes) + + byte_string = b'\x04' + byte_string += int_to_bytes(x, width=num_bytes) + byte_string += int_to_bytes(y, width=num_bytes) + + return cls(byte_string) + + def to_coords(self): + """ + Returns the X and Y coordinates for this EC point, as native Python + integers + + :return: + A 2-element tuple containing integers (X, Y) + """ + + data = self.native + first_byte = data[0:1] + + # Uncompressed + if first_byte == b'\x04': + remaining = data[1:] + field_len = len(remaining) // 2 + x = int_from_bytes(remaining[0:field_len]) + y = int_from_bytes(remaining[field_len:]) + return (x, y) + + if first_byte not in set([b'\x02', b'\x03']): + raise ValueError(unwrap( + ''' + Invalid EC public key - first byte is incorrect + ''' + )) + + raise ValueError(unwrap( + ''' + Compressed representations of EC public keys are not supported due + to patent US6252960 + ''' + )) + + +class ECPoint(OctetString, _ECPoint): + + pass + + +class ECPointBitString(OctetBitString, _ECPoint): + + pass + + +class SpecifiedECDomainVersion(Integer): + """ + Source: http://www.secg.org/sec1-v2.pdf page 104 + """ + _map = { + 1: 'ecdpVer1', + 2: 'ecdpVer2', + 3: 'ecdpVer3', + } + + +class FieldType(ObjectIdentifier): + """ + Original Name: None + Source: http://www.secg.org/sec1-v2.pdf page 101 + """ + + _map = { + '1.2.840.10045.1.1': 'prime_field', + '1.2.840.10045.1.2': 'characteristic_two_field', + } + + +class CharacteristicTwoBasis(ObjectIdentifier): + """ + Original Name: None + Source: http://www.secg.org/sec1-v2.pdf page 102 + """ + + _map = { + '1.2.840.10045.1.2.1.1': 'gn_basis', + '1.2.840.10045.1.2.1.2': 'tp_basis', + '1.2.840.10045.1.2.1.3': 'pp_basis', + } + + +class Pentanomial(Sequence): + """ + Source: http://www.secg.org/sec1-v2.pdf page 102 + """ + + _fields = [ + ('k1', Integer), + ('k2', Integer), + ('k3', Integer), + ] + + +class CharacteristicTwo(Sequence): + """ + Original Name: Characteristic-two + Source: http://www.secg.org/sec1-v2.pdf page 101 + """ + + _fields = [ + ('m', Integer), + ('basis', CharacteristicTwoBasis), + ('parameters', Any), + ] + + _oid_pair = ('basis', 'parameters') + _oid_specs = { + 'gn_basis': Null, + 'tp_basis': Integer, + 'pp_basis': Pentanomial, + } + + +class FieldID(Sequence): + """ + Source: http://www.secg.org/sec1-v2.pdf page 100 + """ + + _fields = [ + ('field_type', FieldType), + ('parameters', Any), + ] + + _oid_pair = ('field_type', 'parameters') + _oid_specs = { + 'prime_field': Integer, + 'characteristic_two_field': CharacteristicTwo, + } + + +class Curve(Sequence): + """ + Source: http://www.secg.org/sec1-v2.pdf page 104 + """ + + _fields = [ + ('a', OctetString), + ('b', OctetString), + ('seed', OctetBitString, {'optional': True}), + ] + + +class SpecifiedECDomain(Sequence): + """ + Source: http://www.secg.org/sec1-v2.pdf page 103 + """ + + _fields = [ + ('version', SpecifiedECDomainVersion), + ('field_id', FieldID), + ('curve', Curve), + ('base', ECPoint), + ('order', Integer), + ('cofactor', Integer, {'optional': True}), + ('hash', DigestAlgorithm, {'optional': True}), + ] + + +class NamedCurve(ObjectIdentifier): + """ + Various named curves + + Original Name: None + Source: https://tools.ietf.org/html/rfc3279#page-23, + https://tools.ietf.org/html/rfc5480#page-5 + """ + + _map = { + # https://tools.ietf.org/html/rfc3279#page-23 + '1.2.840.10045.3.0.1': 'c2pnb163v1', + '1.2.840.10045.3.0.2': 'c2pnb163v2', + '1.2.840.10045.3.0.3': 'c2pnb163v3', + '1.2.840.10045.3.0.4': 'c2pnb176w1', + '1.2.840.10045.3.0.5': 'c2tnb191v1', + '1.2.840.10045.3.0.6': 'c2tnb191v2', + '1.2.840.10045.3.0.7': 'c2tnb191v3', + '1.2.840.10045.3.0.8': 'c2onb191v4', + '1.2.840.10045.3.0.9': 'c2onb191v5', + '1.2.840.10045.3.0.10': 'c2pnb208w1', + '1.2.840.10045.3.0.11': 'c2tnb239v1', + '1.2.840.10045.3.0.12': 'c2tnb239v2', + '1.2.840.10045.3.0.13': 'c2tnb239v3', + '1.2.840.10045.3.0.14': 'c2onb239v4', + '1.2.840.10045.3.0.15': 'c2onb239v5', + '1.2.840.10045.3.0.16': 'c2pnb272w1', + '1.2.840.10045.3.0.17': 'c2pnb304w1', + '1.2.840.10045.3.0.18': 'c2tnb359v1', + '1.2.840.10045.3.0.19': 'c2pnb368w1', + '1.2.840.10045.3.0.20': 'c2tnb431r1', + '1.2.840.10045.3.1.2': 'prime192v2', + '1.2.840.10045.3.1.3': 'prime192v3', + '1.2.840.10045.3.1.4': 'prime239v1', + '1.2.840.10045.3.1.5': 'prime239v2', + '1.2.840.10045.3.1.6': 'prime239v3', + # https://tools.ietf.org/html/rfc5480#page-5 + # http://www.secg.org/SEC2-Ver-1.0.pdf + '1.2.840.10045.3.1.1': 'secp192r1', + '1.2.840.10045.3.1.7': 'secp256r1', + '1.3.132.0.1': 'sect163k1', + '1.3.132.0.2': 'sect163r1', + '1.3.132.0.3': 'sect239k1', + '1.3.132.0.4': 'sect113r1', + '1.3.132.0.5': 'sect113r2', + '1.3.132.0.6': 'secp112r1', + '1.3.132.0.7': 'secp112r2', + '1.3.132.0.8': 'secp160r1', + '1.3.132.0.9': 'secp160k1', + '1.3.132.0.10': 'secp256k1', + '1.3.132.0.15': 'sect163r2', + '1.3.132.0.16': 'sect283k1', + '1.3.132.0.17': 'sect283r1', + '1.3.132.0.22': 'sect131r1', + '1.3.132.0.23': 'sect131r2', + '1.3.132.0.24': 'sect193r1', + '1.3.132.0.25': 'sect193r2', + '1.3.132.0.26': 'sect233k1', + '1.3.132.0.27': 'sect233r1', + '1.3.132.0.28': 'secp128r1', + '1.3.132.0.29': 'secp128r2', + '1.3.132.0.30': 'secp160r2', + '1.3.132.0.31': 'secp192k1', + '1.3.132.0.32': 'secp224k1', + '1.3.132.0.33': 'secp224r1', + '1.3.132.0.34': 'secp384r1', + '1.3.132.0.35': 'secp521r1', + '1.3.132.0.36': 'sect409k1', + '1.3.132.0.37': 'sect409r1', + '1.3.132.0.38': 'sect571k1', + '1.3.132.0.39': 'sect571r1', + # https://tools.ietf.org/html/rfc5639#section-4.1 + '1.3.36.3.3.2.8.1.1.1': 'brainpoolp160r1', + '1.3.36.3.3.2.8.1.1.2': 'brainpoolp160t1', + '1.3.36.3.3.2.8.1.1.3': 'brainpoolp192r1', + '1.3.36.3.3.2.8.1.1.4': 'brainpoolp192t1', + '1.3.36.3.3.2.8.1.1.5': 'brainpoolp224r1', + '1.3.36.3.3.2.8.1.1.6': 'brainpoolp224t1', + '1.3.36.3.3.2.8.1.1.7': 'brainpoolp256r1', + '1.3.36.3.3.2.8.1.1.8': 'brainpoolp256t1', + '1.3.36.3.3.2.8.1.1.9': 'brainpoolp320r1', + '1.3.36.3.3.2.8.1.1.10': 'brainpoolp320t1', + '1.3.36.3.3.2.8.1.1.11': 'brainpoolp384r1', + '1.3.36.3.3.2.8.1.1.12': 'brainpoolp384t1', + '1.3.36.3.3.2.8.1.1.13': 'brainpoolp512r1', + '1.3.36.3.3.2.8.1.1.14': 'brainpoolp512t1', + } + + _key_sizes = { + # Order values used to compute these sourced from + # http://cr.openjdk.java.net/~vinnie/7194075/webrev-3/src/share/classes/sun/security/ec/CurveDB.java.html + '1.2.840.10045.3.0.1': 21, + '1.2.840.10045.3.0.2': 21, + '1.2.840.10045.3.0.3': 21, + '1.2.840.10045.3.0.4': 21, + '1.2.840.10045.3.0.5': 24, + '1.2.840.10045.3.0.6': 24, + '1.2.840.10045.3.0.7': 24, + '1.2.840.10045.3.0.8': 24, + '1.2.840.10045.3.0.9': 24, + '1.2.840.10045.3.0.10': 25, + '1.2.840.10045.3.0.11': 30, + '1.2.840.10045.3.0.12': 30, + '1.2.840.10045.3.0.13': 30, + '1.2.840.10045.3.0.14': 30, + '1.2.840.10045.3.0.15': 30, + '1.2.840.10045.3.0.16': 33, + '1.2.840.10045.3.0.17': 37, + '1.2.840.10045.3.0.18': 45, + '1.2.840.10045.3.0.19': 45, + '1.2.840.10045.3.0.20': 53, + '1.2.840.10045.3.1.2': 24, + '1.2.840.10045.3.1.3': 24, + '1.2.840.10045.3.1.4': 30, + '1.2.840.10045.3.1.5': 30, + '1.2.840.10045.3.1.6': 30, + # Order values used to compute these sourced from + # http://www.secg.org/SEC2-Ver-1.0.pdf + # ceil(n.bit_length() / 8) + '1.2.840.10045.3.1.1': 24, + '1.2.840.10045.3.1.7': 32, + '1.3.132.0.1': 21, + '1.3.132.0.2': 21, + '1.3.132.0.3': 30, + '1.3.132.0.4': 15, + '1.3.132.0.5': 15, + '1.3.132.0.6': 14, + '1.3.132.0.7': 14, + '1.3.132.0.8': 21, + '1.3.132.0.9': 21, + '1.3.132.0.10': 32, + '1.3.132.0.15': 21, + '1.3.132.0.16': 36, + '1.3.132.0.17': 36, + '1.3.132.0.22': 17, + '1.3.132.0.23': 17, + '1.3.132.0.24': 25, + '1.3.132.0.25': 25, + '1.3.132.0.26': 29, + '1.3.132.0.27': 30, + '1.3.132.0.28': 16, + '1.3.132.0.29': 16, + '1.3.132.0.30': 21, + '1.3.132.0.31': 24, + '1.3.132.0.32': 29, + '1.3.132.0.33': 28, + '1.3.132.0.34': 48, + '1.3.132.0.35': 66, + '1.3.132.0.36': 51, + '1.3.132.0.37': 52, + '1.3.132.0.38': 72, + '1.3.132.0.39': 72, + # Order values used to compute these sourced from + # https://tools.ietf.org/html/rfc5639#section-3 + # ceil(q.bit_length() / 8) + '1.3.36.3.3.2.8.1.1.1': 20, + '1.3.36.3.3.2.8.1.1.2': 20, + '1.3.36.3.3.2.8.1.1.3': 24, + '1.3.36.3.3.2.8.1.1.4': 24, + '1.3.36.3.3.2.8.1.1.5': 28, + '1.3.36.3.3.2.8.1.1.6': 28, + '1.3.36.3.3.2.8.1.1.7': 32, + '1.3.36.3.3.2.8.1.1.8': 32, + '1.3.36.3.3.2.8.1.1.9': 40, + '1.3.36.3.3.2.8.1.1.10': 40, + '1.3.36.3.3.2.8.1.1.11': 48, + '1.3.36.3.3.2.8.1.1.12': 48, + '1.3.36.3.3.2.8.1.1.13': 64, + '1.3.36.3.3.2.8.1.1.14': 64, + } + + @classmethod + def register(cls, name, oid, key_size): + """ + Registers a new named elliptic curve that is not included in the + default list of named curves + + :param name: + A unicode string of the curve name + + :param oid: + A unicode string of the dotted format OID + + :param key_size: + An integer of the number of bytes the private key should be + encoded to + """ + + cls._map[oid] = name + if cls._reverse_map is not None: + cls._reverse_map[name] = oid + cls._key_sizes[oid] = key_size + + +class ECDomainParameters(Choice): + """ + Source: http://www.secg.org/sec1-v2.pdf page 102 + """ + + _alternatives = [ + ('specified', SpecifiedECDomain), + ('named', NamedCurve), + ('implicit_ca', Null), + ] + + @property + def key_size(self): + if self.name == 'implicit_ca': + raise ValueError(unwrap( + ''' + Unable to calculate key_size from ECDomainParameters + that are implicitly defined by the CA key + ''' + )) + + if self.name == 'specified': + order = self.chosen['order'].native + return math.ceil(math.log(order, 2.0) / 8.0) + + oid = self.chosen.dotted + if oid not in NamedCurve._key_sizes: + raise ValueError(unwrap( + ''' + The asn1crypto.keys.NamedCurve %s does not have a registered key length, + please call asn1crypto.keys.NamedCurve.register() + ''', + repr(oid) + )) + return NamedCurve._key_sizes[oid] + + +class ECPrivateKeyVersion(Integer): + """ + Original Name: None + Source: http://www.secg.org/sec1-v2.pdf page 108 + """ + + _map = { + 1: 'ecPrivkeyVer1', + } + + +class ECPrivateKey(Sequence): + """ + Source: http://www.secg.org/sec1-v2.pdf page 108 + """ + + _fields = [ + ('version', ECPrivateKeyVersion), + ('private_key', IntegerOctetString), + ('parameters', ECDomainParameters, {'explicit': 0, 'optional': True}), + ('public_key', ECPointBitString, {'explicit': 1, 'optional': True}), + ] + + # Ensures the key is set to the correct length when encoding + _key_size = None + + # This is necessary to ensure the private_key IntegerOctetString is encoded properly + def __setitem__(self, key, value): + res = super(ECPrivateKey, self).__setitem__(key, value) + + if key == 'private_key': + if self._key_size is None: + # Infer the key_size from the existing private key if possible + pkey_contents = self['private_key'].contents + if isinstance(pkey_contents, byte_cls) and len(pkey_contents) > 1: + self.set_key_size(len(self['private_key'].contents)) + + elif self._key_size is not None: + self._update_key_size() + + elif key == 'parameters' and isinstance(self['parameters'], ECDomainParameters) and \ + self['parameters'].name != 'implicit_ca': + self.set_key_size(self['parameters'].key_size) + + return res + + def set_key_size(self, key_size): + """ + Sets the key_size to ensure the private key is encoded to the proper length + + :param key_size: + An integer byte length to encode the private_key to + """ + + self._key_size = key_size + self._update_key_size() + + def _update_key_size(self): + """ + Ensure the private_key explicit encoding width is set + """ + + if self._key_size is not None and isinstance(self['private_key'], IntegerOctetString): + self['private_key'].set_encoded_width(self._key_size) + + +class DSAParams(Sequence): + """ + Parameters for a DSA public or private key + + Original Name: Dss-Parms + Source: https://tools.ietf.org/html/rfc3279#page-9 + """ + + _fields = [ + ('p', Integer), + ('q', Integer), + ('g', Integer), + ] + + +class Attribute(Sequence): + """ + Source: https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-X.501-198811-S!!PDF-E&type=items page 8 + """ + + _fields = [ + ('type', ObjectIdentifier), + ('values', SetOf, {'spec': Any}), + ] + + +class Attributes(SetOf): + """ + Source: https://tools.ietf.org/html/rfc5208#page-3 + """ + + _child_spec = Attribute + + +class PrivateKeyAlgorithmId(ObjectIdentifier): + """ + These OIDs for various public keys are reused when storing private keys + inside of a PKCS#8 structure + + Original Name: None + Source: https://tools.ietf.org/html/rfc3279 + """ + + _map = { + # https://tools.ietf.org/html/rfc3279#page-19 + '1.2.840.113549.1.1.1': 'rsa', + # https://tools.ietf.org/html/rfc4055#page-8 + '1.2.840.113549.1.1.10': 'rsassa_pss', + # https://tools.ietf.org/html/rfc3279#page-18 + '1.2.840.10040.4.1': 'dsa', + # https://tools.ietf.org/html/rfc3279#page-13 + '1.2.840.10045.2.1': 'ec', + # https://tools.ietf.org/html/rfc8410#section-9 + '1.3.101.110': 'x25519', + '1.3.101.111': 'x448', + '1.3.101.112': 'ed25519', + '1.3.101.113': 'ed448', + } + + +class PrivateKeyAlgorithm(_ForceNullParameters, Sequence): + """ + Original Name: PrivateKeyAlgorithmIdentifier + Source: https://tools.ietf.org/html/rfc5208#page-3 + """ + + _fields = [ + ('algorithm', PrivateKeyAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'dsa': DSAParams, + 'ec': ECDomainParameters, + 'rsassa_pss': RSASSAPSSParams, + } + + +class PrivateKeyInfo(Sequence): + """ + Source: https://tools.ietf.org/html/rfc5208#page-3 + """ + + _fields = [ + ('version', Integer), + ('private_key_algorithm', PrivateKeyAlgorithm), + ('private_key', ParsableOctetString), + ('attributes', Attributes, {'implicit': 0, 'optional': True}), + ] + + def _private_key_spec(self): + algorithm = self['private_key_algorithm']['algorithm'].native + return { + 'rsa': RSAPrivateKey, + 'rsassa_pss': RSAPrivateKey, + 'dsa': Integer, + 'ec': ECPrivateKey, + # These should be treated as opaque octet strings according + # to RFC 8410 + 'x25519': OctetString, + 'x448': OctetString, + 'ed25519': OctetString, + 'ed448': OctetString, + }[algorithm] + + _spec_callbacks = { + 'private_key': _private_key_spec + } + + _algorithm = None + _bit_size = None + _public_key = None + _fingerprint = None + + @classmethod + def wrap(cls, private_key, algorithm): + """ + Wraps a private key in a PrivateKeyInfo structure + + :param private_key: + A byte string or Asn1Value object of the private key + + :param algorithm: + A unicode string of "rsa", "dsa" or "ec" + + :return: + A PrivateKeyInfo object + """ + + if not isinstance(private_key, byte_cls) and not isinstance(private_key, Asn1Value): + raise TypeError(unwrap( + ''' + private_key must be a byte string or Asn1Value, not %s + ''', + type_name(private_key) + )) + + if algorithm == 'rsa' or algorithm == 'rsassa_pss': + if not isinstance(private_key, RSAPrivateKey): + private_key = RSAPrivateKey.load(private_key) + params = Null() + elif algorithm == 'dsa': + if not isinstance(private_key, DSAPrivateKey): + private_key = DSAPrivateKey.load(private_key) + params = DSAParams() + params['p'] = private_key['p'] + params['q'] = private_key['q'] + params['g'] = private_key['g'] + public_key = private_key['public_key'] + private_key = private_key['private_key'] + elif algorithm == 'ec': + if not isinstance(private_key, ECPrivateKey): + private_key = ECPrivateKey.load(private_key) + else: + private_key = private_key.copy() + params = private_key['parameters'] + del private_key['parameters'] + else: + raise ValueError(unwrap( + ''' + algorithm must be one of "rsa", "dsa", "ec", not %s + ''', + repr(algorithm) + )) + + private_key_algo = PrivateKeyAlgorithm() + private_key_algo['algorithm'] = PrivateKeyAlgorithmId(algorithm) + private_key_algo['parameters'] = params + + container = cls() + container._algorithm = algorithm + container['version'] = Integer(0) + container['private_key_algorithm'] = private_key_algo + container['private_key'] = private_key + + # Here we save the DSA public key if possible since it is not contained + # within the PKCS#8 structure for a DSA key + if algorithm == 'dsa': + container._public_key = public_key + + return container + + # This is necessary to ensure any contained ECPrivateKey is the + # correct size + def __setitem__(self, key, value): + res = super(PrivateKeyInfo, self).__setitem__(key, value) + + algorithm = self['private_key_algorithm'] + + # When possible, use the parameter info to make sure the private key encoding + # retains any necessary leading bytes, instead of them being dropped + if (key == 'private_key_algorithm' or key == 'private_key') and \ + algorithm['algorithm'].native == 'ec' and \ + isinstance(algorithm['parameters'], ECDomainParameters) and \ + algorithm['parameters'].name != 'implicit_ca' and \ + isinstance(self['private_key'], ParsableOctetString) and \ + isinstance(self['private_key'].parsed, ECPrivateKey): + self['private_key'].parsed.set_key_size(algorithm['parameters'].key_size) + + return res + + def unwrap(self): + """ + Unwraps the private key into an RSAPrivateKey, DSAPrivateKey or + ECPrivateKey object + + :return: + An RSAPrivateKey, DSAPrivateKey or ECPrivateKey object + """ + + raise APIException( + 'asn1crypto.keys.PrivateKeyInfo().unwrap() has been removed, ' + 'please use oscrypto.asymmetric.PrivateKey().unwrap() instead') + + @property + def curve(self): + """ + Returns information about the curve used for an EC key + + :raises: + ValueError - when the key is not an EC key + + :return: + A two-element tuple, with the first element being a unicode string + of "implicit_ca", "specified" or "named". If the first element is + "implicit_ca", the second is None. If "specified", the second is + an OrderedDict that is the native version of SpecifiedECDomain. If + "named", the second is a unicode string of the curve name. + """ + + if self.algorithm != 'ec': + raise ValueError(unwrap( + ''' + Only EC keys have a curve, this key is %s + ''', + self.algorithm.upper() + )) + + params = self['private_key_algorithm']['parameters'] + chosen = params.chosen + + if params.name == 'implicit_ca': + value = None + else: + value = chosen.native + + return (params.name, value) + + @property + def hash_algo(self): + """ + Returns the name of the family of hash algorithms used to generate a + DSA key + + :raises: + ValueError - when the key is not a DSA key + + :return: + A unicode string of "sha1" or "sha2" + """ + + if self.algorithm != 'dsa': + raise ValueError(unwrap( + ''' + Only DSA keys are generated using a hash algorithm, this key is + %s + ''', + self.algorithm.upper() + )) + + byte_len = math.log(self['private_key_algorithm']['parameters']['q'].native, 2) / 8 + + return 'sha1' if byte_len <= 20 else 'sha2' + + @property + def algorithm(self): + """ + :return: + A unicode string of "rsa", "rsassa_pss", "dsa" or "ec" + """ + + if self._algorithm is None: + self._algorithm = self['private_key_algorithm']['algorithm'].native + return self._algorithm + + @property + def bit_size(self): + """ + :return: + The bit size of the private key, as an integer + """ + + if self._bit_size is None: + if self.algorithm == 'rsa' or self.algorithm == 'rsassa_pss': + prime = self['private_key'].parsed['modulus'].native + elif self.algorithm == 'dsa': + prime = self['private_key_algorithm']['parameters']['p'].native + elif self.algorithm == 'ec': + prime = self['private_key'].parsed['private_key'].native + self._bit_size = int(math.ceil(math.log(prime, 2))) + modulus = self._bit_size % 8 + if modulus != 0: + self._bit_size += 8 - modulus + return self._bit_size + + @property + def byte_size(self): + """ + :return: + The byte size of the private key, as an integer + """ + + return int(math.ceil(self.bit_size / 8)) + + @property + def public_key(self): + """ + :return: + If an RSA key, an RSAPublicKey object. If a DSA key, an Integer + object. If an EC key, an ECPointBitString object. + """ + + raise APIException( + 'asn1crypto.keys.PrivateKeyInfo().public_key has been removed, ' + 'please use oscrypto.asymmetric.PrivateKey().public_key.unwrap() instead') + + @property + def public_key_info(self): + """ + :return: + A PublicKeyInfo object derived from this private key. + """ + + raise APIException( + 'asn1crypto.keys.PrivateKeyInfo().public_key_info has been removed, ' + 'please use oscrypto.asymmetric.PrivateKey().public_key.asn1 instead') + + @property + def fingerprint(self): + """ + Creates a fingerprint that can be compared with a public key to see if + the two form a pair. + + This fingerprint is not compatible with fingerprints generated by any + other software. + + :return: + A byte string that is a sha256 hash of selected components (based + on the key type) + """ + + raise APIException( + 'asn1crypto.keys.PrivateKeyInfo().fingerprint has been removed, ' + 'please use oscrypto.asymmetric.PrivateKey().fingerprint instead') + + +class EncryptedPrivateKeyInfo(Sequence): + """ + Source: https://tools.ietf.org/html/rfc5208#page-4 + """ + + _fields = [ + ('encryption_algorithm', EncryptionAlgorithm), + ('encrypted_data', OctetString), + ] + + +# These structures are from https://tools.ietf.org/html/rfc3279 + +class ValidationParms(Sequence): + """ + Source: https://tools.ietf.org/html/rfc3279#page-10 + """ + + _fields = [ + ('seed', BitString), + ('pgen_counter', Integer), + ] + + +class DomainParameters(Sequence): + """ + Source: https://tools.ietf.org/html/rfc3279#page-10 + """ + + _fields = [ + ('p', Integer), + ('g', Integer), + ('q', Integer), + ('j', Integer, {'optional': True}), + ('validation_params', ValidationParms, {'optional': True}), + ] + + +class PublicKeyAlgorithmId(ObjectIdentifier): + """ + Original Name: None + Source: https://tools.ietf.org/html/rfc3279 + """ + + _map = { + # https://tools.ietf.org/html/rfc3279#page-19 + '1.2.840.113549.1.1.1': 'rsa', + # https://tools.ietf.org/html/rfc3447#page-47 + '1.2.840.113549.1.1.7': 'rsaes_oaep', + # https://tools.ietf.org/html/rfc4055#page-8 + '1.2.840.113549.1.1.10': 'rsassa_pss', + # https://tools.ietf.org/html/rfc3279#page-18 + '1.2.840.10040.4.1': 'dsa', + # https://tools.ietf.org/html/rfc3279#page-13 + '1.2.840.10045.2.1': 'ec', + # https://tools.ietf.org/html/rfc3279#page-10 + '1.2.840.10046.2.1': 'dh', + # https://tools.ietf.org/html/rfc8410#section-9 + '1.3.101.110': 'x25519', + '1.3.101.111': 'x448', + '1.3.101.112': 'ed25519', + '1.3.101.113': 'ed448', + } + + +class PublicKeyAlgorithm(_ForceNullParameters, Sequence): + """ + Original Name: AlgorithmIdentifier + Source: https://tools.ietf.org/html/rfc5280#page-18 + """ + + _fields = [ + ('algorithm', PublicKeyAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'dsa': DSAParams, + 'ec': ECDomainParameters, + 'dh': DomainParameters, + 'rsaes_oaep': RSAESOAEPParams, + 'rsassa_pss': RSASSAPSSParams, + } + + +class PublicKeyInfo(Sequence): + """ + Original Name: SubjectPublicKeyInfo + Source: https://tools.ietf.org/html/rfc5280#page-17 + """ + + _fields = [ + ('algorithm', PublicKeyAlgorithm), + ('public_key', ParsableOctetBitString), + ] + + def _public_key_spec(self): + algorithm = self['algorithm']['algorithm'].native + return { + 'rsa': RSAPublicKey, + 'rsaes_oaep': RSAPublicKey, + 'rsassa_pss': RSAPublicKey, + 'dsa': Integer, + # We override the field spec with ECPoint so that users can easily + # decompose the byte string into the constituent X and Y coords + 'ec': (ECPointBitString, None), + 'dh': Integer, + # These should be treated as opaque bit strings according + # to RFC 8410, and need not even be valid ASN.1 + 'x25519': (OctetBitString, None), + 'x448': (OctetBitString, None), + 'ed25519': (OctetBitString, None), + 'ed448': (OctetBitString, None), + }[algorithm] + + _spec_callbacks = { + 'public_key': _public_key_spec + } + + _algorithm = None + _bit_size = None + _fingerprint = None + _sha1 = None + _sha256 = None + + @classmethod + def wrap(cls, public_key, algorithm): + """ + Wraps a public key in a PublicKeyInfo structure + + :param public_key: + A byte string or Asn1Value object of the public key + + :param algorithm: + A unicode string of "rsa" + + :return: + A PublicKeyInfo object + """ + + if not isinstance(public_key, byte_cls) and not isinstance(public_key, Asn1Value): + raise TypeError(unwrap( + ''' + public_key must be a byte string or Asn1Value, not %s + ''', + type_name(public_key) + )) + + if algorithm != 'rsa' and algorithm != 'rsassa_pss': + raise ValueError(unwrap( + ''' + algorithm must "rsa", not %s + ''', + repr(algorithm) + )) + + algo = PublicKeyAlgorithm() + algo['algorithm'] = PublicKeyAlgorithmId(algorithm) + algo['parameters'] = Null() + + container = cls() + container['algorithm'] = algo + if isinstance(public_key, Asn1Value): + public_key = public_key.untag().dump() + container['public_key'] = ParsableOctetBitString(public_key) + + return container + + def unwrap(self): + """ + Unwraps an RSA public key into an RSAPublicKey object. Does not support + DSA or EC public keys since they do not have an unwrapped form. + + :return: + An RSAPublicKey object + """ + + raise APIException( + 'asn1crypto.keys.PublicKeyInfo().unwrap() has been removed, ' + 'please use oscrypto.asymmetric.PublicKey().unwrap() instead') + + @property + def curve(self): + """ + Returns information about the curve used for an EC key + + :raises: + ValueError - when the key is not an EC key + + :return: + A two-element tuple, with the first element being a unicode string + of "implicit_ca", "specified" or "named". If the first element is + "implicit_ca", the second is None. If "specified", the second is + an OrderedDict that is the native version of SpecifiedECDomain. If + "named", the second is a unicode string of the curve name. + """ + + if self.algorithm != 'ec': + raise ValueError(unwrap( + ''' + Only EC keys have a curve, this key is %s + ''', + self.algorithm.upper() + )) + + params = self['algorithm']['parameters'] + chosen = params.chosen + + if params.name == 'implicit_ca': + value = None + else: + value = chosen.native + + return (params.name, value) + + @property + def hash_algo(self): + """ + Returns the name of the family of hash algorithms used to generate a + DSA key + + :raises: + ValueError - when the key is not a DSA key + + :return: + A unicode string of "sha1" or "sha2" or None if no parameters are + present + """ + + if self.algorithm != 'dsa': + raise ValueError(unwrap( + ''' + Only DSA keys are generated using a hash algorithm, this key is + %s + ''', + self.algorithm.upper() + )) + + parameters = self['algorithm']['parameters'] + if parameters.native is None: + return None + + byte_len = math.log(parameters['q'].native, 2) / 8 + + return 'sha1' if byte_len <= 20 else 'sha2' + + @property + def algorithm(self): + """ + :return: + A unicode string of "rsa", "rsassa_pss", "dsa" or "ec" + """ + + if self._algorithm is None: + self._algorithm = self['algorithm']['algorithm'].native + return self._algorithm + + @property + def bit_size(self): + """ + :return: + The bit size of the public key, as an integer + """ + + if self._bit_size is None: + if self.algorithm == 'ec': + self._bit_size = int(((len(self['public_key'].native) - 1) / 2) * 8) + else: + if self.algorithm == 'rsa' or self.algorithm == 'rsassa_pss': + prime = self['public_key'].parsed['modulus'].native + elif self.algorithm == 'dsa': + prime = self['algorithm']['parameters']['p'].native + self._bit_size = int(math.ceil(math.log(prime, 2))) + modulus = self._bit_size % 8 + if modulus != 0: + self._bit_size += 8 - modulus + + return self._bit_size + + @property + def byte_size(self): + """ + :return: + The byte size of the public key, as an integer + """ + + return int(math.ceil(self.bit_size / 8)) + + @property + def sha1(self): + """ + :return: + The SHA1 hash of the DER-encoded bytes of this public key info + """ + + if self._sha1 is None: + self._sha1 = hashlib.sha1(byte_cls(self['public_key'])).digest() + return self._sha1 + + @property + def sha256(self): + """ + :return: + The SHA-256 hash of the DER-encoded bytes of this public key info + """ + + if self._sha256 is None: + self._sha256 = hashlib.sha256(byte_cls(self['public_key'])).digest() + return self._sha256 + + @property + def fingerprint(self): + """ + Creates a fingerprint that can be compared with a private key to see if + the two form a pair. + + This fingerprint is not compatible with fingerprints generated by any + other software. + + :return: + A byte string that is a sha256 hash of selected components (based + on the key type) + """ + + raise APIException( + 'asn1crypto.keys.PublicKeyInfo().fingerprint has been removed, ' + 'please use oscrypto.asymmetric.PublicKey().fingerprint instead') diff --git a/tasks/lib/package_control/deps/asn1crypto/ocsp.py b/tasks/lib/package_control/deps/asn1crypto/ocsp.py new file mode 100644 index 0000000..91c7fbf --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/ocsp.py @@ -0,0 +1,703 @@ +# coding: utf-8 + +""" +ASN.1 type classes for the online certificate status protocol (OCSP). Exports +the following items: + + - OCSPRequest() + - OCSPResponse() + +Other type classes are defined that help compose the types listed above. +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +from ._errors import unwrap +from .algos import DigestAlgorithm, SignedDigestAlgorithm +from .core import ( + Boolean, + Choice, + Enumerated, + GeneralizedTime, + IA5String, + Integer, + Null, + ObjectIdentifier, + OctetBitString, + OctetString, + ParsableOctetString, + Sequence, + SequenceOf, +) +from .crl import AuthorityInfoAccessSyntax, CRLReason +from .keys import PublicKeyAlgorithm +from .x509 import Certificate, GeneralName, GeneralNames, Name + + +# The structures in this file are taken from https://tools.ietf.org/html/rfc6960 + + +class Version(Integer): + _map = { + 0: 'v1' + } + + +class CertId(Sequence): + _fields = [ + ('hash_algorithm', DigestAlgorithm), + ('issuer_name_hash', OctetString), + ('issuer_key_hash', OctetString), + ('serial_number', Integer), + ] + + +class ServiceLocator(Sequence): + _fields = [ + ('issuer', Name), + ('locator', AuthorityInfoAccessSyntax), + ] + + +class RequestExtensionId(ObjectIdentifier): + _map = { + '1.3.6.1.5.5.7.48.1.7': 'service_locator', + } + + +class RequestExtension(Sequence): + _fields = [ + ('extn_id', RequestExtensionId), + ('critical', Boolean, {'default': False}), + ('extn_value', ParsableOctetString), + ] + + _oid_pair = ('extn_id', 'extn_value') + _oid_specs = { + 'service_locator': ServiceLocator, + } + + +class RequestExtensions(SequenceOf): + _child_spec = RequestExtension + + +class Request(Sequence): + _fields = [ + ('req_cert', CertId), + ('single_request_extensions', RequestExtensions, {'explicit': 0, 'optional': True}), + ] + + _processed_extensions = False + _critical_extensions = None + _service_locator_value = None + + def _set_extensions(self): + """ + Sets common named extensions to private attributes and creates a list + of critical extensions + """ + + self._critical_extensions = set() + + for extension in self['single_request_extensions']: + name = extension['extn_id'].native + attribute_name = '_%s_value' % name + if hasattr(self, attribute_name): + setattr(self, attribute_name, extension['extn_value'].parsed) + if extension['critical'].native: + self._critical_extensions.add(name) + + self._processed_extensions = True + + @property + def critical_extensions(self): + """ + Returns a set of the names (or OID if not a known extension) of the + extensions marked as critical + + :return: + A set of unicode strings + """ + + if not self._processed_extensions: + self._set_extensions() + return self._critical_extensions + + @property + def service_locator_value(self): + """ + This extension is used when communicating with an OCSP responder that + acts as a proxy for OCSP requests + + :return: + None or a ServiceLocator object + """ + + if self._processed_extensions is False: + self._set_extensions() + return self._service_locator_value + + +class Requests(SequenceOf): + _child_spec = Request + + +class ResponseType(ObjectIdentifier): + _map = { + '1.3.6.1.5.5.7.48.1.1': 'basic_ocsp_response', + } + + +class AcceptableResponses(SequenceOf): + _child_spec = ResponseType + + +class PreferredSignatureAlgorithm(Sequence): + _fields = [ + ('sig_identifier', SignedDigestAlgorithm), + ('cert_identifier', PublicKeyAlgorithm, {'optional': True}), + ] + + +class PreferredSignatureAlgorithms(SequenceOf): + _child_spec = PreferredSignatureAlgorithm + + +class TBSRequestExtensionId(ObjectIdentifier): + _map = { + '1.3.6.1.5.5.7.48.1.2': 'nonce', + '1.3.6.1.5.5.7.48.1.4': 'acceptable_responses', + '1.3.6.1.5.5.7.48.1.8': 'preferred_signature_algorithms', + } + + +class TBSRequestExtension(Sequence): + _fields = [ + ('extn_id', TBSRequestExtensionId), + ('critical', Boolean, {'default': False}), + ('extn_value', ParsableOctetString), + ] + + _oid_pair = ('extn_id', 'extn_value') + _oid_specs = { + 'nonce': OctetString, + 'acceptable_responses': AcceptableResponses, + 'preferred_signature_algorithms': PreferredSignatureAlgorithms, + } + + +class TBSRequestExtensions(SequenceOf): + _child_spec = TBSRequestExtension + + +class TBSRequest(Sequence): + _fields = [ + ('version', Version, {'explicit': 0, 'default': 'v1'}), + ('requestor_name', GeneralName, {'explicit': 1, 'optional': True}), + ('request_list', Requests), + ('request_extensions', TBSRequestExtensions, {'explicit': 2, 'optional': True}), + ] + + +class Certificates(SequenceOf): + _child_spec = Certificate + + +class Signature(Sequence): + _fields = [ + ('signature_algorithm', SignedDigestAlgorithm), + ('signature', OctetBitString), + ('certs', Certificates, {'explicit': 0, 'optional': True}), + ] + + +class OCSPRequest(Sequence): + _fields = [ + ('tbs_request', TBSRequest), + ('optional_signature', Signature, {'explicit': 0, 'optional': True}), + ] + + _processed_extensions = False + _critical_extensions = None + _nonce_value = None + _acceptable_responses_value = None + _preferred_signature_algorithms_value = None + + def _set_extensions(self): + """ + Sets common named extensions to private attributes and creates a list + of critical extensions + """ + + self._critical_extensions = set() + + for extension in self['tbs_request']['request_extensions']: + name = extension['extn_id'].native + attribute_name = '_%s_value' % name + if hasattr(self, attribute_name): + setattr(self, attribute_name, extension['extn_value'].parsed) + if extension['critical'].native: + self._critical_extensions.add(name) + + self._processed_extensions = True + + @property + def critical_extensions(self): + """ + Returns a set of the names (or OID if not a known extension) of the + extensions marked as critical + + :return: + A set of unicode strings + """ + + if not self._processed_extensions: + self._set_extensions() + return self._critical_extensions + + @property + def nonce_value(self): + """ + This extension is used to prevent replay attacks by including a unique, + random value with each request/response pair + + :return: + None or an OctetString object + """ + + if self._processed_extensions is False: + self._set_extensions() + return self._nonce_value + + @property + def acceptable_responses_value(self): + """ + This extension is used to allow the client and server to communicate + with alternative response formats other than just basic_ocsp_response, + although no other formats are defined in the standard. + + :return: + None or an AcceptableResponses object + """ + + if self._processed_extensions is False: + self._set_extensions() + return self._acceptable_responses_value + + @property + def preferred_signature_algorithms_value(self): + """ + This extension is used by the client to define what signature algorithms + are preferred, including both the hash algorithm and the public key + algorithm, with a level of detail down to even the public key algorithm + parameters, such as curve name. + + :return: + None or a PreferredSignatureAlgorithms object + """ + + if self._processed_extensions is False: + self._set_extensions() + return self._preferred_signature_algorithms_value + + +class OCSPResponseStatus(Enumerated): + _map = { + 0: 'successful', + 1: 'malformed_request', + 2: 'internal_error', + 3: 'try_later', + 5: 'sign_required', + 6: 'unauthorized', + } + + +class ResponderId(Choice): + _alternatives = [ + ('by_name', Name, {'explicit': 1}), + ('by_key', OctetString, {'explicit': 2}), + ] + + +# Custom class to return a meaningful .native attribute from CertStatus() +class StatusGood(Null): + def set(self, value): + """ + Sets the value of the object + + :param value: + None or 'good' + """ + + if value is not None and value != 'good' and not isinstance(value, Null): + raise ValueError(unwrap( + ''' + value must be one of None, "good", not %s + ''', + repr(value) + )) + + self.contents = b'' + + @property + def native(self): + return 'good' + + +# Custom class to return a meaningful .native attribute from CertStatus() +class StatusUnknown(Null): + def set(self, value): + """ + Sets the value of the object + + :param value: + None or 'unknown' + """ + + if value is not None and value != 'unknown' and not isinstance(value, Null): + raise ValueError(unwrap( + ''' + value must be one of None, "unknown", not %s + ''', + repr(value) + )) + + self.contents = b'' + + @property + def native(self): + return 'unknown' + + +class RevokedInfo(Sequence): + _fields = [ + ('revocation_time', GeneralizedTime), + ('revocation_reason', CRLReason, {'explicit': 0, 'optional': True}), + ] + + +class CertStatus(Choice): + _alternatives = [ + ('good', StatusGood, {'implicit': 0}), + ('revoked', RevokedInfo, {'implicit': 1}), + ('unknown', StatusUnknown, {'implicit': 2}), + ] + + +class CrlId(Sequence): + _fields = [ + ('crl_url', IA5String, {'explicit': 0, 'optional': True}), + ('crl_num', Integer, {'explicit': 1, 'optional': True}), + ('crl_time', GeneralizedTime, {'explicit': 2, 'optional': True}), + ] + + +class SingleResponseExtensionId(ObjectIdentifier): + _map = { + '1.3.6.1.5.5.7.48.1.3': 'crl', + '1.3.6.1.5.5.7.48.1.6': 'archive_cutoff', + # These are CRLEntryExtension values from + # https://tools.ietf.org/html/rfc5280 + '2.5.29.21': 'crl_reason', + '2.5.29.24': 'invalidity_date', + '2.5.29.29': 'certificate_issuer', + # https://tools.ietf.org/html/rfc6962.html#page-13 + '1.3.6.1.4.1.11129.2.4.5': 'signed_certificate_timestamp_list', + } + + +class SingleResponseExtension(Sequence): + _fields = [ + ('extn_id', SingleResponseExtensionId), + ('critical', Boolean, {'default': False}), + ('extn_value', ParsableOctetString), + ] + + _oid_pair = ('extn_id', 'extn_value') + _oid_specs = { + 'crl': CrlId, + 'archive_cutoff': GeneralizedTime, + 'crl_reason': CRLReason, + 'invalidity_date': GeneralizedTime, + 'certificate_issuer': GeneralNames, + 'signed_certificate_timestamp_list': OctetString, + } + + +class SingleResponseExtensions(SequenceOf): + _child_spec = SingleResponseExtension + + +class SingleResponse(Sequence): + _fields = [ + ('cert_id', CertId), + ('cert_status', CertStatus), + ('this_update', GeneralizedTime), + ('next_update', GeneralizedTime, {'explicit': 0, 'optional': True}), + ('single_extensions', SingleResponseExtensions, {'explicit': 1, 'optional': True}), + ] + + _processed_extensions = False + _critical_extensions = None + _crl_value = None + _archive_cutoff_value = None + _crl_reason_value = None + _invalidity_date_value = None + _certificate_issuer_value = None + + def _set_extensions(self): + """ + Sets common named extensions to private attributes and creates a list + of critical extensions + """ + + self._critical_extensions = set() + + for extension in self['single_extensions']: + name = extension['extn_id'].native + attribute_name = '_%s_value' % name + if hasattr(self, attribute_name): + setattr(self, attribute_name, extension['extn_value'].parsed) + if extension['critical'].native: + self._critical_extensions.add(name) + + self._processed_extensions = True + + @property + def critical_extensions(self): + """ + Returns a set of the names (or OID if not a known extension) of the + extensions marked as critical + + :return: + A set of unicode strings + """ + + if not self._processed_extensions: + self._set_extensions() + return self._critical_extensions + + @property + def crl_value(self): + """ + This extension is used to locate the CRL that a certificate's revocation + is contained within. + + :return: + None or a CrlId object + """ + + if self._processed_extensions is False: + self._set_extensions() + return self._crl_value + + @property + def archive_cutoff_value(self): + """ + This extension is used to indicate the date at which an archived + (historical) certificate status entry will no longer be available. + + :return: + None or a GeneralizedTime object + """ + + if self._processed_extensions is False: + self._set_extensions() + return self._archive_cutoff_value + + @property + def crl_reason_value(self): + """ + This extension indicates the reason that a certificate was revoked. + + :return: + None or a CRLReason object + """ + + if self._processed_extensions is False: + self._set_extensions() + return self._crl_reason_value + + @property + def invalidity_date_value(self): + """ + This extension indicates the suspected date/time the private key was + compromised or the certificate became invalid. This would usually be + before the revocation date, which is when the CA processed the + revocation. + + :return: + None or a GeneralizedTime object + """ + + if self._processed_extensions is False: + self._set_extensions() + return self._invalidity_date_value + + @property + def certificate_issuer_value(self): + """ + This extension indicates the issuer of the certificate in question. + + :return: + None or an x509.GeneralNames object + """ + + if self._processed_extensions is False: + self._set_extensions() + return self._certificate_issuer_value + + +class Responses(SequenceOf): + _child_spec = SingleResponse + + +class ResponseDataExtensionId(ObjectIdentifier): + _map = { + '1.3.6.1.5.5.7.48.1.2': 'nonce', + '1.3.6.1.5.5.7.48.1.9': 'extended_revoke', + } + + +class ResponseDataExtension(Sequence): + _fields = [ + ('extn_id', ResponseDataExtensionId), + ('critical', Boolean, {'default': False}), + ('extn_value', ParsableOctetString), + ] + + _oid_pair = ('extn_id', 'extn_value') + _oid_specs = { + 'nonce': OctetString, + 'extended_revoke': Null, + } + + +class ResponseDataExtensions(SequenceOf): + _child_spec = ResponseDataExtension + + +class ResponseData(Sequence): + _fields = [ + ('version', Version, {'explicit': 0, 'default': 'v1'}), + ('responder_id', ResponderId), + ('produced_at', GeneralizedTime), + ('responses', Responses), + ('response_extensions', ResponseDataExtensions, {'explicit': 1, 'optional': True}), + ] + + +class BasicOCSPResponse(Sequence): + _fields = [ + ('tbs_response_data', ResponseData), + ('signature_algorithm', SignedDigestAlgorithm), + ('signature', OctetBitString), + ('certs', Certificates, {'explicit': 0, 'optional': True}), + ] + + +class ResponseBytes(Sequence): + _fields = [ + ('response_type', ResponseType), + ('response', ParsableOctetString), + ] + + _oid_pair = ('response_type', 'response') + _oid_specs = { + 'basic_ocsp_response': BasicOCSPResponse, + } + + +class OCSPResponse(Sequence): + _fields = [ + ('response_status', OCSPResponseStatus), + ('response_bytes', ResponseBytes, {'explicit': 0, 'optional': True}), + ] + + _processed_extensions = False + _critical_extensions = None + _nonce_value = None + _extended_revoke_value = None + + def _set_extensions(self): + """ + Sets common named extensions to private attributes and creates a list + of critical extensions + """ + + self._critical_extensions = set() + + for extension in self['response_bytes']['response'].parsed['tbs_response_data']['response_extensions']: + name = extension['extn_id'].native + attribute_name = '_%s_value' % name + if hasattr(self, attribute_name): + setattr(self, attribute_name, extension['extn_value'].parsed) + if extension['critical'].native: + self._critical_extensions.add(name) + + self._processed_extensions = True + + @property + def critical_extensions(self): + """ + Returns a set of the names (or OID if not a known extension) of the + extensions marked as critical + + :return: + A set of unicode strings + """ + + if not self._processed_extensions: + self._set_extensions() + return self._critical_extensions + + @property + def nonce_value(self): + """ + This extension is used to prevent replay attacks on the request/response + exchange + + :return: + None or an OctetString object + """ + + if self._processed_extensions is False: + self._set_extensions() + return self._nonce_value + + @property + def extended_revoke_value(self): + """ + This extension is used to signal that the responder will return a + "revoked" status for non-issued certificates. + + :return: + None or a Null object (if present) + """ + + if self._processed_extensions is False: + self._set_extensions() + return self._extended_revoke_value + + @property + def basic_ocsp_response(self): + """ + A shortcut into the BasicOCSPResponse sequence + + :return: + None or an asn1crypto.ocsp.BasicOCSPResponse object + """ + + return self['response_bytes']['response'].parsed + + @property + def response_data(self): + """ + A shortcut into the parsed, ResponseData sequence + + :return: + None or an asn1crypto.ocsp.ResponseData object + """ + + return self['response_bytes']['response'].parsed['tbs_response_data'] diff --git a/tasks/lib/package_control/deps/asn1crypto/parser.py b/tasks/lib/package_control/deps/asn1crypto/parser.py new file mode 100644 index 0000000..2f5a63e --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/parser.py @@ -0,0 +1,292 @@ +# coding: utf-8 + +""" +Functions for parsing and dumping using the ASN.1 DER encoding. Exports the +following items: + + - emit() + - parse() + - peek() + +Other type classes are defined that help compose the types listed above. +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys + +from ._types import byte_cls, chr_cls, type_name +from .util import int_from_bytes, int_to_bytes + +_PY2 = sys.version_info <= (3,) +_INSUFFICIENT_DATA_MESSAGE = 'Insufficient data - %s bytes requested but only %s available' +_MAX_DEPTH = 10 + + +def emit(class_, method, tag, contents): + """ + Constructs a byte string of an ASN.1 DER-encoded value + + This is typically not useful. Instead, use one of the standard classes from + asn1crypto.core, or construct a new class with specific fields, and call the + .dump() method. + + :param class_: + An integer ASN.1 class value: 0 (universal), 1 (application), + 2 (context), 3 (private) + + :param method: + An integer ASN.1 method value: 0 (primitive), 1 (constructed) + + :param tag: + An integer ASN.1 tag value + + :param contents: + A byte string of the encoded byte contents + + :return: + A byte string of the ASN.1 DER value (header and contents) + """ + + if not isinstance(class_, int): + raise TypeError('class_ must be an integer, not %s' % type_name(class_)) + + if class_ < 0 or class_ > 3: + raise ValueError('class_ must be one of 0, 1, 2 or 3, not %s' % class_) + + if not isinstance(method, int): + raise TypeError('method must be an integer, not %s' % type_name(method)) + + if method < 0 or method > 1: + raise ValueError('method must be 0 or 1, not %s' % method) + + if not isinstance(tag, int): + raise TypeError('tag must be an integer, not %s' % type_name(tag)) + + if tag < 0: + raise ValueError('tag must be greater than zero, not %s' % tag) + + if not isinstance(contents, byte_cls): + raise TypeError('contents must be a byte string, not %s' % type_name(contents)) + + return _dump_header(class_, method, tag, contents) + contents + + +def parse(contents, strict=False): + """ + Parses a byte string of ASN.1 BER/DER-encoded data. + + This is typically not useful. Instead, use one of the standard classes from + asn1crypto.core, or construct a new class with specific fields, and call the + .load() class method. + + :param contents: + A byte string of BER/DER-encoded data + + :param strict: + A boolean indicating if trailing data should be forbidden - if so, a + ValueError will be raised when trailing data exists + + :raises: + ValueError - when the contents do not contain an ASN.1 header or are truncated in some way + TypeError - when contents is not a byte string + + :return: + A 6-element tuple: + - 0: integer class (0 to 3) + - 1: integer method + - 2: integer tag + - 3: byte string header + - 4: byte string content + - 5: byte string trailer + """ + + if not isinstance(contents, byte_cls): + raise TypeError('contents must be a byte string, not %s' % type_name(contents)) + + contents_len = len(contents) + info, consumed = _parse(contents, contents_len) + if strict and consumed != contents_len: + raise ValueError('Extra data - %d bytes of trailing data were provided' % (contents_len - consumed)) + return info + + +def peek(contents): + """ + Parses a byte string of ASN.1 BER/DER-encoded data to find the length + + This is typically used to look into an encoded value to see how long the + next chunk of ASN.1-encoded data is. Primarily it is useful when a + value is a concatenation of multiple values. + + :param contents: + A byte string of BER/DER-encoded data + + :raises: + ValueError - when the contents do not contain an ASN.1 header or are truncated in some way + TypeError - when contents is not a byte string + + :return: + An integer with the number of bytes occupied by the ASN.1 value + """ + + if not isinstance(contents, byte_cls): + raise TypeError('contents must be a byte string, not %s' % type_name(contents)) + + info, consumed = _parse(contents, len(contents)) + return consumed + + +def _parse(encoded_data, data_len, pointer=0, lengths_only=False, depth=0): + """ + Parses a byte string into component parts + + :param encoded_data: + A byte string that contains BER-encoded data + + :param data_len: + The integer length of the encoded data + + :param pointer: + The index in the byte string to parse from + + :param lengths_only: + A boolean to cause the call to return a 2-element tuple of the integer + number of bytes in the header and the integer number of bytes in the + contents. Internal use only. + + :param depth: + The recursion depth when evaluating indefinite-length encoding. + + :return: + A 2-element tuple: + - 0: A tuple of (class_, method, tag, header, content, trailer) + - 1: An integer indicating how many bytes were consumed + """ + + if depth > _MAX_DEPTH: + raise ValueError('Indefinite-length recursion limit exceeded') + + start = pointer + + if data_len < pointer + 1: + raise ValueError(_INSUFFICIENT_DATA_MESSAGE % (1, data_len - pointer)) + first_octet = ord(encoded_data[pointer]) if _PY2 else encoded_data[pointer] + + pointer += 1 + + tag = first_octet & 31 + constructed = (first_octet >> 5) & 1 + # Base 128 length using 8th bit as continuation indicator + if tag == 31: + tag = 0 + while True: + if data_len < pointer + 1: + raise ValueError(_INSUFFICIENT_DATA_MESSAGE % (1, data_len - pointer)) + num = ord(encoded_data[pointer]) if _PY2 else encoded_data[pointer] + pointer += 1 + if num == 0x80 and tag == 0: + raise ValueError('Non-minimal tag encoding') + tag *= 128 + tag += num & 127 + if num >> 7 == 0: + break + if tag < 31: + raise ValueError('Non-minimal tag encoding') + + if data_len < pointer + 1: + raise ValueError(_INSUFFICIENT_DATA_MESSAGE % (1, data_len - pointer)) + length_octet = ord(encoded_data[pointer]) if _PY2 else encoded_data[pointer] + pointer += 1 + trailer = b'' + + if length_octet >> 7 == 0: + contents_end = pointer + (length_octet & 127) + + else: + length_octets = length_octet & 127 + if length_octets: + if data_len < pointer + length_octets: + raise ValueError(_INSUFFICIENT_DATA_MESSAGE % (length_octets, data_len - pointer)) + pointer += length_octets + contents_end = pointer + int_from_bytes(encoded_data[pointer - length_octets:pointer], signed=False) + + else: + # To properly parse indefinite length values, we need to scan forward + # parsing headers until we find a value with a length of zero. If we + # just scanned looking for \x00\x00, nested indefinite length values + # would not work. + if not constructed: + raise ValueError('Indefinite-length element must be constructed') + contents_end = pointer + while data_len < contents_end + 2 or encoded_data[contents_end:contents_end+2] != b'\x00\x00': + _, contents_end = _parse(encoded_data, data_len, contents_end, lengths_only=True, depth=depth+1) + contents_end += 2 + trailer = b'\x00\x00' + + if contents_end > data_len: + raise ValueError(_INSUFFICIENT_DATA_MESSAGE % (contents_end - pointer, data_len - pointer)) + + if lengths_only: + return (pointer, contents_end) + + return ( + ( + first_octet >> 6, + constructed, + tag, + encoded_data[start:pointer], + encoded_data[pointer:contents_end-len(trailer)], + trailer + ), + contents_end + ) + + +def _dump_header(class_, method, tag, contents): + """ + Constructs the header bytes for an ASN.1 object + + :param class_: + An integer ASN.1 class value: 0 (universal), 1 (application), + 2 (context), 3 (private) + + :param method: + An integer ASN.1 method value: 0 (primitive), 1 (constructed) + + :param tag: + An integer ASN.1 tag value + + :param contents: + A byte string of the encoded byte contents + + :return: + A byte string of the ASN.1 DER header + """ + + header = b'' + + id_num = 0 + id_num |= class_ << 6 + id_num |= method << 5 + + if tag >= 31: + cont_bit = 0 + while tag > 0: + header = chr_cls(cont_bit | (tag & 0x7f)) + header + if not cont_bit: + cont_bit = 0x80 + tag = tag >> 7 + header = chr_cls(id_num | 31) + header + else: + header += chr_cls(id_num | tag) + + length = len(contents) + if length <= 127: + header += chr_cls(length) + else: + length_bytes = int_to_bytes(length) + header += chr_cls(0x80 | len(length_bytes)) + header += length_bytes + + return header diff --git a/tasks/lib/package_control/deps/asn1crypto/pdf.py b/tasks/lib/package_control/deps/asn1crypto/pdf.py new file mode 100644 index 0000000..b72c886 --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/pdf.py @@ -0,0 +1,84 @@ +# coding: utf-8 + +""" +ASN.1 type classes for PDF signature structures. Adds extra oid mapping and +value parsing to asn1crypto.x509.Extension() and asn1crypto.xms.CMSAttribute(). +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +from .cms import CMSAttributeType, CMSAttribute +from .core import ( + Boolean, + Integer, + Null, + ObjectIdentifier, + OctetString, + Sequence, + SequenceOf, + SetOf, +) +from .crl import CertificateList +from .ocsp import OCSPResponse +from .x509 import ( + Extension, + ExtensionId, + GeneralName, + KeyPurposeId, +) + + +class AdobeArchiveRevInfo(Sequence): + _fields = [ + ('version', Integer) + ] + + +class AdobeTimestamp(Sequence): + _fields = [ + ('version', Integer), + ('location', GeneralName), + ('requires_auth', Boolean, {'optional': True, 'default': False}), + ] + + +class OtherRevInfo(Sequence): + _fields = [ + ('type', ObjectIdentifier), + ('value', OctetString), + ] + + +class SequenceOfCertificateList(SequenceOf): + _child_spec = CertificateList + + +class SequenceOfOCSPResponse(SequenceOf): + _child_spec = OCSPResponse + + +class SequenceOfOtherRevInfo(SequenceOf): + _child_spec = OtherRevInfo + + +class RevocationInfoArchival(Sequence): + _fields = [ + ('crl', SequenceOfCertificateList, {'explicit': 0, 'optional': True}), + ('ocsp', SequenceOfOCSPResponse, {'explicit': 1, 'optional': True}), + ('other_rev_info', SequenceOfOtherRevInfo, {'explicit': 2, 'optional': True}), + ] + + +class SetOfRevocationInfoArchival(SetOf): + _child_spec = RevocationInfoArchival + + +ExtensionId._map['1.2.840.113583.1.1.9.2'] = 'adobe_archive_rev_info' +ExtensionId._map['1.2.840.113583.1.1.9.1'] = 'adobe_timestamp' +ExtensionId._map['1.2.840.113583.1.1.10'] = 'adobe_ppklite_credential' +Extension._oid_specs['adobe_archive_rev_info'] = AdobeArchiveRevInfo +Extension._oid_specs['adobe_timestamp'] = AdobeTimestamp +Extension._oid_specs['adobe_ppklite_credential'] = Null +KeyPurposeId._map['1.2.840.113583.1.1.5'] = 'pdf_signing' +CMSAttributeType._map['1.2.840.113583.1.1.8'] = 'adobe_revocation_info_archival' +CMSAttribute._oid_specs['adobe_revocation_info_archival'] = SetOfRevocationInfoArchival diff --git a/tasks/lib/package_control/deps/asn1crypto/pem.py b/tasks/lib/package_control/deps/asn1crypto/pem.py new file mode 100644 index 0000000..511ea4b --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/pem.py @@ -0,0 +1,222 @@ +# coding: utf-8 + +""" +Encoding DER to PEM and decoding PEM to DER. Exports the following items: + + - armor() + - detect() + - unarmor() + +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +import base64 +import re +import sys + +from ._errors import unwrap +from ._types import type_name as _type_name, str_cls, byte_cls + +if sys.version_info < (3,): + from cStringIO import StringIO as BytesIO +else: + from io import BytesIO + + +def detect(byte_string): + """ + Detect if a byte string seems to contain a PEM-encoded block + + :param byte_string: + A byte string to look through + + :return: + A boolean, indicating if a PEM-encoded block is contained in the byte + string + """ + + if not isinstance(byte_string, byte_cls): + raise TypeError(unwrap( + ''' + byte_string must be a byte string, not %s + ''', + _type_name(byte_string) + )) + + return byte_string.find(b'-----BEGIN') != -1 or byte_string.find(b'---- BEGIN') != -1 + + +def armor(type_name, der_bytes, headers=None): + """ + Armors a DER-encoded byte string in PEM + + :param type_name: + A unicode string that will be capitalized and placed in the header + and footer of the block. E.g. "CERTIFICATE", "PRIVATE KEY", etc. This + will appear as "-----BEGIN CERTIFICATE-----" and + "-----END CERTIFICATE-----". + + :param der_bytes: + A byte string to be armored + + :param headers: + An OrderedDict of the header lines to write after the BEGIN line + + :return: + A byte string of the PEM block + """ + + if not isinstance(der_bytes, byte_cls): + raise TypeError(unwrap( + ''' + der_bytes must be a byte string, not %s + ''' % _type_name(der_bytes) + )) + + if not isinstance(type_name, str_cls): + raise TypeError(unwrap( + ''' + type_name must be a unicode string, not %s + ''', + _type_name(type_name) + )) + + type_name = type_name.upper().encode('ascii') + + output = BytesIO() + output.write(b'-----BEGIN ') + output.write(type_name) + output.write(b'-----\n') + if headers: + for key in headers: + output.write(key.encode('ascii')) + output.write(b': ') + output.write(headers[key].encode('ascii')) + output.write(b'\n') + output.write(b'\n') + b64_bytes = base64.b64encode(der_bytes) + b64_len = len(b64_bytes) + i = 0 + while i < b64_len: + output.write(b64_bytes[i:i + 64]) + output.write(b'\n') + i += 64 + output.write(b'-----END ') + output.write(type_name) + output.write(b'-----\n') + + return output.getvalue() + + +def _unarmor(pem_bytes): + """ + Convert a PEM-encoded byte string into one or more DER-encoded byte strings + + :param pem_bytes: + A byte string of the PEM-encoded data + + :raises: + ValueError - when the pem_bytes do not appear to be PEM-encoded bytes + + :return: + A generator of 3-element tuples in the format: (object_type, headers, + der_bytes). The object_type is a unicode string of what is between + "-----BEGIN " and "-----". Examples include: "CERTIFICATE", + "PUBLIC KEY", "PRIVATE KEY". The headers is a dict containing any lines + in the form "Name: Value" that are right after the begin line. + """ + + if not isinstance(pem_bytes, byte_cls): + raise TypeError(unwrap( + ''' + pem_bytes must be a byte string, not %s + ''', + _type_name(pem_bytes) + )) + + # Valid states include: "trash", "headers", "body" + state = 'trash' + headers = {} + base64_data = b'' + object_type = None + + found_start = False + found_end = False + + for line in pem_bytes.splitlines(False): + if line == b'': + continue + + if state == "trash": + # Look for a starting line since some CA cert bundle show the cert + # into in a parsed format above each PEM block + type_name_match = re.match(b'^(?:---- |-----)BEGIN ([A-Z0-9 ]+)(?: ----|-----)', line) + if not type_name_match: + continue + object_type = type_name_match.group(1).decode('ascii') + + found_start = True + state = 'headers' + continue + + if state == 'headers': + if line.find(b':') == -1: + state = 'body' + else: + decoded_line = line.decode('ascii') + name, value = decoded_line.split(':', 1) + headers[name] = value.strip() + continue + + if state == 'body': + if line[0:5] in (b'-----', b'---- '): + der_bytes = base64.b64decode(base64_data) + + yield (object_type, headers, der_bytes) + + state = 'trash' + headers = {} + base64_data = b'' + object_type = None + found_end = True + continue + + base64_data += line + + if not found_start or not found_end: + raise ValueError(unwrap( + ''' + pem_bytes does not appear to contain PEM-encoded data - no + BEGIN/END combination found + ''' + )) + + +def unarmor(pem_bytes, multiple=False): + """ + Convert a PEM-encoded byte string into a DER-encoded byte string + + :param pem_bytes: + A byte string of the PEM-encoded data + + :param multiple: + If True, function will return a generator + + :raises: + ValueError - when the pem_bytes do not appear to be PEM-encoded bytes + + :return: + A 3-element tuple (object_name, headers, der_bytes). The object_name is + a unicode string of what is between "-----BEGIN " and "-----". Examples + include: "CERTIFICATE", "PUBLIC KEY", "PRIVATE KEY". The headers is a + dict containing any lines in the form "Name: Value" that are right + after the begin line. + """ + + generator = _unarmor(pem_bytes) + + if not multiple: + return next(generator) + + return generator diff --git a/tasks/lib/package_control/deps/asn1crypto/pkcs12.py b/tasks/lib/package_control/deps/asn1crypto/pkcs12.py new file mode 100644 index 0000000..7ebcefe --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/pkcs12.py @@ -0,0 +1,193 @@ +# coding: utf-8 + +""" +ASN.1 type classes for PKCS#12 files. Exports the following items: + + - CertBag() + - CrlBag() + - Pfx() + - SafeBag() + - SecretBag() + +Other type classes are defined that help compose the types listed above. +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +from .algos import DigestInfo +from .cms import ContentInfo, SignedData +from .core import ( + Any, + BMPString, + Integer, + ObjectIdentifier, + OctetString, + ParsableOctetString, + Sequence, + SequenceOf, + SetOf, +) +from .keys import PrivateKeyInfo, EncryptedPrivateKeyInfo +from .x509 import Certificate, KeyPurposeId + + +# The structures in this file are taken from https://tools.ietf.org/html/rfc7292 + +class MacData(Sequence): + _fields = [ + ('mac', DigestInfo), + ('mac_salt', OctetString), + ('iterations', Integer, {'default': 1}), + ] + + +class Version(Integer): + _map = { + 3: 'v3' + } + + +class AttributeType(ObjectIdentifier): + _map = { + # https://tools.ietf.org/html/rfc2985#page-18 + '1.2.840.113549.1.9.20': 'friendly_name', + '1.2.840.113549.1.9.21': 'local_key_id', + # https://support.microsoft.com/en-us/kb/287547 + '1.3.6.1.4.1.311.17.1': 'microsoft_local_machine_keyset', + # https://github.com/frohoff/jdk8u-dev-jdk/blob/master/src/share/classes/sun/security/pkcs12/PKCS12KeyStore.java + # this is a set of OIDs, representing key usage, the usual value is a SET of one element OID 2.5.29.37.0 + '2.16.840.1.113894.746875.1.1': 'trusted_key_usage', + } + + +class SetOfAny(SetOf): + _child_spec = Any + + +class SetOfBMPString(SetOf): + _child_spec = BMPString + + +class SetOfOctetString(SetOf): + _child_spec = OctetString + + +class SetOfKeyPurposeId(SetOf): + _child_spec = KeyPurposeId + + +class Attribute(Sequence): + _fields = [ + ('type', AttributeType), + ('values', None), + ] + + _oid_specs = { + 'friendly_name': SetOfBMPString, + 'local_key_id': SetOfOctetString, + 'microsoft_csp_name': SetOfBMPString, + 'trusted_key_usage': SetOfKeyPurposeId, + } + + def _values_spec(self): + return self._oid_specs.get(self['type'].native, SetOfAny) + + _spec_callbacks = { + 'values': _values_spec + } + + +class Attributes(SetOf): + _child_spec = Attribute + + +class Pfx(Sequence): + _fields = [ + ('version', Version), + ('auth_safe', ContentInfo), + ('mac_data', MacData, {'optional': True}) + ] + + _authenticated_safe = None + + @property + def authenticated_safe(self): + if self._authenticated_safe is None: + content = self['auth_safe']['content'] + if isinstance(content, SignedData): + content = content['content_info']['content'] + self._authenticated_safe = AuthenticatedSafe.load(content.native) + return self._authenticated_safe + + +class AuthenticatedSafe(SequenceOf): + _child_spec = ContentInfo + + +class BagId(ObjectIdentifier): + _map = { + '1.2.840.113549.1.12.10.1.1': 'key_bag', + '1.2.840.113549.1.12.10.1.2': 'pkcs8_shrouded_key_bag', + '1.2.840.113549.1.12.10.1.3': 'cert_bag', + '1.2.840.113549.1.12.10.1.4': 'crl_bag', + '1.2.840.113549.1.12.10.1.5': 'secret_bag', + '1.2.840.113549.1.12.10.1.6': 'safe_contents', + } + + +class CertId(ObjectIdentifier): + _map = { + '1.2.840.113549.1.9.22.1': 'x509', + '1.2.840.113549.1.9.22.2': 'sdsi', + } + + +class CertBag(Sequence): + _fields = [ + ('cert_id', CertId), + ('cert_value', ParsableOctetString, {'explicit': 0}), + ] + + _oid_pair = ('cert_id', 'cert_value') + _oid_specs = { + 'x509': Certificate, + } + + +class CrlBag(Sequence): + _fields = [ + ('crl_id', ObjectIdentifier), + ('crl_value', OctetString, {'explicit': 0}), + ] + + +class SecretBag(Sequence): + _fields = [ + ('secret_type_id', ObjectIdentifier), + ('secret_value', OctetString, {'explicit': 0}), + ] + + +class SafeContents(SequenceOf): + pass + + +class SafeBag(Sequence): + _fields = [ + ('bag_id', BagId), + ('bag_value', Any, {'explicit': 0}), + ('bag_attributes', Attributes, {'optional': True}), + ] + + _oid_pair = ('bag_id', 'bag_value') + _oid_specs = { + 'key_bag': PrivateKeyInfo, + 'pkcs8_shrouded_key_bag': EncryptedPrivateKeyInfo, + 'cert_bag': CertBag, + 'crl_bag': CrlBag, + 'secret_bag': SecretBag, + 'safe_contents': SafeContents + } + + +SafeContents._child_spec = SafeBag diff --git a/tasks/lib/package_control/deps/asn1crypto/tsp.py b/tasks/lib/package_control/deps/asn1crypto/tsp.py new file mode 100644 index 0000000..f006da9 --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/tsp.py @@ -0,0 +1,310 @@ +# coding: utf-8 + +""" +ASN.1 type classes for the time stamp protocol (TSP). Exports the following +items: + + - TimeStampReq() + - TimeStampResp() + +Also adds TimeStampedData() support to asn1crypto.cms.ContentInfo(), +TimeStampedData() and TSTInfo() support to +asn1crypto.cms.EncapsulatedContentInfo() and some oids and value parsers to +asn1crypto.cms.CMSAttribute(). + +Other type classes are defined that help compose the types listed above. +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +from .algos import DigestAlgorithm +from .cms import ( + CMSAttribute, + CMSAttributeType, + ContentInfo, + ContentType, + EncapsulatedContentInfo, +) +from .core import ( + Any, + BitString, + Boolean, + Choice, + GeneralizedTime, + IA5String, + Integer, + ObjectIdentifier, + OctetString, + Sequence, + SequenceOf, + SetOf, + UTF8String, +) +from .crl import CertificateList +from .x509 import ( + Attributes, + CertificatePolicies, + GeneralName, + GeneralNames, +) + + +# The structures in this file are based on https://tools.ietf.org/html/rfc3161, +# https://tools.ietf.org/html/rfc4998, https://tools.ietf.org/html/rfc5544, +# https://tools.ietf.org/html/rfc5035, https://tools.ietf.org/html/rfc2634 + +class Version(Integer): + _map = { + 0: 'v0', + 1: 'v1', + 2: 'v2', + 3: 'v3', + 4: 'v4', + 5: 'v5', + } + + +class MessageImprint(Sequence): + _fields = [ + ('hash_algorithm', DigestAlgorithm), + ('hashed_message', OctetString), + ] + + +class Accuracy(Sequence): + _fields = [ + ('seconds', Integer, {'optional': True}), + ('millis', Integer, {'implicit': 0, 'optional': True}), + ('micros', Integer, {'implicit': 1, 'optional': True}), + ] + + +class Extension(Sequence): + _fields = [ + ('extn_id', ObjectIdentifier), + ('critical', Boolean, {'default': False}), + ('extn_value', OctetString), + ] + + +class Extensions(SequenceOf): + _child_spec = Extension + + +class TSTInfo(Sequence): + _fields = [ + ('version', Version), + ('policy', ObjectIdentifier), + ('message_imprint', MessageImprint), + ('serial_number', Integer), + ('gen_time', GeneralizedTime), + ('accuracy', Accuracy, {'optional': True}), + ('ordering', Boolean, {'default': False}), + ('nonce', Integer, {'optional': True}), + ('tsa', GeneralName, {'explicit': 0, 'optional': True}), + ('extensions', Extensions, {'implicit': 1, 'optional': True}), + ] + + +class TimeStampReq(Sequence): + _fields = [ + ('version', Version), + ('message_imprint', MessageImprint), + ('req_policy', ObjectIdentifier, {'optional': True}), + ('nonce', Integer, {'optional': True}), + ('cert_req', Boolean, {'default': False}), + ('extensions', Extensions, {'implicit': 0, 'optional': True}), + ] + + +class PKIStatus(Integer): + _map = { + 0: 'granted', + 1: 'granted_with_mods', + 2: 'rejection', + 3: 'waiting', + 4: 'revocation_warning', + 5: 'revocation_notification', + } + + +class PKIFreeText(SequenceOf): + _child_spec = UTF8String + + +class PKIFailureInfo(BitString): + _map = { + 0: 'bad_alg', + 2: 'bad_request', + 5: 'bad_data_format', + 14: 'time_not_available', + 15: 'unaccepted_policy', + 16: 'unaccepted_extensions', + 17: 'add_info_not_available', + 25: 'system_failure', + } + + +class PKIStatusInfo(Sequence): + _fields = [ + ('status', PKIStatus), + ('status_string', PKIFreeText, {'optional': True}), + ('fail_info', PKIFailureInfo, {'optional': True}), + ] + + +class TimeStampResp(Sequence): + _fields = [ + ('status', PKIStatusInfo), + ('time_stamp_token', ContentInfo), + ] + + +class MetaData(Sequence): + _fields = [ + ('hash_protected', Boolean), + ('file_name', UTF8String, {'optional': True}), + ('media_type', IA5String, {'optional': True}), + ('other_meta_data', Attributes, {'optional': True}), + ] + + +class TimeStampAndCRL(Sequence): + _fields = [ + ('time_stamp', EncapsulatedContentInfo), + ('crl', CertificateList, {'optional': True}), + ] + + +class TimeStampTokenEvidence(SequenceOf): + _child_spec = TimeStampAndCRL + + +class DigestAlgorithms(SequenceOf): + _child_spec = DigestAlgorithm + + +class EncryptionInfo(Sequence): + _fields = [ + ('encryption_info_type', ObjectIdentifier), + ('encryption_info_value', Any), + ] + + +class PartialHashtree(SequenceOf): + _child_spec = OctetString + + +class PartialHashtrees(SequenceOf): + _child_spec = PartialHashtree + + +class ArchiveTimeStamp(Sequence): + _fields = [ + ('digest_algorithm', DigestAlgorithm, {'implicit': 0, 'optional': True}), + ('attributes', Attributes, {'implicit': 1, 'optional': True}), + ('reduced_hashtree', PartialHashtrees, {'implicit': 2, 'optional': True}), + ('time_stamp', ContentInfo), + ] + + +class ArchiveTimeStampSequence(SequenceOf): + _child_spec = ArchiveTimeStamp + + +class EvidenceRecord(Sequence): + _fields = [ + ('version', Version), + ('digest_algorithms', DigestAlgorithms), + ('crypto_infos', Attributes, {'implicit': 0, 'optional': True}), + ('encryption_info', EncryptionInfo, {'implicit': 1, 'optional': True}), + ('archive_time_stamp_sequence', ArchiveTimeStampSequence), + ] + + +class OtherEvidence(Sequence): + _fields = [ + ('oe_type', ObjectIdentifier), + ('oe_value', Any), + ] + + +class Evidence(Choice): + _alternatives = [ + ('tst_evidence', TimeStampTokenEvidence, {'implicit': 0}), + ('ers_evidence', EvidenceRecord, {'implicit': 1}), + ('other_evidence', OtherEvidence, {'implicit': 2}), + ] + + +class TimeStampedData(Sequence): + _fields = [ + ('version', Version), + ('data_uri', IA5String, {'optional': True}), + ('meta_data', MetaData, {'optional': True}), + ('content', OctetString, {'optional': True}), + ('temporal_evidence', Evidence), + ] + + +class IssuerSerial(Sequence): + _fields = [ + ('issuer', GeneralNames), + ('serial_number', Integer), + ] + + +class ESSCertID(Sequence): + _fields = [ + ('cert_hash', OctetString), + ('issuer_serial', IssuerSerial, {'optional': True}), + ] + + +class ESSCertIDs(SequenceOf): + _child_spec = ESSCertID + + +class SigningCertificate(Sequence): + _fields = [ + ('certs', ESSCertIDs), + ('policies', CertificatePolicies, {'optional': True}), + ] + + +class SetOfSigningCertificates(SetOf): + _child_spec = SigningCertificate + + +class ESSCertIDv2(Sequence): + _fields = [ + ('hash_algorithm', DigestAlgorithm, {'default': {'algorithm': 'sha256'}}), + ('cert_hash', OctetString), + ('issuer_serial', IssuerSerial, {'optional': True}), + ] + + +class ESSCertIDv2s(SequenceOf): + _child_spec = ESSCertIDv2 + + +class SigningCertificateV2(Sequence): + _fields = [ + ('certs', ESSCertIDv2s), + ('policies', CertificatePolicies, {'optional': True}), + ] + + +class SetOfSigningCertificatesV2(SetOf): + _child_spec = SigningCertificateV2 + + +EncapsulatedContentInfo._oid_specs['tst_info'] = TSTInfo +EncapsulatedContentInfo._oid_specs['timestamped_data'] = TimeStampedData +ContentInfo._oid_specs['timestamped_data'] = TimeStampedData +ContentType._map['1.2.840.113549.1.9.16.1.4'] = 'tst_info' +ContentType._map['1.2.840.113549.1.9.16.1.31'] = 'timestamped_data' +CMSAttributeType._map['1.2.840.113549.1.9.16.2.12'] = 'signing_certificate' +CMSAttribute._oid_specs['signing_certificate'] = SetOfSigningCertificates +CMSAttributeType._map['1.2.840.113549.1.9.16.2.47'] = 'signing_certificate_v2' +CMSAttribute._oid_specs['signing_certificate_v2'] = SetOfSigningCertificatesV2 diff --git a/tasks/lib/package_control/deps/asn1crypto/util.py b/tasks/lib/package_control/deps/asn1crypto/util.py new file mode 100644 index 0000000..7196897 --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/util.py @@ -0,0 +1,878 @@ +# coding: utf-8 + +""" +Miscellaneous data helpers, including functions for converting integers to and +from bytes and UTC timezone. Exports the following items: + + - OrderedDict() + - int_from_bytes() + - int_to_bytes() + - timezone.utc + - utc_with_dst + - create_timezone() + - inet_ntop() + - inet_pton() + - uri_to_iri() + - iri_to_uri() +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +import math +import sys +from datetime import datetime, date, timedelta, tzinfo + +from ._errors import unwrap +from ._iri import iri_to_uri, uri_to_iri # noqa +from ._ordereddict import OrderedDict # noqa +from ._types import type_name + +if sys.platform == 'win32': + from ._inet import inet_ntop, inet_pton +else: + from socket import inet_ntop, inet_pton # noqa + + +# Python 2 +if sys.version_info <= (3,): + + def int_to_bytes(value, signed=False, width=None): + """ + Converts an integer to a byte string + + :param value: + The integer to convert + + :param signed: + If the byte string should be encoded using two's complement + + :param width: + If None, the minimal possible size (but at least 1), + otherwise an integer of the byte width for the return value + + :return: + A byte string + """ + + if value == 0 and width == 0: + return b'' + + # Handle negatives in two's complement + is_neg = False + if signed and value < 0: + is_neg = True + bits = int(math.ceil(len('%x' % abs(value)) / 2.0) * 8) + value = (value + (1 << bits)) % (1 << bits) + + hex_str = '%x' % value + if len(hex_str) & 1: + hex_str = '0' + hex_str + + output = hex_str.decode('hex') + + if signed and not is_neg and ord(output[0:1]) & 0x80: + output = b'\x00' + output + + if width is not None: + if len(output) > width: + raise OverflowError('int too big to convert') + if is_neg: + pad_char = b'\xFF' + else: + pad_char = b'\x00' + output = (pad_char * (width - len(output))) + output + elif is_neg and ord(output[0:1]) & 0x80 == 0: + output = b'\xFF' + output + + return output + + def int_from_bytes(value, signed=False): + """ + Converts a byte string to an integer + + :param value: + The byte string to convert + + :param signed: + If the byte string should be interpreted using two's complement + + :return: + An integer + """ + + if value == b'': + return 0 + + num = long(value.encode("hex"), 16) # noqa + + if not signed: + return num + + # Check for sign bit and handle two's complement + if ord(value[0:1]) & 0x80: + bit_len = len(value) * 8 + return num - (1 << bit_len) + + return num + + class timezone(tzinfo): # noqa + """ + Implements datetime.timezone for py2. + Only full minute offsets are supported. + DST is not supported. + """ + + def __init__(self, offset, name=None): + """ + :param offset: + A timedelta with this timezone's offset from UTC + + :param name: + Name of the timezone; if None, generate one. + """ + + if not timedelta(hours=-24) < offset < timedelta(hours=24): + raise ValueError('Offset must be in [-23:59, 23:59]') + + if offset.seconds % 60 or offset.microseconds: + raise ValueError('Offset must be full minutes') + + self._offset = offset + + if name is not None: + self._name = name + elif not offset: + self._name = 'UTC' + else: + self._name = 'UTC' + _format_offset(offset) + + def __eq__(self, other): + """ + Compare two timezones + + :param other: + The other timezone to compare to + + :return: + A boolean + """ + + if type(other) != timezone: + return False + return self._offset == other._offset + + def __getinitargs__(self): + """ + Called by tzinfo.__reduce__ to support pickle and copy. + + :return: + offset and name, to be used for __init__ + """ + + return self._offset, self._name + + def tzname(self, dt): + """ + :param dt: + A datetime object; ignored. + + :return: + Name of this timezone + """ + + return self._name + + def utcoffset(self, dt): + """ + :param dt: + A datetime object; ignored. + + :return: + A timedelta object with the offset from UTC + """ + + return self._offset + + def dst(self, dt): + """ + :param dt: + A datetime object; ignored. + + :return: + Zero timedelta + """ + + return timedelta(0) + + timezone.utc = timezone(timedelta(0)) + +# Python 3 +else: + + from datetime import timezone # noqa + + def int_to_bytes(value, signed=False, width=None): + """ + Converts an integer to a byte string + + :param value: + The integer to convert + + :param signed: + If the byte string should be encoded using two's complement + + :param width: + If None, the minimal possible size (but at least 1), + otherwise an integer of the byte width for the return value + + :return: + A byte string + """ + + if width is None: + if signed: + if value < 0: + bits_required = abs(value + 1).bit_length() + else: + bits_required = value.bit_length() + if bits_required % 8 == 0: + bits_required += 1 + else: + bits_required = value.bit_length() + width = math.ceil(bits_required / 8) or 1 + return value.to_bytes(width, byteorder='big', signed=signed) + + def int_from_bytes(value, signed=False): + """ + Converts a byte string to an integer + + :param value: + The byte string to convert + + :param signed: + If the byte string should be interpreted using two's complement + + :return: + An integer + """ + + return int.from_bytes(value, 'big', signed=signed) + + +def _format_offset(off): + """ + Format a timedelta into "[+-]HH:MM" format or "" for None + """ + + if off is None: + return '' + mins = off.days * 24 * 60 + off.seconds // 60 + sign = '-' if mins < 0 else '+' + return sign + '%02d:%02d' % divmod(abs(mins), 60) + + +class _UtcWithDst(tzinfo): + """ + Utc class where dst does not return None; required for astimezone + """ + + def tzname(self, dt): + return 'UTC' + + def utcoffset(self, dt): + return timedelta(0) + + def dst(self, dt): + return timedelta(0) + + +utc_with_dst = _UtcWithDst() + +_timezone_cache = {} + + +def create_timezone(offset): + """ + Returns a new datetime.timezone object with the given offset. + Uses cached objects if possible. + + :param offset: + A datetime.timedelta object; It needs to be in full minutes and between -23:59 and +23:59. + + :return: + A datetime.timezone object + """ + + try: + tz = _timezone_cache[offset] + except KeyError: + tz = _timezone_cache[offset] = timezone(offset) + return tz + + +class extended_date(object): + """ + A datetime.datetime-like object that represents the year 0. This is just + to handle 0000-01-01 found in some certificates. Python's datetime does + not support year 0. + + The proleptic gregorian calendar repeats itself every 400 years. Therefore, + the simplest way to format is to substitute year 2000. + """ + + def __init__(self, year, month, day): + """ + :param year: + The integer 0 + + :param month: + An integer from 1 to 12 + + :param day: + An integer from 1 to 31 + """ + + if year != 0: + raise ValueError('year must be 0') + + self._y2k = date(2000, month, day) + + @property + def year(self): + """ + :return: + The integer 0 + """ + + return 0 + + @property + def month(self): + """ + :return: + An integer from 1 to 12 + """ + + return self._y2k.month + + @property + def day(self): + """ + :return: + An integer from 1 to 31 + """ + + return self._y2k.day + + def strftime(self, format): + """ + Formats the date using strftime() + + :param format: + A strftime() format string + + :return: + A str, the formatted date as a unicode string + in Python 3 and a byte string in Python 2 + """ + + # Format the date twice, once with year 2000, once with year 4000. + # The only differences in the result will be in the millennium. Find them and replace by zeros. + y2k = self._y2k.strftime(format) + y4k = self._y2k.replace(year=4000).strftime(format) + return ''.join('0' if (c2, c4) == ('2', '4') else c2 for c2, c4 in zip(y2k, y4k)) + + def isoformat(self): + """ + Formats the date as %Y-%m-%d + + :return: + The date formatted to %Y-%m-%d as a unicode string in Python 3 + and a byte string in Python 2 + """ + + return self.strftime('0000-%m-%d') + + def replace(self, year=None, month=None, day=None): + """ + Returns a new datetime.date or asn1crypto.util.extended_date + object with the specified components replaced + + :return: + A datetime.date or asn1crypto.util.extended_date object + """ + + if year is None: + year = self.year + if month is None: + month = self.month + if day is None: + day = self.day + + if year > 0: + cls = date + else: + cls = extended_date + + return cls( + year, + month, + day + ) + + def __str__(self): + """ + :return: + A str representing this extended_date, e.g. "0000-01-01" + """ + + return self.strftime('%Y-%m-%d') + + def __eq__(self, other): + """ + Compare two extended_date objects + + :param other: + The other extended_date to compare to + + :return: + A boolean + """ + + # datetime.date object wouldn't compare equal because it can't be year 0 + if not isinstance(other, self.__class__): + return False + return self.__cmp__(other) == 0 + + def __ne__(self, other): + """ + Compare two extended_date objects + + :param other: + The other extended_date to compare to + + :return: + A boolean + """ + + return not self.__eq__(other) + + def _comparison_error(self, other): + raise TypeError(unwrap( + ''' + An asn1crypto.util.extended_date object can only be compared to + an asn1crypto.util.extended_date or datetime.date object, not %s + ''', + type_name(other) + )) + + def __cmp__(self, other): + """ + Compare two extended_date or datetime.date objects + + :param other: + The other extended_date object to compare to + + :return: + An integer smaller than, equal to, or larger than 0 + """ + + # self is year 0, other is >= year 1 + if isinstance(other, date): + return -1 + + if not isinstance(other, self.__class__): + self._comparison_error(other) + + if self._y2k < other._y2k: + return -1 + if self._y2k > other._y2k: + return 1 + return 0 + + def __lt__(self, other): + return self.__cmp__(other) < 0 + + def __le__(self, other): + return self.__cmp__(other) <= 0 + + def __gt__(self, other): + return self.__cmp__(other) > 0 + + def __ge__(self, other): + return self.__cmp__(other) >= 0 + + +class extended_datetime(object): + """ + A datetime.datetime-like object that represents the year 0. This is just + to handle 0000-01-01 found in some certificates. Python's datetime does + not support year 0. + + The proleptic gregorian calendar repeats itself every 400 years. Therefore, + the simplest way to format is to substitute year 2000. + """ + + # There are 97 leap days during 400 years. + DAYS_IN_400_YEARS = 400 * 365 + 97 + DAYS_IN_2000_YEARS = 5 * DAYS_IN_400_YEARS + + def __init__(self, year, *args, **kwargs): + """ + :param year: + The integer 0 + + :param args: + Other positional arguments; see datetime.datetime. + + :param kwargs: + Other keyword arguments; see datetime.datetime. + """ + + if year != 0: + raise ValueError('year must be 0') + + self._y2k = datetime(2000, *args, **kwargs) + + @property + def year(self): + """ + :return: + The integer 0 + """ + + return 0 + + @property + def month(self): + """ + :return: + An integer from 1 to 12 + """ + + return self._y2k.month + + @property + def day(self): + """ + :return: + An integer from 1 to 31 + """ + + return self._y2k.day + + @property + def hour(self): + """ + :return: + An integer from 1 to 24 + """ + + return self._y2k.hour + + @property + def minute(self): + """ + :return: + An integer from 1 to 60 + """ + + return self._y2k.minute + + @property + def second(self): + """ + :return: + An integer from 1 to 60 + """ + + return self._y2k.second + + @property + def microsecond(self): + """ + :return: + An integer from 0 to 999999 + """ + + return self._y2k.microsecond + + @property + def tzinfo(self): + """ + :return: + If object is timezone aware, a datetime.tzinfo object, else None. + """ + + return self._y2k.tzinfo + + def utcoffset(self): + """ + :return: + If object is timezone aware, a datetime.timedelta object, else None. + """ + + return self._y2k.utcoffset() + + def time(self): + """ + :return: + A datetime.time object + """ + + return self._y2k.time() + + def date(self): + """ + :return: + An asn1crypto.util.extended_date of the date + """ + + return extended_date(0, self.month, self.day) + + def strftime(self, format): + """ + Performs strftime(), always returning a str + + :param format: + A strftime() format string + + :return: + A str of the formatted datetime + """ + + # Format the datetime twice, once with year 2000, once with year 4000. + # The only differences in the result will be in the millennium. Find them and replace by zeros. + y2k = self._y2k.strftime(format) + y4k = self._y2k.replace(year=4000).strftime(format) + return ''.join('0' if (c2, c4) == ('2', '4') else c2 for c2, c4 in zip(y2k, y4k)) + + def isoformat(self, sep='T'): + """ + Formats the date as "%Y-%m-%d %H:%M:%S" with the sep param between the + date and time portions + + :param set: + A single character of the separator to place between the date and + time + + :return: + The formatted datetime as a unicode string in Python 3 and a byte + string in Python 2 + """ + + s = '0000-%02d-%02d%c%02d:%02d:%02d' % (self.month, self.day, sep, self.hour, self.minute, self.second) + if self.microsecond: + s += '.%06d' % self.microsecond + return s + _format_offset(self.utcoffset()) + + def replace(self, year=None, *args, **kwargs): + """ + Returns a new datetime.datetime or asn1crypto.util.extended_datetime + object with the specified components replaced + + :param year: + The new year to substitute. None to keep it. + + :param args: + Other positional arguments; see datetime.datetime.replace. + + :param kwargs: + Other keyword arguments; see datetime.datetime.replace. + + :return: + A datetime.datetime or asn1crypto.util.extended_datetime object + """ + + if year: + return self._y2k.replace(year, *args, **kwargs) + + return extended_datetime.from_y2k(self._y2k.replace(2000, *args, **kwargs)) + + def astimezone(self, tz): + """ + Convert this extended_datetime to another timezone. + + :param tz: + A datetime.tzinfo object. + + :return: + A new extended_datetime or datetime.datetime object + """ + + return extended_datetime.from_y2k(self._y2k.astimezone(tz)) + + def timestamp(self): + """ + Return POSIX timestamp. Only supported in python >= 3.3 + + :return: + A float representing the seconds since 1970-01-01 UTC. This will be a negative value. + """ + + return self._y2k.timestamp() - self.DAYS_IN_2000_YEARS * 86400 + + def __str__(self): + """ + :return: + A str representing this extended_datetime, e.g. "0000-01-01 00:00:00.000001-10:00" + """ + + return self.isoformat(sep=' ') + + def __eq__(self, other): + """ + Compare two extended_datetime objects + + :param other: + The other extended_datetime to compare to + + :return: + A boolean + """ + + # Only compare against other datetime or extended_datetime objects + if not isinstance(other, (self.__class__, datetime)): + return False + + # Offset-naive and offset-aware datetimes are never the same + if (self.tzinfo is None) != (other.tzinfo is None): + return False + + return self.__cmp__(other) == 0 + + def __ne__(self, other): + """ + Compare two extended_datetime objects + + :param other: + The other extended_datetime to compare to + + :return: + A boolean + """ + + return not self.__eq__(other) + + def _comparison_error(self, other): + """ + Raises a TypeError about the other object not being suitable for + comparison + + :param other: + The object being compared to + """ + + raise TypeError(unwrap( + ''' + An asn1crypto.util.extended_datetime object can only be compared to + an asn1crypto.util.extended_datetime or datetime.datetime object, + not %s + ''', + type_name(other) + )) + + def __cmp__(self, other): + """ + Compare two extended_datetime or datetime.datetime objects + + :param other: + The other extended_datetime or datetime.datetime object to compare to + + :return: + An integer smaller than, equal to, or larger than 0 + """ + + if not isinstance(other, (self.__class__, datetime)): + self._comparison_error(other) + + if (self.tzinfo is None) != (other.tzinfo is None): + raise TypeError("can't compare offset-naive and offset-aware datetimes") + + diff = self - other + zero = timedelta(0) + if diff < zero: + return -1 + if diff > zero: + return 1 + return 0 + + def __lt__(self, other): + return self.__cmp__(other) < 0 + + def __le__(self, other): + return self.__cmp__(other) <= 0 + + def __gt__(self, other): + return self.__cmp__(other) > 0 + + def __ge__(self, other): + return self.__cmp__(other) >= 0 + + def __add__(self, other): + """ + Adds a timedelta + + :param other: + A datetime.timedelta object to add. + + :return: + A new extended_datetime or datetime.datetime object. + """ + + return extended_datetime.from_y2k(self._y2k + other) + + def __sub__(self, other): + """ + Subtracts a timedelta or another datetime. + + :param other: + A datetime.timedelta or datetime.datetime or extended_datetime object to subtract. + + :return: + If a timedelta is passed, a new extended_datetime or datetime.datetime object. + Else a datetime.timedelta object. + """ + + if isinstance(other, timedelta): + return extended_datetime.from_y2k(self._y2k - other) + + if isinstance(other, extended_datetime): + return self._y2k - other._y2k + + if isinstance(other, datetime): + return self._y2k - other - timedelta(days=self.DAYS_IN_2000_YEARS) + + return NotImplemented + + def __rsub__(self, other): + return -(self - other) + + @classmethod + def from_y2k(cls, value): + """ + Revert substitution of year 2000. + + :param value: + A datetime.datetime object which is 2000 years in the future. + :return: + A new extended_datetime or datetime.datetime object. + """ + + year = value.year - 2000 + + if year > 0: + new_cls = datetime + else: + new_cls = cls + + return new_cls( + year, + value.month, + value.day, + value.hour, + value.minute, + value.second, + value.microsecond, + value.tzinfo + ) diff --git a/tasks/lib/package_control/deps/asn1crypto/version.py b/tasks/lib/package_control/deps/asn1crypto/version.py new file mode 100644 index 0000000..966b57a --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/version.py @@ -0,0 +1,6 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + + +__version__ = '1.5.1' +__version_info__ = (1, 5, 1) diff --git a/tasks/lib/package_control/deps/asn1crypto/x509.py b/tasks/lib/package_control/deps/asn1crypto/x509.py new file mode 100644 index 0000000..8cfb2c7 --- /dev/null +++ b/tasks/lib/package_control/deps/asn1crypto/x509.py @@ -0,0 +1,3036 @@ +# coding: utf-8 + +""" +ASN.1 type classes for X.509 certificates. Exports the following items: + + - Attributes() + - Certificate() + - Extensions() + - GeneralName() + - GeneralNames() + - Name() + +Other type classes are defined that help compose the types listed above. +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +from contextlib import contextmanager +from encodings import idna # noqa +import hashlib +import re +import socket +import stringprep +import sys +import unicodedata + +from ._errors import unwrap +from ._iri import iri_to_uri, uri_to_iri +from ._ordereddict import OrderedDict +from ._types import type_name, str_cls, bytes_to_list +from .algos import AlgorithmIdentifier, AnyAlgorithmIdentifier, DigestAlgorithm, SignedDigestAlgorithm +from .core import ( + Any, + BitString, + BMPString, + Boolean, + Choice, + Concat, + Enumerated, + GeneralizedTime, + GeneralString, + IA5String, + Integer, + Null, + NumericString, + ObjectIdentifier, + OctetBitString, + OctetString, + ParsableOctetString, + PrintableString, + Sequence, + SequenceOf, + Set, + SetOf, + TeletexString, + UniversalString, + UTCTime, + UTF8String, + VisibleString, + VOID, +) +from .keys import PublicKeyInfo +from .util import int_to_bytes, int_from_bytes, inet_ntop, inet_pton + + +# The structures in this file are taken from https://tools.ietf.org/html/rfc5280 +# and a few other supplementary sources, mostly due to extra supported +# extension and name OIDs + + +class DNSName(IA5String): + + _encoding = 'idna' + _bad_tag = (12, 19) + + def __ne__(self, other): + return not self == other + + def __eq__(self, other): + """ + Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.2 + + :param other: + Another DNSName object + + :return: + A boolean + """ + + if not isinstance(other, DNSName): + return False + + return self.__unicode__().lower() == other.__unicode__().lower() + + def set(self, value): + """ + Sets the value of the DNS name + + :param value: + A unicode string + """ + + if not isinstance(value, str_cls): + raise TypeError(unwrap( + ''' + %s value must be a unicode string, not %s + ''', + type_name(self), + type_name(value) + )) + + if value.startswith('.'): + encoded_value = b'.' + value[1:].encode(self._encoding) + else: + encoded_value = value.encode(self._encoding) + + self._unicode = value + self.contents = encoded_value + self._header = None + if self._trailer != b'': + self._trailer = b'' + + +class URI(IA5String): + + def set(self, value): + """ + Sets the value of the string + + :param value: + A unicode string + """ + + if not isinstance(value, str_cls): + raise TypeError(unwrap( + ''' + %s value must be a unicode string, not %s + ''', + type_name(self), + type_name(value) + )) + + self._unicode = value + self.contents = iri_to_uri(value) + self._header = None + if self._trailer != b'': + self._trailer = b'' + + def __ne__(self, other): + return not self == other + + def __eq__(self, other): + """ + Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.4 + + :param other: + Another URI object + + :return: + A boolean + """ + + if not isinstance(other, URI): + return False + + return iri_to_uri(self.native, True) == iri_to_uri(other.native, True) + + def __unicode__(self): + """ + :return: + A unicode string + """ + + if self.contents is None: + return '' + if self._unicode is None: + self._unicode = uri_to_iri(self._merge_chunks()) + return self._unicode + + +class EmailAddress(IA5String): + + _contents = None + + # If the value has gone through the .set() method, thus normalizing it + _normalized = False + + # In the wild we've seen this encoded as a UTF8String and PrintableString + _bad_tag = (12, 19) + + @property + def contents(self): + """ + :return: + A byte string of the DER-encoded contents of the sequence + """ + + return self._contents + + @contents.setter + def contents(self, value): + """ + :param value: + A byte string of the DER-encoded contents of the sequence + """ + + self._normalized = False + self._contents = value + + def set(self, value): + """ + Sets the value of the string + + :param value: + A unicode string + """ + + if not isinstance(value, str_cls): + raise TypeError(unwrap( + ''' + %s value must be a unicode string, not %s + ''', + type_name(self), + type_name(value) + )) + + if value.find('@') != -1: + mailbox, hostname = value.rsplit('@', 1) + encoded_value = mailbox.encode('ascii') + b'@' + hostname.encode('idna') + else: + encoded_value = value.encode('ascii') + + self._normalized = True + self._unicode = value + self.contents = encoded_value + self._header = None + if self._trailer != b'': + self._trailer = b'' + + def __unicode__(self): + """ + :return: + A unicode string + """ + + # We've seen this in the wild as a PrintableString, and since ascii is a + # subset of cp1252, we use the later for decoding to be more user friendly + if self._unicode is None: + contents = self._merge_chunks() + if contents.find(b'@') == -1: + self._unicode = contents.decode('cp1252') + else: + mailbox, hostname = contents.rsplit(b'@', 1) + self._unicode = mailbox.decode('cp1252') + '@' + hostname.decode('idna') + return self._unicode + + def __ne__(self, other): + return not self == other + + def __eq__(self, other): + """ + Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.5 + + :param other: + Another EmailAddress object + + :return: + A boolean + """ + + if not isinstance(other, EmailAddress): + return False + + if not self._normalized: + self.set(self.native) + if not other._normalized: + other.set(other.native) + + if self._contents.find(b'@') == -1 or other._contents.find(b'@') == -1: + return self._contents == other._contents + + other_mailbox, other_hostname = other._contents.rsplit(b'@', 1) + mailbox, hostname = self._contents.rsplit(b'@', 1) + + if mailbox != other_mailbox: + return False + + if hostname.lower() != other_hostname.lower(): + return False + + return True + + +class IPAddress(OctetString): + def parse(self, spec=None, spec_params=None): + """ + This method is not applicable to IP addresses + """ + + raise ValueError(unwrap( + ''' + IP address values can not be parsed + ''' + )) + + def set(self, value): + """ + Sets the value of the object + + :param value: + A unicode string containing an IPv4 address, IPv4 address with CIDR, + an IPv6 address or IPv6 address with CIDR + """ + + if not isinstance(value, str_cls): + raise TypeError(unwrap( + ''' + %s value must be a unicode string, not %s + ''', + type_name(self), + type_name(value) + )) + + original_value = value + + has_cidr = value.find('/') != -1 + cidr = 0 + if has_cidr: + parts = value.split('/', 1) + value = parts[0] + cidr = int(parts[1]) + if cidr < 0: + raise ValueError(unwrap( + ''' + %s value contains a CIDR range less than 0 + ''', + type_name(self) + )) + + if value.find(':') != -1: + family = socket.AF_INET6 + if cidr > 128: + raise ValueError(unwrap( + ''' + %s value contains a CIDR range bigger than 128, the maximum + value for an IPv6 address + ''', + type_name(self) + )) + cidr_size = 128 + else: + family = socket.AF_INET + if cidr > 32: + raise ValueError(unwrap( + ''' + %s value contains a CIDR range bigger than 32, the maximum + value for an IPv4 address + ''', + type_name(self) + )) + cidr_size = 32 + + cidr_bytes = b'' + if has_cidr: + cidr_mask = '1' * cidr + cidr_mask += '0' * (cidr_size - len(cidr_mask)) + cidr_bytes = int_to_bytes(int(cidr_mask, 2)) + cidr_bytes = (b'\x00' * ((cidr_size // 8) - len(cidr_bytes))) + cidr_bytes + + self._native = original_value + self.contents = inet_pton(family, value) + cidr_bytes + self._bytes = self.contents + self._header = None + if self._trailer != b'': + self._trailer = b'' + + @property + def native(self): + """ + The native Python datatype representation of this value + + :return: + A unicode string or None + """ + + if self.contents is None: + return None + + if self._native is None: + byte_string = self.__bytes__() + byte_len = len(byte_string) + value = None + cidr_int = None + if byte_len in set([32, 16]): + value = inet_ntop(socket.AF_INET6, byte_string[0:16]) + if byte_len > 16: + cidr_int = int_from_bytes(byte_string[16:]) + elif byte_len in set([8, 4]): + value = inet_ntop(socket.AF_INET, byte_string[0:4]) + if byte_len > 4: + cidr_int = int_from_bytes(byte_string[4:]) + if cidr_int is not None: + cidr_bits = '{0:b}'.format(cidr_int) + cidr = len(cidr_bits.rstrip('0')) + value = value + '/' + str_cls(cidr) + self._native = value + return self._native + + def __ne__(self, other): + return not self == other + + def __eq__(self, other): + """ + :param other: + Another IPAddress object + + :return: + A boolean + """ + + if not isinstance(other, IPAddress): + return False + + return self.__bytes__() == other.__bytes__() + + +class Attribute(Sequence): + _fields = [ + ('type', ObjectIdentifier), + ('values', SetOf, {'spec': Any}), + ] + + +class Attributes(SequenceOf): + _child_spec = Attribute + + +class KeyUsage(BitString): + _map = { + 0: 'digital_signature', + 1: 'non_repudiation', + 2: 'key_encipherment', + 3: 'data_encipherment', + 4: 'key_agreement', + 5: 'key_cert_sign', + 6: 'crl_sign', + 7: 'encipher_only', + 8: 'decipher_only', + } + + +class PrivateKeyUsagePeriod(Sequence): + _fields = [ + ('not_before', GeneralizedTime, {'implicit': 0, 'optional': True}), + ('not_after', GeneralizedTime, {'implicit': 1, 'optional': True}), + ] + + +class NotReallyTeletexString(TeletexString): + """ + OpenSSL (and probably some other libraries) puts ISO-8859-1 + into TeletexString instead of ITU T.61. We use Windows-1252 when + decoding since it is a superset of ISO-8859-1, and less likely to + cause encoding issues, but we stay strict with encoding to prevent + us from creating bad data. + """ + + _decoding_encoding = 'cp1252' + + def __unicode__(self): + """ + :return: + A unicode string + """ + + if self.contents is None: + return '' + if self._unicode is None: + self._unicode = self._merge_chunks().decode(self._decoding_encoding) + return self._unicode + + +@contextmanager +def strict_teletex(): + try: + NotReallyTeletexString._decoding_encoding = 'teletex' + yield + finally: + NotReallyTeletexString._decoding_encoding = 'cp1252' + + +class DirectoryString(Choice): + _alternatives = [ + ('teletex_string', NotReallyTeletexString), + ('printable_string', PrintableString), + ('universal_string', UniversalString), + ('utf8_string', UTF8String), + ('bmp_string', BMPString), + # This is an invalid/bad alternative, but some broken certs use it + ('ia5_string', IA5String), + ] + + +class NameType(ObjectIdentifier): + _map = { + '2.5.4.3': 'common_name', + '2.5.4.4': 'surname', + '2.5.4.5': 'serial_number', + '2.5.4.6': 'country_name', + '2.5.4.7': 'locality_name', + '2.5.4.8': 'state_or_province_name', + '2.5.4.9': 'street_address', + '2.5.4.10': 'organization_name', + '2.5.4.11': 'organizational_unit_name', + '2.5.4.12': 'title', + '2.5.4.15': 'business_category', + '2.5.4.17': 'postal_code', + '2.5.4.20': 'telephone_number', + '2.5.4.41': 'name', + '2.5.4.42': 'given_name', + '2.5.4.43': 'initials', + '2.5.4.44': 'generation_qualifier', + '2.5.4.45': 'unique_identifier', + '2.5.4.46': 'dn_qualifier', + '2.5.4.65': 'pseudonym', + '2.5.4.97': 'organization_identifier', + # https://www.trustedcomputinggroup.org/wp-content/uploads/Credential_Profile_EK_V2.0_R14_published.pdf + '2.23.133.2.1': 'tpm_manufacturer', + '2.23.133.2.2': 'tpm_model', + '2.23.133.2.3': 'tpm_version', + '2.23.133.2.4': 'platform_manufacturer', + '2.23.133.2.5': 'platform_model', + '2.23.133.2.6': 'platform_version', + # https://tools.ietf.org/html/rfc2985#page-26 + '1.2.840.113549.1.9.1': 'email_address', + # Page 10 of https://cabforum.org/wp-content/uploads/EV-V1_5_5.pdf + '1.3.6.1.4.1.311.60.2.1.1': 'incorporation_locality', + '1.3.6.1.4.1.311.60.2.1.2': 'incorporation_state_or_province', + '1.3.6.1.4.1.311.60.2.1.3': 'incorporation_country', + # https://tools.ietf.org/html/rfc4519#section-2.39 + '0.9.2342.19200300.100.1.1': 'user_id', + # https://tools.ietf.org/html/rfc2247#section-4 + '0.9.2342.19200300.100.1.25': 'domain_component', + # http://www.alvestrand.no/objectid/0.2.262.1.10.7.20.html + '0.2.262.1.10.7.20': 'name_distinguisher', + } + + # This order is largely based on observed order seen in EV certs from + # Symantec and DigiCert. Some of the uncommon name-related fields are + # just placed in what seems like a reasonable order. + preferred_order = [ + 'incorporation_country', + 'incorporation_state_or_province', + 'incorporation_locality', + 'business_category', + 'serial_number', + 'country_name', + 'postal_code', + 'state_or_province_name', + 'locality_name', + 'street_address', + 'organization_name', + 'organizational_unit_name', + 'title', + 'common_name', + 'user_id', + 'initials', + 'generation_qualifier', + 'surname', + 'given_name', + 'name', + 'pseudonym', + 'dn_qualifier', + 'telephone_number', + 'email_address', + 'domain_component', + 'name_distinguisher', + 'organization_identifier', + 'tpm_manufacturer', + 'tpm_model', + 'tpm_version', + 'platform_manufacturer', + 'platform_model', + 'platform_version', + ] + + @classmethod + def preferred_ordinal(cls, attr_name): + """ + Returns an ordering value for a particular attribute key. + + Unrecognized attributes and OIDs will be sorted lexically at the end. + + :return: + An orderable value. + + """ + + attr_name = cls.map(attr_name) + if attr_name in cls.preferred_order: + ordinal = cls.preferred_order.index(attr_name) + else: + ordinal = len(cls.preferred_order) + + return (ordinal, attr_name) + + @property + def human_friendly(self): + """ + :return: + A human-friendly unicode string to display to users + """ + + return { + 'common_name': 'Common Name', + 'surname': 'Surname', + 'serial_number': 'Serial Number', + 'country_name': 'Country', + 'locality_name': 'Locality', + 'state_or_province_name': 'State/Province', + 'street_address': 'Street Address', + 'organization_name': 'Organization', + 'organizational_unit_name': 'Organizational Unit', + 'title': 'Title', + 'business_category': 'Business Category', + 'postal_code': 'Postal Code', + 'telephone_number': 'Telephone Number', + 'name': 'Name', + 'given_name': 'Given Name', + 'initials': 'Initials', + 'generation_qualifier': 'Generation Qualifier', + 'unique_identifier': 'Unique Identifier', + 'dn_qualifier': 'DN Qualifier', + 'pseudonym': 'Pseudonym', + 'email_address': 'Email Address', + 'incorporation_locality': 'Incorporation Locality', + 'incorporation_state_or_province': 'Incorporation State/Province', + 'incorporation_country': 'Incorporation Country', + 'domain_component': 'Domain Component', + 'name_distinguisher': 'Name Distinguisher', + 'organization_identifier': 'Organization Identifier', + 'tpm_manufacturer': 'TPM Manufacturer', + 'tpm_model': 'TPM Model', + 'tpm_version': 'TPM Version', + 'platform_manufacturer': 'Platform Manufacturer', + 'platform_model': 'Platform Model', + 'platform_version': 'Platform Version', + 'user_id': 'User ID', + }.get(self.native, self.native) + + +class NameTypeAndValue(Sequence): + _fields = [ + ('type', NameType), + ('value', Any), + ] + + _oid_pair = ('type', 'value') + _oid_specs = { + 'common_name': DirectoryString, + 'surname': DirectoryString, + 'serial_number': DirectoryString, + 'country_name': DirectoryString, + 'locality_name': DirectoryString, + 'state_or_province_name': DirectoryString, + 'street_address': DirectoryString, + 'organization_name': DirectoryString, + 'organizational_unit_name': DirectoryString, + 'title': DirectoryString, + 'business_category': DirectoryString, + 'postal_code': DirectoryString, + 'telephone_number': PrintableString, + 'name': DirectoryString, + 'given_name': DirectoryString, + 'initials': DirectoryString, + 'generation_qualifier': DirectoryString, + 'unique_identifier': OctetBitString, + 'dn_qualifier': DirectoryString, + 'pseudonym': DirectoryString, + # https://tools.ietf.org/html/rfc2985#page-26 + 'email_address': EmailAddress, + # Page 10 of https://cabforum.org/wp-content/uploads/EV-V1_5_5.pdf + 'incorporation_locality': DirectoryString, + 'incorporation_state_or_province': DirectoryString, + 'incorporation_country': DirectoryString, + 'domain_component': DNSName, + 'name_distinguisher': DirectoryString, + 'organization_identifier': DirectoryString, + 'tpm_manufacturer': UTF8String, + 'tpm_model': UTF8String, + 'tpm_version': UTF8String, + 'platform_manufacturer': UTF8String, + 'platform_model': UTF8String, + 'platform_version': UTF8String, + 'user_id': DirectoryString, + } + + _prepped = None + + @property + def prepped_value(self): + """ + Returns the value after being processed by the internationalized string + preparation as specified by RFC 5280 + + :return: + A unicode string + """ + + if self._prepped is None: + self._prepped = self._ldap_string_prep(self['value'].native) + return self._prepped + + def __ne__(self, other): + return not self == other + + def __eq__(self, other): + """ + Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.1 + + :param other: + Another NameTypeAndValue object + + :return: + A boolean + """ + + if not isinstance(other, NameTypeAndValue): + return False + + if other['type'].native != self['type'].native: + return False + + return other.prepped_value == self.prepped_value + + def _ldap_string_prep(self, string): + """ + Implements the internationalized string preparation algorithm from + RFC 4518. https://tools.ietf.org/html/rfc4518#section-2 + + :param string: + A unicode string to prepare + + :return: + A prepared unicode string, ready for comparison + """ + + # Map step + string = re.sub('[\u00ad\u1806\u034f\u180b-\u180d\ufe0f-\uff00\ufffc]+', '', string) + string = re.sub('[\u0009\u000a\u000b\u000c\u000d\u0085]', ' ', string) + if sys.maxunicode == 0xffff: + # Some installs of Python 2.7 don't support 8-digit unicode escape + # ranges, so we have to break them into pieces + # Original was: \U0001D173-\U0001D17A and \U000E0020-\U000E007F + string = re.sub('\ud834[\udd73-\udd7a]|\udb40[\udc20-\udc7f]|\U000e0001', '', string) + else: + string = re.sub('[\U0001D173-\U0001D17A\U000E0020-\U000E007F\U000e0001]', '', string) + string = re.sub( + '[\u0000-\u0008\u000e-\u001f\u007f-\u0084\u0086-\u009f\u06dd\u070f\u180e\u200c-\u200f' + '\u202a-\u202e\u2060-\u2063\u206a-\u206f\ufeff\ufff9-\ufffb]+', + '', + string + ) + string = string.replace('\u200b', '') + string = re.sub('[\u00a0\u1680\u2000-\u200a\u2028-\u2029\u202f\u205f\u3000]', ' ', string) + + string = ''.join(map(stringprep.map_table_b2, string)) + + # Normalize step + string = unicodedata.normalize('NFKC', string) + + # Prohibit step + for char in string: + if stringprep.in_table_a1(char): + raise ValueError(unwrap( + ''' + X.509 Name objects may not contain unassigned code points + ''' + )) + + if stringprep.in_table_c8(char): + raise ValueError(unwrap( + ''' + X.509 Name objects may not contain change display or + zzzzdeprecated characters + ''' + )) + + if stringprep.in_table_c3(char): + raise ValueError(unwrap( + ''' + X.509 Name objects may not contain private use characters + ''' + )) + + if stringprep.in_table_c4(char): + raise ValueError(unwrap( + ''' + X.509 Name objects may not contain non-character code points + ''' + )) + + if stringprep.in_table_c5(char): + raise ValueError(unwrap( + ''' + X.509 Name objects may not contain surrogate code points + ''' + )) + + if char == '\ufffd': + raise ValueError(unwrap( + ''' + X.509 Name objects may not contain the replacement character + ''' + )) + + # Check bidirectional step - here we ensure that we are not mixing + # left-to-right and right-to-left text in the string + has_r_and_al_cat = False + has_l_cat = False + for char in string: + if stringprep.in_table_d1(char): + has_r_and_al_cat = True + elif stringprep.in_table_d2(char): + has_l_cat = True + + if has_r_and_al_cat: + first_is_r_and_al = stringprep.in_table_d1(string[0]) + last_is_r_and_al = stringprep.in_table_d1(string[-1]) + + if has_l_cat or not first_is_r_and_al or not last_is_r_and_al: + raise ValueError(unwrap( + ''' + X.509 Name object contains a malformed bidirectional + sequence + ''' + )) + + # Insignificant space handling step + string = ' ' + re.sub(' +', ' ', string).strip() + ' ' + + return string + + +class RelativeDistinguishedName(SetOf): + _child_spec = NameTypeAndValue + + @property + def hashable(self): + """ + :return: + A unicode string that can be used as a dict key or in a set + """ + + output = [] + values = self._get_values(self) + for key in sorted(values.keys()): + output.append('%s: %s' % (key, values[key])) + # Unit separator is used here since the normalization process for + # values moves any such character, and the keys are all dotted integers + # or under_score_words + return '\x1F'.join(output) + + def __ne__(self, other): + return not self == other + + def __eq__(self, other): + """ + Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.1 + + :param other: + Another RelativeDistinguishedName object + + :return: + A boolean + """ + + if not isinstance(other, RelativeDistinguishedName): + return False + + if len(self) != len(other): + return False + + self_types = self._get_types(self) + other_types = self._get_types(other) + + if self_types != other_types: + return False + + self_values = self._get_values(self) + other_values = self._get_values(other) + + for type_name_ in self_types: + if self_values[type_name_] != other_values[type_name_]: + return False + + return True + + def _get_types(self, rdn): + """ + Returns a set of types contained in an RDN + + :param rdn: + A RelativeDistinguishedName object + + :return: + A set object with unicode strings of NameTypeAndValue type field + values + """ + + return set([ntv['type'].native for ntv in rdn]) + + def _get_values(self, rdn): + """ + Returns a dict of prepped values contained in an RDN + + :param rdn: + A RelativeDistinguishedName object + + :return: + A dict object with unicode strings of NameTypeAndValue value field + values that have been prepped for comparison + """ + + output = {} + [output.update([(ntv['type'].native, ntv.prepped_value)]) for ntv in rdn] + return output + + +class RDNSequence(SequenceOf): + _child_spec = RelativeDistinguishedName + + @property + def hashable(self): + """ + :return: + A unicode string that can be used as a dict key or in a set + """ + + # Record separator is used here since the normalization process for + # values moves any such character, and the keys are all dotted integers + # or under_score_words + return '\x1E'.join(rdn.hashable for rdn in self) + + def __ne__(self, other): + return not self == other + + def __eq__(self, other): + """ + Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.1 + + :param other: + Another RDNSequence object + + :return: + A boolean + """ + + if not isinstance(other, RDNSequence): + return False + + if len(self) != len(other): + return False + + for index, self_rdn in enumerate(self): + if other[index] != self_rdn: + return False + + return True + + +class Name(Choice): + _alternatives = [ + ('', RDNSequence), + ] + + _human_friendly = None + _sha1 = None + _sha256 = None + + @classmethod + def build(cls, name_dict, use_printable=False): + """ + Creates a Name object from a dict of unicode string keys and values. + The keys should be from NameType._map, or a dotted-integer OID unicode + string. + + :param name_dict: + A dict of name information, e.g. {"common_name": "Will Bond", + "country_name": "US", "organization_name": "Codex Non Sufficit LC"} + + :param use_printable: + A bool - if PrintableString should be used for encoding instead of + UTF8String. This is for backwards compatibility with old software. + + :return: + An x509.Name object + """ + + rdns = [] + if not use_printable: + encoding_name = 'utf8_string' + encoding_class = UTF8String + else: + encoding_name = 'printable_string' + encoding_class = PrintableString + + # Sort the attributes according to NameType.preferred_order + name_dict = OrderedDict( + sorted( + name_dict.items(), + key=lambda item: NameType.preferred_ordinal(item[0]) + ) + ) + + for attribute_name, attribute_value in name_dict.items(): + attribute_name = NameType.map(attribute_name) + if attribute_name == 'email_address': + value = EmailAddress(attribute_value) + elif attribute_name == 'domain_component': + value = DNSName(attribute_value) + elif attribute_name in set(['dn_qualifier', 'country_name', 'serial_number']): + value = DirectoryString( + name='printable_string', + value=PrintableString(attribute_value) + ) + else: + value = DirectoryString( + name=encoding_name, + value=encoding_class(attribute_value) + ) + + rdns.append(RelativeDistinguishedName([ + NameTypeAndValue({ + 'type': attribute_name, + 'value': value + }) + ])) + + return cls(name='', value=RDNSequence(rdns)) + + @property + def hashable(self): + """ + :return: + A unicode string that can be used as a dict key or in a set + """ + + return self.chosen.hashable + + def __len__(self): + return len(self.chosen) + + def __ne__(self, other): + return not self == other + + def __eq__(self, other): + """ + Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.1 + + :param other: + Another Name object + + :return: + A boolean + """ + + if not isinstance(other, Name): + return False + return self.chosen == other.chosen + + @property + def native(self): + if self._native is None: + self._native = OrderedDict() + for rdn in self.chosen.native: + for type_val in rdn: + field_name = type_val['type'] + if field_name in self._native: + existing = self._native[field_name] + if not isinstance(existing, list): + existing = self._native[field_name] = [existing] + existing.append(type_val['value']) + else: + self._native[field_name] = type_val['value'] + return self._native + + @property + def human_friendly(self): + """ + :return: + A human-friendly unicode string containing the parts of the name + """ + + if self._human_friendly is None: + data = OrderedDict() + last_field = None + for rdn in self.chosen: + for type_val in rdn: + field_name = type_val['type'].human_friendly + last_field = field_name + if field_name in data: + data[field_name] = [data[field_name]] + data[field_name].append(type_val['value']) + else: + data[field_name] = type_val['value'] + to_join = [] + keys = data.keys() + if last_field == 'Country': + keys = reversed(list(keys)) + for key in keys: + value = data[key] + native_value = self._recursive_humanize(value) + to_join.append('%s: %s' % (key, native_value)) + + has_comma = False + for element in to_join: + if element.find(',') != -1: + has_comma = True + break + + separator = ', ' if not has_comma else '; ' + self._human_friendly = separator.join(to_join[::-1]) + + return self._human_friendly + + def _recursive_humanize(self, value): + """ + Recursively serializes data compiled from the RDNSequence + + :param value: + An Asn1Value object, or a list of Asn1Value objects + + :return: + A unicode string + """ + + if isinstance(value, list): + return ', '.join( + reversed([self._recursive_humanize(sub_value) for sub_value in value]) + ) + return value.native + + @property + def sha1(self): + """ + :return: + The SHA1 hash of the DER-encoded bytes of this name + """ + + if self._sha1 is None: + self._sha1 = hashlib.sha1(self.dump()).digest() + return self._sha1 + + @property + def sha256(self): + """ + :return: + The SHA-256 hash of the DER-encoded bytes of this name + """ + + if self._sha256 is None: + self._sha256 = hashlib.sha256(self.dump()).digest() + return self._sha256 + + +class AnotherName(Sequence): + _fields = [ + ('type_id', ObjectIdentifier), + ('value', Any, {'explicit': 0}), + ] + + +class CountryName(Choice): + class_ = 1 + tag = 1 + + _alternatives = [ + ('x121_dcc_code', NumericString), + ('iso_3166_alpha2_code', PrintableString), + ] + + +class AdministrationDomainName(Choice): + class_ = 1 + tag = 2 + + _alternatives = [ + ('numeric', NumericString), + ('printable', PrintableString), + ] + + +class PrivateDomainName(Choice): + _alternatives = [ + ('numeric', NumericString), + ('printable', PrintableString), + ] + + +class PersonalName(Set): + _fields = [ + ('surname', PrintableString, {'implicit': 0}), + ('given_name', PrintableString, {'implicit': 1, 'optional': True}), + ('initials', PrintableString, {'implicit': 2, 'optional': True}), + ('generation_qualifier', PrintableString, {'implicit': 3, 'optional': True}), + ] + + +class TeletexPersonalName(Set): + _fields = [ + ('surname', TeletexString, {'implicit': 0}), + ('given_name', TeletexString, {'implicit': 1, 'optional': True}), + ('initials', TeletexString, {'implicit': 2, 'optional': True}), + ('generation_qualifier', TeletexString, {'implicit': 3, 'optional': True}), + ] + + +class OrganizationalUnitNames(SequenceOf): + _child_spec = PrintableString + + +class TeletexOrganizationalUnitNames(SequenceOf): + _child_spec = TeletexString + + +class BuiltInStandardAttributes(Sequence): + _fields = [ + ('country_name', CountryName, {'optional': True}), + ('administration_domain_name', AdministrationDomainName, {'optional': True}), + ('network_address', NumericString, {'implicit': 0, 'optional': True}), + ('terminal_identifier', PrintableString, {'implicit': 1, 'optional': True}), + ('private_domain_name', PrivateDomainName, {'explicit': 2, 'optional': True}), + ('organization_name', PrintableString, {'implicit': 3, 'optional': True}), + ('numeric_user_identifier', NumericString, {'implicit': 4, 'optional': True}), + ('personal_name', PersonalName, {'implicit': 5, 'optional': True}), + ('organizational_unit_names', OrganizationalUnitNames, {'implicit': 6, 'optional': True}), + ] + + +class BuiltInDomainDefinedAttribute(Sequence): + _fields = [ + ('type', PrintableString), + ('value', PrintableString), + ] + + +class BuiltInDomainDefinedAttributes(SequenceOf): + _child_spec = BuiltInDomainDefinedAttribute + + +class TeletexDomainDefinedAttribute(Sequence): + _fields = [ + ('type', TeletexString), + ('value', TeletexString), + ] + + +class TeletexDomainDefinedAttributes(SequenceOf): + _child_spec = TeletexDomainDefinedAttribute + + +class PhysicalDeliveryCountryName(Choice): + _alternatives = [ + ('x121_dcc_code', NumericString), + ('iso_3166_alpha2_code', PrintableString), + ] + + +class PostalCode(Choice): + _alternatives = [ + ('numeric_code', NumericString), + ('printable_code', PrintableString), + ] + + +class PDSParameter(Set): + _fields = [ + ('printable_string', PrintableString, {'optional': True}), + ('teletex_string', TeletexString, {'optional': True}), + ] + + +class PrintableAddress(SequenceOf): + _child_spec = PrintableString + + +class UnformattedPostalAddress(Set): + _fields = [ + ('printable_address', PrintableAddress, {'optional': True}), + ('teletex_string', TeletexString, {'optional': True}), + ] + + +class E1634Address(Sequence): + _fields = [ + ('number', NumericString, {'implicit': 0}), + ('sub_address', NumericString, {'implicit': 1, 'optional': True}), + ] + + +class NAddresses(SetOf): + _child_spec = OctetString + + +class PresentationAddress(Sequence): + _fields = [ + ('p_selector', OctetString, {'explicit': 0, 'optional': True}), + ('s_selector', OctetString, {'explicit': 1, 'optional': True}), + ('t_selector', OctetString, {'explicit': 2, 'optional': True}), + ('n_addresses', NAddresses, {'explicit': 3}), + ] + + +class ExtendedNetworkAddress(Choice): + _alternatives = [ + ('e163_4_address', E1634Address), + ('psap_address', PresentationAddress, {'implicit': 0}) + ] + + +class TerminalType(Integer): + _map = { + 3: 'telex', + 4: 'teletex', + 5: 'g3_facsimile', + 6: 'g4_facsimile', + 7: 'ia5_terminal', + 8: 'videotex', + } + + +class ExtensionAttributeType(Integer): + _map = { + 1: 'common_name', + 2: 'teletex_common_name', + 3: 'teletex_organization_name', + 4: 'teletex_personal_name', + 5: 'teletex_organization_unit_names', + 6: 'teletex_domain_defined_attributes', + 7: 'pds_name', + 8: 'physical_delivery_country_name', + 9: 'postal_code', + 10: 'physical_delivery_office_name', + 11: 'physical_delivery_office_number', + 12: 'extension_of_address_components', + 13: 'physical_delivery_personal_name', + 14: 'physical_delivery_organization_name', + 15: 'extension_physical_delivery_address_components', + 16: 'unformatted_postal_address', + 17: 'street_address', + 18: 'post_office_box_address', + 19: 'poste_restante_address', + 20: 'unique_postal_name', + 21: 'local_postal_attributes', + 22: 'extended_network_address', + 23: 'terminal_type', + } + + +class ExtensionAttribute(Sequence): + _fields = [ + ('extension_attribute_type', ExtensionAttributeType, {'implicit': 0}), + ('extension_attribute_value', Any, {'explicit': 1}), + ] + + _oid_pair = ('extension_attribute_type', 'extension_attribute_value') + _oid_specs = { + 'common_name': PrintableString, + 'teletex_common_name': TeletexString, + 'teletex_organization_name': TeletexString, + 'teletex_personal_name': TeletexPersonalName, + 'teletex_organization_unit_names': TeletexOrganizationalUnitNames, + 'teletex_domain_defined_attributes': TeletexDomainDefinedAttributes, + 'pds_name': PrintableString, + 'physical_delivery_country_name': PhysicalDeliveryCountryName, + 'postal_code': PostalCode, + 'physical_delivery_office_name': PDSParameter, + 'physical_delivery_office_number': PDSParameter, + 'extension_of_address_components': PDSParameter, + 'physical_delivery_personal_name': PDSParameter, + 'physical_delivery_organization_name': PDSParameter, + 'extension_physical_delivery_address_components': PDSParameter, + 'unformatted_postal_address': UnformattedPostalAddress, + 'street_address': PDSParameter, + 'post_office_box_address': PDSParameter, + 'poste_restante_address': PDSParameter, + 'unique_postal_name': PDSParameter, + 'local_postal_attributes': PDSParameter, + 'extended_network_address': ExtendedNetworkAddress, + 'terminal_type': TerminalType, + } + + +class ExtensionAttributes(SequenceOf): + _child_spec = ExtensionAttribute + + +class ORAddress(Sequence): + _fields = [ + ('built_in_standard_attributes', BuiltInStandardAttributes), + ('built_in_domain_defined_attributes', BuiltInDomainDefinedAttributes, {'optional': True}), + ('extension_attributes', ExtensionAttributes, {'optional': True}), + ] + + +class EDIPartyName(Sequence): + _fields = [ + ('name_assigner', DirectoryString, {'implicit': 0, 'optional': True}), + ('party_name', DirectoryString, {'implicit': 1}), + ] + + +class GeneralName(Choice): + _alternatives = [ + ('other_name', AnotherName, {'implicit': 0}), + ('rfc822_name', EmailAddress, {'implicit': 1}), + ('dns_name', DNSName, {'implicit': 2}), + ('x400_address', ORAddress, {'implicit': 3}), + ('directory_name', Name, {'explicit': 4}), + ('edi_party_name', EDIPartyName, {'implicit': 5}), + ('uniform_resource_identifier', URI, {'implicit': 6}), + ('ip_address', IPAddress, {'implicit': 7}), + ('registered_id', ObjectIdentifier, {'implicit': 8}), + ] + + def __ne__(self, other): + return not self == other + + def __eq__(self, other): + """ + Does not support other_name, x400_address or edi_party_name + + :param other: + The other GeneralName to compare to + + :return: + A boolean + """ + + if self.name in ('other_name', 'x400_address', 'edi_party_name'): + raise ValueError(unwrap( + ''' + Comparison is not supported for GeneralName objects of + choice %s + ''', + self.name + )) + + if other.name in ('other_name', 'x400_address', 'edi_party_name'): + raise ValueError(unwrap( + ''' + Comparison is not supported for GeneralName objects of choice + %s''', + other.name + )) + + if self.name != other.name: + return False + + return self.chosen == other.chosen + + +class GeneralNames(SequenceOf): + _child_spec = GeneralName + + +class Time(Choice): + _alternatives = [ + ('utc_time', UTCTime), + ('general_time', GeneralizedTime), + ] + + +class Validity(Sequence): + _fields = [ + ('not_before', Time), + ('not_after', Time), + ] + + +class BasicConstraints(Sequence): + _fields = [ + ('ca', Boolean, {'default': False}), + ('path_len_constraint', Integer, {'optional': True}), + ] + + +class AuthorityKeyIdentifier(Sequence): + _fields = [ + ('key_identifier', OctetString, {'implicit': 0, 'optional': True}), + ('authority_cert_issuer', GeneralNames, {'implicit': 1, 'optional': True}), + ('authority_cert_serial_number', Integer, {'implicit': 2, 'optional': True}), + ] + + +class DistributionPointName(Choice): + _alternatives = [ + ('full_name', GeneralNames, {'implicit': 0}), + ('name_relative_to_crl_issuer', RelativeDistinguishedName, {'implicit': 1}), + ] + + +class ReasonFlags(BitString): + _map = { + 0: 'unused', + 1: 'key_compromise', + 2: 'ca_compromise', + 3: 'affiliation_changed', + 4: 'superseded', + 5: 'cessation_of_operation', + 6: 'certificate_hold', + 7: 'privilege_withdrawn', + 8: 'aa_compromise', + } + + +class GeneralSubtree(Sequence): + _fields = [ + ('base', GeneralName), + ('minimum', Integer, {'implicit': 0, 'default': 0}), + ('maximum', Integer, {'implicit': 1, 'optional': True}), + ] + + +class GeneralSubtrees(SequenceOf): + _child_spec = GeneralSubtree + + +class NameConstraints(Sequence): + _fields = [ + ('permitted_subtrees', GeneralSubtrees, {'implicit': 0, 'optional': True}), + ('excluded_subtrees', GeneralSubtrees, {'implicit': 1, 'optional': True}), + ] + + +class DistributionPoint(Sequence): + _fields = [ + ('distribution_point', DistributionPointName, {'explicit': 0, 'optional': True}), + ('reasons', ReasonFlags, {'implicit': 1, 'optional': True}), + ('crl_issuer', GeneralNames, {'implicit': 2, 'optional': True}), + ] + + _url = False + + @property + def url(self): + """ + :return: + None or a unicode string of the distribution point's URL + """ + + if self._url is False: + self._url = None + name = self['distribution_point'] + if name.name != 'full_name': + raise ValueError(unwrap( + ''' + CRL distribution points that are relative to the issuer are + not supported + ''' + )) + + for general_name in name.chosen: + if general_name.name == 'uniform_resource_identifier': + url = general_name.native + if url.lower().startswith(('http://', 'https://', 'ldap://', 'ldaps://')): + self._url = url + break + + return self._url + + +class CRLDistributionPoints(SequenceOf): + _child_spec = DistributionPoint + + +class DisplayText(Choice): + _alternatives = [ + ('ia5_string', IA5String), + ('visible_string', VisibleString), + ('bmp_string', BMPString), + ('utf8_string', UTF8String), + ] + + +class NoticeNumbers(SequenceOf): + _child_spec = Integer + + +class NoticeReference(Sequence): + _fields = [ + ('organization', DisplayText), + ('notice_numbers', NoticeNumbers), + ] + + +class UserNotice(Sequence): + _fields = [ + ('notice_ref', NoticeReference, {'optional': True}), + ('explicit_text', DisplayText, {'optional': True}), + ] + + +class PolicyQualifierId(ObjectIdentifier): + _map = { + '1.3.6.1.5.5.7.2.1': 'certification_practice_statement', + '1.3.6.1.5.5.7.2.2': 'user_notice', + } + + +class PolicyQualifierInfo(Sequence): + _fields = [ + ('policy_qualifier_id', PolicyQualifierId), + ('qualifier', Any), + ] + + _oid_pair = ('policy_qualifier_id', 'qualifier') + _oid_specs = { + 'certification_practice_statement': IA5String, + 'user_notice': UserNotice, + } + + +class PolicyQualifierInfos(SequenceOf): + _child_spec = PolicyQualifierInfo + + +class PolicyIdentifier(ObjectIdentifier): + _map = { + '2.5.29.32.0': 'any_policy', + } + + +class PolicyInformation(Sequence): + _fields = [ + ('policy_identifier', PolicyIdentifier), + ('policy_qualifiers', PolicyQualifierInfos, {'optional': True}) + ] + + +class CertificatePolicies(SequenceOf): + _child_spec = PolicyInformation + + +class PolicyMapping(Sequence): + _fields = [ + ('issuer_domain_policy', PolicyIdentifier), + ('subject_domain_policy', PolicyIdentifier), + ] + + +class PolicyMappings(SequenceOf): + _child_spec = PolicyMapping + + +class PolicyConstraints(Sequence): + _fields = [ + ('require_explicit_policy', Integer, {'implicit': 0, 'optional': True}), + ('inhibit_policy_mapping', Integer, {'implicit': 1, 'optional': True}), + ] + + +class KeyPurposeId(ObjectIdentifier): + _map = { + # https://tools.ietf.org/html/rfc5280#page-45 + '2.5.29.37.0': 'any_extended_key_usage', + '1.3.6.1.5.5.7.3.1': 'server_auth', + '1.3.6.1.5.5.7.3.2': 'client_auth', + '1.3.6.1.5.5.7.3.3': 'code_signing', + '1.3.6.1.5.5.7.3.4': 'email_protection', + '1.3.6.1.5.5.7.3.5': 'ipsec_end_system', + '1.3.6.1.5.5.7.3.6': 'ipsec_tunnel', + '1.3.6.1.5.5.7.3.7': 'ipsec_user', + '1.3.6.1.5.5.7.3.8': 'time_stamping', + '1.3.6.1.5.5.7.3.9': 'ocsp_signing', + # http://tools.ietf.org/html/rfc3029.html#page-9 + '1.3.6.1.5.5.7.3.10': 'dvcs', + # http://tools.ietf.org/html/rfc6268.html#page-16 + '1.3.6.1.5.5.7.3.13': 'eap_over_ppp', + '1.3.6.1.5.5.7.3.14': 'eap_over_lan', + # https://tools.ietf.org/html/rfc5055#page-76 + '1.3.6.1.5.5.7.3.15': 'scvp_server', + '1.3.6.1.5.5.7.3.16': 'scvp_client', + # https://tools.ietf.org/html/rfc4945#page-31 + '1.3.6.1.5.5.7.3.17': 'ipsec_ike', + # https://tools.ietf.org/html/rfc5415#page-38 + '1.3.6.1.5.5.7.3.18': 'capwap_ac', + '1.3.6.1.5.5.7.3.19': 'capwap_wtp', + # https://tools.ietf.org/html/rfc5924#page-8 + '1.3.6.1.5.5.7.3.20': 'sip_domain', + # https://tools.ietf.org/html/rfc6187#page-7 + '1.3.6.1.5.5.7.3.21': 'secure_shell_client', + '1.3.6.1.5.5.7.3.22': 'secure_shell_server', + # https://tools.ietf.org/html/rfc6494#page-7 + '1.3.6.1.5.5.7.3.23': 'send_router', + '1.3.6.1.5.5.7.3.24': 'send_proxied_router', + '1.3.6.1.5.5.7.3.25': 'send_owner', + '1.3.6.1.5.5.7.3.26': 'send_proxied_owner', + # https://tools.ietf.org/html/rfc6402#page-10 + '1.3.6.1.5.5.7.3.27': 'cmc_ca', + '1.3.6.1.5.5.7.3.28': 'cmc_ra', + '1.3.6.1.5.5.7.3.29': 'cmc_archive', + # https://tools.ietf.org/html/draft-ietf-sidr-bgpsec-pki-profiles-15#page-6 + '1.3.6.1.5.5.7.3.30': 'bgpspec_router', + # https://www.ietf.org/proceedings/44/I-D/draft-ietf-ipsec-pki-req-01.txt + '1.3.6.1.5.5.8.2.2': 'ike_intermediate', + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa378132(v=vs.85).aspx + # and https://support.microsoft.com/en-us/kb/287547 + '1.3.6.1.4.1.311.10.3.1': 'microsoft_trust_list_signing', + '1.3.6.1.4.1.311.10.3.2': 'microsoft_time_stamp_signing', + '1.3.6.1.4.1.311.10.3.3': 'microsoft_server_gated', + '1.3.6.1.4.1.311.10.3.3.1': 'microsoft_serialized', + '1.3.6.1.4.1.311.10.3.4': 'microsoft_efs', + '1.3.6.1.4.1.311.10.3.4.1': 'microsoft_efs_recovery', + '1.3.6.1.4.1.311.10.3.5': 'microsoft_whql', + '1.3.6.1.4.1.311.10.3.6': 'microsoft_nt5', + '1.3.6.1.4.1.311.10.3.7': 'microsoft_oem_whql', + '1.3.6.1.4.1.311.10.3.8': 'microsoft_embedded_nt', + '1.3.6.1.4.1.311.10.3.9': 'microsoft_root_list_signer', + '1.3.6.1.4.1.311.10.3.10': 'microsoft_qualified_subordination', + '1.3.6.1.4.1.311.10.3.11': 'microsoft_key_recovery', + '1.3.6.1.4.1.311.10.3.12': 'microsoft_document_signing', + '1.3.6.1.4.1.311.10.3.13': 'microsoft_lifetime_signing', + '1.3.6.1.4.1.311.10.3.14': 'microsoft_mobile_device_software', + # https://support.microsoft.com/en-us/help/287547/object-ids-associated-with-microsoft-cryptography + '1.3.6.1.4.1.311.20.2.2': 'microsoft_smart_card_logon', + # https://opensource.apple.com/source + # - /Security/Security-57031.40.6/Security/libsecurity_keychain/lib/SecPolicy.cpp + # - /libsecurity_cssm/libsecurity_cssm-36064/lib/oidsalg.c + '1.2.840.113635.100.1.2': 'apple_x509_basic', + '1.2.840.113635.100.1.3': 'apple_ssl', + '1.2.840.113635.100.1.4': 'apple_local_cert_gen', + '1.2.840.113635.100.1.5': 'apple_csr_gen', + '1.2.840.113635.100.1.6': 'apple_revocation_crl', + '1.2.840.113635.100.1.7': 'apple_revocation_ocsp', + '1.2.840.113635.100.1.8': 'apple_smime', + '1.2.840.113635.100.1.9': 'apple_eap', + '1.2.840.113635.100.1.10': 'apple_software_update_signing', + '1.2.840.113635.100.1.11': 'apple_ipsec', + '1.2.840.113635.100.1.12': 'apple_ichat', + '1.2.840.113635.100.1.13': 'apple_resource_signing', + '1.2.840.113635.100.1.14': 'apple_pkinit_client', + '1.2.840.113635.100.1.15': 'apple_pkinit_server', + '1.2.840.113635.100.1.16': 'apple_code_signing', + '1.2.840.113635.100.1.17': 'apple_package_signing', + '1.2.840.113635.100.1.18': 'apple_id_validation', + '1.2.840.113635.100.1.20': 'apple_time_stamping', + '1.2.840.113635.100.1.21': 'apple_revocation', + '1.2.840.113635.100.1.22': 'apple_passbook_signing', + '1.2.840.113635.100.1.23': 'apple_mobile_store', + '1.2.840.113635.100.1.24': 'apple_escrow_service', + '1.2.840.113635.100.1.25': 'apple_profile_signer', + '1.2.840.113635.100.1.26': 'apple_qa_profile_signer', + '1.2.840.113635.100.1.27': 'apple_test_mobile_store', + '1.2.840.113635.100.1.28': 'apple_otapki_signer', + '1.2.840.113635.100.1.29': 'apple_test_otapki_signer', + '1.2.840.113625.100.1.30': 'apple_id_validation_record_signing_policy', + '1.2.840.113625.100.1.31': 'apple_smp_encryption', + '1.2.840.113625.100.1.32': 'apple_test_smp_encryption', + '1.2.840.113635.100.1.33': 'apple_server_authentication', + '1.2.840.113635.100.1.34': 'apple_pcs_escrow_service', + # http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.201-2.pdf + '2.16.840.1.101.3.6.8': 'piv_card_authentication', + '2.16.840.1.101.3.6.7': 'piv_content_signing', + # https://tools.ietf.org/html/rfc4556.html + '1.3.6.1.5.2.3.4': 'pkinit_kpclientauth', + '1.3.6.1.5.2.3.5': 'pkinit_kpkdc', + # https://www.adobe.com/devnet-docs/acrobatetk/tools/DigSig/changes.html + '1.2.840.113583.1.1.5': 'adobe_authentic_documents_trust', + # https://www.idmanagement.gov/wp-content/uploads/sites/1171/uploads/fpki-pivi-cert-profiles.pdf + '2.16.840.1.101.3.8.7': 'fpki_pivi_content_signing' + } + + +class ExtKeyUsageSyntax(SequenceOf): + _child_spec = KeyPurposeId + + +class AccessMethod(ObjectIdentifier): + _map = { + '1.3.6.1.5.5.7.48.1': 'ocsp', + '1.3.6.1.5.5.7.48.2': 'ca_issuers', + '1.3.6.1.5.5.7.48.3': 'time_stamping', + '1.3.6.1.5.5.7.48.5': 'ca_repository', + } + + +class AccessDescription(Sequence): + _fields = [ + ('access_method', AccessMethod), + ('access_location', GeneralName), + ] + + +class AuthorityInfoAccessSyntax(SequenceOf): + _child_spec = AccessDescription + + +class SubjectInfoAccessSyntax(SequenceOf): + _child_spec = AccessDescription + + +# https://tools.ietf.org/html/rfc7633 +class Features(SequenceOf): + _child_spec = Integer + + +class EntrustVersionInfo(Sequence): + _fields = [ + ('entrust_vers', GeneralString), + ('entrust_info_flags', BitString) + ] + + +class NetscapeCertificateType(BitString): + _map = { + 0: 'ssl_client', + 1: 'ssl_server', + 2: 'email', + 3: 'object_signing', + 4: 'reserved', + 5: 'ssl_ca', + 6: 'email_ca', + 7: 'object_signing_ca', + } + + +class Version(Integer): + _map = { + 0: 'v1', + 1: 'v2', + 2: 'v3', + } + + +class TPMSpecification(Sequence): + _fields = [ + ('family', UTF8String), + ('level', Integer), + ('revision', Integer), + ] + + +class SetOfTPMSpecification(SetOf): + _child_spec = TPMSpecification + + +class TCGSpecificationVersion(Sequence): + _fields = [ + ('major_version', Integer), + ('minor_version', Integer), + ('revision', Integer), + ] + + +class TCGPlatformSpecification(Sequence): + _fields = [ + ('version', TCGSpecificationVersion), + ('platform_class', OctetString), + ] + + +class SetOfTCGPlatformSpecification(SetOf): + _child_spec = TCGPlatformSpecification + + +class EKGenerationType(Enumerated): + _map = { + 0: 'internal', + 1: 'injected', + 2: 'internal_revocable', + 3: 'injected_revocable', + } + + +class EKGenerationLocation(Enumerated): + _map = { + 0: 'tpm_manufacturer', + 1: 'platform_manufacturer', + 2: 'ek_cert_signer', + } + + +class EKCertificateGenerationLocation(Enumerated): + _map = { + 0: 'tpm_manufacturer', + 1: 'platform_manufacturer', + 2: 'ek_cert_signer', + } + + +class EvaluationAssuranceLevel(Enumerated): + _map = { + 1: 'level1', + 2: 'level2', + 3: 'level3', + 4: 'level4', + 5: 'level5', + 6: 'level6', + 7: 'level7', + } + + +class EvaluationStatus(Enumerated): + _map = { + 0: 'designed_to_meet', + 1: 'evaluation_in_progress', + 2: 'evaluation_completed', + } + + +class StrengthOfFunction(Enumerated): + _map = { + 0: 'basic', + 1: 'medium', + 2: 'high', + } + + +class URIReference(Sequence): + _fields = [ + ('uniform_resource_identifier', IA5String), + ('hash_algorithm', DigestAlgorithm, {'optional': True}), + ('hash_value', BitString, {'optional': True}), + ] + + +class CommonCriteriaMeasures(Sequence): + _fields = [ + ('version', IA5String), + ('assurance_level', EvaluationAssuranceLevel), + ('evaluation_status', EvaluationStatus), + ('plus', Boolean, {'default': False}), + ('strengh_of_function', StrengthOfFunction, {'implicit': 0, 'optional': True}), + ('profile_oid', ObjectIdentifier, {'implicit': 1, 'optional': True}), + ('profile_url', URIReference, {'implicit': 2, 'optional': True}), + ('target_oid', ObjectIdentifier, {'implicit': 3, 'optional': True}), + ('target_uri', URIReference, {'implicit': 4, 'optional': True}), + ] + + +class SecurityLevel(Enumerated): + _map = { + 1: 'level1', + 2: 'level2', + 3: 'level3', + 4: 'level4', + } + + +class FIPSLevel(Sequence): + _fields = [ + ('version', IA5String), + ('level', SecurityLevel), + ('plus', Boolean, {'default': False}), + ] + + +class TPMSecurityAssertions(Sequence): + _fields = [ + ('version', Version, {'default': 'v1'}), + ('field_upgradable', Boolean, {'default': False}), + ('ek_generation_type', EKGenerationType, {'implicit': 0, 'optional': True}), + ('ek_generation_location', EKGenerationLocation, {'implicit': 1, 'optional': True}), + ('ek_certificate_generation_location', EKCertificateGenerationLocation, {'implicit': 2, 'optional': True}), + ('cc_info', CommonCriteriaMeasures, {'implicit': 3, 'optional': True}), + ('fips_level', FIPSLevel, {'implicit': 4, 'optional': True}), + ('iso_9000_certified', Boolean, {'implicit': 5, 'default': False}), + ('iso_9000_uri', IA5String, {'optional': True}), + ] + + +class SetOfTPMSecurityAssertions(SetOf): + _child_spec = TPMSecurityAssertions + + +class SubjectDirectoryAttributeId(ObjectIdentifier): + _map = { + # https://tools.ietf.org/html/rfc2256#page-11 + '2.5.4.52': 'supported_algorithms', + # https://www.trustedcomputinggroup.org/wp-content/uploads/Credential_Profile_EK_V2.0_R14_published.pdf + '2.23.133.2.16': 'tpm_specification', + '2.23.133.2.17': 'tcg_platform_specification', + '2.23.133.2.18': 'tpm_security_assertions', + # https://tools.ietf.org/html/rfc3739#page-18 + '1.3.6.1.5.5.7.9.1': 'pda_date_of_birth', + '1.3.6.1.5.5.7.9.2': 'pda_place_of_birth', + '1.3.6.1.5.5.7.9.3': 'pda_gender', + '1.3.6.1.5.5.7.9.4': 'pda_country_of_citizenship', + '1.3.6.1.5.5.7.9.5': 'pda_country_of_residence', + # https://holtstrom.com/michael/tools/asn1decoder.php + '1.2.840.113533.7.68.29': 'entrust_user_role', + } + + +class SetOfGeneralizedTime(SetOf): + _child_spec = GeneralizedTime + + +class SetOfDirectoryString(SetOf): + _child_spec = DirectoryString + + +class SetOfPrintableString(SetOf): + _child_spec = PrintableString + + +class SupportedAlgorithm(Sequence): + _fields = [ + ('algorithm_identifier', AnyAlgorithmIdentifier), + ('intended_usage', KeyUsage, {'explicit': 0, 'optional': True}), + ('intended_certificate_policies', CertificatePolicies, {'explicit': 1, 'optional': True}), + ] + + +class SetOfSupportedAlgorithm(SetOf): + _child_spec = SupportedAlgorithm + + +class SubjectDirectoryAttribute(Sequence): + _fields = [ + ('type', SubjectDirectoryAttributeId), + ('values', Any), + ] + + _oid_pair = ('type', 'values') + _oid_specs = { + 'supported_algorithms': SetOfSupportedAlgorithm, + 'tpm_specification': SetOfTPMSpecification, + 'tcg_platform_specification': SetOfTCGPlatformSpecification, + 'tpm_security_assertions': SetOfTPMSecurityAssertions, + 'pda_date_of_birth': SetOfGeneralizedTime, + 'pda_place_of_birth': SetOfDirectoryString, + 'pda_gender': SetOfPrintableString, + 'pda_country_of_citizenship': SetOfPrintableString, + 'pda_country_of_residence': SetOfPrintableString, + } + + def _values_spec(self): + type_ = self['type'].native + if type_ in self._oid_specs: + return self._oid_specs[type_] + return SetOf + + _spec_callbacks = { + 'values': _values_spec + } + + +class SubjectDirectoryAttributes(SequenceOf): + _child_spec = SubjectDirectoryAttribute + + +class ExtensionId(ObjectIdentifier): + _map = { + '2.5.29.9': 'subject_directory_attributes', + '2.5.29.14': 'key_identifier', + '2.5.29.15': 'key_usage', + '2.5.29.16': 'private_key_usage_period', + '2.5.29.17': 'subject_alt_name', + '2.5.29.18': 'issuer_alt_name', + '2.5.29.19': 'basic_constraints', + '2.5.29.30': 'name_constraints', + '2.5.29.31': 'crl_distribution_points', + '2.5.29.32': 'certificate_policies', + '2.5.29.33': 'policy_mappings', + '2.5.29.35': 'authority_key_identifier', + '2.5.29.36': 'policy_constraints', + '2.5.29.37': 'extended_key_usage', + '2.5.29.46': 'freshest_crl', + '2.5.29.54': 'inhibit_any_policy', + '1.3.6.1.5.5.7.1.1': 'authority_information_access', + '1.3.6.1.5.5.7.1.11': 'subject_information_access', + # https://tools.ietf.org/html/rfc7633 + '1.3.6.1.5.5.7.1.24': 'tls_feature', + '1.3.6.1.5.5.7.48.1.5': 'ocsp_no_check', + '1.2.840.113533.7.65.0': 'entrust_version_extension', + '2.16.840.1.113730.1.1': 'netscape_certificate_type', + # https://tools.ietf.org/html/rfc6962.html#page-14 + '1.3.6.1.4.1.11129.2.4.2': 'signed_certificate_timestamp_list', + # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wcce/3aec3e50-511a-42f9-a5d5-240af503e470 + '1.3.6.1.4.1.311.20.2': 'microsoft_enroll_certtype', + } + + +class Extension(Sequence): + _fields = [ + ('extn_id', ExtensionId), + ('critical', Boolean, {'default': False}), + ('extn_value', ParsableOctetString), + ] + + _oid_pair = ('extn_id', 'extn_value') + _oid_specs = { + 'subject_directory_attributes': SubjectDirectoryAttributes, + 'key_identifier': OctetString, + 'key_usage': KeyUsage, + 'private_key_usage_period': PrivateKeyUsagePeriod, + 'subject_alt_name': GeneralNames, + 'issuer_alt_name': GeneralNames, + 'basic_constraints': BasicConstraints, + 'name_constraints': NameConstraints, + 'crl_distribution_points': CRLDistributionPoints, + 'certificate_policies': CertificatePolicies, + 'policy_mappings': PolicyMappings, + 'authority_key_identifier': AuthorityKeyIdentifier, + 'policy_constraints': PolicyConstraints, + 'extended_key_usage': ExtKeyUsageSyntax, + 'freshest_crl': CRLDistributionPoints, + 'inhibit_any_policy': Integer, + 'authority_information_access': AuthorityInfoAccessSyntax, + 'subject_information_access': SubjectInfoAccessSyntax, + 'tls_feature': Features, + 'ocsp_no_check': Null, + 'entrust_version_extension': EntrustVersionInfo, + 'netscape_certificate_type': NetscapeCertificateType, + 'signed_certificate_timestamp_list': OctetString, + # Not UTF8String as Microsofts docs claim, see: + # https://www.alvestrand.no/objectid/1.3.6.1.4.1.311.20.2.html + 'microsoft_enroll_certtype': BMPString, + } + + +class Extensions(SequenceOf): + _child_spec = Extension + + +class TbsCertificate(Sequence): + _fields = [ + ('version', Version, {'explicit': 0, 'default': 'v1'}), + ('serial_number', Integer), + ('signature', SignedDigestAlgorithm), + ('issuer', Name), + ('validity', Validity), + ('subject', Name), + ('subject_public_key_info', PublicKeyInfo), + ('issuer_unique_id', OctetBitString, {'implicit': 1, 'optional': True}), + ('subject_unique_id', OctetBitString, {'implicit': 2, 'optional': True}), + ('extensions', Extensions, {'explicit': 3, 'optional': True}), + ] + + +class Certificate(Sequence): + _fields = [ + ('tbs_certificate', TbsCertificate), + ('signature_algorithm', SignedDigestAlgorithm), + ('signature_value', OctetBitString), + ] + + _processed_extensions = False + _critical_extensions = None + _subject_directory_attributes_value = None + _key_identifier_value = None + _key_usage_value = None + _subject_alt_name_value = None + _issuer_alt_name_value = None + _basic_constraints_value = None + _name_constraints_value = None + _crl_distribution_points_value = None + _certificate_policies_value = None + _policy_mappings_value = None + _authority_key_identifier_value = None + _policy_constraints_value = None + _freshest_crl_value = None + _inhibit_any_policy_value = None + _extended_key_usage_value = None + _authority_information_access_value = None + _subject_information_access_value = None + _private_key_usage_period_value = None + _tls_feature_value = None + _ocsp_no_check_value = None + _issuer_serial = None + _authority_issuer_serial = False + _crl_distribution_points = None + _delta_crl_distribution_points = None + _valid_domains = None + _valid_ips = None + _self_issued = None + _self_signed = None + _sha1 = None + _sha256 = None + + def _set_extensions(self): + """ + Sets common named extensions to private attributes and creates a list + of critical extensions + """ + + self._critical_extensions = set() + + for extension in self['tbs_certificate']['extensions']: + name = extension['extn_id'].native + attribute_name = '_%s_value' % name + if hasattr(self, attribute_name): + setattr(self, attribute_name, extension['extn_value'].parsed) + if extension['critical'].native: + self._critical_extensions.add(name) + + self._processed_extensions = True + + @property + def critical_extensions(self): + """ + Returns a set of the names (or OID if not a known extension) of the + extensions marked as critical + + :return: + A set of unicode strings + """ + + if not self._processed_extensions: + self._set_extensions() + return self._critical_extensions + + @property + def private_key_usage_period_value(self): + """ + This extension is used to constrain the period over which the subject + private key may be used + + :return: + None or a PrivateKeyUsagePeriod object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._private_key_usage_period_value + + @property + def subject_directory_attributes_value(self): + """ + This extension is used to contain additional identification attributes + about the subject. + + :return: + None or a SubjectDirectoryAttributes object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._subject_directory_attributes_value + + @property + def key_identifier_value(self): + """ + This extension is used to help in creating certificate validation paths. + It contains an identifier that should generally, but is not guaranteed + to, be unique. + + :return: + None or an OctetString object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._key_identifier_value + + @property + def key_usage_value(self): + """ + This extension is used to define the purpose of the public key + contained within the certificate. + + :return: + None or a KeyUsage + """ + + if not self._processed_extensions: + self._set_extensions() + return self._key_usage_value + + @property + def subject_alt_name_value(self): + """ + This extension allows for additional names to be associate with the + subject of the certificate. While it may contain a whole host of + possible names, it is usually used to allow certificates to be used + with multiple different domain names. + + :return: + None or a GeneralNames object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._subject_alt_name_value + + @property + def issuer_alt_name_value(self): + """ + This extension allows associating one or more alternative names with + the issuer of the certificate. + + :return: + None or an x509.GeneralNames object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._issuer_alt_name_value + + @property + def basic_constraints_value(self): + """ + This extension is used to determine if the subject of the certificate + is a CA, and if so, what the maximum number of intermediate CA certs + after this are, before an end-entity certificate is found. + + :return: + None or a BasicConstraints object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._basic_constraints_value + + @property + def name_constraints_value(self): + """ + This extension is used in CA certificates, and is used to limit the + possible names of certificates issued. + + :return: + None or a NameConstraints object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._name_constraints_value + + @property + def crl_distribution_points_value(self): + """ + This extension is used to help in locating the CRL for this certificate. + + :return: + None or a CRLDistributionPoints object + extension + """ + + if not self._processed_extensions: + self._set_extensions() + return self._crl_distribution_points_value + + @property + def certificate_policies_value(self): + """ + This extension defines policies in CA certificates under which + certificates may be issued. In end-entity certificates, the inclusion + of a policy indicates the issuance of the certificate follows the + policy. + + :return: + None or a CertificatePolicies object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._certificate_policies_value + + @property + def policy_mappings_value(self): + """ + This extension allows mapping policy OIDs to other OIDs. This is used + to allow different policies to be treated as equivalent in the process + of validation. + + :return: + None or a PolicyMappings object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._policy_mappings_value + + @property + def authority_key_identifier_value(self): + """ + This extension helps in identifying the public key with which to + validate the authenticity of the certificate. + + :return: + None or an AuthorityKeyIdentifier object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._authority_key_identifier_value + + @property + def policy_constraints_value(self): + """ + This extension is used to control if policy mapping is allowed and + when policies are required. + + :return: + None or a PolicyConstraints object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._policy_constraints_value + + @property + def freshest_crl_value(self): + """ + This extension is used to help locate any available delta CRLs + + :return: + None or an CRLDistributionPoints object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._freshest_crl_value + + @property + def inhibit_any_policy_value(self): + """ + This extension is used to prevent mapping of the any policy to + specific requirements + + :return: + None or a Integer object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._inhibit_any_policy_value + + @property + def extended_key_usage_value(self): + """ + This extension is used to define additional purposes for the public key + beyond what is contained in the basic constraints. + + :return: + None or an ExtKeyUsageSyntax object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._extended_key_usage_value + + @property + def authority_information_access_value(self): + """ + This extension is used to locate the CA certificate used to sign this + certificate, or the OCSP responder for this certificate. + + :return: + None or an AuthorityInfoAccessSyntax object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._authority_information_access_value + + @property + def subject_information_access_value(self): + """ + This extension is used to access information about the subject of this + certificate. + + :return: + None or a SubjectInfoAccessSyntax object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._subject_information_access_value + + @property + def tls_feature_value(self): + """ + This extension is used to list the TLS features a server must respond + with if a client initiates a request supporting them. + + :return: + None or a Features object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._tls_feature_value + + @property + def ocsp_no_check_value(self): + """ + This extension is used on certificates of OCSP responders, indicating + that revocation information for the certificate should never need to + be verified, thus preventing possible loops in path validation. + + :return: + None or a Null object (if present) + """ + + if not self._processed_extensions: + self._set_extensions() + return self._ocsp_no_check_value + + @property + def signature(self): + """ + :return: + A byte string of the signature + """ + + return self['signature_value'].native + + @property + def signature_algo(self): + """ + :return: + A unicode string of "rsassa_pkcs1v15", "rsassa_pss", "dsa", "ecdsa" + """ + + return self['signature_algorithm'].signature_algo + + @property + def hash_algo(self): + """ + :return: + A unicode string of "md2", "md5", "sha1", "sha224", "sha256", + "sha384", "sha512", "sha512_224", "sha512_256" + """ + + return self['signature_algorithm'].hash_algo + + @property + def public_key(self): + """ + :return: + The PublicKeyInfo object for this certificate + """ + + return self['tbs_certificate']['subject_public_key_info'] + + @property + def subject(self): + """ + :return: + The Name object for the subject of this certificate + """ + + return self['tbs_certificate']['subject'] + + @property + def issuer(self): + """ + :return: + The Name object for the issuer of this certificate + """ + + return self['tbs_certificate']['issuer'] + + @property + def serial_number(self): + """ + :return: + An integer of the certificate's serial number + """ + + return self['tbs_certificate']['serial_number'].native + + @property + def key_identifier(self): + """ + :return: + None or a byte string of the certificate's key identifier from the + key identifier extension + """ + + if not self.key_identifier_value: + return None + + return self.key_identifier_value.native + + @property + def issuer_serial(self): + """ + :return: + A byte string of the SHA-256 hash of the issuer concatenated with + the ascii character ":", concatenated with the serial number as + an ascii string + """ + + if self._issuer_serial is None: + self._issuer_serial = self.issuer.sha256 + b':' + str_cls(self.serial_number).encode('ascii') + return self._issuer_serial + + @property + def not_valid_after(self): + """ + :return: + A datetime of latest time when the certificate is still valid + """ + return self['tbs_certificate']['validity']['not_after'].native + + @property + def not_valid_before(self): + """ + :return: + A datetime of the earliest time when the certificate is valid + """ + return self['tbs_certificate']['validity']['not_before'].native + + @property + def authority_key_identifier(self): + """ + :return: + None or a byte string of the key_identifier from the authority key + identifier extension + """ + + if not self.authority_key_identifier_value: + return None + + return self.authority_key_identifier_value['key_identifier'].native + + @property + def authority_issuer_serial(self): + """ + :return: + None or a byte string of the SHA-256 hash of the isser from the + authority key identifier extension concatenated with the ascii + character ":", concatenated with the serial number from the + authority key identifier extension as an ascii string + """ + + if self._authority_issuer_serial is False: + akiv = self.authority_key_identifier_value + if akiv and akiv['authority_cert_issuer'].native: + issuer = self.authority_key_identifier_value['authority_cert_issuer'][0].chosen + # We untag the element since it is tagged via being a choice from GeneralName + issuer = issuer.untag() + authority_serial = self.authority_key_identifier_value['authority_cert_serial_number'].native + self._authority_issuer_serial = issuer.sha256 + b':' + str_cls(authority_serial).encode('ascii') + else: + self._authority_issuer_serial = None + return self._authority_issuer_serial + + @property + def crl_distribution_points(self): + """ + Returns complete CRL URLs - does not include delta CRLs + + :return: + A list of zero or more DistributionPoint objects + """ + + if self._crl_distribution_points is None: + self._crl_distribution_points = self._get_http_crl_distribution_points(self.crl_distribution_points_value) + return self._crl_distribution_points + + @property + def delta_crl_distribution_points(self): + """ + Returns delta CRL URLs - does not include complete CRLs + + :return: + A list of zero or more DistributionPoint objects + """ + + if self._delta_crl_distribution_points is None: + self._delta_crl_distribution_points = self._get_http_crl_distribution_points(self.freshest_crl_value) + return self._delta_crl_distribution_points + + def _get_http_crl_distribution_points(self, crl_distribution_points): + """ + Fetches the DistributionPoint object for non-relative, HTTP CRLs + referenced by the certificate + + :param crl_distribution_points: + A CRLDistributionPoints object to grab the DistributionPoints from + + :return: + A list of zero or more DistributionPoint objects + """ + + output = [] + + if crl_distribution_points is None: + return [] + + for distribution_point in crl_distribution_points: + distribution_point_name = distribution_point['distribution_point'] + if distribution_point_name is VOID: + continue + # RFC 5280 indicates conforming CA should not use the relative form + if distribution_point_name.name == 'name_relative_to_crl_issuer': + continue + # This library is currently only concerned with HTTP-based CRLs + for general_name in distribution_point_name.chosen: + if general_name.name == 'uniform_resource_identifier': + output.append(distribution_point) + + return output + + @property + def ocsp_urls(self): + """ + :return: + A list of zero or more unicode strings of the OCSP URLs for this + cert + """ + + if not self.authority_information_access_value: + return [] + + output = [] + for entry in self.authority_information_access_value: + if entry['access_method'].native == 'ocsp': + location = entry['access_location'] + if location.name != 'uniform_resource_identifier': + continue + url = location.native + if url.lower().startswith(('http://', 'https://', 'ldap://', 'ldaps://')): + output.append(url) + return output + + @property + def valid_domains(self): + """ + :return: + A list of unicode strings of valid domain names for the certificate. + Wildcard certificates will have a domain in the form: *.example.com + """ + + if self._valid_domains is None: + self._valid_domains = [] + + # For the subject alt name extension, we can look at the name of + # the choice selected since it distinguishes between domain names, + # email addresses, IPs, etc + if self.subject_alt_name_value: + for general_name in self.subject_alt_name_value: + if general_name.name == 'dns_name' and general_name.native not in self._valid_domains: + self._valid_domains.append(general_name.native) + + # If there was no subject alt name extension, and the common name + # in the subject looks like a domain, that is considered the valid + # list. This is done because according to + # https://tools.ietf.org/html/rfc6125#section-6.4.4, the common + # name should not be used if the subject alt name is present. + else: + pattern = re.compile('^(\\*\\.)?(?:[a-zA-Z0-9](?:[a-zA-Z0-9\\-]*[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$') + for rdn in self.subject.chosen: + for name_type_value in rdn: + if name_type_value['type'].native == 'common_name': + value = name_type_value['value'].native + if pattern.match(value): + self._valid_domains.append(value) + + return self._valid_domains + + @property + def valid_ips(self): + """ + :return: + A list of unicode strings of valid IP addresses for the certificate + """ + + if self._valid_ips is None: + self._valid_ips = [] + + if self.subject_alt_name_value: + for general_name in self.subject_alt_name_value: + if general_name.name == 'ip_address': + self._valid_ips.append(general_name.native) + + return self._valid_ips + + @property + def ca(self): + """ + :return; + A boolean - if the certificate is marked as a CA + """ + + return self.basic_constraints_value and self.basic_constraints_value['ca'].native + + @property + def max_path_length(self): + """ + :return; + None or an integer of the maximum path length + """ + + if not self.ca: + return None + return self.basic_constraints_value['path_len_constraint'].native + + @property + def self_issued(self): + """ + :return: + A boolean - if the certificate is self-issued, as defined by RFC + 5280 + """ + + if self._self_issued is None: + self._self_issued = self.subject == self.issuer + return self._self_issued + + @property + def self_signed(self): + """ + :return: + A unicode string of "no" or "maybe". The "maybe" result will + be returned if the certificate issuer and subject are the same. + If a key identifier and authority key identifier are present, + they will need to match otherwise "no" will be returned. + + To verify is a certificate is truly self-signed, the signature + will need to be verified. See the certvalidator package for + one possible solution. + """ + + if self._self_signed is None: + self._self_signed = 'no' + if self.self_issued: + if self.key_identifier: + if not self.authority_key_identifier: + self._self_signed = 'maybe' + elif self.authority_key_identifier == self.key_identifier: + self._self_signed = 'maybe' + else: + self._self_signed = 'maybe' + return self._self_signed + + @property + def sha1(self): + """ + :return: + The SHA-1 hash of the DER-encoded bytes of this complete certificate + """ + + if self._sha1 is None: + self._sha1 = hashlib.sha1(self.dump()).digest() + return self._sha1 + + @property + def sha1_fingerprint(self): + """ + :return: + A unicode string of the SHA-1 hash, formatted using hex encoding + with a space between each pair of characters, all uppercase + """ + + return ' '.join('%02X' % c for c in bytes_to_list(self.sha1)) + + @property + def sha256(self): + """ + :return: + The SHA-256 hash of the DER-encoded bytes of this complete + certificate + """ + + if self._sha256 is None: + self._sha256 = hashlib.sha256(self.dump()).digest() + return self._sha256 + + @property + def sha256_fingerprint(self): + """ + :return: + A unicode string of the SHA-256 hash, formatted using hex encoding + with a space between each pair of characters, all uppercase + """ + + return ' '.join('%02X' % c for c in bytes_to_list(self.sha256)) + + def is_valid_domain_ip(self, domain_ip): + """ + Check if a domain name or IP address is valid according to the + certificate + + :param domain_ip: + A unicode string of a domain name or IP address + + :return: + A boolean - if the domain or IP is valid for the certificate + """ + + if not isinstance(domain_ip, str_cls): + raise TypeError(unwrap( + ''' + domain_ip must be a unicode string, not %s + ''', + type_name(domain_ip) + )) + + encoded_domain_ip = domain_ip.encode('idna').decode('ascii').lower() + + is_ipv6 = encoded_domain_ip.find(':') != -1 + is_ipv4 = not is_ipv6 and re.match('^\\d+\\.\\d+\\.\\d+\\.\\d+$', encoded_domain_ip) + is_domain = not is_ipv6 and not is_ipv4 + + # Handle domain name checks + if is_domain: + if not self.valid_domains: + return False + + domain_labels = encoded_domain_ip.split('.') + + for valid_domain in self.valid_domains: + encoded_valid_domain = valid_domain.encode('idna').decode('ascii').lower() + valid_domain_labels = encoded_valid_domain.split('.') + + # The domain must be equal in label length to match + if len(valid_domain_labels) != len(domain_labels): + continue + + if valid_domain_labels == domain_labels: + return True + + is_wildcard = self._is_wildcard_domain(encoded_valid_domain) + if is_wildcard and self._is_wildcard_match(domain_labels, valid_domain_labels): + return True + + return False + + # Handle IP address checks + if not self.valid_ips: + return False + + family = socket.AF_INET if is_ipv4 else socket.AF_INET6 + normalized_ip = inet_pton(family, encoded_domain_ip) + + for valid_ip in self.valid_ips: + valid_family = socket.AF_INET if valid_ip.find('.') != -1 else socket.AF_INET6 + normalized_valid_ip = inet_pton(valid_family, valid_ip) + + if normalized_valid_ip == normalized_ip: + return True + + return False + + def _is_wildcard_domain(self, domain): + """ + Checks if a domain is a valid wildcard according to + https://tools.ietf.org/html/rfc6125#section-6.4.3 + + :param domain: + A unicode string of the domain name, where any U-labels from an IDN + have been converted to A-labels + + :return: + A boolean - if the domain is a valid wildcard domain + """ + + # The * character must be present for a wildcard match, and if there is + # most than one, it is an invalid wildcard specification + if domain.count('*') != 1: + return False + + labels = domain.lower().split('.') + + if not labels: + return False + + # Wildcards may only appear in the left-most label + if labels[0].find('*') == -1: + return False + + # Wildcards may not be embedded in an A-label from an IDN + if labels[0][0:4] == 'xn--': + return False + + return True + + def _is_wildcard_match(self, domain_labels, valid_domain_labels): + """ + Determines if the labels in a domain are a match for labels from a + wildcard valid domain name + + :param domain_labels: + A list of unicode strings, with A-label form for IDNs, of the labels + in the domain name to check + + :param valid_domain_labels: + A list of unicode strings, with A-label form for IDNs, of the labels + in a wildcard domain pattern + + :return: + A boolean - if the domain matches the valid domain + """ + + first_domain_label = domain_labels[0] + other_domain_labels = domain_labels[1:] + + wildcard_label = valid_domain_labels[0] + other_valid_domain_labels = valid_domain_labels[1:] + + # The wildcard is only allowed in the first label, so if + # The subsequent labels are not equal, there is no match + if other_domain_labels != other_valid_domain_labels: + return False + + if wildcard_label == '*': + return True + + wildcard_regex = re.compile('^' + wildcard_label.replace('*', '.*') + '$') + if wildcard_regex.match(first_domain_label): + return True + + return False + + +# The structures are taken from the OpenSSL source file x_x509a.c, and specify +# extra information that is added to X.509 certificates to store trust +# information about the certificate. + +class KeyPurposeIdentifiers(SequenceOf): + _child_spec = KeyPurposeId + + +class SequenceOfAlgorithmIdentifiers(SequenceOf): + _child_spec = AlgorithmIdentifier + + +class CertificateAux(Sequence): + _fields = [ + ('trust', KeyPurposeIdentifiers, {'optional': True}), + ('reject', KeyPurposeIdentifiers, {'implicit': 0, 'optional': True}), + ('alias', UTF8String, {'optional': True}), + ('keyid', OctetString, {'optional': True}), + ('other', SequenceOfAlgorithmIdentifiers, {'implicit': 1, 'optional': True}), + ] + + +class TrustedCertificate(Concat): + _child_specs = [Certificate, CertificateAux] diff --git a/tasks/lib/package_control/deps/oscrypto/__init__.py b/tasks/lib/package_control/deps/oscrypto/__init__.py new file mode 100644 index 0000000..eb27313 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/__init__.py @@ -0,0 +1,304 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import os +import platform +import sys +import threading + +from ._types import str_cls, type_name +from .errors import LibraryNotFoundError +from .version import __version__, __version_info__ + + +__all__ = [ + '__version__', + '__version_info__', + 'backend', + 'ffi', + 'load_order', + 'use_ctypes', + 'use_openssl', + 'use_winlegacy', +] + + +_backend_lock = threading.Lock() +_module_values = { + 'backend': None, + 'backend_config': None, + 'ffi': None +} + + +def backend(): + """ + :return: + A unicode string of the backend being used: "openssl", "mac", "win", + "winlegacy" + """ + + if _module_values['backend'] is not None: + return _module_values['backend'] + + with _backend_lock: + if _module_values['backend'] is not None: + return _module_values['backend'] + + if sys.platform == 'win32': + # Windows XP was major version 5, Vista was 6 + if sys.getwindowsversion()[0] < 6: + _module_values['backend'] = 'winlegacy' + else: + _module_values['backend'] = 'win' + elif sys.platform == 'darwin': + _module_values['backend'] = 'mac' + else: + _module_values['backend'] = 'openssl' + + return _module_values['backend'] + + +def _backend_config(): + """ + :return: + A dict of config info for the backend. Only currently used by "openssl", + it may contains zero or more of the following keys: + - "libcrypto_path" + - "libssl_path" + """ + + if backend() != 'openssl': + return {} + + if _module_values['backend_config'] is not None: + return _module_values['backend_config'] + + with _backend_lock: + if _module_values['backend_config'] is not None: + return _module_values['backend_config'] + + _module_values['backend_config'] = {} + return _module_values['backend_config'] + + +def use_openssl(libcrypto_path, libssl_path, trust_list_path=None): + """ + Forces using OpenSSL dynamic libraries on OS X (.dylib) or Windows (.dll), + or using a specific dynamic library on Linux/BSD (.so). + + This can also be used to configure oscrypto to use LibreSSL dynamic + libraries. + + This method must be called before any oscrypto submodules are imported. + + :param libcrypto_path: + A unicode string of the file path to the OpenSSL/LibreSSL libcrypto + dynamic library. + + :param libssl_path: + A unicode string of the file path to the OpenSSL/LibreSSL libssl + dynamic library. + + :param trust_list_path: + An optional unicode string of the path to a file containing + OpenSSL-compatible CA certificates in PEM format. If this is not + provided and the platform is OS X or Windows, the system trust roots + will be exported from the OS and used for all TLS connections. + + :raises: + ValueError - when one of the paths is not a unicode string + OSError - when the trust_list_path does not exist on the filesystem + oscrypto.errors.LibraryNotFoundError - when one of the path does not exist on the filesystem + RuntimeError - when this function is called after another part of oscrypto has been imported + """ + + if not isinstance(libcrypto_path, str_cls): + raise ValueError('libcrypto_path must be a unicode string, not %s' % type_name(libcrypto_path)) + + if not isinstance(libssl_path, str_cls): + raise ValueError('libssl_path must be a unicode string, not %s' % type_name(libssl_path)) + + if not os.path.exists(libcrypto_path): + raise LibraryNotFoundError('libcrypto does not exist at %s' % libcrypto_path) + + if not os.path.exists(libssl_path): + raise LibraryNotFoundError('libssl does not exist at %s' % libssl_path) + + if trust_list_path is not None: + if not isinstance(trust_list_path, str_cls): + raise ValueError('trust_list_path must be a unicode string, not %s' % type_name(trust_list_path)) + if not os.path.exists(trust_list_path): + raise OSError('trust_list_path does not exist at %s' % trust_list_path) + + with _backend_lock: + new_config = { + 'libcrypto_path': libcrypto_path, + 'libssl_path': libssl_path, + 'trust_list_path': trust_list_path, + } + + if _module_values['backend'] == 'openssl' and _module_values['backend_config'] == new_config: + return + + if _module_values['backend'] is not None: + raise RuntimeError('Another part of oscrypto has already been imported, unable to force use of OpenSSL') + + _module_values['backend'] = 'openssl' + _module_values['backend_config'] = new_config + + +def use_winlegacy(): + """ + Forces use of the legacy Windows CryptoAPI. This should only be used on + Windows XP or for testing. It is less full-featured than the Cryptography + Next Generation (CNG) API, and as a result the elliptic curve and PSS + padding features are implemented in pure Python. This isn't ideal, but it + a shim for end-user client code. No one is going to run a server on Windows + XP anyway, right?! + + :raises: + EnvironmentError - when this function is called on an operating system other than Windows + RuntimeError - when this function is called after another part of oscrypto has been imported + """ + + if sys.platform != 'win32': + plat = platform.system() or sys.platform + if plat == 'Darwin': + plat = 'OS X' + raise EnvironmentError('The winlegacy backend can only be used on Windows, not %s' % plat) + + with _backend_lock: + if _module_values['backend'] == 'winlegacy': + return + + if _module_values['backend'] is not None: + raise RuntimeError( + 'Another part of oscrypto has already been imported, unable to force use of Windows legacy CryptoAPI' + ) + + _module_values['backend'] = 'winlegacy' + + +def use_ctypes(): + """ + Forces use of ctypes instead of cffi for the FFI layer + + :raises: + RuntimeError - when this function is called after another part of oscrypto has been imported + """ + + with _backend_lock: + if _module_values['ffi'] == 'ctypes': + return + + if _module_values['backend'] is not None: + raise RuntimeError( + 'Another part of oscrypto has already been imported, unable to force use of ctypes' + ) + + _module_values['ffi'] = 'ctypes' + + +def ffi(): + """ + Returns the FFI module being used + + :return: + A unicode string of "cffi" or "ctypes" + """ + + if _module_values['ffi'] is not None: + return _module_values['ffi'] + + with _backend_lock: + try: + import cffi # noqa: F401 + _module_values['ffi'] = 'cffi' + except (ImportError): + _module_values['ffi'] = 'ctypes' + + return _module_values['ffi'] + + +def load_order(): + """ + Returns a list of the module and sub-module names for oscrypto in + dependency load order, for the sake of live reloading code + + :return: + A list of unicode strings of module names, as they would appear in + sys.modules, ordered by which module should be reloaded first + """ + + return [ + 'oscrypto._asn1', + 'oscrypto._cipher_suites', + 'oscrypto._errors', + 'oscrypto._int', + 'oscrypto._types', + 'oscrypto.errors', + 'oscrypto.version', + 'oscrypto', + 'oscrypto._ffi', + 'oscrypto._pkcs12', + 'oscrypto._pkcs5', + 'oscrypto._rand', + 'oscrypto._tls', + 'oscrypto._linux_bsd.trust_list', + 'oscrypto._mac._common_crypto_cffi', + 'oscrypto._mac._common_crypto_ctypes', + 'oscrypto._mac._common_crypto', + 'oscrypto._mac._core_foundation_cffi', + 'oscrypto._mac._core_foundation_ctypes', + 'oscrypto._mac._core_foundation', + 'oscrypto._mac._security_cffi', + 'oscrypto._mac._security_ctypes', + 'oscrypto._mac._security', + 'oscrypto._mac.trust_list', + 'oscrypto._mac.util', + 'oscrypto._openssl._libcrypto_cffi', + 'oscrypto._openssl._libcrypto_ctypes', + 'oscrypto._openssl._libcrypto', + 'oscrypto._openssl._libssl_cffi', + 'oscrypto._openssl._libssl_ctypes', + 'oscrypto._openssl._libssl', + 'oscrypto._openssl.util', + 'oscrypto._win._cng_cffi', + 'oscrypto._win._cng_ctypes', + 'oscrypto._win._cng', + 'oscrypto._win._decode', + 'oscrypto._win._advapi32_cffi', + 'oscrypto._win._advapi32_ctypes', + 'oscrypto._win._advapi32', + 'oscrypto._win._kernel32_cffi', + 'oscrypto._win._kernel32_ctypes', + 'oscrypto._win._kernel32', + 'oscrypto._win._secur32_cffi', + 'oscrypto._win._secur32_ctypes', + 'oscrypto._win._secur32', + 'oscrypto._win._crypt32_cffi', + 'oscrypto._win._crypt32_ctypes', + 'oscrypto._win._crypt32', + 'oscrypto._win.trust_list', + 'oscrypto._win.util', + 'oscrypto.trust_list', + 'oscrypto.util', + 'oscrypto.kdf', + 'oscrypto._mac.symmetric', + 'oscrypto._openssl.symmetric', + 'oscrypto._win.symmetric', + 'oscrypto.symmetric', + 'oscrypto._asymmetric', + 'oscrypto._ecdsa', + 'oscrypto._pkcs1', + 'oscrypto._mac.asymmetric', + 'oscrypto._openssl.asymmetric', + 'oscrypto._win.asymmetric', + 'oscrypto.asymmetric', + 'oscrypto.keys', + 'oscrypto._mac.tls', + 'oscrypto._openssl.tls', + 'oscrypto._win.tls', + 'oscrypto.tls', + ] diff --git a/tasks/lib/package_control/deps/oscrypto/_asn1.py b/tasks/lib/package_control/deps/oscrypto/_asn1.py new file mode 100644 index 0000000..e52edcc --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_asn1.py @@ -0,0 +1,80 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +# This file exists strictly to make it easier to vendor a combination of +# oscrypto and asn1crypto + +from ..asn1crypto import algos, cms, core, keys, pem, pkcs12, util, x509 + +DHParameters = algos.DHParameters +DSASignature = algos.DSASignature +KeyExchangeAlgorithm = algos.KeyExchangeAlgorithm +Pbkdf2Salt = algos.Pbkdf2Salt + +EncryptedData = cms.EncryptedData + +Integer = core.Integer +Null = core.Null +OctetString = core.OctetString + +DSAParams = keys.DSAParams +DSAPrivateKey = keys.DSAPrivateKey +ECDomainParameters = keys.ECDomainParameters +ECPointBitString = keys.ECPointBitString +ECPrivateKey = keys.ECPrivateKey +EncryptedPrivateKeyInfo = keys.EncryptedPrivateKeyInfo +PrivateKeyAlgorithm = keys.PrivateKeyAlgorithm +PrivateKeyInfo = keys.PrivateKeyInfo +PublicKeyAlgorithm = keys.PublicKeyAlgorithm +PublicKeyInfo = keys.PublicKeyInfo +RSAPrivateKey = keys.RSAPrivateKey +RSAPublicKey = keys.RSAPublicKey + +int_from_bytes = util.int_from_bytes +int_to_bytes = util.int_to_bytes +OrderedDict = util.OrderedDict +timezone = util.timezone + +armor = pem.armor +unarmor = pem.unarmor + +CertBag = pkcs12.CertBag +Pfx = pkcs12.Pfx +SafeContents = pkcs12.SafeContents + +Certificate = x509.Certificate +TrustedCertificate = x509.TrustedCertificate + +__all__ = [ + 'armor', + 'CertBag', + 'Certificate', + 'DHParameters', + 'DSAParams', + 'DSAPrivateKey', + 'DSASignature', + 'ECDomainParameters', + 'ECPointBitString', + 'ECPrivateKey', + 'EncryptedData', + 'EncryptedPrivateKeyInfo', + 'int_from_bytes', + 'int_to_bytes', + 'Integer', + 'KeyExchangeAlgorithm', + 'Null', + 'OctetString', + 'OrderedDict', + 'Pbkdf2Salt', + 'Pfx', + 'PrivateKeyAlgorithm', + 'PrivateKeyInfo', + 'PublicKeyAlgorithm', + 'PublicKeyInfo', + 'RSAPrivateKey', + 'RSAPublicKey', + 'SafeContents', + 'timezone', + 'TrustedCertificate', + 'unarmor', +] diff --git a/tasks/lib/package_control/deps/oscrypto/_asymmetric.py b/tasks/lib/package_control/deps/oscrypto/_asymmetric.py new file mode 100644 index 0000000..aec0385 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_asymmetric.py @@ -0,0 +1,1040 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import hashlib +import hmac +import re +import binascii + +from ._asn1 import ( + CertBag, + Certificate, + DSAPrivateKey, + ECPrivateKey, + EncryptedData, + EncryptedPrivateKeyInfo, + Integer, + OctetString, + Pfx, + PrivateKeyInfo, + PublicKeyInfo, + RSAPrivateKey, + RSAPublicKey, + SafeContents, + unarmor, +) + +from .kdf import pbkdf1, pbkdf2, pkcs12_kdf +from .symmetric import ( + aes_cbc_pkcs7_decrypt, + des_cbc_pkcs5_decrypt, + rc2_cbc_pkcs5_decrypt, + rc4_decrypt, + tripledes_cbc_pkcs5_decrypt, +) +from .util import constant_compare +from ._errors import pretty_message +from ._types import byte_cls, str_cls, type_name + + +class _PrivateKeyBase(): + + asn1 = None + _fingerprint = None + + def unwrap(self): + """ + Unwraps the private key into an asn1crypto.keys.RSAPrivateKey, + asn1crypto.keys.DSAPrivateKey or asn1crypto.keys.ECPrivateKey object + + :return: + An asn1crypto.keys.RSAPrivateKey, asn1crypto.keys.DSAPrivateKey or + asn1crypto.keys.ECPrivateKey object + """ + + if self.algorithm == 'rsa': + return self.asn1['private_key'].parsed + + if self.algorithm == 'dsa': + params = self.asn1['private_key_algorithm']['parameters'] + return DSAPrivateKey({ + 'version': 0, + 'p': params['p'], + 'q': params['q'], + 'g': params['g'], + 'public_key': self.public_key.unwrap(), + 'private_key': self.asn1['private_key'].parsed, + }) + + if self.algorithm == 'ec': + output = self.asn1['private_key'].parsed + output['parameters'] = self.asn1['private_key_algorithm']['parameters'] + output['public_key'] = self.public_key.unwrap() + return output + + @property + def algorithm(self): + """ + :return: + A unicode string of "rsa", "dsa" or "ec" + """ + + return self.asn1.algorithm + + @property + def curve(self): + """ + :return: + A unicode string of EC curve name + """ + + return self.asn1.curve[1] + + @property + def bit_size(self): + """ + :return: + The number of bits in the key, as an integer + """ + + return self.asn1.bit_size + + @property + def byte_size(self): + """ + :return: + The number of bytes in the key, as an integer + """ + + return self.asn1.byte_size + + +class _PublicKeyBase(): + + asn1 = None + _fingerprint = None + + def unwrap(self): + """ + Unwraps a public key into an asn1crypto.keys.RSAPublicKey, + asn1crypto.core.Integer (for DSA) or asn1crypto.keys.ECPointBitString + object + + :return: + An asn1crypto.keys.RSAPublicKey, asn1crypto.core.Integer or + asn1crypto.keys.ECPointBitString object + """ + + if self.algorithm == 'ec': + return self.asn1['public_key'] + return self.asn1['public_key'].parsed + + @property + def fingerprint(self): + """ + Creates a fingerprint that can be compared with a private key to see if + the two form a pair. + + This fingerprint is not compatible with fingerprints generated by any + other software. + + :return: + A byte string that is a sha256 hash of selected components (based + on the key type) + """ + + if self._fingerprint is None: + self._fingerprint = _fingerprint(self.asn1, None) + return self._fingerprint + + @property + def algorithm(self): + """ + :return: + A unicode string of "rsa", "dsa" or "ec" + """ + + return self.asn1.algorithm + + @property + def curve(self): + """ + :return: + A unicode string of EC curve name + """ + + return self.asn1.curve[1] + + @property + def bit_size(self): + """ + :return: + The number of bits in the key, as an integer + """ + + return self.asn1.bit_size + + @property + def byte_size(self): + """ + :return: + The number of bytes in the key, as an integer + """ + + return self.asn1.byte_size + + +class _CertificateBase(): + + asn1 = None + + @property + def algorithm(self): + """ + :return: + A unicode string of "rsa", "dsa" or "ec" + """ + + return self.public_key.algorithm + + @property + def curve(self): + """ + :return: + A unicode string of EC curve name + """ + + return self.public_key.curve + + @property + def bit_size(self): + """ + :return: + The number of bits in the public key, as an integer + """ + + return self.public_key.bit_size + + @property + def byte_size(self): + """ + :return: + The number of bytes in the public key, as an integer + """ + + return self.public_key.byte_size + + +def _unwrap_private_key_info(key_info): + """ + Unwraps an asn1crypto.keys.PrivateKeyInfo object into an + asn1crypto.keys.RSAPrivateKey, asn1crypto.keys.DSAPrivateKey + or asn1crypto.keys.ECPrivateKey. + + :param key_info: + An asn1crypto.keys.PrivateKeyInfo object + + :return: + One of: + - asn1crypto.keys.RSAPrivateKey + - asn1crypto.keys.DSAPrivateKey + - asn1crypto.keys.ECPrivateKey + """ + + key_alg = key_info.algorithm + + if key_alg == 'rsa' or key_alg == 'rsassa_pss': + return key_info['private_key'].parsed + + if key_alg == 'dsa': + params = key_info['private_key_algorithm']['parameters'] + parsed = key_info['private_key'].parsed + return DSAPrivateKey({ + 'version': 0, + 'p': params['p'], + 'q': params['q'], + 'g': params['g'], + 'public_key': Integer(pow( + params['g'].native, + parsed.native, + params['p'].native + )), + 'private_key': parsed, + }) + + if key_alg == 'ec': + parsed = key_info['private_key'].parsed + parsed['parameters'] = key_info['private_key_algorithm']['parameters'] + return parsed + + raise ValueError('Unsupported key_info.algorithm "%s"' % key_info.algorithm) + + +def _fingerprint(key_object, load_private_key): + """ + Returns a fingerprint used for correlating public keys and private keys + + :param key_object: + An asn1crypto.keys.PrivateKeyInfo or asn1crypto.keys.PublicKeyInfo + + :raises: + ValueError - when the key_object is not of the proper type + + ;return: + A byte string fingerprint + """ + + if isinstance(key_object, PrivateKeyInfo): + key = key_object['private_key'].parsed + + if key_object.algorithm == 'rsa': + to_hash = '%d:%d' % ( + key['modulus'].native, + key['public_exponent'].native, + ) + + elif key_object.algorithm == 'dsa': + params = key_object['private_key_algorithm']['parameters'] + public_key = Integer(pow( + params['g'].native, + key_object['private_key'].parsed.native, + params['p'].native + )) + + to_hash = '%d:%d:%d:%d' % ( + params['p'].native, + params['q'].native, + params['g'].native, + public_key.native, + ) + + elif key_object.algorithm == 'ec': + public_key = key['public_key'].native + if public_key is None: + # This is gross, but since the EC public key is optional, + # and we need to load the private key and use the crypto lib + # to get the public key, we have to import the platform-specific + # asymmetric implementation. This is the reason a bunch of the + # imports are module imports, so we don't get an import cycle. + public_key_object = load_private_key(key_object).public_key + public_key = public_key_object.asn1['public_key'].parsed.native + + to_hash = '%s:' % key_object.curve[1] + to_hash = to_hash.encode('utf-8') + to_hash += public_key + + if isinstance(to_hash, str_cls): + to_hash = to_hash.encode('utf-8') + + return hashlib.sha256(to_hash).digest() + + if isinstance(key_object, PublicKeyInfo): + if key_object.algorithm == 'rsa': + key = key_object['public_key'].parsed + + to_hash = '%d:%d' % ( + key['modulus'].native, + key['public_exponent'].native, + ) + + elif key_object.algorithm == 'dsa': + key = key_object['public_key'].parsed + params = key_object['algorithm']['parameters'] + + to_hash = '%d:%d:%d:%d' % ( + params['p'].native, + params['q'].native, + params['g'].native, + key.native, + ) + + elif key_object.algorithm == 'ec': + public_key = key_object['public_key'].native + + to_hash = '%s:' % key_object.curve[1] + to_hash = to_hash.encode('utf-8') + to_hash += public_key + + if isinstance(to_hash, str_cls): + to_hash = to_hash.encode('utf-8') + + return hashlib.sha256(to_hash).digest() + + raise ValueError(pretty_message( + ''' + key_object must be an instance of the + asn1crypto.keys.PrivateKeyInfo or asn1crypto.keys.PublicKeyInfo + classes, not %s + ''', + type_name(key_object) + )) + + +crypto_funcs = { + 'rc2': rc2_cbc_pkcs5_decrypt, + 'rc4': rc4_decrypt, + 'des': des_cbc_pkcs5_decrypt, + 'tripledes': tripledes_cbc_pkcs5_decrypt, + 'aes': aes_cbc_pkcs7_decrypt, +} + + +def parse_public(data): + """ + Loads a public key from a DER or PEM-formatted file. Supports RSA, DSA and + EC public keys. For RSA keys, both the old RSAPublicKey and + SubjectPublicKeyInfo structures are supported. Also allows extracting a + public key from an X.509 certificate. + + :param data: + A byte string to load the public key from + + :raises: + ValueError - when the data does not appear to contain a public key + + :return: + An asn1crypto.keys.PublicKeyInfo object + """ + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + key_type = None + + # Appears to be PEM formatted + if re.match(b'\\s*-----', data) is not None: + key_type, algo, data = _unarmor_pem(data) + + if key_type == 'private key': + raise ValueError(pretty_message( + ''' + The data specified does not appear to be a public key or + certificate, but rather a private key + ''' + )) + + # When a public key returning from _unarmor_pem has a known algorithm + # of RSA, that means the DER structure is of the type RSAPublicKey, so + # we need to wrap it in the PublicKeyInfo structure. + if algo == 'rsa': + return PublicKeyInfo.wrap(data, 'rsa') + + if key_type is None or key_type == 'public key': + try: + pki = PublicKeyInfo.load(data) + # Call .native to fully parse since asn1crypto is lazy + pki.native + return pki + except (ValueError): + pass # Data was not PublicKeyInfo + + try: + rpk = RSAPublicKey.load(data) + # Call .native to fully parse since asn1crypto is lazy + rpk.native + return PublicKeyInfo.wrap(rpk, 'rsa') + except (ValueError): + pass # Data was not an RSAPublicKey + + if key_type is None or key_type == 'certificate': + try: + parsed_cert = Certificate.load(data) + key_info = parsed_cert['tbs_certificate']['subject_public_key_info'] + return key_info + except (ValueError): + pass # Data was not a cert + + raise ValueError('The data specified does not appear to be a known public key or certificate format') + + +def parse_certificate(data): + """ + Loads a certificate from a DER or PEM-formatted file. Supports X.509 + certificates only. + + :param data: + A byte string to load the certificate from + + :raises: + ValueError - when the data does not appear to contain a certificate + + :return: + An asn1crypto.x509.Certificate object + """ + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + key_type = None + + # Appears to be PEM formatted + if re.match(b'\\s*-----', data) is not None: + key_type, _, data = _unarmor_pem(data) + + if key_type == 'private key': + raise ValueError(pretty_message( + ''' + The data specified does not appear to be a certificate, but + rather a private key + ''' + )) + + if key_type == 'public key': + raise ValueError(pretty_message( + ''' + The data specified does not appear to be a certificate, but + rather a public key + ''' + )) + + if key_type is None or key_type == 'certificate': + try: + return Certificate.load(data) + except (ValueError): + pass # Data was not a Certificate + + raise ValueError(pretty_message( + ''' + The data specified does not appear to be a known certificate format + ''' + )) + + +def parse_private(data, password=None): + """ + Loads a private key from a DER or PEM-formatted file. Supports RSA, DSA and + EC private keys. Works with the follow formats: + + - RSAPrivateKey (PKCS#1) + - ECPrivateKey (SECG SEC1 V2) + - DSAPrivateKey (OpenSSL) + - PrivateKeyInfo (RSA/DSA/EC - PKCS#8) + - EncryptedPrivateKeyInfo (RSA/DSA/EC - PKCS#8) + - Encrypted RSAPrivateKey (PEM only, OpenSSL) + - Encrypted DSAPrivateKey (PEM only, OpenSSL) + - Encrypted ECPrivateKey (PEM only, OpenSSL) + + :param data: + A byte string to load the private key from + + :param password: + The password to unencrypt the private key + + :raises: + ValueError - when the data does not appear to contain a private key, or the password is invalid + + :return: + An asn1crypto.keys.PrivateKeyInfo object + """ + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + if password is not None: + if not isinstance(password, byte_cls): + raise TypeError(pretty_message( + ''' + password must be a byte string, not %s + ''', + type_name(password) + )) + else: + password = b'' + + # Appears to be PEM formatted + if re.match(b'\\s*-----', data) is not None: + key_type, _, data = _unarmor_pem(data, password) + + if key_type == 'public key': + raise ValueError(pretty_message( + ''' + The data specified does not appear to be a private key, but + rather a public key + ''' + )) + + if key_type == 'certificate': + raise ValueError(pretty_message( + ''' + The data specified does not appear to be a private key, but + rather a certificate + ''' + )) + + try: + pki = PrivateKeyInfo.load(data) + # Call .native to fully parse since asn1crypto is lazy + pki.native + return pki + except (ValueError): + pass # Data was not PrivateKeyInfo + + try: + parsed_wrapper = EncryptedPrivateKeyInfo.load(data) + encryption_algorithm_info = parsed_wrapper['encryption_algorithm'] + encrypted_data = parsed_wrapper['encrypted_data'].native + decrypted_data = _decrypt_encrypted_data(encryption_algorithm_info, encrypted_data, password) + pki = PrivateKeyInfo.load(decrypted_data) + # Call .native to fully parse since asn1crypto is lazy + pki.native + return pki + except (ValueError): + pass # Data was not EncryptedPrivateKeyInfo + + try: + parsed = RSAPrivateKey.load(data) + # Call .native to fully parse since asn1crypto is lazy + parsed.native + return PrivateKeyInfo.wrap(parsed, 'rsa') + except (ValueError): + pass # Data was not an RSAPrivateKey + + try: + parsed = DSAPrivateKey.load(data) + # Call .native to fully parse since asn1crypto is lazy + parsed.native + return PrivateKeyInfo.wrap(parsed, 'dsa') + except (ValueError): + pass # Data was not a DSAPrivateKey + + try: + parsed = ECPrivateKey.load(data) + # Call .native to fully parse since asn1crypto is lazy + parsed.native + return PrivateKeyInfo.wrap(parsed, 'ec') + except (ValueError): + pass # Data was not an ECPrivateKey + + raise ValueError(pretty_message( + ''' + The data specified does not appear to be a known private key format + ''' + )) + + +def _unarmor_pem(data, password=None): + """ + Removes PEM-encoding from a public key, private key or certificate. If the + private key is encrypted, the password will be used to decrypt it. + + :param data: + A byte string of the PEM-encoded data + + :param password: + A byte string of the encryption password, or None + + :return: + A 3-element tuple in the format: (key_type, algorithm, der_bytes). The + key_type will be a unicode string of "public key", "private key" or + "certificate". The algorithm will be a unicode string of "rsa", "dsa" + or "ec". + """ + + object_type, headers, der_bytes = unarmor(data) + + type_regex = '^((DSA|EC|RSA) PRIVATE KEY|ENCRYPTED PRIVATE KEY|PRIVATE KEY|PUBLIC KEY|RSA PUBLIC KEY|CERTIFICATE)' + armor_type = re.match(type_regex, object_type) + if not armor_type: + raise ValueError(pretty_message( + ''' + data does not seem to contain a PEM-encoded certificate, private + key or public key + ''' + )) + + pem_header = armor_type.group(1) + + data = data.strip() + + # RSA private keys are encrypted after being DER-encoded, but before base64 + # encoding, so they need to be handled specially + if pem_header in set(['RSA PRIVATE KEY', 'DSA PRIVATE KEY', 'EC PRIVATE KEY']): + algo = armor_type.group(2).lower() + return ('private key', algo, _unarmor_pem_openssl_private(headers, der_bytes, password)) + + key_type = pem_header.lower() + algo = None + if key_type == 'encrypted private key': + key_type = 'private key' + elif key_type == 'rsa public key': + key_type = 'public key' + algo = 'rsa' + + return (key_type, algo, der_bytes) + + +def _unarmor_pem_openssl_private(headers, data, password): + """ + Parses a PKCS#1 private key, or encrypted private key + + :param headers: + A dict of "Name: Value" lines from right after the PEM header + + :param data: + A byte string of the DER-encoded PKCS#1 private key + + :param password: + A byte string of the password to use if the private key is encrypted + + :return: + A byte string of the DER-encoded private key + """ + + enc_algo = None + enc_iv_hex = None + enc_iv = None + + if 'DEK-Info' in headers: + params = headers['DEK-Info'] + if params.find(',') != -1: + enc_algo, enc_iv_hex = params.strip().split(',') + else: + enc_algo = 'RC4' + + if not enc_algo: + return data + + if enc_iv_hex: + enc_iv = binascii.unhexlify(enc_iv_hex.encode('ascii')) + enc_algo = enc_algo.lower() + + enc_key_length = { + 'aes-128-cbc': 16, + 'aes-128': 16, + 'aes-192-cbc': 24, + 'aes-192': 24, + 'aes-256-cbc': 32, + 'aes-256': 32, + 'rc4': 16, + 'rc4-64': 8, + 'rc4-40': 5, + 'rc2-64-cbc': 8, + 'rc2-40-cbc': 5, + 'rc2-cbc': 16, + 'rc2': 16, + 'des-ede3-cbc': 24, + 'des-ede3': 24, + 'des3': 24, + 'des-ede-cbc': 16, + 'des-cbc': 8, + 'des': 8, + }[enc_algo] + + enc_key = hashlib.md5(password + enc_iv[0:8]).digest() + while enc_key_length > len(enc_key): + enc_key += hashlib.md5(enc_key + password + enc_iv[0:8]).digest() + enc_key = enc_key[0:enc_key_length] + + enc_algo_name = { + 'aes-128-cbc': 'aes', + 'aes-128': 'aes', + 'aes-192-cbc': 'aes', + 'aes-192': 'aes', + 'aes-256-cbc': 'aes', + 'aes-256': 'aes', + 'rc4': 'rc4', + 'rc4-64': 'rc4', + 'rc4-40': 'rc4', + 'rc2-64-cbc': 'rc2', + 'rc2-40-cbc': 'rc2', + 'rc2-cbc': 'rc2', + 'rc2': 'rc2', + 'des-ede3-cbc': 'tripledes', + 'des-ede3': 'tripledes', + 'des3': 'tripledes', + 'des-ede-cbc': 'tripledes', + 'des-cbc': 'des', + 'des': 'des', + }[enc_algo] + decrypt_func = crypto_funcs[enc_algo_name] + + if enc_algo_name == 'rc4': + return decrypt_func(enc_key, data) + + return decrypt_func(enc_key, data, enc_iv) + + +def _parse_pkcs12(data, password, load_private_key): + """ + Parses a PKCS#12 ANS.1 DER-encoded structure and extracts certs and keys + + :param data: + A byte string of a DER-encoded PKCS#12 file + + :param password: + A byte string of the password to any encrypted data + + :param load_private_key: + A callable that will accept a byte string and return an + oscrypto.asymmetric.PrivateKey object + + :raises: + ValueError - when any of the parameters are of the wrong type or value + OSError - when an error is returned by one of the OS decryption functions + + :return: + A three-element tuple of: + 1. An asn1crypto.keys.PrivateKeyInfo object + 2. An asn1crypto.x509.Certificate object + 3. A list of zero or more asn1crypto.x509.Certificate objects that are + "extra" certificates, possibly intermediates from the cert chain + """ + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + if password is not None: + if not isinstance(password, byte_cls): + raise TypeError(pretty_message( + ''' + password must be a byte string, not %s + ''', + type_name(password) + )) + else: + password = b'' + + certs = {} + private_keys = {} + + pfx = Pfx.load(data) + + auth_safe = pfx['auth_safe'] + if auth_safe['content_type'].native != 'data': + raise ValueError(pretty_message( + ''' + Only password-protected PKCS12 files are currently supported + ''' + )) + authenticated_safe = pfx.authenticated_safe + + mac_data = pfx['mac_data'] + if mac_data: + mac_algo = mac_data['mac']['digest_algorithm']['algorithm'].native + key_length = { + 'sha1': 20, + 'sha224': 28, + 'sha256': 32, + 'sha384': 48, + 'sha512': 64, + 'sha512_224': 28, + 'sha512_256': 32, + }[mac_algo] + mac_key = pkcs12_kdf( + mac_algo, + password, + mac_data['mac_salt'].native, + mac_data['iterations'].native, + key_length, + 3 # ID 3 is for generating an HMAC key + ) + hash_mod = getattr(hashlib, mac_algo) + computed_hmac = hmac.new(mac_key, auth_safe['content'].contents, hash_mod).digest() + stored_hmac = mac_data['mac']['digest'].native + if not constant_compare(computed_hmac, stored_hmac): + raise ValueError('Password provided is invalid') + + for content_info in authenticated_safe: + content = content_info['content'] + + if isinstance(content, OctetString): + _parse_safe_contents(content.native, certs, private_keys, password, load_private_key) + + elif isinstance(content, EncryptedData): + encrypted_content_info = content['encrypted_content_info'] + + encryption_algorithm_info = encrypted_content_info['content_encryption_algorithm'] + encrypted_content = encrypted_content_info['encrypted_content'].native + decrypted_content = _decrypt_encrypted_data(encryption_algorithm_info, encrypted_content, password) + + _parse_safe_contents(decrypted_content, certs, private_keys, password, load_private_key) + + else: + raise ValueError(pretty_message( + ''' + Public-key-based PKCS12 files are not currently supported + ''' + )) + + key_fingerprints = set(private_keys.keys()) + cert_fingerprints = set(certs.keys()) + + common_fingerprints = sorted(list(key_fingerprints & cert_fingerprints)) + + key = None + cert = None + other_certs = [] + + if len(common_fingerprints) >= 1: + fingerprint = common_fingerprints[0] + key = private_keys[fingerprint] + cert = certs[fingerprint] + other_certs = [certs[f] for f in certs if f != fingerprint] + return (key, cert, other_certs) + + if len(private_keys) > 0: + first_key = sorted(list(private_keys.keys()))[0] + key = private_keys[first_key] + + if len(certs) > 0: + first_key = sorted(list(certs.keys()))[0] + cert = certs[first_key] + del certs[first_key] + + if len(certs) > 0: + other_certs = sorted(list(certs.values()), key=lambda c: c.subject.human_friendly) + + return (key, cert, other_certs) + + +def _parse_safe_contents(safe_contents, certs, private_keys, password, load_private_key): + """ + Parses a SafeContents PKCS#12 ANS.1 structure and extracts certs and keys + + :param safe_contents: + A byte string of ber-encoded SafeContents, or a asn1crypto.pkcs12.SafeContents + parsed object + + :param certs: + A dict to store certificates in + + :param keys: + A dict to store keys in + + :param password: + A byte string of the password to any encrypted data + + :param load_private_key: + A callable that will accept a byte string and return an + oscrypto.asymmetric.PrivateKey object + """ + + if isinstance(safe_contents, byte_cls): + safe_contents = SafeContents.load(safe_contents) + + for safe_bag in safe_contents: + bag_value = safe_bag['bag_value'] + + if isinstance(bag_value, CertBag): + if bag_value['cert_id'].native == 'x509': + cert = bag_value['cert_value'].parsed + public_key_info = cert['tbs_certificate']['subject_public_key_info'] + certs[_fingerprint(public_key_info, None)] = bag_value['cert_value'].parsed + + elif isinstance(bag_value, PrivateKeyInfo): + private_keys[_fingerprint(bag_value, load_private_key)] = bag_value + + elif isinstance(bag_value, EncryptedPrivateKeyInfo): + encryption_algorithm_info = bag_value['encryption_algorithm'] + encrypted_key_bytes = bag_value['encrypted_data'].native + decrypted_key_bytes = _decrypt_encrypted_data(encryption_algorithm_info, encrypted_key_bytes, password) + private_key = PrivateKeyInfo.load(decrypted_key_bytes) + private_keys[_fingerprint(private_key, load_private_key)] = private_key + + elif isinstance(bag_value, SafeContents): + _parse_safe_contents(bag_value, certs, private_keys, password, load_private_key) + + else: + # We don't care about CRL bags or secret bags + pass + + +def _decrypt_encrypted_data(encryption_algorithm_info, encrypted_content, password): + """ + Decrypts encrypted ASN.1 data + + :param encryption_algorithm_info: + An instance of asn1crypto.pkcs5.Pkcs5EncryptionAlgorithm + + :param encrypted_content: + A byte string of the encrypted content + + :param password: + A byte string of the encrypted content's password + + :return: + A byte string of the decrypted plaintext + """ + + decrypt_func = crypto_funcs[encryption_algorithm_info.encryption_cipher] + + # Modern, PKCS#5 PBES2-based encryption + if encryption_algorithm_info.kdf == 'pbkdf2': + + if encryption_algorithm_info.encryption_cipher == 'rc5': + raise ValueError(pretty_message( + ''' + PBES2 encryption scheme utilizing RC5 encryption is not supported + ''' + )) + + enc_key = pbkdf2( + encryption_algorithm_info.kdf_hmac, + password, + encryption_algorithm_info.kdf_salt, + encryption_algorithm_info.kdf_iterations, + encryption_algorithm_info.key_length + ) + enc_iv = encryption_algorithm_info.encryption_iv + + plaintext = decrypt_func(enc_key, encrypted_content, enc_iv) + + elif encryption_algorithm_info.kdf == 'pbkdf1': + derived_output = pbkdf1( + encryption_algorithm_info.kdf_hmac, + password, + encryption_algorithm_info.kdf_salt, + encryption_algorithm_info.kdf_iterations, + encryption_algorithm_info.key_length + 8 + ) + enc_key = derived_output[0:8] + enc_iv = derived_output[8:16] + + plaintext = decrypt_func(enc_key, encrypted_content, enc_iv) + + elif encryption_algorithm_info.kdf == 'pkcs12_kdf': + enc_key = pkcs12_kdf( + encryption_algorithm_info.kdf_hmac, + password, + encryption_algorithm_info.kdf_salt, + encryption_algorithm_info.kdf_iterations, + encryption_algorithm_info.key_length, + 1 # ID 1 is for generating a key + ) + + # Since RC4 is a stream cipher, we don't use an IV + if encryption_algorithm_info.encryption_cipher == 'rc4': + plaintext = decrypt_func(enc_key, encrypted_content) + + else: + enc_iv = pkcs12_kdf( + encryption_algorithm_info.kdf_hmac, + password, + encryption_algorithm_info.kdf_salt, + encryption_algorithm_info.kdf_iterations, + encryption_algorithm_info.encryption_block_size, + 2 # ID 2 is for generating an IV + ) + plaintext = decrypt_func(enc_key, encrypted_content, enc_iv) + + return plaintext diff --git a/tasks/lib/package_control/deps/oscrypto/_cipher_suites.py b/tasks/lib/package_control/deps/oscrypto/_cipher_suites.py new file mode 100644 index 0000000..c7cdea9 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_cipher_suites.py @@ -0,0 +1,337 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + + +__all__ = [ + 'CIPHER_SUITE_MAP', +] + + +CIPHER_SUITE_MAP = { + b'\x00\x00': 'TLS_NULL_WITH_NULL_NULL', + b'\x00\x01': 'TLS_RSA_WITH_NULL_MD5', + b'\x00\x02': 'TLS_RSA_WITH_NULL_SHA', + b'\x00\x03': 'TLS_RSA_EXPORT_WITH_RC4_40_MD5', + b'\x00\x04': 'TLS_RSA_WITH_RC4_128_MD5', + b'\x00\x05': 'TLS_RSA_WITH_RC4_128_SHA', + b'\x00\x06': 'TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5', + b'\x00\x07': 'TLS_RSA_WITH_IDEA_CBC_SHA', + b'\x00\x08': 'TLS_RSA_EXPORT_WITH_DES40_CBC_SHA', + b'\x00\x09': 'TLS_RSA_WITH_DES_CBC_SHA', + b'\x00\x0A': 'TLS_RSA_WITH_3DES_EDE_CBC_SHA', + b'\x00\x0B': 'TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA', + b'\x00\x0C': 'TLS_DH_DSS_WITH_DES_CBC_SHA', + b'\x00\x0D': 'TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA', + b'\x00\x0E': 'TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA', + b'\x00\x0F': 'TLS_DH_RSA_WITH_DES_CBC_SHA', + b'\x00\x10': 'TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA', + b'\x00\x11': 'TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA', + b'\x00\x12': 'TLS_DHE_DSS_WITH_DES_CBC_SHA', + b'\x00\x13': 'TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA', + b'\x00\x14': 'TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA', + b'\x00\x15': 'TLS_DHE_RSA_WITH_DES_CBC_SHA', + b'\x00\x16': 'TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA', + b'\x00\x17': 'TLS_DH_anon_EXPORT_WITH_RC4_40_MD5', + b'\x00\x18': 'TLS_DH_anon_WITH_RC4_128_MD5', + b'\x00\x19': 'TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA', + b'\x00\x1A': 'TLS_DH_anon_WITH_DES_CBC_SHA', + b'\x00\x1B': 'TLS_DH_anon_WITH_3DES_EDE_CBC_SHA', + b'\x00\x1E': 'TLS_KRB5_WITH_DES_CBC_SHA', + b'\x00\x1F': 'TLS_KRB5_WITH_3DES_EDE_CBC_SHA', + b'\x00\x20': 'TLS_KRB5_WITH_RC4_128_SHA', + b'\x00\x21': 'TLS_KRB5_WITH_IDEA_CBC_SHA', + b'\x00\x22': 'TLS_KRB5_WITH_DES_CBC_MD5', + b'\x00\x23': 'TLS_KRB5_WITH_3DES_EDE_CBC_MD5', + b'\x00\x24': 'TLS_KRB5_WITH_RC4_128_MD5', + b'\x00\x25': 'TLS_KRB5_WITH_IDEA_CBC_MD5', + b'\x00\x26': 'TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA', + b'\x00\x27': 'TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA', + b'\x00\x28': 'TLS_KRB5_EXPORT_WITH_RC4_40_SHA', + b'\x00\x29': 'TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5', + b'\x00\x2A': 'TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5', + b'\x00\x2B': 'TLS_KRB5_EXPORT_WITH_RC4_40_MD5', + b'\x00\x2C': 'TLS_PSK_WITH_NULL_SHA', + b'\x00\x2D': 'TLS_DHE_PSK_WITH_NULL_SHA', + b'\x00\x2E': 'TLS_RSA_PSK_WITH_NULL_SHA', + b'\x00\x2F': 'TLS_RSA_WITH_AES_128_CBC_SHA', + b'\x00\x30': 'TLS_DH_DSS_WITH_AES_128_CBC_SHA', + b'\x00\x31': 'TLS_DH_RSA_WITH_AES_128_CBC_SHA', + b'\x00\x32': 'TLS_DHE_DSS_WITH_AES_128_CBC_SHA', + b'\x00\x33': 'TLS_DHE_RSA_WITH_AES_128_CBC_SHA', + b'\x00\x34': 'TLS_DH_anon_WITH_AES_128_CBC_SHA', + b'\x00\x35': 'TLS_RSA_WITH_AES_256_CBC_SHA', + b'\x00\x36': 'TLS_DH_DSS_WITH_AES_256_CBC_SHA', + b'\x00\x37': 'TLS_DH_RSA_WITH_AES_256_CBC_SHA', + b'\x00\x38': 'TLS_DHE_DSS_WITH_AES_256_CBC_SHA', + b'\x00\x39': 'TLS_DHE_RSA_WITH_AES_256_CBC_SHA', + b'\x00\x3A': 'TLS_DH_anon_WITH_AES_256_CBC_SHA', + b'\x00\x3B': 'TLS_RSA_WITH_NULL_SHA256', + b'\x00\x3C': 'TLS_RSA_WITH_AES_128_CBC_SHA256', + b'\x00\x3D': 'TLS_RSA_WITH_AES_256_CBC_SHA256', + b'\x00\x3E': 'TLS_DH_DSS_WITH_AES_128_CBC_SHA256', + b'\x00\x3F': 'TLS_DH_RSA_WITH_AES_128_CBC_SHA256', + b'\x00\x40': 'TLS_DHE_DSS_WITH_AES_128_CBC_SHA256', + b'\x00\x41': 'TLS_RSA_WITH_CAMELLIA_128_CBC_SHA', + b'\x00\x42': 'TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA', + b'\x00\x43': 'TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA', + b'\x00\x44': 'TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA', + b'\x00\x45': 'TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA', + b'\x00\x46': 'TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA', + b'\x00\x67': 'TLS_DHE_RSA_WITH_AES_128_CBC_SHA256', + b'\x00\x68': 'TLS_DH_DSS_WITH_AES_256_CBC_SHA256', + b'\x00\x69': 'TLS_DH_RSA_WITH_AES_256_CBC_SHA256', + b'\x00\x6A': 'TLS_DHE_DSS_WITH_AES_256_CBC_SHA256', + b'\x00\x6B': 'TLS_DHE_RSA_WITH_AES_256_CBC_SHA256', + b'\x00\x6C': 'TLS_DH_anon_WITH_AES_128_CBC_SHA256', + b'\x00\x6D': 'TLS_DH_anon_WITH_AES_256_CBC_SHA256', + b'\x00\x84': 'TLS_RSA_WITH_CAMELLIA_256_CBC_SHA', + b'\x00\x85': 'TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA', + b'\x00\x86': 'TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA', + b'\x00\x87': 'TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA', + b'\x00\x88': 'TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA', + b'\x00\x89': 'TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA', + b'\x00\x8A': 'TLS_PSK_WITH_RC4_128_SHA', + b'\x00\x8B': 'TLS_PSK_WITH_3DES_EDE_CBC_SHA', + b'\x00\x8C': 'TLS_PSK_WITH_AES_128_CBC_SHA', + b'\x00\x8D': 'TLS_PSK_WITH_AES_256_CBC_SHA', + b'\x00\x8E': 'TLS_DHE_PSK_WITH_RC4_128_SHA', + b'\x00\x8F': 'TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA', + b'\x00\x90': 'TLS_DHE_PSK_WITH_AES_128_CBC_SHA', + b'\x00\x91': 'TLS_DHE_PSK_WITH_AES_256_CBC_SHA', + b'\x00\x92': 'TLS_RSA_PSK_WITH_RC4_128_SHA', + b'\x00\x93': 'TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA', + b'\x00\x94': 'TLS_RSA_PSK_WITH_AES_128_CBC_SHA', + b'\x00\x95': 'TLS_RSA_PSK_WITH_AES_256_CBC_SHA', + b'\x00\x96': 'TLS_RSA_WITH_SEED_CBC_SHA', + b'\x00\x97': 'TLS_DH_DSS_WITH_SEED_CBC_SHA', + b'\x00\x98': 'TLS_DH_RSA_WITH_SEED_CBC_SHA', + b'\x00\x99': 'TLS_DHE_DSS_WITH_SEED_CBC_SHA', + b'\x00\x9A': 'TLS_DHE_RSA_WITH_SEED_CBC_SHA', + b'\x00\x9B': 'TLS_DH_anon_WITH_SEED_CBC_SHA', + b'\x00\x9C': 'TLS_RSA_WITH_AES_128_GCM_SHA256', + b'\x00\x9D': 'TLS_RSA_WITH_AES_256_GCM_SHA384', + b'\x00\x9E': 'TLS_DHE_RSA_WITH_AES_128_GCM_SHA256', + b'\x00\x9F': 'TLS_DHE_RSA_WITH_AES_256_GCM_SHA384', + b'\x00\xA0': 'TLS_DH_RSA_WITH_AES_128_GCM_SHA256', + b'\x00\xA1': 'TLS_DH_RSA_WITH_AES_256_GCM_SHA384', + b'\x00\xA2': 'TLS_DHE_DSS_WITH_AES_128_GCM_SHA256', + b'\x00\xA3': 'TLS_DHE_DSS_WITH_AES_256_GCM_SHA384', + b'\x00\xA4': 'TLS_DH_DSS_WITH_AES_128_GCM_SHA256', + b'\x00\xA5': 'TLS_DH_DSS_WITH_AES_256_GCM_SHA384', + b'\x00\xA6': 'TLS_DH_anon_WITH_AES_128_GCM_SHA256', + b'\x00\xA7': 'TLS_DH_anon_WITH_AES_256_GCM_SHA384', + b'\x00\xA8': 'TLS_PSK_WITH_AES_128_GCM_SHA256', + b'\x00\xA9': 'TLS_PSK_WITH_AES_256_GCM_SHA384', + b'\x00\xAA': 'TLS_DHE_PSK_WITH_AES_128_GCM_SHA256', + b'\x00\xAB': 'TLS_DHE_PSK_WITH_AES_256_GCM_SHA384', + b'\x00\xAC': 'TLS_RSA_PSK_WITH_AES_128_GCM_SHA256', + b'\x00\xAD': 'TLS_RSA_PSK_WITH_AES_256_GCM_SHA384', + b'\x00\xAE': 'TLS_PSK_WITH_AES_128_CBC_SHA256', + b'\x00\xAF': 'TLS_PSK_WITH_AES_256_CBC_SHA384', + b'\x00\xB0': 'TLS_PSK_WITH_NULL_SHA256', + b'\x00\xB1': 'TLS_PSK_WITH_NULL_SHA384', + b'\x00\xB2': 'TLS_DHE_PSK_WITH_AES_128_CBC_SHA256', + b'\x00\xB3': 'TLS_DHE_PSK_WITH_AES_256_CBC_SHA384', + b'\x00\xB4': 'TLS_DHE_PSK_WITH_NULL_SHA256', + b'\x00\xB5': 'TLS_DHE_PSK_WITH_NULL_SHA384', + b'\x00\xB6': 'TLS_RSA_PSK_WITH_AES_128_CBC_SHA256', + b'\x00\xB7': 'TLS_RSA_PSK_WITH_AES_256_CBC_SHA384', + b'\x00\xB8': 'TLS_RSA_PSK_WITH_NULL_SHA256', + b'\x00\xB9': 'TLS_RSA_PSK_WITH_NULL_SHA384', + b'\x00\xBA': 'TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256', + b'\x00\xBB': 'TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256', + b'\x00\xBC': 'TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256', + b'\x00\xBD': 'TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256', + b'\x00\xBE': 'TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256', + b'\x00\xBF': 'TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256', + b'\x00\xC0': 'TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256', + b'\x00\xC1': 'TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256', + b'\x00\xC2': 'TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256', + b'\x00\xC3': 'TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256', + b'\x00\xC4': 'TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256', + b'\x00\xC5': 'TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256', + b'\x00\xFF': 'TLS_EMPTY_RENEGOTIATION_INFO_SCSV', + b'\x13\x01': 'TLS_AES_128_GCM_SHA256', + b'\x13\x02': 'TLS_AES_256_GCM_SHA384', + b'\x13\x03': 'TLS_CHACHA20_POLY1305_SHA256', + b'\x13\x04': 'TLS_AES_128_CCM_SHA256', + b'\x13\x05': 'TLS_AES_128_CCM_8_SHA256', + b'\xC0\x01': 'TLS_ECDH_ECDSA_WITH_NULL_SHA', + b'\xC0\x02': 'TLS_ECDH_ECDSA_WITH_RC4_128_SHA', + b'\xC0\x03': 'TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA', + b'\xC0\x04': 'TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA', + b'\xC0\x05': 'TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA', + b'\xC0\x06': 'TLS_ECDHE_ECDSA_WITH_NULL_SHA', + b'\xC0\x07': 'TLS_ECDHE_ECDSA_WITH_RC4_128_SHA', + b'\xC0\x08': 'TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA', + b'\xC0\x09': 'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA', + b'\xC0\x0A': 'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA', + b'\xC0\x0B': 'TLS_ECDH_RSA_WITH_NULL_SHA', + b'\xC0\x0C': 'TLS_ECDH_RSA_WITH_RC4_128_SHA', + b'\xC0\x0D': 'TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA', + b'\xC0\x0E': 'TLS_ECDH_RSA_WITH_AES_128_CBC_SHA', + b'\xC0\x0F': 'TLS_ECDH_RSA_WITH_AES_256_CBC_SHA', + b'\xC0\x10': 'TLS_ECDHE_RSA_WITH_NULL_SHA', + b'\xC0\x11': 'TLS_ECDHE_RSA_WITH_RC4_128_SHA', + b'\xC0\x12': 'TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA', + b'\xC0\x13': 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA', + b'\xC0\x14': 'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA', + b'\xC0\x15': 'TLS_ECDH_anon_WITH_NULL_SHA', + b'\xC0\x16': 'TLS_ECDH_anon_WITH_RC4_128_SHA', + b'\xC0\x17': 'TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA', + b'\xC0\x18': 'TLS_ECDH_anon_WITH_AES_128_CBC_SHA', + b'\xC0\x19': 'TLS_ECDH_anon_WITH_AES_256_CBC_SHA', + b'\xC0\x1A': 'TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA', + b'\xC0\x1B': 'TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA', + b'\xC0\x1C': 'TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA', + b'\xC0\x1D': 'TLS_SRP_SHA_WITH_AES_128_CBC_SHA', + b'\xC0\x1E': 'TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA', + b'\xC0\x1F': 'TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA', + b'\xC0\x20': 'TLS_SRP_SHA_WITH_AES_256_CBC_SHA', + b'\xC0\x21': 'TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA', + b'\xC0\x22': 'TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA', + b'\xC0\x23': 'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256', + b'\xC0\x24': 'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384', + b'\xC0\x25': 'TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256', + b'\xC0\x26': 'TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384', + b'\xC0\x27': 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256', + b'\xC0\x28': 'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384', + b'\xC0\x29': 'TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256', + b'\xC0\x2A': 'TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384', + b'\xC0\x2B': 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256', + b'\xC0\x2C': 'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384', + b'\xC0\x2D': 'TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256', + b'\xC0\x2E': 'TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384', + b'\xC0\x2F': 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256', + b'\xC0\x30': 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384', + b'\xC0\x31': 'TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256', + b'\xC0\x32': 'TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384', + b'\xC0\x33': 'TLS_ECDHE_PSK_WITH_RC4_128_SHA', + b'\xC0\x34': 'TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA', + b'\xC0\x35': 'TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA', + b'\xC0\x36': 'TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA', + b'\xC0\x37': 'TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256', + b'\xC0\x38': 'TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384', + b'\xC0\x39': 'TLS_ECDHE_PSK_WITH_NULL_SHA', + b'\xC0\x3A': 'TLS_ECDHE_PSK_WITH_NULL_SHA256', + b'\xC0\x3B': 'TLS_ECDHE_PSK_WITH_NULL_SHA384', + b'\xC0\x3C': 'TLS_RSA_WITH_ARIA_128_CBC_SHA256', + b'\xC0\x3D': 'TLS_RSA_WITH_ARIA_256_CBC_SHA384', + b'\xC0\x3E': 'TLS_DH_DSS_WITH_ARIA_128_CBC_SHA256', + b'\xC0\x3F': 'TLS_DH_DSS_WITH_ARIA_256_CBC_SHA384', + b'\xC0\x40': 'TLS_DH_RSA_WITH_ARIA_128_CBC_SHA256', + b'\xC0\x41': 'TLS_DH_RSA_WITH_ARIA_256_CBC_SHA384', + b'\xC0\x42': 'TLS_DHE_DSS_WITH_ARIA_128_CBC_SHA256', + b'\xC0\x43': 'TLS_DHE_DSS_WITH_ARIA_256_CBC_SHA384', + b'\xC0\x44': 'TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256', + b'\xC0\x45': 'TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384', + b'\xC0\x46': 'TLS_DH_anon_WITH_ARIA_128_CBC_SHA256', + b'\xC0\x47': 'TLS_DH_anon_WITH_ARIA_256_CBC_SHA384', + b'\xC0\x48': 'TLS_ECDHE_ECDSA_WITH_ARIA_128_CBC_SHA256', + b'\xC0\x49': 'TLS_ECDHE_ECDSA_WITH_ARIA_256_CBC_SHA384', + b'\xC0\x4A': 'TLS_ECDH_ECDSA_WITH_ARIA_128_CBC_SHA256', + b'\xC0\x4B': 'TLS_ECDH_ECDSA_WITH_ARIA_256_CBC_SHA384', + b'\xC0\x4C': 'TLS_ECDHE_RSA_WITH_ARIA_128_CBC_SHA256', + b'\xC0\x4D': 'TLS_ECDHE_RSA_WITH_ARIA_256_CBC_SHA384', + b'\xC0\x4E': 'TLS_ECDH_RSA_WITH_ARIA_128_CBC_SHA256', + b'\xC0\x4F': 'TLS_ECDH_RSA_WITH_ARIA_256_CBC_SHA384', + b'\xC0\x50': 'TLS_RSA_WITH_ARIA_128_GCM_SHA256', + b'\xC0\x51': 'TLS_RSA_WITH_ARIA_256_GCM_SHA384', + b'\xC0\x52': 'TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256', + b'\xC0\x53': 'TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384', + b'\xC0\x54': 'TLS_DH_RSA_WITH_ARIA_128_GCM_SHA256', + b'\xC0\x55': 'TLS_DH_RSA_WITH_ARIA_256_GCM_SHA384', + b'\xC0\x56': 'TLS_DHE_DSS_WITH_ARIA_128_GCM_SHA256', + b'\xC0\x57': 'TLS_DHE_DSS_WITH_ARIA_256_GCM_SHA384', + b'\xC0\x58': 'TLS_DH_DSS_WITH_ARIA_128_GCM_SHA256', + b'\xC0\x59': 'TLS_DH_DSS_WITH_ARIA_256_GCM_SHA384', + b'\xC0\x5A': 'TLS_DH_anon_WITH_ARIA_128_GCM_SHA256', + b'\xC0\x5B': 'TLS_DH_anon_WITH_ARIA_256_GCM_SHA384', + b'\xC0\x5C': 'TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256', + b'\xC0\x5D': 'TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384', + b'\xC0\x5E': 'TLS_ECDH_ECDSA_WITH_ARIA_128_GCM_SHA256', + b'\xC0\x5F': 'TLS_ECDH_ECDSA_WITH_ARIA_256_GCM_SHA384', + b'\xC0\x60': 'TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256', + b'\xC0\x61': 'TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384', + b'\xC0\x62': 'TLS_ECDH_RSA_WITH_ARIA_128_GCM_SHA256', + b'\xC0\x63': 'TLS_ECDH_RSA_WITH_ARIA_256_GCM_SHA384', + b'\xC0\x64': 'TLS_PSK_WITH_ARIA_128_CBC_SHA256', + b'\xC0\x65': 'TLS_PSK_WITH_ARIA_256_CBC_SHA384', + b'\xC0\x66': 'TLS_DHE_PSK_WITH_ARIA_128_CBC_SHA256', + b'\xC0\x67': 'TLS_DHE_PSK_WITH_ARIA_256_CBC_SHA384', + b'\xC0\x68': 'TLS_RSA_PSK_WITH_ARIA_128_CBC_SHA256', + b'\xC0\x69': 'TLS_RSA_PSK_WITH_ARIA_256_CBC_SHA384', + b'\xC0\x6A': 'TLS_PSK_WITH_ARIA_128_GCM_SHA256', + b'\xC0\x6B': 'TLS_PSK_WITH_ARIA_256_GCM_SHA384', + b'\xC0\x6C': 'TLS_DHE_PSK_WITH_ARIA_128_GCM_SHA256', + b'\xC0\x6D': 'TLS_DHE_PSK_WITH_ARIA_256_GCM_SHA384', + b'\xC0\x6E': 'TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256', + b'\xC0\x6F': 'TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384', + b'\xC0\x70': 'TLS_ECDHE_PSK_WITH_ARIA_128_CBC_SHA256', + b'\xC0\x71': 'TLS_ECDHE_PSK_WITH_ARIA_256_CBC_SHA384', + b'\xC0\x72': 'TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256', + b'\xC0\x73': 'TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384', + b'\xC0\x74': 'TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256', + b'\xC0\x75': 'TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384', + b'\xC0\x76': 'TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256', + b'\xC0\x77': 'TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384', + b'\xC0\x78': 'TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256', + b'\xC0\x79': 'TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384', + b'\xC0\x7A': 'TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256', + b'\xC0\x7B': 'TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384', + b'\xC0\x7C': 'TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256', + b'\xC0\x7D': 'TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384', + b'\xC0\x7E': 'TLS_DH_RSA_WITH_CAMELLIA_128_GCM_SHA256', + b'\xC0\x7F': 'TLS_DH_RSA_WITH_CAMELLIA_256_GCM_SHA384', + b'\xC0\x80': 'TLS_DHE_DSS_WITH_CAMELLIA_128_GCM_SHA256', + b'\xC0\x81': 'TLS_DHE_DSS_WITH_CAMELLIA_256_GCM_SHA384', + b'\xC0\x82': 'TLS_DH_DSS_WITH_CAMELLIA_128_GCM_SHA256', + b'\xC0\x83': 'TLS_DH_DSS_WITH_CAMELLIA_256_GCM_SHA384', + b'\xC0\x84': 'TLS_DH_anon_WITH_CAMELLIA_128_GCM_SHA256', + b'\xC0\x85': 'TLS_DH_anon_WITH_CAMELLIA_256_GCM_SHA384', + b'\xC0\x86': 'TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_GCM_SHA256', + b'\xC0\x87': 'TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_GCM_SHA384', + b'\xC0\x88': 'TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256', + b'\xC0\x89': 'TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384', + b'\xC0\x8A': 'TLS_ECDHE_RSA_WITH_CAMELLIA_128_GCM_SHA256', + b'\xC0\x8B': 'TLS_ECDHE_RSA_WITH_CAMELLIA_256_GCM_SHA384', + b'\xC0\x8C': 'TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256', + b'\xC0\x8D': 'TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384', + b'\xC0\x8E': 'TLS_PSK_WITH_CAMELLIA_128_GCM_SHA256', + b'\xC0\x8F': 'TLS_PSK_WITH_CAMELLIA_256_GCM_SHA384', + b'\xC0\x90': 'TLS_DHE_PSK_WITH_CAMELLIA_128_GCM_SHA256', + b'\xC0\x91': 'TLS_DHE_PSK_WITH_CAMELLIA_256_GCM_SHA384', + b'\xC0\x92': 'TLS_RSA_PSK_WITH_CAMELLIA_128_GCM_SHA256', + b'\xC0\x93': 'TLS_RSA_PSK_WITH_CAMELLIA_256_GCM_SHA384', + b'\xC0\x94': 'TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256', + b'\xC0\x95': 'TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384', + b'\xC0\x96': 'TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256', + b'\xC0\x97': 'TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384', + b'\xC0\x98': 'TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256', + b'\xC0\x99': 'TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384', + b'\xC0\x9A': 'TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256', + b'\xC0\x9B': 'TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384', + b'\xC0\x9C': 'TLS_RSA_WITH_AES_128_CCM', + b'\xC0\x9D': 'TLS_RSA_WITH_AES_256_CCM', + b'\xC0\x9E': 'TLS_DHE_RSA_WITH_AES_128_CCM', + b'\xC0\x9F': 'TLS_DHE_RSA_WITH_AES_256_CCM', + b'\xC0\xA0': 'TLS_RSA_WITH_AES_128_CCM_8', + b'\xC0\xA1': 'TLS_RSA_WITH_AES_256_CCM_8', + b'\xC0\xA2': 'TLS_DHE_RSA_WITH_AES_128_CCM_8', + b'\xC0\xA3': 'TLS_DHE_RSA_WITH_AES_256_CCM_8', + b'\xC0\xA4': 'TLS_PSK_WITH_AES_128_CCM', + b'\xC0\xA5': 'TLS_PSK_WITH_AES_256_CCM', + b'\xC0\xA6': 'TLS_DHE_PSK_WITH_AES_128_CCM', + b'\xC0\xA7': 'TLS_DHE_PSK_WITH_AES_256_CCM', + b'\xC0\xA8': 'TLS_PSK_WITH_AES_128_CCM_8', + b'\xC0\xA9': 'TLS_PSK_WITH_AES_256_CCM_8', + b'\xC0\xAA': 'TLS_PSK_DHE_WITH_AES_128_CCM_8', + b'\xC0\xAB': 'TLS_PSK_DHE_WITH_AES_256_CCM_8', + b'\xCC\xA8': 'TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256', + b'\xCC\xA9': 'TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256', + b'\xCC\xAA': 'TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256', + b'\xCC\xAB': 'TLS_PSK_WITH_CHACHA20_POLY1305_SHA256', + b'\xCC\xAC': 'TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256', + b'\xCC\xAD': 'TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256', + b'\xCC\xAE': 'TLS_RSA_PSK_WITH_CHACHA20_POLY1305_SHA256', +} diff --git a/tasks/lib/package_control/deps/oscrypto/_ecdsa.py b/tasks/lib/package_control/deps/oscrypto/_ecdsa.py new file mode 100644 index 0000000..66cbbee --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_ecdsa.py @@ -0,0 +1,811 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import hashlib +import hmac +import sys + +from . import backend +from ._asn1 import ( + Certificate, + DSASignature, + ECDomainParameters, + ECPointBitString, + ECPrivateKey, + int_from_bytes, + PrivateKeyAlgorithm, + PrivateKeyInfo, + PublicKeyAlgorithm, + PublicKeyInfo, +) +from ._errors import pretty_message +from ._types import type_name, byte_cls +from .util import rand_bytes +from .errors import SignatureError + +if sys.version_info < (3,): + chr_cls = chr + range = xrange # noqa + +else: + def chr_cls(num): + return bytes([num]) + + +_backend = backend() + + +if _backend != 'winlegacy': + # This pure-Python ECDSA code is only suitable for use on client machines, + # and is only needed on Windows 5.x (XP/2003). For testing sake it is + # possible to force use of it on newer versions of Windows. + raise SystemError('Pure-python ECDSA code is only for Windows XP/2003') + + +__all__ = [ + 'ec_generate_pair', + 'ec_compute_public_key_point', + 'ec_public_key_info', + 'ecdsa_sign', + 'ecdsa_verify', +] + + +CURVE_BYTES = { + 'secp256r1': 32, + 'secp384r1': 48, + 'secp521r1': 66, +} + +CURVE_EXTRA_BITS = { + 'secp256r1': 0, + 'secp384r1': 0, + 'secp521r1': 7, +} + + +def ec_generate_pair(curve): + """ + Generates a EC public/private key pair + + :param curve: + A unicode string. Valid values include "secp256r1", "secp384r1" and + "secp521r1". + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + + :return: + A 2-element tuple of (asn1crypto.keys.PublicKeyInfo, + asn1crypto.keys.PrivateKeyInfo) + """ + + if curve not in set(['secp256r1', 'secp384r1', 'secp521r1']): + raise ValueError(pretty_message( + ''' + curve must be one of "secp256r1", "secp384r1", "secp521r1", not %s + ''', + repr(curve) + )) + + curve_num_bytes = CURVE_BYTES[curve] + curve_base_point = { + 'secp256r1': SECP256R1_BASE_POINT, + 'secp384r1': SECP384R1_BASE_POINT, + 'secp521r1': SECP521R1_BASE_POINT, + }[curve] + + while True: + private_key_bytes = rand_bytes(curve_num_bytes) + private_key_int = int_from_bytes(private_key_bytes, signed=False) + + if private_key_int > 0 and private_key_int < curve_base_point.order: + break + + private_key_info = PrivateKeyInfo({ + 'version': 0, + 'private_key_algorithm': PrivateKeyAlgorithm({ + 'algorithm': 'ec', + 'parameters': ECDomainParameters( + name='named', + value=curve + ) + }), + 'private_key': ECPrivateKey({ + 'version': 'ecPrivkeyVer1', + 'private_key': private_key_int + }), + }) + + ec_point = ec_compute_public_key_point(private_key_info) + private_key_info['private_key'].parsed['public_key'] = ec_point.copy() + + return (ec_public_key_info(ec_point, curve), private_key_info) + + +def ec_compute_public_key_point(private_key): + """ + Constructs the PublicKeyInfo for a PrivateKeyInfo + + :param private_key: + An asn1crypto.keys.PrivateKeyInfo object + + :raises: + ValueError - when any of the parameters contain an invalid value + + :return: + An asn1crypto.keys.ECPointBitString object + """ + + if not isinstance(private_key, PrivateKeyInfo): + raise TypeError(pretty_message( + ''' + private_key must be an instance of the + asn1crypto.keys.PrivateKeyInfo class, not %s + ''', + type_name(private_key) + )) + + curve_type, details = private_key.curve + + if curve_type == 'implicit_ca': + raise ValueError(pretty_message( + ''' + Unable to compute public key for EC key using Implicit CA + parameters + ''' + )) + + if curve_type == 'specified': + raise ValueError(pretty_message( + ''' + Unable to compute public key for EC key over a specified field + ''' + )) + + elif curve_type == 'named': + if details not in set(['secp256r1', 'secp384r1', 'secp521r1']): + raise ValueError(pretty_message( + ''' + Named curve must be one of "secp256r1", "secp384r1", "secp521r1", not %s + ''', + repr(details) + )) + + base_point = { + 'secp256r1': SECP256R1_BASE_POINT, + 'secp384r1': SECP384R1_BASE_POINT, + 'secp521r1': SECP521R1_BASE_POINT, + }[details] + + public_point = base_point * private_key['private_key'].parsed['private_key'].native + return ECPointBitString.from_coords(public_point.x, public_point.y) + + +def ec_public_key_info(public_key_point, curve): + """ + Constructs the PublicKeyInfo for an ECPointBitString + + :param private_key: + An asn1crypto.keys.ECPointBitString object + + :param curve: + A unicode string of the curve name - one of secp256r1, secp384r1 or secp521r1 + + :raises: + ValueError - when any of the parameters contain an invalid value + + :return: + An asn1crypto.keys.PublicKeyInfo object + """ + + if curve not in set(['secp256r1', 'secp384r1', 'secp521r1']): + raise ValueError(pretty_message( + ''' + curve must be one of "secp256r1", "secp384r1", "secp521r1", not %s + ''', + repr(curve) + )) + + return PublicKeyInfo({ + 'algorithm': PublicKeyAlgorithm({ + 'algorithm': 'ec', + 'parameters': ECDomainParameters( + name='named', + value=curve + ) + }), + 'public_key': public_key_point, + }) + + +def ecdsa_sign(private_key, data, hash_algorithm): + """ + Generates an ECDSA signature in pure Python (thus slow) + + :param private_key: + The PrivateKey to generate the signature with + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "sha1", "sha256", "sha384" or "sha512" + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the signature + """ + + if not hasattr(private_key, 'asn1') or not isinstance(private_key.asn1, PrivateKeyInfo): + raise TypeError(pretty_message( + ''' + private_key must be an instance of the + oscrypto.asymmetric.PrivateKey class, not %s + ''', + type_name(private_key) + )) + + curve_name = private_key.curve + if curve_name not in set(['secp256r1', 'secp384r1', 'secp521r1']): + raise ValueError(pretty_message( + ''' + private_key does not use one of the named curves secp256r1, + secp384r1 or secp521r1 + ''' + )) + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + if hash_algorithm not in set(['sha1', 'sha224', 'sha256', 'sha384', 'sha512']): + raise ValueError(pretty_message( + ''' + hash_algorithm must be one of "sha1", "sha224", "sha256", "sha384", + "sha512", not %s + ''', + repr(hash_algorithm) + )) + + hash_func = getattr(hashlib, hash_algorithm) + + ec_private_key = private_key.asn1['private_key'].parsed + private_key_bytes = ec_private_key['private_key'].contents + private_key_int = ec_private_key['private_key'].native + + curve_num_bytes = CURVE_BYTES[curve_name] + curve_base_point = { + 'secp256r1': SECP256R1_BASE_POINT, + 'secp384r1': SECP384R1_BASE_POINT, + 'secp521r1': SECP521R1_BASE_POINT, + }[curve_name] + + n = curve_base_point.order + + # RFC 6979 section 3.2 + + # a. + digest = hash_func(data).digest() + hash_length = len(digest) + + h = int_from_bytes(digest, signed=False) % n + + # b. + V = b'\x01' * hash_length + + # c. + K = b'\x00' * hash_length + + # d. + K = hmac.new(K, V + b'\x00' + private_key_bytes + digest, hash_func).digest() + + # e. + V = hmac.new(K, V, hash_func).digest() + + # f. + K = hmac.new(K, V + b'\x01' + private_key_bytes + digest, hash_func).digest() + + # g. + V = hmac.new(K, V, hash_func).digest() + + # h. + r = 0 + s = 0 + while True: + # h. 1 + T = b'' + + # h. 2 + while len(T) < curve_num_bytes: + V = hmac.new(K, V, hash_func).digest() + T += V + + # h. 3 + k = int_from_bytes(T[0:curve_num_bytes], signed=False) + if k == 0 or k >= n: + continue + + # Calculate the signature in the loop in case we need a new k + r = (curve_base_point * k).x % n + if r == 0: + continue + + s = (inverse_mod(k, n) * (h + (private_key_int * r) % n)) % n + if s == 0: + continue + + break + + return DSASignature({'r': r, 's': s}).dump() + + +def ecdsa_verify(certificate_or_public_key, signature, data, hash_algorithm): + """ + Verifies an ECDSA signature in pure Python (thus slow) + + :param certificate_or_public_key: + A Certificate or PublicKey instance to verify the signature with + + :param signature: + A byte string of the signature to verify + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha256", "sha384" or "sha512" + + :raises: + oscrypto.errors.SignatureError - when the signature is determined to be invalid + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + has_asn1 = hasattr(certificate_or_public_key, 'asn1') + if not has_asn1 or not isinstance(certificate_or_public_key.asn1, (PublicKeyInfo, Certificate)): + raise TypeError(pretty_message( + ''' + certificate_or_public_key must be an instance of the + oscrypto.asymmetric.PublicKey or oscrypto.asymmetric.Certificate + classes, not %s + ''', + type_name(certificate_or_public_key) + )) + + curve_name = certificate_or_public_key.curve + if curve_name not in set(['secp256r1', 'secp384r1', 'secp521r1']): + raise ValueError(pretty_message( + ''' + certificate_or_public_key does not use one of the named curves + secp256r1, secp384r1 or secp521r1 + ''' + )) + + if not isinstance(signature, byte_cls): + raise TypeError(pretty_message( + ''' + signature must be a byte string, not %s + ''', + type_name(signature) + )) + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + if hash_algorithm not in set(['sha1', 'sha224', 'sha256', 'sha384', 'sha512']): + raise ValueError(pretty_message( + ''' + hash_algorithm must be one of "sha1", "sha224", "sha256", "sha384", + "sha512", not %s + ''', + repr(hash_algorithm) + )) + + asn1 = certificate_or_public_key.asn1 + if isinstance(asn1, Certificate): + asn1 = asn1.public_key + + curve_base_point = { + 'secp256r1': SECP256R1_BASE_POINT, + 'secp384r1': SECP384R1_BASE_POINT, + 'secp521r1': SECP521R1_BASE_POINT, + }[curve_name] + + x, y = asn1['public_key'].to_coords() + n = curve_base_point.order + + # Validates that the point is valid + public_key_point = PrimePoint(curve_base_point.curve, x, y, n) + + try: + signature = DSASignature.load(signature) + r = signature['r'].native + s = signature['s'].native + except (ValueError): + raise SignatureError('Signature is invalid') + + invalid = 0 + + # Check r is valid + invalid |= r < 1 + invalid |= r >= n + + # Check s is valid + invalid |= s < 1 + invalid |= s >= n + + if invalid: + raise SignatureError('Signature is invalid') + + hash_func = getattr(hashlib, hash_algorithm) + + digest = hash_func(data).digest() + + z = int_from_bytes(digest, signed=False) % n + w = inverse_mod(s, n) + u1 = (z * w) % n + u2 = (r * w) % n + hash_point = (curve_base_point * u1) + (public_key_point * u2) + if r != (hash_point.x % n): + raise SignatureError('Signature is invalid') + + +""" +Classes and objects to represent prime-field elliptic curves and points on them. +Exports the following items: + + - PrimeCurve() + - PrimePoint() + - SECP192R1_CURVE + - SECP192R1_BASE_POINT + - SECP224R1_CURVE + - SECP224R1_BASE_POINT + - SECP256R1_CURVE + - SECP256R1_BASE_POINT + - SECP384R1_CURVE + - SECP384R1_BASE_POINT + - SECP521R1_CURVE + - SECP521R1_BASE_POINT + +The curve constants are all PrimeCurve() objects and the base point constants +are all PrimePoint() objects. + +Some of the following source code is derived from +http://webpages.charter.net/curryfans/peter/downloads.html, but has been heavily +modified to fit into this projects lint settings. The original project license +is listed below: + +Copyright (c) 2014 Peter Pearson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + + +def inverse_mod(a, p): + """ + Compute the modular inverse of a (mod p) + + :param a: + An integer + + :param p: + An integer + + :return: + An integer + """ + + if a < 0 or p <= a: + a = a % p + + # From Ferguson and Schneier, roughly: + + c, d = a, p + uc, vc, ud, vd = 1, 0, 0, 1 + while c != 0: + q, c, d = divmod(d, c) + (c,) + uc, vc, ud, vd = ud - q * uc, vd - q * vc, uc, vc + + # At this point, d is the GCD, and ud*a+vd*p = d. + # If d == 1, this means that ud is a inverse. + + assert d == 1 + if ud > 0: + return ud + else: + return ud + p + + +class PrimeCurve(): + """ + Elliptic curve over a prime field. Characteristic two field curves are not + supported. + """ + + def __init__(self, p, a, b): + """ + The curve of points satisfying y^2 = x^3 + a*x + b (mod p) + + :param p: + The prime number as an integer + + :param a: + The component a as an integer + + :param b: + The component b as an integer + """ + + self.p = p + self.a = a + self.b = b + + def contains(self, point): + """ + :param point: + A Point object + + :return: + Boolean if the point is on this curve + """ + + y2 = point.y * point.y + x3 = point.x * point.x * point.x + return (y2 - (x3 + self.a * point.x + self.b)) % self.p == 0 + + +class PrimePoint(): + """ + A point on a prime-field elliptic curve + """ + + def __init__(self, curve, x, y, order=None): + """ + :param curve: + A PrimeCurve object + + :param x: + The x coordinate of the point as an integer + + :param y: + The y coordinate of the point as an integer + + :param order: + The order of the point, as an integer - optional + """ + + self.curve = curve + self.x = x + self.y = y + self.order = order + + # self.curve is allowed to be None only for INFINITY: + if self.curve: + if not self.curve.contains(self): + raise ValueError('Invalid EC point') + + if self.order: + if self * self.order != INFINITY: + raise ValueError('Invalid EC point') + + def __cmp__(self, other): + """ + :param other: + A PrimePoint object + + :return: + 0 if identical, 1 otherwise + """ + if self.curve == other.curve and self.x == other.x and self.y == other.y: + return 0 + else: + return 1 + + def __add__(self, other): + """ + :param other: + A PrimePoint object + + :return: + A PrimePoint object + """ + + # X9.62 B.3: + + if other == INFINITY: + return self + if self == INFINITY: + return other + assert self.curve == other.curve + if self.x == other.x: + if (self.y + other.y) % self.curve.p == 0: + return INFINITY + else: + return self.double() + + p = self.curve.p + + l_ = ((other.y - self.y) * inverse_mod(other.x - self.x, p)) % p + + x3 = (l_ * l_ - self.x - other.x) % p + y3 = (l_ * (self.x - x3) - self.y) % p + + return PrimePoint(self.curve, x3, y3) + + def __mul__(self, other): + """ + :param other: + An integer to multiple the Point by + + :return: + A PrimePoint object + """ + + def leftmost_bit(x): + assert x > 0 + result = 1 + while result <= x: + result = 2 * result + return result // 2 + + e = other + if self.order: + e = e % self.order + if e == 0: + return INFINITY + if self == INFINITY: + return INFINITY + assert e > 0 + + # From X9.62 D.3.2: + + e3 = 3 * e + negative_self = PrimePoint(self.curve, self.x, -self.y, self.order) + i = leftmost_bit(e3) // 2 + result = self + # print "Multiplying %s by %d (e3 = %d):" % ( self, other, e3 ) + while i > 1: + result = result.double() + if (e3 & i) != 0 and (e & i) == 0: + result = result + self + if (e3 & i) == 0 and (e & i) != 0: + result = result + negative_self + # print ". . . i = %d, result = %s" % ( i, result ) + i = i // 2 + + return result + + def __rmul__(self, other): + """ + :param other: + An integer to multiple the Point by + + :return: + A PrimePoint object + """ + + return self * other + + def double(self): + """ + :return: + A PrimePoint object that is twice this point + """ + + # X9.62 B.3: + + p = self.curve.p + a = self.curve.a + + l_ = ((3 * self.x * self.x + a) * inverse_mod(2 * self.y, p)) % p + + x3 = (l_ * l_ - 2 * self.x) % p + y3 = (l_ * (self.x - x3) - self.y) % p + + return PrimePoint(self.curve, x3, y3) + + +# This one point is the Point At Infinity for all purposes: +INFINITY = PrimePoint(None, None, None) + + +# NIST Curve P-192: +SECP192R1_CURVE = PrimeCurve( + 6277101735386680763835789423207666416083908700390324961279, + -3, + 0x64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1 +) +SECP192R1_BASE_POINT = PrimePoint( + SECP192R1_CURVE, + 0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012, + 0x07192b95ffc8da78631011ed6b24cdd573f977a11e794811, + 6277101735386680763835789423176059013767194773182842284081 +) + + +# NIST Curve P-224: +SECP224R1_CURVE = PrimeCurve( + 26959946667150639794667015087019630673557916260026308143510066298881, + -3, + 0xb4050a850c04b3abf54132565044b0b7d7bfd8ba270b39432355ffb4 +) +SECP224R1_BASE_POINT = PrimePoint( + SECP224R1_CURVE, + 0xb70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21, + 0xbd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34, + 26959946667150639794667015087019625940457807714424391721682722368061 +) + + +# NIST Curve P-256: +SECP256R1_CURVE = PrimeCurve( + 115792089210356248762697446949407573530086143415290314195533631308867097853951, + -3, + 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b +) +SECP256R1_BASE_POINT = PrimePoint( + SECP256R1_CURVE, + 0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296, + 0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5, + 115792089210356248762697446949407573529996955224135760342422259061068512044369 +) + + +# NIST Curve P-384: +SECP384R1_CURVE = PrimeCurve( + 39402006196394479212279040100143613805079739270465446667948293404245721771496870329047266088258938001861606973112319, # noqa + -3, + 0xb3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef +) +SECP384R1_BASE_POINT = PrimePoint( + SECP384R1_CURVE, + 0xaa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7, + 0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f, + 39402006196394479212279040100143613805079739270465446667946905279627659399113263569398956308152294913554433653942643 +) + + +# NIST Curve P-521: +SECP521R1_CURVE = PrimeCurve( + 6864797660130609714981900799081393217269435300143305409394463459185543183397656052122559640661454554977296311391480858037121987999716643812574028291115057151, # noqa + -3, + 0x051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00 # noqa +) +SECP521R1_BASE_POINT = PrimePoint( + SECP521R1_CURVE, + 0xc6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66, # noqa + 0x11839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650, # noqa + 6864797660130609714981900799081393217269435300143305409394463459185543183397655394245057746333217197532963996371363321113864768612440380340372808892707005449 # noqa +) diff --git a/tasks/lib/package_control/deps/oscrypto/_errors.py b/tasks/lib/package_control/deps/oscrypto/_errors.py new file mode 100644 index 0000000..42aefa2 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_errors.py @@ -0,0 +1,50 @@ +# coding: utf-8 + +""" +Helper for formatting exception messages. Exports the following items: + + - pretty_message() +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +import re +import textwrap + + +__all__ = [ + 'pretty_message', +] + + +def pretty_message(string, *params): + """ + Takes a multi-line string and does the following: + + - dedents + - converts newlines with text before and after into a single line + - strips leading and trailing whitespace + + :param string: + The string to format + + :param *params: + Params to interpolate into the string + + :return: + The formatted string + """ + + output = textwrap.dedent(string) + + # Unwrap lines, taking into account bulleted lists, ordered lists and + # underlines consisting of = signs + if output.find('\n') != -1: + output = re.sub('(?<=\\S)\n(?=[^ \n\t\\d\\*\\-=])', ' ', output) + + if params: + output = output % params + + output = output.strip() + + return output diff --git a/tasks/lib/package_control/deps/oscrypto/_ffi.py b/tasks/lib/package_control/deps/oscrypto/_ffi.py new file mode 100644 index 0000000..fe6bdee --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_ffi.py @@ -0,0 +1,437 @@ +# coding: utf-8 + +""" +Exceptions and compatibility shims for consistently using ctypes and cffi +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys +import platform + +from ctypes.util import find_library + +from . import ffi +from ._types import str_cls, byte_cls, int_types, bytes_to_list + + +__all__ = [ + 'array_from_pointer', + 'array_set', + 'buffer_from_bytes', + 'buffer_from_unicode', + 'buffer_pointer', + 'byte_array', + 'byte_string_from_buffer', + 'bytes_from_buffer', + 'callback', + 'cast', + 'deref', + 'errno', + 'FFIEngineError', + 'get_library', + 'is_null', + 'native', + 'new', + 'null', + 'pointer_set', + 'ref', + 'register_ffi', + 'sizeof', + 'struct', + 'struct_bytes', + 'struct_from_buffer', + 'unwrap', + 'write_to_buffer', +] + + +if ffi() == 'cffi': + from cffi import FFI + + _ffi_registry = {} + + ffi = FFI() + + def register_ffi(library, ffi_obj): + _ffi_registry[library] = ffi_obj + + def _get_ffi(library): + if library in _ffi_registry: + return _ffi_registry[library] + return ffi + + def buffer_from_bytes(initializer): + if sys.platform == 'win32': + return ffi.new('unsigned char[]', initializer) + return ffi.new('char[]', initializer) + + def buffer_from_unicode(initializer): + return ffi.new('wchar_t []', initializer) + + def write_to_buffer(buffer, data, offset=0): + buffer[offset:offset + len(data)] = data + + def buffer_pointer(buffer): + return ffi.new('char *[]', [buffer]) + + def cast(library, type_, value): + ffi_obj = _get_ffi(library) + return ffi_obj.cast(type_, value) + + def sizeof(library, value): + ffi_obj = _get_ffi(library) + return ffi_obj.sizeof(value) + + def bytes_from_buffer(buffer, maxlen=None): + if maxlen is not None: + return ffi.buffer(buffer, maxlen)[:] + return ffi.buffer(buffer)[:] + + def byte_string_from_buffer(buffer): + return ffi.string(buffer) + + def byte_array(byte_string): + return byte_string + + def pointer_set(pointer_, value): + pointer_[0] = value + + def array_set(array, value): + for index, val in enumerate(value): + array[index] = val + + def null(): + return ffi.NULL + + def is_null(point): + if point is None: + return True + if point == ffi.NULL: + return True + if ffi.getctype(ffi.typeof(point)) == 'void *': + return False + if point[0] == ffi.NULL: + return True + return False + + def errno(): + return ffi.errno + + def new(library, type_, value=None): + ffi_obj = _get_ffi(library) + + params = [] + if value is not None: + params.append(value) + if type_ in set(['BCRYPT_KEY_HANDLE', 'BCRYPT_ALG_HANDLE']): + return ffi_obj.cast(type_, 0) + return ffi_obj.new(type_, *params) + + def ref(value, offset=0): + return value + offset + + def native(type_, value): + if type_ == str_cls: + return ffi.string(value) + if type_ == byte_cls: + return ffi.buffer(value)[:] + return type_(value) + + def deref(point): + return point[0] + + def unwrap(point): + return point[0] + + def struct(library, name): + ffi_obj = _get_ffi(library) + return ffi_obj.new('%s *' % name) + + def struct_bytes(struct_): + return ffi.buffer(struct_)[:] + + def struct_from_buffer(library, name, buffer): + ffi_obj = _get_ffi(library) + new_struct_pointer = ffi_obj.new('%s *' % name) + new_struct = new_struct_pointer[0] + struct_size = sizeof(library, new_struct) + struct_buffer = ffi_obj.buffer(new_struct_pointer) + struct_buffer[:] = ffi_obj.buffer(buffer, struct_size)[:] + return new_struct_pointer + + def array_from_pointer(library, name, point, size): + ffi_obj = _get_ffi(library) + array = ffi_obj.cast('%s[%s]' % (name, size), point) + total_bytes = ffi_obj.sizeof(array) + if total_bytes == 0: + return [] + output = [] + + string_types = { + 'LPSTR': True, + 'LPCSTR': True, + 'LPWSTR': True, + 'LPCWSTR': True, + 'char *': True, + 'wchar_t *': True, + } + string_type = name in string_types + + for i in range(0, size): + value = array[i] + if string_type: + value = ffi_obj.string(value) + output.append(value) + return output + + def callback(library, signature_name, func): + ffi_obj = _get_ffi(library) + return ffi_obj.callback(signature_name, func) + + engine = 'cffi' + +else: + + import ctypes + from ctypes import pointer, c_int, c_char_p, c_uint, c_void_p, c_wchar_p + + _pointer_int_types = int_types + (c_char_p, ctypes.POINTER(ctypes.c_byte)) + + _pointer_types = { + 'void *': True, + 'wchar_t *': True, + 'char *': True, + 'char **': True, + } + _type_map = { + 'void *': c_void_p, + 'wchar_t *': c_wchar_p, + 'char *': c_char_p, + 'char **': ctypes.POINTER(c_char_p), + 'int': c_int, + 'unsigned int': c_uint, + 'size_t': ctypes.c_size_t, + 'uint32_t': ctypes.c_uint32, + } + if sys.platform == 'win32': + from ctypes import wintypes + _pointer_types.update({ + 'LPSTR': True, + 'LPWSTR': True, + 'LPCSTR': True, + 'LPCWSTR': True, + }) + _type_map.update({ + 'BYTE': ctypes.c_byte, + 'LPSTR': c_char_p, + 'LPWSTR': c_wchar_p, + 'LPCSTR': c_char_p, + 'LPCWSTR': c_wchar_p, + 'ULONG': wintypes.ULONG, + 'DWORD': wintypes.DWORD, + 'char *': ctypes.POINTER(ctypes.c_byte), + 'char **': ctypes.POINTER(ctypes.POINTER(ctypes.c_byte)), + }) + + def _type_info(library, type_): + is_double_pointer = type_[-3:] == ' **' + if is_double_pointer: + type_ = type_[:-1] + is_pointer = type_[-2:] == ' *' and type_ not in _pointer_types + if is_pointer: + type_ = type_[:-2] + + is_array = type_.find('[') != -1 + if is_array: + is_array = type_[type_.find('[') + 1:type_.find(']')] + if is_array == '': + is_array = True + else: + is_array = int(is_array) + type_ = type_[0:type_.find('[')] + + if type_ in _type_map: + type_ = _type_map[type_] + else: + type_ = getattr(library, type_) + + if is_double_pointer: + type_ = ctypes.POINTER(type_) + + return (is_pointer, is_array, type_) + + def register_ffi(library, ffi_obj): + pass + + def buffer_from_bytes(initializer): + return ctypes.create_string_buffer(initializer) + + def buffer_from_unicode(initializer): + return ctypes.create_unicode_buffer(initializer) + + def write_to_buffer(buffer, data, offset=0): + if isinstance(buffer, ctypes.POINTER(ctypes.c_byte)): + ctypes.memmove(buffer, data, len(data)) + return + + if offset == 0: + buffer.value = data + else: + buffer.value = buffer.raw[0:offset] + data + + def buffer_pointer(buffer): + return pointer(ctypes.cast(buffer, c_char_p)) + + def cast(library, type_, value): + is_pointer, is_array, type_ = _type_info(library, type_) + + if is_pointer: + type_ = ctypes.POINTER(type_) + elif is_array: + type_ = type_ * is_array + + return ctypes.cast(value, type_) + + def sizeof(library, value): + return ctypes.sizeof(value) + + def bytes_from_buffer(buffer, maxlen=None): + if isinstance(buffer, _pointer_int_types): + return ctypes.string_at(buffer, maxlen) + if maxlen is not None: + return buffer.raw[0:maxlen] + return buffer.raw + + def byte_string_from_buffer(buffer): + return buffer.value + + def byte_array(byte_string): + return (ctypes.c_byte * len(byte_string))(*bytes_to_list(byte_string)) + + def pointer_set(pointer_, value): + pointer_.contents.value = value + + def array_set(array, value): + for index, val in enumerate(value): + array[index] = val + + def null(): + return None + + def is_null(point): + return not bool(point) + + def errno(): + return ctypes.get_errno() + + def new(library, type_, value=None): + is_pointer, is_array, type_ = _type_info(library, type_) + if is_array: + if is_array is True: + type_ = type_ * value + value = None + else: + type_ = type_ * is_array + + params = [] + if value is not None: + params.append(value) + output = type_(*params) + + if is_pointer: + output = pointer(output) + + return output + + def ref(value, offset=0): + if offset == 0: + return ctypes.byref(value) + return ctypes.cast(ctypes.addressof(value) + offset, ctypes.POINTER(ctypes.c_byte)) + + def native(type_, value): + if isinstance(value, type_): + return value + if sys.version_info < (3,) and type_ == int and isinstance(value, int_types): + return value + if isinstance(value, ctypes.Array) and value._type_ == ctypes.c_byte: + return ctypes.string_at(ctypes.addressof(value), value._length_) + return type_(value.value) + + def deref(point): + return point[0] + + def unwrap(point): + return point.contents + + def struct(library, name): + return pointer(getattr(library, name)()) + + def struct_bytes(struct_): + return ctypes.string_at(struct_, ctypes.sizeof(struct_.contents)) + + def struct_from_buffer(library, type_, buffer): + class_ = getattr(library, type_) + value = class_() + ctypes.memmove(ctypes.addressof(value), buffer, ctypes.sizeof(class_)) + return ctypes.pointer(value) + + def array_from_pointer(library, type_, point, size): + _, _, type_ = _type_info(library, type_) + array = ctypes.cast(point, ctypes.POINTER(type_)) + output = [] + for i in range(0, size): + output.append(array[i]) + return output + + def callback(library, signature_type, func): + return getattr(library, signature_type)(func) + + engine = 'ctypes' + + +def get_library(name, dylib_name, version): + """ + Retrieve the C library path with special handling for Mac + + :param name: + A unicode string of the library to search the system for + + :param dylib_name: + Mac only - a unicode string of the unversioned dylib name + + :param version: + Mac only - a unicode string of the dylib version to use. Used on macOS + 10.15+ when the unversioned dylib is found, since unversioned + OpenSSL/LibreSSL are just placeholders, and a versioned dylib must be + imported. Used on macOS 10.16+ when find_library() doesn't return a + result, due to system dylibs not being present on the filesystem any + longer. + + :return: + A unicode string of the path to the library + """ + + library = find_library(name) + + if sys.platform == 'darwin': + unversioned = '/usr/lib/%s' % dylib_name + versioned = unversioned.replace('.dylib', '.%s.dylib' % version) + mac_ver = tuple(map(int, platform.mac_ver()[0].split('.'))) + if not library and mac_ver >= (10, 16): + # On macOS 10.16+, find_library doesn't work, so we set a static path + library = versioned + elif mac_ver >= (10, 15) and library == unversioned: + # On macOS 10.15+, we want to strongly version since unversioned libcrypto has a non-stable ABI + library = versioned + + return library + + +class FFIEngineError(Exception): + + """ + An exception when trying to instantiate ctypes or cffi + """ + + pass diff --git a/tasks/lib/package_control/deps/oscrypto/_int.py b/tasks/lib/package_control/deps/oscrypto/_int.py new file mode 100644 index 0000000..0721c34 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_int.py @@ -0,0 +1,35 @@ +# coding: utf-8 + +""" +Function to fill ensure integers converted to a byte string are a specific +width. Exports the following items: + + - fill_width() +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + + +__all__ = [ + 'fill_width', +] + + +def fill_width(bytes_, width): + """ + Ensure a byte string representing a positive integer is a specific width + (in bytes) + + :param bytes_: + The integer byte string + + :param width: + The desired width as an integer + + :return: + A byte string of the width specified + """ + + while len(bytes_) < width: + bytes_ = b'\x00' + bytes_ + return bytes_ diff --git a/tasks/lib/package_control/deps/oscrypto/_linux_bsd/__init__.py b/tasks/lib/package_control/deps/oscrypto/_linux_bsd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tasks/lib/package_control/deps/oscrypto/_linux_bsd/trust_list.py b/tasks/lib/package_control/deps/oscrypto/_linux_bsd/trust_list.py new file mode 100644 index 0000000..4f4185e --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_linux_bsd/trust_list.py @@ -0,0 +1,120 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import os + +from .._asn1 import Certificate, TrustedCertificate, unarmor +from .._errors import pretty_message + + +__all__ = [ + 'extract_from_system', + 'system_path', +] + + +def system_path(): + """ + Tries to find a CA certs bundle in common locations + + :raises: + OSError - when no valid CA certs bundle was found on the filesystem + + :return: + The full filesystem path to a CA certs bundle file + """ + + ca_path = None + + # Common CA cert paths + paths = [ + '/usr/lib/ssl/certs/ca-certificates.crt', + '/etc/ssl/certs/ca-certificates.crt', + '/etc/ssl/certs/ca-bundle.crt', + '/etc/pki/tls/certs/ca-bundle.crt', + '/etc/ssl/ca-bundle.pem', + '/usr/local/share/certs/ca-root-nss.crt', + '/etc/ssl/cert.pem' + ] + + # First try SSL_CERT_FILE + if 'SSL_CERT_FILE' in os.environ: + paths.insert(0, os.environ['SSL_CERT_FILE']) + + for path in paths: + if os.path.exists(path) and os.path.getsize(path) > 0: + ca_path = path + break + + if not ca_path: + raise OSError(pretty_message( + ''' + Unable to find a CA certs bundle in common locations - try + setting the SSL_CERT_FILE environmental variable + ''' + )) + + return ca_path + + +def extract_from_system(cert_callback=None, callback_only_on_failure=False): + """ + Extracts trusted CA certs from the system CA cert bundle + + :param cert_callback: + A callback that is called once for each certificate in the trust store. + It should accept two parameters: an asn1crypto.x509.Certificate object, + and a reason. The reason will be None if the certificate is being + exported, otherwise it will be a unicode string of the reason it won't. + + :param callback_only_on_failure: + A boolean - if the callback should only be called when a certificate is + not exported. + + :return: + A list of 3-element tuples: + - 0: a byte string of a DER-encoded certificate + - 1: a set of unicode strings that are OIDs of purposes to trust the + certificate for + - 2: a set of unicode strings that are OIDs of purposes to reject the + certificate for + """ + + all_purposes = '2.5.29.37.0' + ca_path = system_path() + + output = [] + with open(ca_path, 'rb') as f: + for armor_type, _, cert_bytes in unarmor(f.read(), multiple=True): + # Without more info, a certificate is trusted for all purposes + if armor_type == 'CERTIFICATE': + if cert_callback: + cert_callback(Certificate.load(cert_bytes), None) + output.append((cert_bytes, set(), set())) + + # The OpenSSL TRUSTED CERTIFICATE construct adds OIDs for trusted + # and rejected purposes, so we extract that info. + elif armor_type == 'TRUSTED CERTIFICATE': + cert, aux = TrustedCertificate.load(cert_bytes) + reject_all = False + trust_oids = set() + reject_oids = set() + for purpose in aux['trust']: + if purpose.dotted == all_purposes: + trust_oids = set([purpose.dotted]) + break + trust_oids.add(purpose.dotted) + for purpose in aux['reject']: + if purpose.dotted == all_purposes: + reject_all = True + break + reject_oids.add(purpose.dotted) + if reject_all: + if cert_callback: + cert_callback(cert, 'explicitly distrusted') + continue + if cert_callback and not callback_only_on_failure: + cert_callback(cert, None) + output.append((cert.dump(), trust_oids, reject_oids)) + + return output diff --git a/tasks/lib/package_control/deps/oscrypto/_mac/__init__.py b/tasks/lib/package_control/deps/oscrypto/_mac/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tasks/lib/package_control/deps/oscrypto/_mac/_common_crypto.py b/tasks/lib/package_control/deps/oscrypto/_mac/_common_crypto.py new file mode 100644 index 0000000..3b290b8 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_mac/_common_crypto.py @@ -0,0 +1,24 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from .. import ffi + +if ffi() == 'cffi': + from ._common_crypto_cffi import CommonCrypto +else: + from ._common_crypto_ctypes import CommonCrypto + + +__all__ = [ + 'CommonCrypto', + 'CommonCryptoConst', +] + + +class CommonCryptoConst(): + kCCPBKDF2 = 2 + kCCPRFHmacAlgSHA1 = 1 + kCCPRFHmacAlgSHA224 = 2 + kCCPRFHmacAlgSHA256 = 3 + kCCPRFHmacAlgSHA384 = 4 + kCCPRFHmacAlgSHA512 = 5 diff --git a/tasks/lib/package_control/deps/oscrypto/_mac/_common_crypto_cffi.py b/tasks/lib/package_control/deps/oscrypto/_mac/_common_crypto_cffi.py new file mode 100644 index 0000000..30c768b --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_mac/_common_crypto_cffi.py @@ -0,0 +1,29 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from .._ffi import register_ffi + +from cffi import FFI + + +__all__ = [ + 'CommonCrypto', +] + + +ffi = FFI() +ffi.cdef(""" + typedef uint32_t CCPBKDFAlgorithm; + + typedef uint32_t CCPseudoRandomAlgorithm; + typedef unsigned int uint; + + int CCKeyDerivationPBKDF(CCPBKDFAlgorithm algorithm, const char *password, size_t passwordLen, + const char *salt, size_t saltLen, CCPseudoRandomAlgorithm prf, uint rounds, + char *derivedKey, size_t derivedKeyLen); +""") + +common_crypto_path = '/usr/lib/system/libcommonCrypto.dylib' + +CommonCrypto = ffi.dlopen(common_crypto_path) +register_ffi(CommonCrypto, ffi) diff --git a/tasks/lib/package_control/deps/oscrypto/_mac/_common_crypto_ctypes.py b/tasks/lib/package_control/deps/oscrypto/_mac/_common_crypto_ctypes.py new file mode 100644 index 0000000..2b4ee8c --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_mac/_common_crypto_ctypes.py @@ -0,0 +1,32 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from ctypes import CDLL, c_uint32, c_char_p, c_size_t, c_int, c_uint + +from .._ffi import FFIEngineError + + +__all__ = [ + 'CommonCrypto', +] + + +common_crypto_path = '/usr/lib/system/libcommonCrypto.dylib' + +CommonCrypto = CDLL(common_crypto_path, use_errno=True) + +try: + CommonCrypto.CCKeyDerivationPBKDF.argtypes = [ + c_uint32, + c_char_p, + c_size_t, + c_char_p, + c_size_t, + c_uint32, + c_uint, + c_char_p, + c_size_t + ] + CommonCrypto.CCKeyDerivationPBKDF.restype = c_int +except (AttributeError): + raise FFIEngineError('Error initializing ctypes') diff --git a/tasks/lib/package_control/deps/oscrypto/_mac/_core_foundation.py b/tasks/lib/package_control/deps/oscrypto/_mac/_core_foundation.py new file mode 100644 index 0000000..06b61a1 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_mac/_core_foundation.py @@ -0,0 +1,228 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from .. import ffi +from .._ffi import is_null, unwrap + +if ffi() == 'cffi': + from ._core_foundation_cffi import CoreFoundation, CFHelpers +else: + from ._core_foundation_ctypes import CoreFoundation, CFHelpers + + +_all__ = [ + 'CFHelpers', + 'CoreFoundation', + 'handle_cf_error', +] + + +def handle_cf_error(error_pointer): + """ + Checks a CFErrorRef and throws an exception if there is an error to report + + :param error_pointer: + A CFErrorRef + + :raises: + OSError - when the CFErrorRef contains an error + """ + + if is_null(error_pointer): + return + + error = unwrap(error_pointer) + if is_null(error): + return + + cf_string_domain = CoreFoundation.CFErrorGetDomain(error) + domain = CFHelpers.cf_string_to_unicode(cf_string_domain) + CoreFoundation.CFRelease(cf_string_domain) + num = CoreFoundation.CFErrorGetCode(error) + + cf_string_ref = CoreFoundation.CFErrorCopyDescription(error) + output = CFHelpers.cf_string_to_unicode(cf_string_ref) + CoreFoundation.CFRelease(cf_string_ref) + + if output is None: + if domain == 'NSOSStatusErrorDomain': + code_map = { + -2147416010: 'ACL add failed', + -2147416025: 'ACL base certs not supported', + -2147416019: 'ACL challenge callback failed', + -2147416015: 'ACL change failed', + -2147416012: 'ACL delete failed', + -2147416017: 'ACL entry tag not found', + -2147416011: 'ACL replace failed', + -2147416021: 'ACL subject type not supported', + -2147415789: 'Algid mismatch', + -2147415726: 'Already logged in', + -2147415040: 'Apple add application ACL subject', + -2147415036: 'Apple invalid key end date', + -2147415037: 'Apple invalid key start date', + -2147415039: 'Apple public key incomplete', + -2147415038: 'Apple signature mismatch', + -2147415034: 'Apple SSLv2 rollback', + -2147415802: 'Attach handle busy', + -2147415731: 'Block size mismatch', + -2147415722: 'Crypto data callback failed', + -2147415804: 'Device error', + -2147415835: 'Device failed', + -2147415803: 'Device memory error', + -2147415836: 'Device reset', + -2147415728: 'Device verify failed', + -2147416054: 'Function failed', + -2147416057: 'Function not implemented', + -2147415807: 'Input length error', + -2147415837: 'Insufficient client identification', + -2147416063: 'Internal error', + -2147416027: 'Invalid access credentials', + -2147416026: 'Invalid ACL base certs', + -2147416020: 'Invalid ACL challenge callback', + -2147416016: 'Invalid ACL edit mode', + -2147416018: 'Invalid ACL entry tag', + -2147416022: 'Invalid ACL subject value', + -2147415759: 'Invalid algorithm', + -2147415678: 'Invalid attr access credentials', + -2147415704: 'Invalid attr alg params', + -2147415686: 'Invalid attr base', + -2147415738: 'Invalid attr block size', + -2147415680: 'Invalid attr dl db handle', + -2147415696: 'Invalid attr effective bits', + -2147415692: 'Invalid attr end date', + -2147415752: 'Invalid attr init vector', + -2147415682: 'Invalid attr iteration count', + -2147415754: 'Invalid attr key', + -2147415740: 'Invalid attr key length', + -2147415700: 'Invalid attr key type', + -2147415702: 'Invalid attr label', + -2147415698: 'Invalid attr mode', + -2147415708: 'Invalid attr output size', + -2147415748: 'Invalid attr padding', + -2147415742: 'Invalid attr passphrase', + -2147415688: 'Invalid attr prime', + -2147415674: 'Invalid attr private key format', + -2147415676: 'Invalid attr public key format', + -2147415746: 'Invalid attr random', + -2147415706: 'Invalid attr rounds', + -2147415750: 'Invalid attr salt', + -2147415744: 'Invalid attr seed', + -2147415694: 'Invalid attr start date', + -2147415684: 'Invalid attr subprime', + -2147415672: 'Invalid attr symmetric key format', + -2147415690: 'Invalid attr version', + -2147415670: 'Invalid attr wrapped key format', + -2147415760: 'Invalid context', + -2147416000: 'Invalid context handle', + -2147415976: 'Invalid crypto data', + -2147415994: 'Invalid data', + -2147415768: 'Invalid data count', + -2147415723: 'Invalid digest algorithm', + -2147416059: 'Invalid input pointer', + -2147415766: 'Invalid input vector', + -2147415792: 'Invalid key', + -2147415780: 'Invalid keyattr mask', + -2147415782: 'Invalid keyusage mask', + -2147415790: 'Invalid key class', + -2147415776: 'Invalid key format', + -2147415778: 'Invalid key label', + -2147415783: 'Invalid key pointer', + -2147415791: 'Invalid key reference', + -2147415727: 'Invalid login name', + -2147416014: 'Invalid new ACL entry', + -2147416013: 'Invalid new ACL owner', + -2147416058: 'Invalid output pointer', + -2147415765: 'Invalid output vector', + -2147415978: 'Invalid passthrough id', + -2147416060: 'Invalid pointer', + -2147416024: 'Invalid sample value', + -2147415733: 'Invalid signature', + -2147415787: 'Key blob type incorrect', + -2147415786: 'Key header inconsistent', + -2147415724: 'Key label already exists', + -2147415788: 'Key usage incorrect', + -2147416061: 'Mds error', + -2147416062: 'Memory error', + -2147415677: 'Missing attr access credentials', + -2147415703: 'Missing attr alg params', + -2147415685: 'Missing attr base', + -2147415737: 'Missing attr block size', + -2147415679: 'Missing attr dl db handle', + -2147415695: 'Missing attr effective bits', + -2147415691: 'Missing attr end date', + -2147415751: 'Missing attr init vector', + -2147415681: 'Missing attr iteration count', + -2147415753: 'Missing attr key', + -2147415739: 'Missing attr key length', + -2147415699: 'Missing attr key type', + -2147415701: 'Missing attr label', + -2147415697: 'Missing attr mode', + -2147415707: 'Missing attr output size', + -2147415747: 'Missing attr padding', + -2147415741: 'Missing attr passphrase', + -2147415687: 'Missing attr prime', + -2147415673: 'Missing attr private key format', + -2147415675: 'Missing attr public key format', + -2147415745: 'Missing attr random', + -2147415705: 'Missing attr rounds', + -2147415749: 'Missing attr salt', + -2147415743: 'Missing attr seed', + -2147415693: 'Missing attr start date', + -2147415683: 'Missing attr subprime', + -2147415671: 'Missing attr symmetric key format', + -2147415689: 'Missing attr version', + -2147415669: 'Missing attr wrapped key format', + -2147415801: 'Not logged in', + -2147415840: 'No user interaction', + -2147416029: 'Object ACL not supported', + -2147416028: 'Object ACL required', + -2147416030: 'Object manip auth denied', + -2147416031: 'Object use auth denied', + -2147416032: 'Operation auth denied', + -2147416055: 'OS access denied', + -2147415806: 'Output length error', + -2147415725: 'Private key already exists', + -2147415730: 'Private key not found', + -2147415989: 'Privilege not granted', + -2147415805: 'Privilege not supported', + -2147415729: 'Public key inconsistent', + -2147415732: 'Query size unknown', + -2147416023: 'Sample value not supported', + -2147416056: 'Self check failed', + -2147415838: 'Service not available', + -2147415736: 'Staged operation in progress', + -2147415735: 'Staged operation not started', + -2147415779: 'Unsupported keyattr mask', + -2147415781: 'Unsupported keyusage mask', + -2147415785: 'Unsupported key format', + -2147415777: 'Unsupported key label', + -2147415784: 'Unsupported key size', + -2147415839: 'User canceled', + -2147415767: 'Vector of bufs unsupported', + -2147415734: 'Verify failed', + } + if num in code_map: + output = code_map[num] + + if not output: + output = '%s %s' % (domain, num) + + raise OSError(output) + + +CFHelpers.register_native_mapping( + CoreFoundation.CFStringGetTypeID(), + CFHelpers.cf_string_to_unicode +) +CFHelpers.register_native_mapping( + CoreFoundation.CFNumberGetTypeID(), + CFHelpers.cf_number_to_number +) +CFHelpers.register_native_mapping( + CoreFoundation.CFDataGetTypeID(), + CFHelpers.cf_data_to_bytes +) +CFHelpers.register_native_mapping( + CoreFoundation.CFDictionaryGetTypeID(), + CFHelpers.cf_dictionary_to_dict +) diff --git a/tasks/lib/package_control/deps/oscrypto/_mac/_core_foundation_cffi.py b/tasks/lib/package_control/deps/oscrypto/_mac/_core_foundation_cffi.py new file mode 100644 index 0000000..d0c7951 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_mac/_core_foundation_cffi.py @@ -0,0 +1,375 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from .._ffi import ( + buffer_from_bytes, + byte_string_from_buffer, + deref, + is_null, + new, + register_ffi, +) + +from cffi import FFI + + +__all__ = [ + 'CFHelpers', + 'CoreFoundation', +] + + +ffi = FFI() +ffi.cdef(""" + typedef bool Boolean; + typedef long CFIndex; + typedef unsigned long CFStringEncoding; + typedef unsigned long CFNumberType; + typedef unsigned long CFTypeID; + + typedef void *CFTypeRef; + typedef CFTypeRef CFArrayRef; + typedef CFTypeRef CFDataRef; + typedef CFTypeRef CFStringRef; + typedef CFTypeRef CFNumberRef; + typedef CFTypeRef CFBooleanRef; + typedef CFTypeRef CFDictionaryRef; + typedef CFTypeRef CFErrorRef; + typedef CFTypeRef CFAllocatorRef; + + typedef struct { + CFIndex version; + void *retain; + void *release; + void *copyDescription; + void *equal; + void *hash; + } CFDictionaryKeyCallBacks; + + typedef struct { + CFIndex version; + void *retain; + void *release; + void *copyDescription; + void *equal; + } CFDictionaryValueCallBacks; + + typedef struct { + CFIndex version; + void *retain; + void *release; + void *copyDescription; + void *equal; + } CFArrayCallBacks; + + CFIndex CFDataGetLength(CFDataRef theData); + const char *CFDataGetBytePtr(CFDataRef theData); + CFDataRef CFDataCreate(CFAllocatorRef allocator, const char *bytes, CFIndex length); + + CFDictionaryRef CFDictionaryCreate(CFAllocatorRef allocator, const void **keys, const void **values, + CFIndex numValues, const CFDictionaryKeyCallBacks *keyCallBacks, + const CFDictionaryValueCallBacks *valueCallBacks); + CFIndex CFDictionaryGetCount(CFDictionaryRef theDict); + + const char *CFStringGetCStringPtr(CFStringRef theString, CFStringEncoding encoding); + Boolean CFStringGetCString(CFStringRef theString, char *buffer, CFIndex bufferSize, CFStringEncoding encoding); + CFStringRef CFStringCreateWithCString(CFAllocatorRef alloc, const char *cStr, CFStringEncoding encoding); + + CFNumberRef CFNumberCreate(CFAllocatorRef allocator, CFNumberType theType, const void *valuePtr); + + CFStringRef CFCopyTypeIDDescription(CFTypeID type_id); + + void CFRelease(CFTypeRef cf); + void CFRetain(CFTypeRef cf); + + CFStringRef CFErrorCopyDescription(CFErrorRef err); + CFStringRef CFErrorGetDomain(CFErrorRef err); + CFIndex CFErrorGetCode(CFErrorRef err); + + Boolean CFBooleanGetValue(CFBooleanRef boolean); + + CFTypeID CFDictionaryGetTypeID(void); + CFTypeID CFNumberGetTypeID(void); + CFTypeID CFStringGetTypeID(void); + CFTypeID CFDataGetTypeID(void); + + CFArrayRef CFArrayCreate(CFAllocatorRef allocator, const void **values, CFIndex numValues, + const CFArrayCallBacks *callBacks); + CFIndex CFArrayGetCount(CFArrayRef theArray); + CFTypeRef CFArrayGetValueAtIndex(CFArrayRef theArray, CFIndex idx); + CFNumberType CFNumberGetType(CFNumberRef number); + Boolean CFNumberGetValue(CFNumberRef number, CFNumberType theType, void *valuePtr); + CFIndex CFDictionaryGetKeysAndValues(CFDictionaryRef theDict, const void **keys, const void **values); + CFTypeID CFGetTypeID(CFTypeRef cf); + + extern CFAllocatorRef kCFAllocatorDefault; + extern CFArrayCallBacks kCFTypeArrayCallBacks; + extern CFBooleanRef kCFBooleanTrue; + extern CFDictionaryKeyCallBacks kCFTypeDictionaryKeyCallBacks; + extern CFDictionaryValueCallBacks kCFTypeDictionaryValueCallBacks; +""") + +core_foundation_path = '/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation' + +CoreFoundation = ffi.dlopen(core_foundation_path) +register_ffi(CoreFoundation, ffi) + +kCFNumberCFIndexType = 14 +kCFStringEncodingUTF8 = 0x08000100 + + +class CFHelpers(): + """ + Namespace for core foundation helpers + """ + + _native_map = {} + + @classmethod + def register_native_mapping(cls, type_id, callback): + """ + Register a function to convert a core foundation data type into its + equivalent in python + + :param type_id: + The CFTypeId for the type + + :param callback: + A callback to pass the CFType object to + """ + + cls._native_map[int(type_id)] = callback + + @staticmethod + def cf_number_to_number(value): + """ + Converts a CFNumber object to a python float or integer + + :param value: + The CFNumber object + + :return: + A python number (float or integer) + """ + + type_ = CoreFoundation.CFNumberGetType(value) + type_name_ = { + 1: 'int8_t', # kCFNumberSInt8Type + 2: 'in16_t', # kCFNumberSInt16Type + 3: 'int32_t', # kCFNumberSInt32Type + 4: 'int64_t', # kCFNumberSInt64Type + 5: 'float', # kCFNumberFloat32Type + 6: 'double', # kCFNumberFloat64Type + 7: 'char', # kCFNumberCharType + 8: 'short', # kCFNumberShortType + 9: 'int', # kCFNumberIntType + 10: 'long', # kCFNumberLongType + 11: 'long long', # kCFNumberLongLongType + 12: 'float', # kCFNumberFloatType + 13: 'double', # kCFNumberDoubleType + 14: 'long', # kCFNumberCFIndexType + 15: 'int', # kCFNumberNSIntegerType + 16: 'double', # kCFNumberCGFloatType + }[type_] + output = new(CoreFoundation, type_name_ + ' *') + CoreFoundation.CFNumberGetValue(value, type_, output) + return deref(output) + + @staticmethod + def cf_dictionary_to_dict(dictionary): + """ + Converts a CFDictionary object into a python dictionary + + :param dictionary: + The CFDictionary to convert + + :return: + A python dict + """ + + dict_length = CoreFoundation.CFDictionaryGetCount(dictionary) + + keys = new(CoreFoundation, 'CFTypeRef[%s]' % dict_length) + values = new(CoreFoundation, 'CFTypeRef[%s]' % dict_length) + CoreFoundation.CFDictionaryGetKeysAndValues( + dictionary, + keys, + values + ) + + output = {} + for index in range(0, dict_length): + output[CFHelpers.native(keys[index])] = CFHelpers.native(values[index]) + + return output + + @classmethod + def native(cls, value): + """ + Converts a CF* object into its python equivalent + + :param value: + The CF* object to convert + + :return: + The native python object + """ + + type_id = CoreFoundation.CFGetTypeID(value) + if type_id in cls._native_map: + return cls._native_map[type_id](value) + else: + return value + + @staticmethod + def cf_string_to_unicode(value): + """ + Creates a python unicode string from a CFString object + + :param value: + The CFString to convert + + :return: + A python unicode string + """ + + string_ptr = CoreFoundation.CFStringGetCStringPtr( + value, + kCFStringEncodingUTF8 + ) + string = None if is_null(string_ptr) else ffi.string(string_ptr) + if string is None: + buffer = buffer_from_bytes(1024) + result = CoreFoundation.CFStringGetCString( + value, + buffer, + 1024, + kCFStringEncodingUTF8 + ) + if not result: + raise OSError('Error copying C string from CFStringRef') + string = byte_string_from_buffer(buffer) + if string is not None: + string = string.decode('utf-8') + return string + + @staticmethod + def cf_string_from_unicode(string): + """ + Creates a CFStringRef object from a unicode string + + :param string: + The unicode string to create the CFString object from + + :return: + A CFStringRef + """ + + return CoreFoundation.CFStringCreateWithCString( + CoreFoundation.kCFAllocatorDefault, + string.encode('utf-8'), + kCFStringEncodingUTF8 + ) + + @staticmethod + def cf_data_to_bytes(value): + """ + Extracts a bytestring from a CFData object + + :param value: + A CFData object + + :return: + A byte string + """ + + start = CoreFoundation.CFDataGetBytePtr(value) + num_bytes = CoreFoundation.CFDataGetLength(value) + return ffi.buffer(start, num_bytes)[:] + + @staticmethod + def cf_data_from_bytes(bytes_): + """ + Creates a CFDataRef object from a byte string + + :param bytes_: + The data to create the CFData object from + + :return: + A CFDataRef + """ + + return CoreFoundation.CFDataCreate( + CoreFoundation.kCFAllocatorDefault, + bytes_, + len(bytes_) + ) + + @staticmethod + def cf_dictionary_from_pairs(pairs): + """ + Creates a CFDictionaryRef object from a list of 2-element tuples + representing the key and value. Each key should be a CFStringRef and each + value some sort of CF* type. + + :param pairs: + A list of 2-element tuples + + :return: + A CFDictionaryRef + """ + + length = len(pairs) + keys = [] + values = [] + for pair in pairs: + key, value = pair + keys.append(key) + values.append(value) + return CoreFoundation.CFDictionaryCreate( + CoreFoundation.kCFAllocatorDefault, + keys, + values, + length, + ffi.addressof(CoreFoundation.kCFTypeDictionaryKeyCallBacks), + ffi.addressof(CoreFoundation.kCFTypeDictionaryValueCallBacks) + ) + + @staticmethod + def cf_array_from_list(values): + """ + Creates a CFArrayRef object from a list of CF* type objects. + + :param values: + A list of CF* type object + + :return: + A CFArrayRef + """ + + length = len(values) + return CoreFoundation.CFArrayCreate( + CoreFoundation.kCFAllocatorDefault, + values, + length, + ffi.addressof(CoreFoundation.kCFTypeArrayCallBacks) + ) + + @staticmethod + def cf_number_from_integer(integer): + """ + Creates a CFNumber object from an integer + + :param integer: + The integer to create the CFNumber for + + :return: + A CFNumber + """ + + integer_as_long = ffi.new('long *', integer) + return CoreFoundation.CFNumberCreate( + CoreFoundation.kCFAllocatorDefault, + kCFNumberCFIndexType, + integer_as_long + ) diff --git a/tasks/lib/package_control/deps/oscrypto/_mac/_core_foundation_ctypes.py b/tasks/lib/package_control/deps/oscrypto/_mac/_core_foundation_ctypes.py new file mode 100644 index 0000000..683934e --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_mac/_core_foundation_ctypes.py @@ -0,0 +1,487 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from ctypes import c_void_p, c_long, c_uint32, c_char_p, c_byte, c_ulong, c_bool +from ctypes import CDLL, string_at, cast, POINTER, byref +import ctypes + +from .._ffi import FFIEngineError, buffer_from_bytes, byte_string_from_buffer + + +__all__ = [ + 'CFHelpers', + 'CoreFoundation', +] + + +core_foundation_path = '/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation' + +CoreFoundation = CDLL(core_foundation_path, use_errno=True) + +CFIndex = c_long +CFStringEncoding = c_uint32 +CFArray = c_void_p +CFData = c_void_p +CFString = c_void_p +CFNumber = c_void_p +CFDictionary = c_void_p +CFError = c_void_p +CFType = c_void_p +CFTypeID = c_ulong +CFBoolean = c_void_p +CFNumberType = c_uint32 + +CFTypeRef = POINTER(CFType) +CFArrayRef = POINTER(CFArray) +CFDataRef = POINTER(CFData) +CFStringRef = POINTER(CFString) +CFNumberRef = POINTER(CFNumber) +CFBooleanRef = POINTER(CFBoolean) +CFDictionaryRef = POINTER(CFDictionary) +CFErrorRef = POINTER(CFError) +CFAllocatorRef = c_void_p +CFDictionaryKeyCallBacks = c_void_p +CFDictionaryValueCallBacks = c_void_p +CFArrayCallBacks = c_void_p + +pointer_p = POINTER(c_void_p) + +try: + CoreFoundation.CFDataGetLength.argtypes = [ + CFDataRef + ] + CoreFoundation.CFDataGetLength.restype = CFIndex + + CoreFoundation.CFDataGetBytePtr.argtypes = [ + CFDataRef + ] + CoreFoundation.CFDataGetBytePtr.restype = c_void_p + + CoreFoundation.CFDataCreate.argtypes = [ + CFAllocatorRef, + c_char_p, + CFIndex + ] + CoreFoundation.CFDataCreate.restype = CFDataRef + + CoreFoundation.CFDictionaryCreate.argtypes = [ + CFAllocatorRef, + CFStringRef, + CFTypeRef, + CFIndex, + CFDictionaryKeyCallBacks, + CFDictionaryValueCallBacks + ] + CoreFoundation.CFDictionaryCreate.restype = CFDictionaryRef + + CoreFoundation.CFDictionaryGetCount.argtypes = [ + CFDictionaryRef + ] + CoreFoundation.CFDictionaryGetCount.restype = CFIndex + + CoreFoundation.CFStringGetCStringPtr.argtypes = [ + CFStringRef, + CFStringEncoding + ] + CoreFoundation.CFStringGetCStringPtr.restype = c_char_p + + CoreFoundation.CFStringGetCString.argtypes = [ + CFStringRef, + c_char_p, + CFIndex, + CFStringEncoding + ] + CoreFoundation.CFStringGetCString.restype = c_bool + + CoreFoundation.CFStringCreateWithCString.argtypes = [ + CFAllocatorRef, + c_char_p, + CFStringEncoding + ] + CoreFoundation.CFStringCreateWithCString.restype = CFStringRef + + CoreFoundation.CFNumberCreate.argtypes = [ + CFAllocatorRef, + CFNumberType, + c_void_p + ] + CoreFoundation.CFNumberCreate.restype = CFNumberRef + + CoreFoundation.CFCopyTypeIDDescription.argtypes = [ + CFTypeID + ] + CoreFoundation.CFCopyTypeIDDescription.restype = CFStringRef + + CoreFoundation.CFRelease.argtypes = [ + CFTypeRef + ] + CoreFoundation.CFRelease.restype = None + + CoreFoundation.CFRetain.argtypes = [ + CFTypeRef + ] + CoreFoundation.CFRetain.restype = None + + CoreFoundation.CFErrorCopyDescription.argtypes = [ + CFErrorRef + ] + CoreFoundation.CFErrorCopyDescription.restype = CFStringRef + + CoreFoundation.CFErrorGetDomain.argtypes = [ + CFErrorRef + ] + CoreFoundation.CFErrorGetDomain.restype = CFStringRef + + CoreFoundation.CFErrorGetCode.argtypes = [ + CFErrorRef + ] + CoreFoundation.CFErrorGetCode.restype = CFIndex + + CoreFoundation.CFBooleanGetValue.argtypes = [ + CFBooleanRef + ] + CoreFoundation.CFBooleanGetValue.restype = c_byte + + CoreFoundation.CFDictionaryGetTypeID.argtypes = [] + CoreFoundation.CFDictionaryGetTypeID.restype = CFTypeID + + CoreFoundation.CFNumberGetTypeID.argtypes = [] + CoreFoundation.CFNumberGetTypeID.restype = CFTypeID + + CoreFoundation.CFStringGetTypeID.argtypes = [] + CoreFoundation.CFStringGetTypeID.restype = CFTypeID + + CoreFoundation.CFDataGetTypeID.argtypes = [] + CoreFoundation.CFDataGetTypeID.restype = CFTypeID + + CoreFoundation.CFArrayCreate.argtypes = [ + CFAllocatorRef, + POINTER(c_void_p), + CFIndex, + CFArrayCallBacks + ] + CoreFoundation.CFArrayCreate.restype = CFArrayRef + + CoreFoundation.CFArrayGetCount.argtypes = [ + CFArrayRef + ] + CoreFoundation.CFArrayGetCount.restype = CFIndex + + CoreFoundation.CFArrayGetValueAtIndex.argtypes = [ + CFArrayRef, + CFIndex + ] + CoreFoundation.CFArrayGetValueAtIndex.restype = CFTypeRef + + CoreFoundation.CFNumberGetType.argtypes = [ + CFNumberRef + ] + CoreFoundation.CFNumberGetType.restype = CFNumberType + + CoreFoundation.CFNumberGetValue.argtypes = [ + CFNumberRef, + CFNumberType, + c_void_p + ] + CoreFoundation.CFNumberGetValue.restype = c_bool + + CoreFoundation.CFDictionaryGetKeysAndValues.argtypes = [ + CFDictionaryRef, + pointer_p, + pointer_p + ] + CoreFoundation.CFDictionaryGetKeysAndValues.restype = CFIndex + + CoreFoundation.CFGetTypeID.argtypes = [ + CFTypeRef + ] + CoreFoundation.CFGetTypeID.restype = CFTypeID + + setattr(CoreFoundation, 'kCFAllocatorDefault', CFAllocatorRef.in_dll(CoreFoundation, 'kCFAllocatorDefault')) + setattr(CoreFoundation, 'kCFBooleanTrue', CFTypeRef.in_dll(CoreFoundation, 'kCFBooleanTrue')) + + kCFTypeDictionaryKeyCallBacks = c_void_p.in_dll(CoreFoundation, 'kCFTypeDictionaryKeyCallBacks') + kCFTypeDictionaryValueCallBacks = c_void_p.in_dll(CoreFoundation, 'kCFTypeDictionaryValueCallBacks') + kCFTypeArrayCallBacks = c_void_p.in_dll(CoreFoundation, 'kCFTypeArrayCallBacks') + +except (AttributeError): + raise FFIEngineError('Error initializing ctypes') + +setattr(CoreFoundation, 'CFDataRef', CFDataRef) +setattr(CoreFoundation, 'CFErrorRef', CFErrorRef) +setattr(CoreFoundation, 'CFArrayRef', CFArrayRef) +kCFNumberCFIndexType = CFNumberType(14) +kCFStringEncodingUTF8 = CFStringEncoding(0x08000100) + + +def _cast_pointer_p(value): + """ + Casts a value to a pointer of a pointer + + :param value: + A ctypes object + + :return: + A POINTER(c_void_p) object + """ + + return cast(value, pointer_p) + + +class CFHelpers(): + """ + Namespace for core foundation helpers + """ + + _native_map = {} + + @classmethod + def register_native_mapping(cls, type_id, callback): + """ + Register a function to convert a core foundation data type into its + equivalent in python + + :param type_id: + The CFTypeId for the type + + :param callback: + A callback to pass the CFType object to + """ + + cls._native_map[int(type_id)] = callback + + @staticmethod + def cf_number_to_number(value): + """ + Converts a CFNumber object to a python float or integer + + :param value: + The CFNumber object + + :return: + A python number (float or integer) + """ + + type_ = CoreFoundation.CFNumberGetType(_cast_pointer_p(value)) + c_type = { + 1: c_byte, # kCFNumberSInt8Type + 2: ctypes.c_short, # kCFNumberSInt16Type + 3: ctypes.c_int32, # kCFNumberSInt32Type + 4: ctypes.c_int64, # kCFNumberSInt64Type + 5: ctypes.c_float, # kCFNumberFloat32Type + 6: ctypes.c_double, # kCFNumberFloat64Type + 7: c_byte, # kCFNumberCharType + 8: ctypes.c_short, # kCFNumberShortType + 9: ctypes.c_int, # kCFNumberIntType + 10: c_long, # kCFNumberLongType + 11: ctypes.c_longlong, # kCFNumberLongLongType + 12: ctypes.c_float, # kCFNumberFloatType + 13: ctypes.c_double, # kCFNumberDoubleType + 14: c_long, # kCFNumberCFIndexType + 15: ctypes.c_int, # kCFNumberNSIntegerType + 16: ctypes.c_double, # kCFNumberCGFloatType + }[type_] + output = c_type(0) + CoreFoundation.CFNumberGetValue(_cast_pointer_p(value), type_, byref(output)) + return output.value + + @staticmethod + def cf_dictionary_to_dict(dictionary): + """ + Converts a CFDictionary object into a python dictionary + + :param dictionary: + The CFDictionary to convert + + :return: + A python dict + """ + + dict_length = CoreFoundation.CFDictionaryGetCount(dictionary) + + keys = (CFTypeRef * dict_length)() + values = (CFTypeRef * dict_length)() + CoreFoundation.CFDictionaryGetKeysAndValues( + dictionary, + _cast_pointer_p(keys), + _cast_pointer_p(values) + ) + + output = {} + for index in range(0, dict_length): + output[CFHelpers.native(keys[index])] = CFHelpers.native(values[index]) + + return output + + @classmethod + def native(cls, value): + """ + Converts a CF* object into its python equivalent + + :param value: + The CF* object to convert + + :return: + The native python object + """ + + type_id = CoreFoundation.CFGetTypeID(value) + if type_id in cls._native_map: + return cls._native_map[type_id](value) + else: + return value + + @staticmethod + def cf_string_to_unicode(value): + """ + Creates a python unicode string from a CFString object + + :param value: + The CFString to convert + + :return: + A python unicode string + """ + + string = CoreFoundation.CFStringGetCStringPtr( + _cast_pointer_p(value), + kCFStringEncodingUTF8 + ) + if string is None: + buffer = buffer_from_bytes(1024) + result = CoreFoundation.CFStringGetCString( + _cast_pointer_p(value), + buffer, + 1024, + kCFStringEncodingUTF8 + ) + if not result: + raise OSError('Error copying C string from CFStringRef') + string = byte_string_from_buffer(buffer) + if string is not None: + string = string.decode('utf-8') + return string + + @staticmethod + def cf_string_from_unicode(string): + """ + Creates a CFStringRef object from a unicode string + + :param string: + The unicode string to create the CFString object from + + :return: + A CFStringRef + """ + + return CoreFoundation.CFStringCreateWithCString( + CoreFoundation.kCFAllocatorDefault, + string.encode('utf-8'), + kCFStringEncodingUTF8 + ) + + @staticmethod + def cf_data_to_bytes(value): + """ + Extracts a bytestring from a CFData object + + :param value: + A CFData object + + :return: + A byte string + """ + + start = CoreFoundation.CFDataGetBytePtr(value) + num_bytes = CoreFoundation.CFDataGetLength(value) + return string_at(start, num_bytes) + + @staticmethod + def cf_data_from_bytes(bytes_): + """ + Creates a CFDataRef object from a byte string + + :param bytes_: + The data to create the CFData object from + + :return: + A CFDataRef + """ + + return CoreFoundation.CFDataCreate( + CoreFoundation.kCFAllocatorDefault, + bytes_, + len(bytes_) + ) + + @staticmethod + def cf_dictionary_from_pairs(pairs): + """ + Creates a CFDictionaryRef object from a list of 2-element tuples + representing the key and value. Each key should be a CFStringRef and each + value some sort of CF* type. + + :param pairs: + A list of 2-element tuples + + :return: + A CFDictionaryRef + """ + + length = len(pairs) + keys = [] + values = [] + for pair in pairs: + key, value = pair + keys.append(key) + values.append(value) + keys = (CFStringRef * length)(*keys) + values = (CFTypeRef * length)(*values) + return CoreFoundation.CFDictionaryCreate( + CoreFoundation.kCFAllocatorDefault, + _cast_pointer_p(byref(keys)), + _cast_pointer_p(byref(values)), + length, + kCFTypeDictionaryKeyCallBacks, + kCFTypeDictionaryValueCallBacks + ) + + @staticmethod + def cf_array_from_list(values): + """ + Creates a CFArrayRef object from a list of CF* type objects. + + :param values: + A list of CF* type object + + :return: + A CFArrayRef + """ + + length = len(values) + values = (CFTypeRef * length)(*values) + return CoreFoundation.CFArrayCreate( + CoreFoundation.kCFAllocatorDefault, + _cast_pointer_p(byref(values)), + length, + kCFTypeArrayCallBacks + ) + + @staticmethod + def cf_number_from_integer(integer): + """ + Creates a CFNumber object from an integer + + :param integer: + The integer to create the CFNumber for + + :return: + A CFNumber + """ + + integer_as_long = c_long(integer) + return CoreFoundation.CFNumberCreate( + CoreFoundation.kCFAllocatorDefault, + kCFNumberCFIndexType, + byref(integer_as_long) + ) diff --git a/tasks/lib/package_control/deps/oscrypto/_mac/_security.py b/tasks/lib/package_control/deps/oscrypto/_mac/_security.py new file mode 100644 index 0000000..0d2e08d --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_mac/_security.py @@ -0,0 +1,151 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from .. import ffi +from .._ffi import null +from ..errors import TLSDisconnectError, TLSGracefulDisconnectError + +if ffi() == 'cffi': + from ._security_cffi import Security, version_info as osx_version_info + from ._core_foundation_cffi import CoreFoundation, CFHelpers +else: + from ._security_ctypes import Security, version_info as osx_version_info + from ._core_foundation_ctypes import CoreFoundation, CFHelpers + + +__all__ = [ + 'handle_sec_error', + 'osx_version_info', + 'Security', + 'SecurityConst', +] + + +def handle_sec_error(error, exception_class=None): + """ + Checks a Security OSStatus error code and throws an exception if there is an + error to report + + :param error: + An OSStatus + + :param exception_class: + The exception class to use for the exception if an error occurred + + :raises: + OSError - when the OSStatus contains an error + """ + + if error == 0: + return + + if error in set([SecurityConst.errSSLClosedNoNotify, SecurityConst.errSSLClosedAbort]): + raise TLSDisconnectError('The remote end closed the connection') + if error == SecurityConst.errSSLClosedGraceful: + raise TLSGracefulDisconnectError('The remote end closed the connection') + + cf_error_string = Security.SecCopyErrorMessageString(error, null()) + output = CFHelpers.cf_string_to_unicode(cf_error_string) + CoreFoundation.CFRelease(cf_error_string) + + if output is None or output == '': + output = 'OSStatus %s' % error + + if exception_class is None: + exception_class = OSError + + raise exception_class(output) + + +def _extract_policy_properties(value): + properties_dict = Security.SecPolicyCopyProperties(value) + return CFHelpers.cf_dictionary_to_dict(properties_dict) + + +CFHelpers.register_native_mapping( + Security.SecPolicyGetTypeID(), + _extract_policy_properties +) + + +class SecurityConst(): + kSecTrustSettingsDomainUser = 0 + kSecTrustSettingsDomainAdmin = 1 + kSecTrustSettingsDomainSystem = 2 + + kSecTrustResultProceed = 1 + kSecTrustResultUnspecified = 4 + kSecTrustOptionImplicitAnchors = 0x00000040 + + kSecFormatOpenSSL = 1 + + kSecItemTypePrivateKey = 1 + kSecItemTypePublicKey = 2 + + kSSLSessionOptionBreakOnServerAuth = 0 + + kSSLProtocol2 = 1 + kSSLProtocol3 = 2 + kTLSProtocol1 = 4 + kTLSProtocol11 = 7 + kTLSProtocol12 = 8 + + kSSLClientSide = 1 + kSSLStreamType = 0 + + errSSLProtocol = -9800 + errSSLWouldBlock = -9803 + errSSLClosedGraceful = -9805 + errSSLClosedNoNotify = -9816 + errSSLClosedAbort = -9806 + + errSSLXCertChainInvalid = -9807 + errSSLCrypto = -9809 + errSSLInternal = -9810 + errSSLCertExpired = -9814 + errSSLCertNotYetValid = -9815 + errSSLUnknownRootCert = -9812 + errSSLNoRootCert = -9813 + errSSLHostNameMismatch = -9843 + errSSLPeerHandshakeFail = -9824 + errSSLPeerProtocolVersion = -9836 + errSSLPeerUserCancelled = -9839 + errSSLWeakPeerEphemeralDHKey = -9850 + errSSLServerAuthCompleted = -9841 + errSSLRecordOverflow = -9847 + + CSSMERR_APPLETP_HOSTNAME_MISMATCH = -2147408896 + CSSMERR_TP_CERT_EXPIRED = -2147409654 + CSSMERR_TP_CERT_NOT_VALID_YET = -2147409653 + CSSMERR_TP_CERT_REVOKED = -2147409652 + CSSMERR_TP_NOT_TRUSTED = -2147409622 + CSSMERR_TP_CERT_SUSPENDED = -2147409651 + + CSSM_CERT_X_509v3 = 0x00000004 + + APPLE_TP_REVOCATION_CRL = b'*\x86H\x86\xf7cd\x01\x06' + APPLE_TP_REVOCATION_OCSP = b'*\x86H\x86\xf7cd\x01\x07' + + CSSM_APPLE_TP_OCSP_OPTS_VERSION = 0 + CSSM_TP_ACTION_OCSP_DISABLE_NET = 0x00000004 + CSSM_TP_ACTION_OCSP_CACHE_READ_DISABLE = 0x00000008 + + CSSM_APPLE_TP_CRL_OPTS_VERSION = 0 + + errSecVerifyFailed = -67808 + errSecNoTrustSettings = -25263 + errSecItemNotFound = -25300 + errSecInvalidTrustSettings = -25262 + + kSecPaddingNone = 0 + kSecPaddingPKCS1 = 1 + + CSSM_KEYUSE_SIGN = 0x00000004 + CSSM_KEYUSE_VERIFY = 0x00000008 + + CSSM_ALGID_DH = 2 + CSSM_ALGID_RSA = 42 + CSSM_ALGID_DSA = 43 + CSSM_ALGID_ECDSA = 73 + CSSM_KEYATTR_PERMANENT = 0x00000001 + CSSM_KEYATTR_EXTRACTABLE = 0x00000020 diff --git a/tasks/lib/package_control/deps/oscrypto/_mac/_security_cffi.py b/tasks/lib/package_control/deps/oscrypto/_mac/_security_cffi.py new file mode 100644 index 0000000..d277d98 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_mac/_security_cffi.py @@ -0,0 +1,238 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import platform + +from .._ffi import register_ffi + +from cffi import FFI + + +__all__ = [ + 'Security', + 'version', + 'version_info', +] + + +version = platform.mac_ver()[0] +version_info = tuple(map(int, version.split('.'))) + +if version_info < (10, 7): + raise OSError('Only OS X 10.7 and newer are supported, not %s.%s' % (version_info[0], version_info[1])) + +ffi = FFI() +ffi.cdef(""" + typedef bool Boolean; + typedef long CFIndex; + typedef int32_t OSStatus; + typedef unsigned long CFTypeID; + typedef uint32_t SecTrustSettingsDomain; + typedef uint32_t SecPadding; + typedef uint32_t SecItemImportExportFlags; + typedef uint32_t SecKeyImportExportFlags; + typedef uint32_t SecExternalFormat; + typedef uint32_t SecExternalItemType; + typedef uint32_t CSSM_ALGORITHMS; + typedef uint64_t CSSM_CC_HANDLE; + typedef uint32_t CSSM_KEYUSE; + typedef uint32_t CSSM_CERT_TYPE; + typedef uint32_t SSLProtocol; + typedef uint32_t SSLCipherSuite; + typedef uint32_t SecTrustResultType; + + typedef void *CFTypeRef; + typedef CFTypeRef CFArrayRef; + typedef CFTypeRef CFDataRef; + typedef CFTypeRef CFStringRef; + typedef CFTypeRef CFDictionaryRef; + typedef CFTypeRef CFErrorRef; + typedef CFTypeRef CFAllocatorRef; + + typedef ... *SecKeyRef; + typedef ... *SecCertificateRef; + typedef ... *SecTransformRef; + typedef ... *SecRandomRef; + typedef ... *SecPolicyRef; + typedef ... *SecPolicySearchRef; + typedef ... *SecAccessRef; + typedef struct + { + uint32_t version; + SecKeyImportExportFlags flags; + CFTypeRef passphrase; + CFStringRef alertTitle; + CFStringRef alertPrompt; + SecAccessRef accessRef; + CFArrayRef keyUsage; + CFArrayRef keyAttributes; + } SecItemImportExportKeyParameters; + typedef ... *SecKeychainRef; + typedef ... *SSLContextRef; + typedef ... *SecTrustRef; + typedef uint32_t SSLConnectionRef; + + typedef struct { + uint32_t Length; + char *Data; + } CSSM_DATA, CSSM_OID; + + typedef struct { + uint32_t Version; + uint32_t Flags; + CSSM_DATA *LocalResponder; + CSSM_DATA *LocalResponderCert; + } CSSM_APPLE_TP_OCSP_OPTIONS; + + typedef struct { + uint32_t Version; + uint32_t CrlFlags; + void *crlStore; + } CSSM_APPLE_TP_CRL_OPTIONS; + + OSStatus SecKeychainCreate(char *path, uint32_t pass_len, void *pass, + Boolean prompt, SecAccessRef initialAccess, SecKeychainRef *keychain); + OSStatus SecKeychainDelete(SecKeychainRef keychain); + int SecRandomCopyBytes(SecRandomRef rnd, size_t count, char *bytes); + SecKeyRef SecKeyCreateFromData(CFDictionaryRef parameters, CFDataRef keyData, CFErrorRef *error); + SecTransformRef SecEncryptTransformCreate(SecKeyRef keyRef, CFErrorRef *error); + SecTransformRef SecDecryptTransformCreate(SecKeyRef keyRef, CFErrorRef *error); + Boolean SecTransformSetAttribute(SecTransformRef transformRef, CFStringRef key, CFTypeRef value, CFErrorRef *error); + CFTypeRef SecTransformExecute(SecTransformRef transformRef, CFErrorRef *errorRef); + SecTransformRef SecVerifyTransformCreate(SecKeyRef key, CFDataRef signature, CFErrorRef *error); + SecTransformRef SecSignTransformCreate(SecKeyRef key, CFErrorRef *error); + SecCertificateRef SecCertificateCreateWithData(CFAllocatorRef allocator, CFDataRef data); + OSStatus SecCertificateCopyPublicKey(SecCertificateRef certificate, SecKeyRef *key); + CFStringRef SecCopyErrorMessageString(OSStatus status, void *reserved); + OSStatus SecTrustCopyAnchorCertificates(CFArrayRef *anchors); + CFDataRef SecCertificateCopyData(SecCertificateRef certificate); + OSStatus SecTrustSettingsCopyCertificates(SecTrustSettingsDomain domain, CFArrayRef *certArray); + OSStatus SecTrustSettingsCopyTrustSettings(SecCertificateRef certRef, SecTrustSettingsDomain domain, + CFArrayRef *trustSettings); + CFDictionaryRef SecPolicyCopyProperties(SecPolicyRef policyRef); + CFTypeID SecPolicyGetTypeID(void); + OSStatus SecKeyEncrypt(SecKeyRef key, SecPadding padding, const char *plainText, size_t plainTextLen, + char *cipherText, size_t *cipherTextLen); + OSStatus SecKeyDecrypt(SecKeyRef key, SecPadding padding, const char *cipherText, size_t cipherTextLen, + char *plainText, size_t *plainTextLen); + OSStatus SecKeyRawSign(SecKeyRef key, SecPadding padding, const char *dataToSign, size_t dataToSignLen, + char *sig, size_t * sigLen); + OSStatus SecKeyRawVerify(SecKeyRef key, SecPadding padding, const char *signedData, size_t signedDataLen, + const char *sig, size_t sigLen); + OSStatus SecItemImport(CFDataRef importedData, CFStringRef fileNameOrExtension, + SecExternalFormat *inputFormat, SecExternalItemType *itemType, + SecItemImportExportFlags flags, const SecItemImportExportKeyParameters *keyParams, + SecKeychainRef importKeychain, CFArrayRef *outItems); + OSStatus SecItemExport(CFTypeRef secItemOrArray, SecExternalFormat outputFormat, SecItemImportExportFlags flags, + const SecItemImportExportKeyParameters *keyParams, CFDataRef *exportedData); + OSStatus SecAccessCreate(CFStringRef descriptor, CFArrayRef trustedlist, SecAccessRef *accessRef); + OSStatus SecKeyCreatePair(SecKeychainRef keychainRef, CSSM_ALGORITHMS algorithm, uint32_t keySizeInBits, + CSSM_CC_HANDLE contextHandle, CSSM_KEYUSE publicKeyUsage, uint32_t publicKeyAttr, + CSSM_KEYUSE privateKeyUsage, uint32_t privateKeyAttr, SecAccessRef initialAccess, + SecKeyRef* publicKeyRef, SecKeyRef* privateKeyRef); + OSStatus SecKeychainItemDelete(SecKeyRef itemRef); + + typedef OSStatus (*SSLReadFunc)(SSLConnectionRef connection, char *data, size_t *dataLength); + typedef OSStatus (*SSLWriteFunc)(SSLConnectionRef connection, const char *data, size_t *dataLength); + OSStatus SSLSetIOFuncs(SSLContextRef context, SSLReadFunc readFunc, SSLWriteFunc writeFunc); + + OSStatus SSLSetPeerID(SSLContextRef context, const char *peerID, size_t peerIDLen); + + OSStatus SSLSetConnection(SSLContextRef context, SSLConnectionRef connection); + OSStatus SSLSetPeerDomainName(SSLContextRef context, const char *peerName, size_t peerNameLen); + OSStatus SSLHandshake(SSLContextRef context); + OSStatus SSLGetBufferedReadSize(SSLContextRef context, size_t *bufSize); + OSStatus SSLRead(SSLContextRef context, char *data, size_t dataLength, size_t *processed); + OSStatus SSLWrite(SSLContextRef context, const char *data, size_t dataLength, size_t *processed); + OSStatus SSLClose(SSLContextRef context); + + OSStatus SSLGetNumberSupportedCiphers(SSLContextRef context, size_t *numCiphers); + OSStatus SSLGetSupportedCiphers(SSLContextRef context, SSLCipherSuite *ciphers, size_t *numCiphers); + OSStatus SSLSetEnabledCiphers(SSLContextRef context, const SSLCipherSuite *ciphers, size_t numCiphers); + OSStatus SSLGetNumberEnabledCiphers(SSLContextRef context, size_t *numCiphers); + OSStatus SSLGetEnabledCiphers(SSLContextRef context, SSLCipherSuite *ciphers, size_t *numCiphers); + + OSStatus SSLGetNegotiatedCipher(SSLContextRef context, SSLCipherSuite *cipherSuite); + OSStatus SSLGetNegotiatedProtocolVersion(SSLContextRef context, SSLProtocol *protocol); + + OSStatus SSLCopyPeerTrust(SSLContextRef context, SecTrustRef *trust); + OSStatus SecTrustGetCssmResultCode(SecTrustRef trust, OSStatus *resultCode); + CFIndex SecTrustGetCertificateCount(SecTrustRef trust); + SecCertificateRef SecTrustGetCertificateAtIndex(SecTrustRef trust, CFIndex ix); + OSStatus SecTrustSetAnchorCertificates(SecTrustRef trust, CFArrayRef anchorCertificates); + OSStatus SecTrustSetAnchorCertificatesOnly(SecTrustRef trust, Boolean anchorCertificatesOnly); + OSStatus SecTrustSetPolicies(SecTrustRef trust, CFArrayRef policies); + SecPolicyRef SecPolicyCreateSSL(Boolean server, CFStringRef hostname); + OSStatus SecPolicySearchCreate(CSSM_CERT_TYPE certType, const CSSM_OID *policyOID, const CSSM_DATA *value, + SecPolicySearchRef *searchRef); + OSStatus SecPolicySearchCopyNext(SecPolicySearchRef searchRef, SecPolicyRef *policyRef); + OSStatus SecPolicySetValue(SecPolicyRef policyRef, const CSSM_DATA *value); + OSStatus SecTrustEvaluate(SecTrustRef trust, SecTrustResultType *result); + + extern SecRandomRef kSecRandomDefault; + + extern CFStringRef kSecPaddingKey; + extern CFStringRef kSecPaddingPKCS7Key; + extern CFStringRef kSecPaddingPKCS5Key; + extern CFStringRef kSecPaddingPKCS1Key; + extern CFStringRef kSecPaddingOAEPKey; + extern CFStringRef kSecPaddingNoneKey; + extern CFStringRef kSecModeCBCKey; + extern CFStringRef kSecTransformInputAttributeName; + extern CFStringRef kSecDigestTypeAttribute; + extern CFStringRef kSecDigestLengthAttribute; + extern CFStringRef kSecIVKey; + + extern CFStringRef kSecAttrIsExtractable; + + extern CFStringRef kSecDigestSHA1; + extern CFStringRef kSecDigestSHA2; + extern CFStringRef kSecDigestMD5; + + extern CFStringRef kSecAttrKeyType; + + extern CFTypeRef kSecAttrKeyTypeRSA; + extern CFTypeRef kSecAttrKeyTypeDSA; + extern CFTypeRef kSecAttrKeyTypeECDSA; + + extern CFStringRef kSecAttrKeySizeInBits; + extern CFStringRef kSecAttrLabel; + + extern CFTypeRef kSecAttrCanSign; + extern CFTypeRef kSecAttrCanVerify; + + extern CFTypeRef kSecAttrKeyTypeAES; + extern CFTypeRef kSecAttrKeyTypeRC4; + extern CFTypeRef kSecAttrKeyTypeRC2; + extern CFTypeRef kSecAttrKeyType3DES; + extern CFTypeRef kSecAttrKeyTypeDES; +""") + +if version_info < (10, 8): + ffi.cdef(""" + OSStatus SSLNewContext(Boolean isServer, SSLContextRef *contextPtr); + OSStatus SSLDisposeContext(SSLContextRef context); + + OSStatus SSLSetEnableCertVerify(SSLContextRef context, Boolean enableVerify); + + OSStatus SSLSetProtocolVersionEnabled(SSLContextRef context, SSLProtocol protocol, Boolean enable); + """) +else: + ffi.cdef(""" + typedef uint32_t SSLProtocolSide; + typedef uint32_t SSLConnectionType; + typedef uint32_t SSLSessionOption; + + SSLContextRef SSLCreateContext(CFAllocatorRef alloc, SSLProtocolSide protocolSide, + SSLConnectionType connectionType); + + OSStatus SSLSetSessionOption(SSLContextRef context, SSLSessionOption option, Boolean value); + + OSStatus SSLSetProtocolVersionMin(SSLContextRef context, SSLProtocol minVersion); + OSStatus SSLSetProtocolVersionMax(SSLContextRef context, SSLProtocol maxVersion); + """) + +security_path = '/System/Library/Frameworks/Security.framework/Security' + +Security = ffi.dlopen(security_path) +register_ffi(Security, ffi) diff --git a/tasks/lib/package_control/deps/oscrypto/_mac/_security_ctypes.py b/tasks/lib/package_control/deps/oscrypto/_mac/_security_ctypes.py new file mode 100644 index 0000000..ee1be00 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_mac/_security_ctypes.py @@ -0,0 +1,628 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import platform +from ctypes import c_void_p, c_int32, c_char_p, c_size_t, c_byte, c_int, c_uint32, c_uint64, c_ulong, c_long, c_bool +from ctypes import CDLL, POINTER, CFUNCTYPE, Structure + +from .._ffi import FFIEngineError + + +__all__ = [ + 'Security', + 'version', + 'version_info', +] + + +version = platform.mac_ver()[0] +version_info = tuple(map(int, platform.mac_ver()[0].split('.'))) + +if version_info < (10, 7): + raise OSError('Only OS X 10.7 and newer are supported, not %s.%s' % (version_info[0], version_info[1])) + +security_path = '/System/Library/Frameworks/Security.framework/Security' + +Security = CDLL(security_path, use_errno=True) + +Boolean = c_bool +CFIndex = c_long +CFData = c_void_p +CFString = c_void_p +CFArray = c_void_p +CFDictionary = c_void_p +CFError = c_void_p +CFType = c_void_p +CFTypeID = c_ulong + +CFTypeRef = POINTER(CFType) +CFAllocatorRef = c_void_p + +OSStatus = c_int32 + +CFDataRef = POINTER(CFData) +CFStringRef = POINTER(CFString) +CFArrayRef = POINTER(CFArray) +CFDictionaryRef = POINTER(CFDictionary) +CFErrorRef = POINTER(CFError) + +SecKeyRef = POINTER(c_void_p) +SecCertificateRef = POINTER(c_void_p) +SecTransformRef = POINTER(c_void_p) +SecRandomRef = c_void_p +SecTrustSettingsDomain = c_uint32 +SecItemImportExportFlags = c_uint32 +SecKeyImportExportFlags = c_uint32 +SecExternalFormat = c_uint32 +SecExternalItemType = c_uint32 +SecPadding = c_uint32 +SSLProtocol = c_uint32 +SSLCipherSuite = c_uint32 +SecPolicyRef = POINTER(c_void_p) +CSSM_CC_HANDLE = c_uint64 +CSSM_ALGORITHMS = c_uint32 +CSSM_KEYUSE = c_uint32 +SecAccessRef = POINTER(c_void_p) +SecKeychainRef = POINTER(c_void_p) +SSLContextRef = POINTER(c_void_p) +SecTrustRef = POINTER(c_void_p) +SSLConnectionRef = c_uint32 +SecTrustResultType = c_uint32 +SecTrustOptionFlags = c_uint32 +SecPolicySearchRef = c_void_p +CSSM_CERT_TYPE = c_uint32 + + +class CSSM_DATA(Structure): # noqa + _fields_ = [ + ('Length', c_uint32), + ('Data', c_char_p) + ] + + +CSSM_OID = CSSM_DATA + + +class CSSM_APPLE_TP_OCSP_OPTIONS(Structure): # noqa + _fields_ = [ + ('Version', c_uint32), + ('Flags', c_uint32), + ('LocalResponder', POINTER(CSSM_DATA)), + ('LocalResponderCert', POINTER(CSSM_DATA)), + ] + + +class CSSM_APPLE_TP_CRL_OPTIONS(Structure): # noqa + _fields_ = [ + ('Version', c_uint32), + ('CrlFlags', c_uint32), + ('crlStore', c_void_p), + ] + + +class SecItemImportExportKeyParameters(Structure): + _fields_ = [ + ('version', c_uint32), + ('flags', SecKeyImportExportFlags), + ('passphrase', CFTypeRef), + ('alertTitle', CFStringRef), + ('alertPrompt', CFStringRef), + ('accessRef', SecAccessRef), + ('keyUsage', CFArrayRef), + ('keyAttributes', CFArrayRef), + ] + + +try: + Security.SecKeychainCreate.argtypes = [ + c_char_p, + c_uint32, + c_void_p, + Boolean, + SecAccessRef, + POINTER(SecKeychainRef) + ] + Security.SecKeychainCreate.restype = OSStatus + + Security.SecKeychainDelete.argtypes = [SecKeychainRef] + Security.SecKeychainDelete.restype = OSStatus + + Security.SecRandomCopyBytes.argtypes = [ + SecRandomRef, + c_size_t, + c_char_p + ] + Security.SecRandomCopyBytes.restype = c_int + + Security.SecKeyCreateFromData.argtypes = [ + CFDictionaryRef, + CFDataRef, + POINTER(CFErrorRef) + ] + Security.SecKeyCreateFromData.restype = SecKeyRef + + Security.SecEncryptTransformCreate.argtypes = [ + SecKeyRef, + POINTER(CFErrorRef) + ] + Security.SecEncryptTransformCreate.restype = SecTransformRef + + Security.SecDecryptTransformCreate.argtypes = [ + SecKeyRef, + POINTER(CFErrorRef) + ] + Security.SecDecryptTransformCreate.restype = SecTransformRef + + Security.SecTransformSetAttribute.argtypes = [ + SecTransformRef, + CFStringRef, + CFTypeRef, + POINTER(CFErrorRef) + ] + Security.SecTransformSetAttribute.restype = Boolean + + Security.SecTransformExecute.argtypes = [ + SecTransformRef, + POINTER(CFErrorRef) + ] + Security.SecTransformExecute.restype = CFTypeRef + + Security.SecVerifyTransformCreate.argtypes = [ + SecKeyRef, + CFDataRef, + POINTER(CFErrorRef) + ] + Security.SecVerifyTransformCreate.restype = SecTransformRef + + Security.SecSignTransformCreate.argtypes = [ + SecKeyRef, + POINTER(CFErrorRef) + ] + Security.SecSignTransformCreate.restype = SecTransformRef + + Security.SecCertificateCreateWithData.argtypes = [ + CFAllocatorRef, + CFDataRef + ] + Security.SecCertificateCreateWithData.restype = SecCertificateRef + + Security.SecCertificateCopyPublicKey.argtypes = [ + SecCertificateRef, + POINTER(SecKeyRef) + ] + Security.SecCertificateCopyPublicKey.restype = OSStatus + + Security.SecCopyErrorMessageString.argtypes = [ + OSStatus, + c_void_p + ] + Security.SecCopyErrorMessageString.restype = CFStringRef + + Security.SecTrustCopyAnchorCertificates.argtypes = [ + POINTER(CFArrayRef) + ] + Security.SecTrustCopyAnchorCertificates.restype = OSStatus + + Security.SecCertificateCopyData.argtypes = [ + SecCertificateRef + ] + Security.SecCertificateCopyData.restype = CFDataRef + + Security.SecTrustSettingsCopyCertificates.argtypes = [ + SecTrustSettingsDomain, + POINTER(CFArrayRef) + ] + Security.SecTrustSettingsCopyCertificates.restype = OSStatus + + Security.SecTrustSettingsCopyTrustSettings.argtypes = [ + SecCertificateRef, + SecTrustSettingsDomain, + POINTER(CFArrayRef) + ] + Security.SecTrustSettingsCopyTrustSettings.restype = OSStatus + + Security.SecPolicyCopyProperties.argtypes = [ + SecPolicyRef + ] + Security.SecPolicyCopyProperties.restype = CFDictionaryRef + + Security.SecPolicyGetTypeID.argtypes = [] + Security.SecPolicyGetTypeID.restype = CFTypeID + + Security.SecKeyEncrypt.argtypes = [ + SecKeyRef, + SecPadding, + c_char_p, + c_size_t, + c_char_p, + POINTER(c_size_t) + ] + Security.SecKeyEncrypt.restype = OSStatus + + Security.SecKeyDecrypt.argtypes = [ + SecKeyRef, + SecPadding, + c_char_p, + c_size_t, + c_char_p, + POINTER(c_size_t) + ] + Security.SecKeyDecrypt.restype = OSStatus + + Security.SecKeyRawSign.argtypes = [ + SecKeyRef, + SecPadding, + c_char_p, + c_size_t, + c_char_p, + POINTER(c_size_t) + ] + Security.SecKeyRawSign.restype = OSStatus + + Security.SecKeyRawVerify.argtypes = [ + SecKeyRef, + SecPadding, + c_char_p, + c_size_t, + c_char_p, + c_size_t + ] + Security.SecKeyRawVerify.restype = OSStatus + + Security.SecAccessCreate.argtypes = [ + CFStringRef, + CFArrayRef, + POINTER(SecAccessRef) + ] + Security.SecAccessCreate.restype = OSStatus + + Security.SecKeyCreatePair.argtypes = [ + SecKeychainRef, + CSSM_ALGORITHMS, + c_uint32, + CSSM_CC_HANDLE, + CSSM_KEYUSE, + c_uint32, + CSSM_KEYUSE, + c_uint32, + SecAccessRef, + POINTER(SecKeyRef), + POINTER(SecKeyRef) + ] + Security.SecKeyCreatePair.restype = OSStatus + + Security.SecItemImport.argtypes = [ + CFDataRef, + CFStringRef, + POINTER(SecExternalFormat), + POINTER(SecExternalItemType), + SecItemImportExportFlags, + POINTER(SecItemImportExportKeyParameters), + SecKeychainRef, + POINTER(CFArrayRef) + ] + Security.SecItemImport.restype = OSStatus + + Security.SecItemExport.argtypes = [ + CFTypeRef, + SecExternalFormat, + SecItemImportExportFlags, + POINTER(SecItemImportExportKeyParameters), + POINTER(CFDataRef) + ] + Security.SecItemExport.restype = OSStatus + + Security.SecKeychainItemDelete.argtypes = [ + SecKeyRef + ] + Security.SecKeychainItemDelete.restype = OSStatus + + SSLReadFunc = CFUNCTYPE(OSStatus, SSLConnectionRef, POINTER(c_byte), POINTER(c_size_t)) + SSLWriteFunc = CFUNCTYPE(OSStatus, SSLConnectionRef, POINTER(c_byte), POINTER(c_size_t)) + + Security.SSLSetIOFuncs.argtypes = [ + SSLContextRef, + SSLReadFunc, + SSLWriteFunc + ] + Security.SSLSetIOFuncs.restype = OSStatus + + Security.SSLSetPeerID.argtypes = [ + SSLContextRef, + c_char_p, + c_size_t + ] + Security.SSLSetPeerID.restype = OSStatus + + Security.SSLSetCertificateAuthorities.argtypes = [ + SSLContextRef, + CFTypeRef, + Boolean + ] + Security.SSLSetCertificateAuthorities.restype = OSStatus + + Security.SecTrustSetPolicies.argtypes = [ + SecTrustRef, + CFArrayRef + ] + Security.SecTrustSetPolicies.restype = OSStatus + + Security.SecPolicyCreateSSL.argtypes = [ + Boolean, + CFStringRef + ] + Security.SecPolicyCreateSSL.restype = SecPolicyRef + + Security.SecPolicySearchCreate.argtypes = [ + CSSM_CERT_TYPE, + POINTER(CSSM_OID), + POINTER(CSSM_DATA), + POINTER(SecPolicySearchRef) + ] + Security.SecPolicySearchCreate.restype = OSStatus + + Security.SecPolicySearchCopyNext.argtypes = [ + SecPolicySearchRef, + POINTER(SecPolicyRef) + ] + Security.SecPolicySearchCopyNext.restype = OSStatus + + Security.SecPolicySetValue.argtypes = [ + SecPolicyRef, + POINTER(CSSM_DATA) + ] + Security.SecPolicySetValue.restype = OSStatus + + Security.SSLSetConnection.argtypes = [ + SSLContextRef, + SSLConnectionRef + ] + Security.SSLSetConnection.restype = OSStatus + + Security.SSLSetPeerDomainName.argtypes = [ + SSLContextRef, + c_char_p, + c_size_t + ] + Security.SSLSetPeerDomainName.restype = OSStatus + + Security.SSLHandshake.argtypes = [ + SSLContextRef + ] + Security.SSLHandshake.restype = OSStatus + + Security.SSLGetBufferedReadSize.argtypes = [ + SSLContextRef, + POINTER(c_size_t) + ] + Security.SSLGetBufferedReadSize.restype = OSStatus + + Security.SSLRead.argtypes = [ + SSLContextRef, + c_char_p, + c_size_t, + POINTER(c_size_t) + ] + Security.SSLRead.restype = OSStatus + + Security.SSLWrite.argtypes = [ + SSLContextRef, + c_char_p, + c_size_t, + POINTER(c_size_t) + ] + Security.SSLWrite.restype = OSStatus + + Security.SSLClose.argtypes = [ + SSLContextRef + ] + Security.SSLClose.restype = OSStatus + + Security.SSLGetNumberSupportedCiphers.argtypes = [ + SSLContextRef, + POINTER(c_size_t) + ] + Security.SSLGetNumberSupportedCiphers.restype = OSStatus + + Security.SSLGetSupportedCiphers.argtypes = [ + SSLContextRef, + POINTER(SSLCipherSuite), + POINTER(c_size_t) + ] + Security.SSLGetSupportedCiphers.restype = OSStatus + + Security.SSLSetEnabledCiphers.argtypes = [ + SSLContextRef, + POINTER(SSLCipherSuite), + c_size_t + ] + Security.SSLSetEnabledCiphers.restype = OSStatus + + Security.SSLGetNumberEnabledCiphers.argtype = [ + SSLContextRef, + POINTER(c_size_t) + ] + Security.SSLGetNumberEnabledCiphers.restype = OSStatus + + Security.SSLGetEnabledCiphers.argtypes = [ + SSLContextRef, + POINTER(SSLCipherSuite), + POINTER(c_size_t) + ] + Security.SSLGetEnabledCiphers.restype = OSStatus + + Security.SSLGetNegotiatedCipher.argtypes = [ + SSLContextRef, + POINTER(SSLCipherSuite) + ] + Security.SSLGetNegotiatedCipher.restype = OSStatus + + Security.SSLGetNegotiatedProtocolVersion.argtypes = [ + SSLContextRef, + POINTER(SSLProtocol) + ] + Security.SSLGetNegotiatedProtocolVersion.restype = OSStatus + + Security.SSLCopyPeerTrust.argtypes = [ + SSLContextRef, + POINTER(SecTrustRef) + ] + Security.SSLCopyPeerTrust.restype = OSStatus + + Security.SecTrustGetCssmResultCode.argtypes = [ + SecTrustRef, + POINTER(OSStatus) + ] + Security.SecTrustGetCssmResultCode.restype = OSStatus + + Security.SecTrustGetCertificateCount.argtypes = [ + SecTrustRef + ] + Security.SecTrustGetCertificateCount.restype = CFIndex + + Security.SecTrustGetCertificateAtIndex.argtypes = [ + SecTrustRef, + CFIndex + ] + Security.SecTrustGetCertificateAtIndex.restype = SecCertificateRef + + Security.SecTrustSetAnchorCertificates.argtypes = [ + SecTrustRef, + CFArrayRef + ] + Security.SecTrustSetAnchorCertificates.restype = OSStatus + + Security.SecTrustSetAnchorCertificatesOnly.argstypes = [ + SecTrustRef, + Boolean + ] + Security.SecTrustSetAnchorCertificatesOnly.restype = OSStatus + + Security.SecTrustEvaluate.argtypes = [ + SecTrustRef, + POINTER(SecTrustResultType) + ] + Security.SecTrustEvaluate.restype = OSStatus + + if version_info < (10, 8): + Security.SSLNewContext.argtypes = [ + Boolean, + POINTER(SSLContextRef) + ] + Security.SSLNewContext.restype = OSStatus + + Security.SSLDisposeContext.argtypes = [ + SSLContextRef + ] + Security.SSLDisposeContext.restype = OSStatus + + Security.SSLSetEnableCertVerify.argtypes = [ + SSLContextRef, + Boolean + ] + Security.SSLSetEnableCertVerify.restype = OSStatus + + Security.SSLSetProtocolVersionEnabled.argtypes = [ + SSLContextRef, + SSLProtocol, + Boolean + ] + Security.SSLSetProtocolVersionEnabled.restype = OSStatus + + else: + SSLProtocolSide = c_uint32 + SSLConnectionType = c_uint32 + SSLSessionOption = c_uint32 + + Security.SSLCreateContext.argtypes = [ + CFAllocatorRef, + SSLProtocolSide, + SSLConnectionType + ] + Security.SSLCreateContext.restype = SSLContextRef + + Security.SSLSetSessionOption.argtypes = [ + SSLContextRef, + SSLSessionOption, + Boolean + ] + Security.SSLSetSessionOption.restype = OSStatus + + Security.SSLSetProtocolVersionMin.argtypes = [ + SSLContextRef, + SSLProtocol + ] + Security.SSLSetProtocolVersionMin.restype = OSStatus + + Security.SSLSetProtocolVersionMax.argtypes = [ + SSLContextRef, + SSLProtocol + ] + Security.SSLSetProtocolVersionMax.restype = OSStatus + + setattr(Security, 'SSLReadFunc', SSLReadFunc) + setattr(Security, 'SSLWriteFunc', SSLWriteFunc) + setattr(Security, 'SSLContextRef', SSLContextRef) + setattr(Security, 'SSLProtocol', SSLProtocol) + setattr(Security, 'SSLCipherSuite', SSLCipherSuite) + setattr(Security, 'SecTrustRef', SecTrustRef) + setattr(Security, 'SecTrustResultType', SecTrustResultType) + setattr(Security, 'OSStatus', OSStatus) + + setattr(Security, 'SecAccessRef', SecAccessRef) + setattr(Security, 'SecKeychainRef', SecKeychainRef) + setattr(Security, 'SecKeyRef', SecKeyRef) + + setattr(Security, 'SecPolicySearchRef', SecPolicySearchRef) + setattr(Security, 'SecPolicyRef', SecPolicyRef) + + setattr(Security, 'CSSM_DATA', CSSM_DATA) + setattr(Security, 'CSSM_OID', CSSM_OID) + setattr(Security, 'CSSM_APPLE_TP_OCSP_OPTIONS', CSSM_APPLE_TP_OCSP_OPTIONS) + setattr(Security, 'CSSM_APPLE_TP_CRL_OPTIONS', CSSM_APPLE_TP_CRL_OPTIONS) + setattr(Security, 'SecItemImportExportKeyParameters', SecItemImportExportKeyParameters) + + setattr(Security, 'kSecRandomDefault', SecRandomRef.in_dll(Security, 'kSecRandomDefault')) + + setattr(Security, 'kSecPaddingKey', CFStringRef.in_dll(Security, 'kSecPaddingKey')) + setattr(Security, 'kSecPaddingPKCS7Key', CFStringRef.in_dll(Security, 'kSecPaddingPKCS7Key')) + setattr(Security, 'kSecPaddingPKCS5Key', CFStringRef.in_dll(Security, 'kSecPaddingPKCS5Key')) + setattr(Security, 'kSecPaddingPKCS1Key', CFStringRef.in_dll(Security, 'kSecPaddingPKCS1Key')) + setattr(Security, 'kSecPaddingOAEPKey', CFStringRef.in_dll(Security, 'kSecPaddingOAEPKey')) + setattr(Security, 'kSecPaddingNoneKey', CFStringRef.in_dll(Security, 'kSecPaddingNoneKey')) + setattr(Security, 'kSecModeCBCKey', CFStringRef.in_dll(Security, 'kSecModeCBCKey')) + setattr( + Security, + 'kSecTransformInputAttributeName', + CFStringRef.in_dll(Security, 'kSecTransformInputAttributeName') + ) + setattr(Security, 'kSecDigestTypeAttribute', CFStringRef.in_dll(Security, 'kSecDigestTypeAttribute')) + setattr(Security, 'kSecDigestLengthAttribute', CFStringRef.in_dll(Security, 'kSecDigestLengthAttribute')) + setattr(Security, 'kSecIVKey', CFStringRef.in_dll(Security, 'kSecIVKey')) + + setattr(Security, 'kSecAttrIsExtractable', CFStringRef.in_dll(Security, 'kSecAttrIsExtractable')) + + setattr(Security, 'kSecDigestSHA1', CFStringRef.in_dll(Security, 'kSecDigestSHA1')) + setattr(Security, 'kSecDigestSHA2', CFStringRef.in_dll(Security, 'kSecDigestSHA2')) + setattr(Security, 'kSecDigestMD5', CFStringRef.in_dll(Security, 'kSecDigestMD5')) + + setattr(Security, 'kSecAttrKeyType', CFStringRef.in_dll(Security, 'kSecAttrKeyType')) + + setattr(Security, 'kSecAttrKeyTypeRSA', CFTypeRef.in_dll(Security, 'kSecAttrKeyTypeRSA')) + setattr(Security, 'kSecAttrKeyTypeDSA', CFTypeRef.in_dll(Security, 'kSecAttrKeyTypeDSA')) + setattr(Security, 'kSecAttrKeyTypeECDSA', CFTypeRef.in_dll(Security, 'kSecAttrKeyTypeECDSA')) + + setattr(Security, 'kSecAttrKeySizeInBits', CFStringRef.in_dll(Security, 'kSecAttrKeySizeInBits')) + setattr(Security, 'kSecAttrLabel', CFStringRef.in_dll(Security, 'kSecAttrLabel')) + + setattr(Security, 'kSecAttrCanSign', CFTypeRef.in_dll(Security, 'kSecAttrCanSign')) + setattr(Security, 'kSecAttrCanVerify', CFTypeRef.in_dll(Security, 'kSecAttrCanVerify')) + + setattr(Security, 'kSecAttrKeyTypeAES', CFTypeRef.in_dll(Security, 'kSecAttrKeyTypeAES')) + setattr(Security, 'kSecAttrKeyTypeRC4', CFTypeRef.in_dll(Security, 'kSecAttrKeyTypeRC4')) + setattr(Security, 'kSecAttrKeyTypeRC2', CFTypeRef.in_dll(Security, 'kSecAttrKeyTypeRC2')) + setattr(Security, 'kSecAttrKeyType3DES', CFTypeRef.in_dll(Security, 'kSecAttrKeyType3DES')) + setattr(Security, 'kSecAttrKeyTypeDES', CFTypeRef.in_dll(Security, 'kSecAttrKeyTypeDES')) + +except (AttributeError): + raise FFIEngineError('Error initializing ctypes') diff --git a/tasks/lib/package_control/deps/oscrypto/_mac/asymmetric.py b/tasks/lib/package_control/deps/oscrypto/_mac/asymmetric.py new file mode 100644 index 0000000..131197b --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_mac/asymmetric.py @@ -0,0 +1,2004 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function +from base64 import b32encode +import os +import shutil +import tempfile + +from .._asn1 import ( + Certificate as Asn1Certificate, + ECDomainParameters, + Integer, + KeyExchangeAlgorithm, + Null, + PrivateKeyInfo, + PublicKeyAlgorithm, + PublicKeyInfo, + RSAPublicKey, +) +from .._asymmetric import ( + _CertificateBase, + _fingerprint, + _parse_pkcs12, + _PrivateKeyBase, + _PublicKeyBase, + _unwrap_private_key_info, + parse_certificate, + parse_private, + parse_public, +) +from .._errors import pretty_message +from .._ffi import new, unwrap, bytes_from_buffer, buffer_from_bytes, deref, null, is_null, pointer_set +from ._security import Security, SecurityConst, handle_sec_error, osx_version_info +from ._core_foundation import CoreFoundation, CFHelpers, handle_cf_error +from .util import rand_bytes +from ..errors import AsymmetricKeyError, IncompleteAsymmetricKeyError, SignatureError +from .._pkcs1 import add_pss_padding, verify_pss_padding, remove_pkcs1v15_encryption_padding +from .._types import type_name, str_cls, byte_cls, int_types + + +__all__ = [ + 'Certificate', + 'dsa_sign', + 'dsa_verify', + 'ecdsa_sign', + 'ecdsa_verify', + 'generate_pair', + 'load_certificate', + 'load_pkcs12', + 'load_private_key', + 'load_public_key', + 'parse_pkcs12', + 'PrivateKey', + 'PublicKey', + 'rsa_oaep_decrypt', + 'rsa_oaep_encrypt', + 'rsa_pkcs1v15_decrypt', + 'rsa_pkcs1v15_encrypt', + 'rsa_pkcs1v15_sign', + 'rsa_pkcs1v15_verify', + 'rsa_pss_sign', + 'rsa_pss_verify', +] + + +class PrivateKey(_PrivateKeyBase): + """ + Container for the OS crypto library representation of a private key + """ + + sec_key_ref = None + _public_key = None + + # A reference to the library used in the destructor to make sure it hasn't + # been garbage collected by the time this object is garbage collected + _lib = None + + def __init__(self, sec_key_ref, asn1): + """ + :param sec_key_ref: + A Security framework SecKeyRef value from loading/importing the + key + + :param asn1: + An asn1crypto.keys.PrivateKeyInfo object + """ + + self.sec_key_ref = sec_key_ref + self.asn1 = asn1 + self._lib = CoreFoundation + + @property + def public_key(self): + """ + :return: + A PublicKey object corresponding to this private key. + """ + + if self._public_key is None: + cf_data_private = None + try: + # We export here so that Security.framework will fill in the EC + # public key for us, instead of us having to compute it + cf_data_private_pointer = new(CoreFoundation, 'CFDataRef *') + result = Security.SecItemExport(self.sec_key_ref, 0, 0, null(), cf_data_private_pointer) + handle_sec_error(result) + cf_data_private = unwrap(cf_data_private_pointer) + private_key_bytes = CFHelpers.cf_data_to_bytes(cf_data_private) + + key = parse_private(private_key_bytes) + + if key.algorithm == 'rsa': + public_asn1 = PublicKeyInfo({ + 'algorithm': PublicKeyAlgorithm({ + 'algorithm': 'rsa', + 'parameters': Null() + }), + 'public_key': RSAPublicKey({ + 'modulus': key['private_key'].parsed['modulus'], + 'public_exponent': key['private_key'].parsed['public_exponent'], + }) + }) + + elif key.algorithm == 'dsa': + params = key['private_key_algorithm']['parameters'] + public_asn1 = PublicKeyInfo({ + 'algorithm': PublicKeyAlgorithm({ + 'algorithm': 'dsa', + 'parameters': params.copy() + }), + 'public_key': Integer(pow( + params['g'].native, + key['private_key'].parsed.native, + params['p'].native + )) + }) + + elif key.algorithm == 'ec': + public_asn1 = PublicKeyInfo({ + 'algorithm': PublicKeyAlgorithm({ + 'algorithm': 'ec', + 'parameters': ECDomainParameters( + name='named', + value=self.curve + ) + }), + 'public_key': key['private_key'].parsed['public_key'], + }) + + finally: + if cf_data_private: + CoreFoundation.CFRelease(cf_data_private) + + self._public_key = _load_key(public_asn1) + + return self._public_key + + @property + def fingerprint(self): + """ + Creates a fingerprint that can be compared with a public key to see if + the two form a pair. + + This fingerprint is not compatible with fingerprints generated by any + other software. + + :return: + A byte string that is a sha256 hash of selected components (based + on the key type) + """ + + if self._fingerprint is None: + self._fingerprint = _fingerprint(self.asn1, load_private_key) + return self._fingerprint + + def __del__(self): + if self.sec_key_ref: + self._lib.CFRelease(self.sec_key_ref) + self._lib = None + self.sec_key_ref = None + + +class PublicKey(_PublicKeyBase): + """ + Container for the OS crypto library representation of a public key + """ + + sec_key_ref = None + + # A reference to the library used in the destructor to make sure it hasn't + # been garbage collected by the time this object is garbage collected + _lib = None + + def __init__(self, sec_key_ref, asn1): + """ + :param sec_key_ref: + A Security framework SecKeyRef value from loading/importing the + key + + :param asn1: + An asn1crypto.keys.PublicKeyInfo object + """ + + self.sec_key_ref = sec_key_ref + self.asn1 = asn1 + self._lib = CoreFoundation + + def __del__(self): + if self.sec_key_ref: + self._lib.CFRelease(self.sec_key_ref) + self._lib = None + self.sec_key_ref = None + + +class Certificate(_CertificateBase): + """ + Container for the OS crypto library representation of a certificate + """ + + sec_certificate_ref = None + _public_key = None + _self_signed = None + + def __init__(self, sec_certificate_ref, asn1): + """ + :param sec_certificate_ref: + A Security framework SecCertificateRef value from loading/importing + the certificate + + :param asn1: + An asn1crypto.x509.Certificate object + """ + + self.sec_certificate_ref = sec_certificate_ref + self.asn1 = asn1 + + @property + def sec_key_ref(self): + """ + :return: + The SecKeyRef of the public key + """ + + return self.public_key.sec_key_ref + + @property + def public_key(self): + """ + :return: + The PublicKey object for the public key this certificate contains + """ + + if not self._public_key and self.sec_certificate_ref: + if self.asn1.signature_algo == "rsassa_pss": + # macOS doesn't like importing RSA PSS certs, so we treat it like a + # traditional RSA cert + asn1 = self.asn1.copy() + asn1['tbs_certificate']['subject_public_key_info']['algorithm']['algorithm'] = 'rsa' + temp_cert = _load_x509(asn1) + sec_cert_ref = temp_cert.sec_certificate_ref + else: + sec_cert_ref = self.sec_certificate_ref + + sec_public_key_ref_pointer = new(Security, 'SecKeyRef *') + res = Security.SecCertificateCopyPublicKey(sec_cert_ref, sec_public_key_ref_pointer) + handle_sec_error(res) + sec_public_key_ref = unwrap(sec_public_key_ref_pointer) + self._public_key = PublicKey(sec_public_key_ref, self.asn1['tbs_certificate']['subject_public_key_info']) + + return self._public_key + + @property + def self_signed(self): + """ + :return: + A boolean - if the certificate is self-signed + """ + + if self._self_signed is None: + self._self_signed = False + if self.asn1.self_signed in set(['yes', 'maybe']): + + signature_algo = self.asn1['signature_algorithm'].signature_algo + hash_algo = self.asn1['signature_algorithm'].hash_algo + + if signature_algo == 'rsassa_pkcs1v15': + verify_func = rsa_pkcs1v15_verify + elif signature_algo == 'rsassa_pss': + verify_func = rsa_pss_verify + elif signature_algo == 'dsa': + verify_func = dsa_verify + elif signature_algo == 'ecdsa': + verify_func = ecdsa_verify + else: + raise OSError(pretty_message( + ''' + Unable to verify the signature of the certificate since + it uses the unsupported algorithm %s + ''', + signature_algo + )) + + try: + verify_func( + self.public_key, + self.asn1['signature_value'].native, + self.asn1['tbs_certificate'].dump(), + hash_algo + ) + self._self_signed = True + except (SignatureError): + pass + + return self._self_signed + + def __del__(self): + if self._public_key: + self._public_key.__del__() + self._public_key = None + + if self.sec_certificate_ref: + CoreFoundation.CFRelease(self.sec_certificate_ref) + self.sec_certificate_ref = None + + +def generate_pair(algorithm, bit_size=None, curve=None): + """ + Generates a public/private key pair + + :param algorithm: + The key algorithm - "rsa", "dsa" or "ec" + + :param bit_size: + An integer - used for "rsa" and "dsa". For "rsa" the value maye be 1024, + 2048, 3072 or 4096. For "dsa" the value may be 1024. + + :param curve: + A unicode string - used for "ec" keys. Valid values include "secp256r1", + "secp384r1" and "secp521r1". + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A 2-element tuple of (PublicKey, PrivateKey). The contents of each key + may be saved by calling .asn1.dump(). + """ + + if algorithm not in set(['rsa', 'dsa', 'ec']): + raise ValueError(pretty_message( + ''' + algorithm must be one of "rsa", "dsa", "ec", not %s + ''', + repr(algorithm) + )) + + if algorithm == 'rsa': + if bit_size not in set([1024, 2048, 3072, 4096]): + raise ValueError(pretty_message( + ''' + bit_size must be one of 1024, 2048, 3072, 4096, not %s + ''', + repr(bit_size) + )) + + elif algorithm == 'dsa': + if bit_size not in set([1024]): + raise ValueError(pretty_message( + ''' + bit_size must be 1024, not %s + ''', + repr(bit_size) + )) + + elif algorithm == 'ec': + if curve not in set(['secp256r1', 'secp384r1', 'secp521r1']): + raise ValueError(pretty_message( + ''' + curve must be one of "secp256r1", "secp384r1", "secp521r1", not %s + ''', + repr(curve) + )) + + cf_dict = None + public_key_ref = None + private_key_ref = None + cf_data_public = None + cf_data_private = None + cf_string = None + sec_access_ref = None + sec_keychain_ref = None + temp_dir = None + + try: + alg_id = { + 'dsa': SecurityConst.CSSM_ALGID_DSA, + 'ec': SecurityConst.CSSM_ALGID_ECDSA, + 'rsa': SecurityConst.CSSM_ALGID_RSA, + }[algorithm] + + if algorithm == 'ec': + key_size = { + 'secp256r1': 256, + 'secp384r1': 384, + 'secp521r1': 521, + }[curve] + else: + key_size = bit_size + + private_key_pointer = new(Security, 'SecKeyRef *') + public_key_pointer = new(Security, 'SecKeyRef *') + + cf_string = CFHelpers.cf_string_from_unicode("Temporary oscrypto key") + + # We used to use SecKeyGeneratePair() for everything but DSA keys, but due to changes + # in macOS security, we can't reliably access the default keychain, and instead + # get an "OSError: User interaction is not allowed." result. Because of this we now + # use SecKeyCreatePair() for everything, but we even use a throw-away keychain. + passphrase_len = 16 + rand_data = rand_bytes(10 + passphrase_len) + passphrase = rand_data[10:] + + temp_filename = b32encode(rand_data[:10]).decode('utf-8') + temp_dir = tempfile.mkdtemp() + temp_path = os.path.join(temp_dir, temp_filename).encode('utf-8') + + sec_keychain_ref_pointer = new(Security, 'SecKeychainRef *') + result = Security.SecKeychainCreate( + temp_path, + passphrase_len, + passphrase, + False, + null(), + sec_keychain_ref_pointer + ) + handle_sec_error(result) + sec_keychain_ref = unwrap(sec_keychain_ref_pointer) + + sec_access_ref_pointer = new(Security, 'SecAccessRef *') + result = Security.SecAccessCreate(cf_string, null(), sec_access_ref_pointer) + handle_sec_error(result) + sec_access_ref = unwrap(sec_access_ref_pointer) + + result = Security.SecKeyCreatePair( + sec_keychain_ref, + alg_id, + key_size, + 0, + SecurityConst.CSSM_KEYUSE_VERIFY, + SecurityConst.CSSM_KEYATTR_EXTRACTABLE | SecurityConst.CSSM_KEYATTR_PERMANENT, + SecurityConst.CSSM_KEYUSE_SIGN, + SecurityConst.CSSM_KEYATTR_EXTRACTABLE | SecurityConst.CSSM_KEYATTR_PERMANENT, + sec_access_ref, + public_key_pointer, + private_key_pointer + ) + handle_sec_error(result) + + public_key_ref = unwrap(public_key_pointer) + private_key_ref = unwrap(private_key_pointer) + + cf_data_public_pointer = new(CoreFoundation, 'CFDataRef *') + result = Security.SecItemExport(public_key_ref, 0, 0, null(), cf_data_public_pointer) + handle_sec_error(result) + cf_data_public = unwrap(cf_data_public_pointer) + public_key_bytes = CFHelpers.cf_data_to_bytes(cf_data_public) + + cf_data_private_pointer = new(CoreFoundation, 'CFDataRef *') + result = Security.SecItemExport(private_key_ref, 0, 0, null(), cf_data_private_pointer) + handle_sec_error(result) + cf_data_private = unwrap(cf_data_private_pointer) + private_key_bytes = CFHelpers.cf_data_to_bytes(cf_data_private) + + # Clean the new keys out of the keychain + result = Security.SecKeychainItemDelete(public_key_ref) + handle_sec_error(result) + result = Security.SecKeychainItemDelete(private_key_ref) + handle_sec_error(result) + + finally: + if cf_dict: + CoreFoundation.CFRelease(cf_dict) + if public_key_ref: + CoreFoundation.CFRelease(public_key_ref) + if private_key_ref: + CoreFoundation.CFRelease(private_key_ref) + if cf_data_public: + CoreFoundation.CFRelease(cf_data_public) + if cf_data_private: + CoreFoundation.CFRelease(cf_data_private) + if cf_string: + CoreFoundation.CFRelease(cf_string) + if sec_keychain_ref: + Security.SecKeychainDelete(sec_keychain_ref) + CoreFoundation.CFRelease(sec_keychain_ref) + if temp_dir: + shutil.rmtree(temp_dir) + if sec_access_ref: + CoreFoundation.CFRelease(sec_access_ref) + + return (load_public_key(public_key_bytes), load_private_key(private_key_bytes)) + + +def generate_dh_parameters(bit_size): + """ + Generates DH parameters for use with Diffie-Hellman key exchange. Returns + a structure in the format of DHParameter defined in PKCS#3, which is also + used by the OpenSSL dhparam tool. + + THIS CAN BE VERY TIME CONSUMING! + + :param bit_size: + The integer bit size of the parameters to generate. Must be between 512 + and 4096, and divisible by 64. Recommended secure value as of early 2016 + is 2048, with an absolute minimum of 1024. + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + An asn1crypto.algos.DHParameters object. Use + oscrypto.asymmetric.dump_dh_parameters() to save to disk for usage with + web servers. + """ + + if not isinstance(bit_size, int_types): + raise TypeError(pretty_message( + ''' + bit_size must be an integer, not %s + ''', + type_name(bit_size) + )) + + if bit_size < 512: + raise ValueError('bit_size must be greater than or equal to 512') + + if bit_size > 4096: + raise ValueError('bit_size must be less than or equal to 4096') + + if bit_size % 64 != 0: + raise ValueError('bit_size must be a multiple of 64') + + public_key_ref = None + private_key_ref = None + cf_data_public = None + cf_data_private = None + cf_string = None + sec_keychain_ref = None + sec_access_ref = None + temp_dir = None + + try: + public_key_pointer = new(Security, 'SecKeyRef *') + private_key_pointer = new(Security, 'SecKeyRef *') + + cf_string = CFHelpers.cf_string_from_unicode("Temporary oscrypto key") + + passphrase_len = 16 + rand_data = rand_bytes(10 + passphrase_len) + passphrase = rand_data[10:] + + temp_filename = b32encode(rand_data[:10]).decode('utf-8') + temp_dir = tempfile.mkdtemp() + temp_path = os.path.join(temp_dir, temp_filename).encode('utf-8') + + sec_keychain_ref_pointer = new(Security, 'SecKeychainRef *') + result = Security.SecKeychainCreate( + temp_path, + passphrase_len, + passphrase, + False, + null(), + sec_keychain_ref_pointer + ) + handle_sec_error(result) + sec_keychain_ref = unwrap(sec_keychain_ref_pointer) + + sec_access_ref_pointer = new(Security, 'SecAccessRef *') + result = Security.SecAccessCreate(cf_string, null(), sec_access_ref_pointer) + handle_sec_error(result) + sec_access_ref = unwrap(sec_access_ref_pointer) + + result = Security.SecKeyCreatePair( + sec_keychain_ref, + SecurityConst.CSSM_ALGID_DH, + bit_size, + 0, + 0, + SecurityConst.CSSM_KEYATTR_EXTRACTABLE | SecurityConst.CSSM_KEYATTR_PERMANENT, + 0, + SecurityConst.CSSM_KEYATTR_EXTRACTABLE | SecurityConst.CSSM_KEYATTR_PERMANENT, + sec_access_ref, + public_key_pointer, + private_key_pointer + ) + handle_sec_error(result) + + public_key_ref = unwrap(public_key_pointer) + private_key_ref = unwrap(private_key_pointer) + + cf_data_private_pointer = new(CoreFoundation, 'CFDataRef *') + result = Security.SecItemExport(private_key_ref, 0, 0, null(), cf_data_private_pointer) + handle_sec_error(result) + cf_data_private = unwrap(cf_data_private_pointer) + private_key_bytes = CFHelpers.cf_data_to_bytes(cf_data_private) + + # Clean the new keys out of the keychain + result = Security.SecKeychainItemDelete(public_key_ref) + handle_sec_error(result) + + result = Security.SecKeychainItemDelete(private_key_ref) + handle_sec_error(result) + + return KeyExchangeAlgorithm.load(private_key_bytes)['parameters'] + + finally: + if public_key_ref: + CoreFoundation.CFRelease(public_key_ref) + if private_key_ref: + CoreFoundation.CFRelease(private_key_ref) + if cf_data_public: + CoreFoundation.CFRelease(cf_data_public) + if cf_data_private: + CoreFoundation.CFRelease(cf_data_private) + if cf_string: + CoreFoundation.CFRelease(cf_string) + if sec_keychain_ref: + Security.SecKeychainDelete(sec_keychain_ref) + CoreFoundation.CFRelease(sec_keychain_ref) + if temp_dir: + shutil.rmtree(temp_dir) + if sec_access_ref: + CoreFoundation.CFRelease(sec_access_ref) + + +def load_certificate(source): + """ + Loads an x509 certificate into a Certificate object + + :param source: + A byte string of file contents, a unicode string filename or an + asn1crypto.x509.Certificate object + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A Certificate object + """ + + if isinstance(source, Asn1Certificate): + certificate = source + + elif isinstance(source, byte_cls): + certificate = parse_certificate(source) + + elif isinstance(source, str_cls): + with open(source, 'rb') as f: + certificate = parse_certificate(f.read()) + + else: + raise TypeError(pretty_message( + ''' + source must be a byte string, unicode string or + asn1crypto.x509.Certificate object, not %s + ''', + type_name(source) + )) + + return _load_x509(certificate) + + +def _load_x509(certificate): + """ + Loads an ASN.1 object of an x509 certificate into a Certificate object + + :param certificate: + An asn1crypto.x509.Certificate object + + :return: + A Certificate object + """ + + source = certificate.dump() + + cf_source = None + try: + cf_source = CFHelpers.cf_data_from_bytes(source) + sec_key_ref = Security.SecCertificateCreateWithData(CoreFoundation.kCFAllocatorDefault, cf_source) + return Certificate(sec_key_ref, certificate) + + finally: + if cf_source: + CoreFoundation.CFRelease(cf_source) + + +def load_private_key(source, password=None): + """ + Loads a private key into a PrivateKey object + + :param source: + A byte string of file contents, a unicode string filename or an + asn1crypto.keys.PrivateKeyInfo object + + :param password: + A byte or unicode string to decrypt the private key file. Unicode + strings will be encoded using UTF-8. Not used is the source is a + PrivateKeyInfo object. + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + oscrypto.errors.AsymmetricKeyError - when the private key is incompatible with the OS crypto library + OSError - when an error is returned by the OS crypto library + + :return: + A PrivateKey object + """ + + if isinstance(source, PrivateKeyInfo): + private_object = source + + else: + if password is not None: + if isinstance(password, str_cls): + password = password.encode('utf-8') + if not isinstance(password, byte_cls): + raise TypeError(pretty_message( + ''' + password must be a byte string, not %s + ''', + type_name(password) + )) + + if isinstance(source, str_cls): + with open(source, 'rb') as f: + source = f.read() + + elif not isinstance(source, byte_cls): + raise TypeError(pretty_message( + ''' + source must be a byte string, unicode string or + asn1crypto.keys.PrivateKeyInfo object, not %s + ''', + type_name(source) + )) + + private_object = parse_private(source, password) + + return _load_key(private_object) + + +def load_public_key(source): + """ + Loads a public key into a PublicKey object + + :param source: + A byte string of file contents, a unicode string filename or an + asn1crypto.keys.PublicKeyInfo object + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + oscrypto.errors.AsymmetricKeyError - when the public key is incompatible with the OS crypto library + OSError - when an error is returned by the OS crypto library + + :return: + A PublicKey object + """ + + if isinstance(source, PublicKeyInfo): + public_key = source + + elif isinstance(source, byte_cls): + public_key = parse_public(source) + + elif isinstance(source, str_cls): + with open(source, 'rb') as f: + public_key = parse_public(f.read()) + + else: + raise TypeError(pretty_message( + ''' + source must be a byte string, unicode string or + asn1crypto.keys.PublicKeyInfo object, not %s + ''', + type_name(source) + )) + + return _load_key(public_key) + + +def _load_key(key_object): + """ + Common code to load public and private keys into PublicKey and PrivateKey + objects + + :param key_object: + An asn1crypto.keys.PublicKeyInfo or asn1crypto.keys.PrivateKeyInfo + object + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + oscrypto.errors.AsymmetricKeyError - when the key is incompatible with the OS crypto library + OSError - when an error is returned by the OS crypto library + + :return: + A PublicKey or PrivateKey object + """ + + if key_object.algorithm == 'ec': + curve_type, details = key_object.curve + if curve_type != 'named': + raise AsymmetricKeyError('OS X only supports EC keys using named curves') + if details not in set(['secp256r1', 'secp384r1', 'secp521r1']): + raise AsymmetricKeyError(pretty_message( + ''' + OS X only supports EC keys using the named curves secp256r1, + secp384r1 and secp521r1 + ''' + )) + + elif key_object.algorithm == 'dsa' and key_object.hash_algo == 'sha2': + raise AsymmetricKeyError(pretty_message( + ''' + OS X only supports DSA keys based on SHA1 (2048 bits or less) - this + key is based on SHA2 and is %s bits + ''', + key_object.bit_size + )) + + elif key_object.algorithm == 'dsa' and key_object.hash_algo is None: + raise IncompleteAsymmetricKeyError(pretty_message( + ''' + The DSA key does not contain the necessary p, q and g parameters + and can not be used + ''' + )) + + if isinstance(key_object, PublicKeyInfo): + if key_object.algorithm == 'rsassa_pss': + # We have to masquerade an RSA PSS key as plain RSA or it won't + # import properly + temp_key_object = key_object.copy() + temp_key_object['algorithm']['algorithm'] = 'rsa' + source = temp_key_object.dump() + else: + source = key_object.dump() + item_type = SecurityConst.kSecItemTypePublicKey + + else: + source = _unwrap_private_key_info(key_object).dump() + item_type = SecurityConst.kSecItemTypePrivateKey + + cf_source = None + keys_array = None + attr_array = None + + try: + cf_source = CFHelpers.cf_data_from_bytes(source) + + format_pointer = new(Security, 'uint32_t *') + pointer_set(format_pointer, SecurityConst.kSecFormatOpenSSL) + type_pointer = new(Security, 'uint32_t *') + pointer_set(type_pointer, item_type) + keys_pointer = new(CoreFoundation, 'CFArrayRef *') + + attr_array = CFHelpers.cf_array_from_list([ + Security.kSecAttrIsExtractable + ]) + + import_export_params_pointer = new(Security, 'SecItemImportExportKeyParameters *') + import_export_params = unwrap(import_export_params_pointer) + import_export_params.version = 0 + import_export_params.flags = 0 + import_export_params.passphrase = null() + import_export_params.alertTitle = null() + import_export_params.alertPrompt = null() + import_export_params.accessRef = null() + import_export_params.keyUsage = null() + import_export_params.keyAttributes = attr_array + + res = Security.SecItemImport( + cf_source, + null(), + format_pointer, + type_pointer, + 0, + import_export_params_pointer, + null(), + keys_pointer + ) + handle_sec_error(res) + keys_array = unwrap(keys_pointer) + + length = CoreFoundation.CFArrayGetCount(keys_array) + if length > 0: + sec_key_ref = CoreFoundation.CFArrayGetValueAtIndex(keys_array, 0) + CoreFoundation.CFRetain(sec_key_ref) + + if item_type == SecurityConst.kSecItemTypePublicKey: + return PublicKey(sec_key_ref, key_object) + + if item_type == SecurityConst.kSecItemTypePrivateKey: + return PrivateKey(sec_key_ref, key_object) + + finally: + if attr_array: + CoreFoundation.CFRelease(attr_array) + if keys_array: + CoreFoundation.CFRelease(keys_array) + if cf_source: + CoreFoundation.CFRelease(cf_source) + + +def parse_pkcs12(data, password=None): + """ + Parses a PKCS#12 ANS.1 DER-encoded structure and extracts certs and keys + + :param data: + A byte string of a DER-encoded PKCS#12 file + + :param password: + A byte string of the password to any encrypted data + + :raises: + ValueError - when any of the parameters are of the wrong type or value + OSError - when an error is returned by one of the OS decryption functions + + :return: + A three-element tuple of: + 1. An asn1crypto.keys.PrivateKeyInfo object + 2. An asn1crypto.x509.Certificate object + 3. A list of zero or more asn1crypto.x509.Certificate objects that are + "extra" certificates, possibly intermediates from the cert chain + """ + + return _parse_pkcs12(data, password, load_private_key) + + +def load_pkcs12(source, password=None): + """ + Loads a .p12 or .pfx file into a PrivateKey object and one or more + Certificates objects + + :param source: + A byte string of file contents or a unicode string filename + + :param password: + A byte or unicode string to decrypt the PKCS12 file. Unicode strings + will be encoded using UTF-8. + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + oscrypto.errors.AsymmetricKeyError - when a contained key is incompatible with the OS crypto library + OSError - when an error is returned by the OS crypto library + + :return: + A three-element tuple containing (PrivateKey, Certificate, [Certificate, ...]) + """ + + if password is not None: + if isinstance(password, str_cls): + password = password.encode('utf-8') + if not isinstance(password, byte_cls): + raise TypeError(pretty_message( + ''' + password must be a byte string, not %s + ''', + type_name(password) + )) + + if isinstance(source, str_cls): + with open(source, 'rb') as f: + source = f.read() + + elif not isinstance(source, byte_cls): + raise TypeError(pretty_message( + ''' + source must be a byte string or a unicode string, not %s + ''', + type_name(source) + )) + + key_info, cert_info, extra_certs_info = parse_pkcs12(source, password) + + key = None + cert = None + + if key_info: + key = _load_key(key_info) + + if cert_info: + cert = _load_x509(cert_info) + + extra_certs = [_load_x509(info) for info in extra_certs_info] + + return (key, cert, extra_certs) + + +def rsa_pkcs1v15_encrypt(certificate_or_public_key, data): + """ + Encrypts a byte string using an RSA public key or certificate. Uses PKCS#1 + v1.5 padding. + + :param certificate_or_public_key: + A PublicKey or Certificate object + + :param data: + A byte string, with a maximum length 11 bytes less than the key length + (in bytes) + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the encrypted data + """ + + if not isinstance(certificate_or_public_key, (Certificate, PublicKey)): + raise TypeError(pretty_message( + ''' + certificate_or_public_key must be an instance of the Certificate or + PublicKey class, not %s + ''', + type_name(certificate_or_public_key) + )) + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + key_length = certificate_or_public_key.byte_size + buffer = buffer_from_bytes(key_length) + output_length = new(Security, 'size_t *', key_length) + result = Security.SecKeyEncrypt( + certificate_or_public_key.sec_key_ref, + SecurityConst.kSecPaddingPKCS1, + data, + len(data), + buffer, + output_length + ) + handle_sec_error(result) + + return bytes_from_buffer(buffer, deref(output_length)) + + +def rsa_pkcs1v15_decrypt(private_key, ciphertext): + """ + Decrypts a byte string using an RSA private key. Uses PKCS#1 v1.5 padding. + + :param private_key: + A PrivateKey object + + :param ciphertext: + A byte string of the encrypted data + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the original plaintext + """ + + if not isinstance(private_key, PrivateKey): + raise TypeError(pretty_message( + ''' + private_key must an instance of the PrivateKey class, not %s + ''', + type_name(private_key) + )) + + if not isinstance(ciphertext, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(ciphertext) + )) + + key_length = private_key.byte_size + buffer = buffer_from_bytes(key_length) + output_length = new(Security, 'size_t *', key_length) + + if osx_version_info < (10, 8): + padding = SecurityConst.kSecPaddingNone + else: + padding = SecurityConst.kSecPaddingPKCS1 + + result = Security.SecKeyDecrypt( + private_key.sec_key_ref, + padding, + ciphertext, + len(ciphertext), + buffer, + output_length + ) + handle_sec_error(result) + + output = bytes_from_buffer(buffer, deref(output_length)) + + if osx_version_info < (10, 8): + output = remove_pkcs1v15_encryption_padding(key_length, output) + + return output + + +def rsa_oaep_encrypt(certificate_or_public_key, data): + """ + Encrypts a byte string using an RSA public key or certificate. Uses PKCS#1 + OAEP padding with SHA1. + + :param certificate_or_public_key: + A PublicKey or Certificate object + + :param data: + A byte string, with a maximum length 41 bytes (or more) less than the + key length (in bytes) + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the encrypted data + """ + + return _encrypt(certificate_or_public_key, data, Security.kSecPaddingOAEPKey) + + +def rsa_oaep_decrypt(private_key, ciphertext): + """ + Decrypts a byte string using an RSA private key. Uses PKCS#1 OAEP padding + with SHA1. + + :param private_key: + A PrivateKey object + + :param ciphertext: + A byte string of the encrypted data + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the original plaintext + """ + + return _decrypt(private_key, ciphertext, Security.kSecPaddingOAEPKey) + + +def _encrypt(certificate_or_public_key, data, padding): + """ + Encrypts plaintext using an RSA public key or certificate + + :param certificate_or_public_key: + A Certificate or PublicKey object + + :param data: + The plaintext - a byte string + + :param padding: + The padding mode to use, specified as a kSecPadding*Key value + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the ciphertext + """ + + if not isinstance(certificate_or_public_key, (Certificate, PublicKey)): + raise TypeError(pretty_message( + ''' + certificate_or_public_key must be an instance of the Certificate or + PublicKey class, not %s + ''', + type_name(certificate_or_public_key) + )) + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + if not padding: + raise ValueError('padding must be specified') + + cf_data = None + sec_transform = None + + try: + cf_data = CFHelpers.cf_data_from_bytes(data) + + error_pointer = new(CoreFoundation, 'CFErrorRef *') + sec_transform = Security.SecEncryptTransformCreate( + certificate_or_public_key.sec_key_ref, + error_pointer + ) + handle_cf_error(error_pointer) + + if padding: + Security.SecTransformSetAttribute( + sec_transform, + Security.kSecPaddingKey, + padding, + error_pointer + ) + handle_cf_error(error_pointer) + + Security.SecTransformSetAttribute( + sec_transform, + Security.kSecTransformInputAttributeName, + cf_data, + error_pointer + ) + handle_cf_error(error_pointer) + + ciphertext = Security.SecTransformExecute(sec_transform, error_pointer) + handle_cf_error(error_pointer) + + return CFHelpers.cf_data_to_bytes(ciphertext) + + finally: + if cf_data: + CoreFoundation.CFRelease(cf_data) + if sec_transform: + CoreFoundation.CFRelease(sec_transform) + + +def _decrypt(private_key, ciphertext, padding): + """ + Decrypts RSA ciphertext using a private key + + :param private_key: + A PrivateKey object + + :param ciphertext: + The ciphertext - a byte string + + :param padding: + The padding mode to use, specified as a kSecPadding*Key value + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the plaintext + """ + + if not isinstance(private_key, PrivateKey): + raise TypeError(pretty_message( + ''' + private_key must be an instance of the PrivateKey class, not %s + ''', + type_name(private_key) + )) + + if not isinstance(ciphertext, byte_cls): + raise TypeError(pretty_message( + ''' + ciphertext must be a byte string, not %s + ''', + type_name(ciphertext) + )) + + if not padding: + raise ValueError('padding must be specified') + + cf_data = None + sec_transform = None + + try: + cf_data = CFHelpers.cf_data_from_bytes(ciphertext) + + error_pointer = new(CoreFoundation, 'CFErrorRef *') + sec_transform = Security.SecDecryptTransformCreate( + private_key.sec_key_ref, + error_pointer + ) + handle_cf_error(error_pointer) + + Security.SecTransformSetAttribute( + sec_transform, + Security.kSecPaddingKey, + padding, + error_pointer + ) + handle_cf_error(error_pointer) + + Security.SecTransformSetAttribute( + sec_transform, + Security.kSecTransformInputAttributeName, + cf_data, + error_pointer + ) + handle_cf_error(error_pointer) + + plaintext = Security.SecTransformExecute(sec_transform, error_pointer) + handle_cf_error(error_pointer) + + return CFHelpers.cf_data_to_bytes(plaintext) + + finally: + if cf_data: + CoreFoundation.CFRelease(cf_data) + if sec_transform: + CoreFoundation.CFRelease(sec_transform) + + +def rsa_pkcs1v15_verify(certificate_or_public_key, signature, data, hash_algorithm): + """ + Verifies an RSASSA-PKCS-v1.5 signature. + + When the hash_algorithm is "raw", the operation is identical to RSA + public key decryption. That is: the data is not hashed and no ASN.1 + structure with an algorithm identifier of the hash algorithm is placed in + the encrypted byte string. + + :param certificate_or_public_key: + A Certificate or PublicKey instance to verify the signature with + + :param signature: + A byte string of the signature to verify + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha224", "sha256", "sha384", "sha512" or "raw" + + :raises: + oscrypto.errors.SignatureError - when the signature is determined to be invalid + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + if certificate_or_public_key.algorithm != 'rsa': + raise ValueError('The key specified is not an RSA public key') + + return _verify(certificate_or_public_key, signature, data, hash_algorithm) + + +def rsa_pss_verify(certificate_or_public_key, signature, data, hash_algorithm): + """ + Verifies an RSASSA-PSS signature. For the PSS padding the mask gen algorithm + will be mgf1 using the same hash algorithm as the signature. The salt length + with be the length of the hash algorithm, and the trailer field with be the + standard 0xBC byte. + + :param certificate_or_public_key: + A Certificate or PublicKey instance to verify the signature with + + :param signature: + A byte string of the signature to verify + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or "sha512" + + :raises: + oscrypto.errors.SignatureError - when the signature is determined to be invalid + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + if not isinstance(certificate_or_public_key, (Certificate, PublicKey)): + raise TypeError(pretty_message( + ''' + certificate_or_public_key must be an instance of the Certificate or + PublicKey class, not %s + ''', + type_name(certificate_or_public_key) + )) + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + cp_algo = certificate_or_public_key.algorithm + if cp_algo != 'rsa' and cp_algo != 'rsassa_pss': + raise ValueError('The key specified is not an RSA public key') + + hash_length = { + 'sha1': 20, + 'sha224': 28, + 'sha256': 32, + 'sha384': 48, + 'sha512': 64 + }.get(hash_algorithm, 0) + + key_length = certificate_or_public_key.byte_size + buffer = buffer_from_bytes(key_length) + output_length = new(Security, 'size_t *', key_length) + result = Security.SecKeyEncrypt( + certificate_or_public_key.sec_key_ref, + SecurityConst.kSecPaddingNone, + signature, + len(signature), + buffer, + output_length + ) + handle_sec_error(result) + + plaintext = bytes_from_buffer(buffer, deref(output_length)) + if not verify_pss_padding(hash_algorithm, hash_length, certificate_or_public_key.bit_size, data, plaintext): + raise SignatureError('Signature is invalid') + + +def dsa_verify(certificate_or_public_key, signature, data, hash_algorithm): + """ + Verifies a DSA signature + + :param certificate_or_public_key: + A Certificate or PublicKey instance to verify the signature with + + :param signature: + A byte string of the signature to verify + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or "sha512" + + :raises: + oscrypto.errors.SignatureError - when the signature is determined to be invalid + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + if certificate_or_public_key.algorithm != 'dsa': + raise ValueError('The key specified is not a DSA public key') + + return _verify(certificate_or_public_key, signature, data, hash_algorithm) + + +def ecdsa_verify(certificate_or_public_key, signature, data, hash_algorithm): + """ + Verifies an ECDSA signature + + :param certificate_or_public_key: + A Certificate or PublicKey instance to verify the signature with + + :param signature: + A byte string of the signature to verify + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or "sha512" + + :raises: + oscrypto.errors.SignatureError - when the signature is determined to be invalid + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + if certificate_or_public_key.algorithm != 'ec': + raise ValueError('The key specified is not an EC public key') + + return _verify(certificate_or_public_key, signature, data, hash_algorithm) + + +def _verify(certificate_or_public_key, signature, data, hash_algorithm): + """ + Verifies an RSA, DSA or ECDSA signature + + :param certificate_or_public_key: + A Certificate or PublicKey instance to verify the signature with + + :param signature: + A byte string of the signature to verify + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or "sha512" + + :raises: + oscrypto.errors.SignatureError - when the signature is determined to be invalid + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + if not isinstance(certificate_or_public_key, (Certificate, PublicKey)): + raise TypeError(pretty_message( + ''' + certificate_or_public_key must be an instance of the Certificate or + PublicKey class, not %s + ''', + type_name(certificate_or_public_key) + )) + + if not isinstance(signature, byte_cls): + raise TypeError(pretty_message( + ''' + signature must be a byte string, not %s + ''', + type_name(signature) + )) + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + valid_hash_algorithms = set(['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']) + if certificate_or_public_key.algorithm == 'rsa': + valid_hash_algorithms |= set(['raw']) + + if hash_algorithm not in valid_hash_algorithms: + valid_hash_algorithms_error = '"md5", "sha1", "sha224", "sha256", "sha384", "sha512"' + if certificate_or_public_key.algorithm == 'rsa': + valid_hash_algorithms_error += ', "raw"' + raise ValueError(pretty_message( + ''' + hash_algorithm must be one of %s, not %s + ''', + valid_hash_algorithms_error, + repr(hash_algorithm) + )) + + if certificate_or_public_key.algorithm == 'rsa' and hash_algorithm == 'raw': + if len(data) > certificate_or_public_key.byte_size - 11: + raise ValueError(pretty_message( + ''' + data must be 11 bytes shorter than the key size when + hash_algorithm is "raw" - key size is %s bytes, but data + is %s bytes long + ''', + certificate_or_public_key.byte_size, + len(data) + )) + + result = Security.SecKeyRawVerify( + certificate_or_public_key.sec_key_ref, + SecurityConst.kSecPaddingPKCS1, + data, + len(data), + signature, + len(signature) + ) + # errSSLCrypto is returned in some situations on macOS 10.12 + if result == SecurityConst.errSecVerifyFailed or result == SecurityConst.errSSLCrypto: + raise SignatureError('Signature is invalid') + handle_sec_error(result) + return + + cf_signature = None + cf_data = None + cf_hash_length = None + sec_transform = None + + try: + error_pointer = new(CoreFoundation, 'CFErrorRef *') + cf_signature = CFHelpers.cf_data_from_bytes(signature) + sec_transform = Security.SecVerifyTransformCreate( + certificate_or_public_key.sec_key_ref, + cf_signature, + error_pointer + ) + handle_cf_error(error_pointer) + + hash_constant = { + 'md5': Security.kSecDigestMD5, + 'sha1': Security.kSecDigestSHA1, + 'sha224': Security.kSecDigestSHA2, + 'sha256': Security.kSecDigestSHA2, + 'sha384': Security.kSecDigestSHA2, + 'sha512': Security.kSecDigestSHA2 + }[hash_algorithm] + + Security.SecTransformSetAttribute( + sec_transform, + Security.kSecDigestTypeAttribute, + hash_constant, + error_pointer + ) + handle_cf_error(error_pointer) + + if hash_algorithm in set(['sha224', 'sha256', 'sha384', 'sha512']): + hash_length = { + 'sha224': 224, + 'sha256': 256, + 'sha384': 384, + 'sha512': 512 + }[hash_algorithm] + + cf_hash_length = CFHelpers.cf_number_from_integer(hash_length) + + Security.SecTransformSetAttribute( + sec_transform, + Security.kSecDigestLengthAttribute, + cf_hash_length, + error_pointer + ) + handle_cf_error(error_pointer) + + if certificate_or_public_key.algorithm == 'rsa': + Security.SecTransformSetAttribute( + sec_transform, + Security.kSecPaddingKey, + Security.kSecPaddingPKCS1Key, + error_pointer + ) + handle_cf_error(error_pointer) + + cf_data = CFHelpers.cf_data_from_bytes(data) + Security.SecTransformSetAttribute( + sec_transform, + Security.kSecTransformInputAttributeName, + cf_data, + error_pointer + ) + handle_cf_error(error_pointer) + + res = Security.SecTransformExecute(sec_transform, error_pointer) + if not is_null(error_pointer): + error = unwrap(error_pointer) + if not is_null(error): + raise SignatureError('Signature is invalid') + + res = bool(CoreFoundation.CFBooleanGetValue(res)) + + if not res: + raise SignatureError('Signature is invalid') + + finally: + if sec_transform: + CoreFoundation.CFRelease(sec_transform) + if cf_signature: + CoreFoundation.CFRelease(cf_signature) + if cf_data: + CoreFoundation.CFRelease(cf_data) + if cf_hash_length: + CoreFoundation.CFRelease(cf_hash_length) + + +def rsa_pkcs1v15_sign(private_key, data, hash_algorithm): + """ + Generates an RSASSA-PKCS-v1.5 signature. + + When the hash_algorithm is "raw", the operation is identical to RSA + private key encryption. That is: the data is not hashed and no ASN.1 + structure with an algorithm identifier of the hash algorithm is placed in + the encrypted byte string. + + :param private_key: + The PrivateKey to generate the signature with + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha224", "sha256", "sha384", + "sha512" or "raw" + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the signature + """ + + if private_key.algorithm != 'rsa': + raise ValueError('The key specified is not an RSA private key') + + return _sign(private_key, data, hash_algorithm) + + +def rsa_pss_sign(private_key, data, hash_algorithm): + """ + Generates an RSASSA-PSS signature. For the PSS padding the mask gen + algorithm will be mgf1 using the same hash algorithm as the signature. The + salt length with be the length of the hash algorithm, and the trailer field + with be the standard 0xBC byte. + + :param private_key: + The PrivateKey to generate the signature with + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or + "sha512" + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the signature + """ + + if not isinstance(private_key, PrivateKey): + raise TypeError(pretty_message( + ''' + private_key must be an instance of the PrivateKey class, not %s + ''', + type_name(private_key) + )) + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + pk_algo = private_key.algorithm + if pk_algo != 'rsa' and pk_algo != 'rsassa_pss': + raise ValueError('The key specified is not an RSA private key') + + hash_length = { + 'sha1': 20, + 'sha224': 28, + 'sha256': 32, + 'sha384': 48, + 'sha512': 64 + }.get(hash_algorithm, 0) + + encoded_data = add_pss_padding(hash_algorithm, hash_length, private_key.bit_size, data) + + key_length = private_key.byte_size + buffer = buffer_from_bytes(key_length) + output_length = new(Security, 'size_t *', key_length) + result = Security.SecKeyDecrypt( + private_key.sec_key_ref, + SecurityConst.kSecPaddingNone, + encoded_data, + len(encoded_data), + buffer, + output_length + ) + handle_sec_error(result) + + return bytes_from_buffer(buffer, deref(output_length)) + + +def dsa_sign(private_key, data, hash_algorithm): + """ + Generates a DSA signature + + :param private_key: + The PrivateKey to generate the signature with + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or + "sha512" + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the signature + """ + + if private_key.algorithm != 'dsa': + raise ValueError('The key specified is not a DSA private key') + + return _sign(private_key, data, hash_algorithm) + + +def ecdsa_sign(private_key, data, hash_algorithm): + """ + Generates an ECDSA signature + + :param private_key: + The PrivateKey to generate the signature with + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or + "sha512" + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the signature + """ + + if private_key.algorithm != 'ec': + raise ValueError('The key specified is not an EC private key') + + return _sign(private_key, data, hash_algorithm) + + +def _sign(private_key, data, hash_algorithm): + """ + Generates an RSA, DSA or ECDSA signature + + :param private_key: + The PrivateKey to generate the signature with + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or + "sha512" + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the signature + """ + + if not isinstance(private_key, PrivateKey): + raise TypeError(pretty_message( + ''' + private_key must be an instance of PrivateKey, not %s + ''', + type_name(private_key) + )) + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + valid_hash_algorithms = set(['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']) + if private_key.algorithm == 'rsa': + valid_hash_algorithms |= set(['raw']) + + if hash_algorithm not in valid_hash_algorithms: + valid_hash_algorithms_error = '"md5", "sha1", "sha224", "sha256", "sha384", "sha512"' + if private_key.algorithm == 'rsa': + valid_hash_algorithms_error += ', "raw"' + raise ValueError(pretty_message( + ''' + hash_algorithm must be one of %s, not %s + ''', + valid_hash_algorithms_error, + repr(hash_algorithm) + )) + + if private_key.algorithm == 'rsa' and hash_algorithm == 'raw': + if len(data) > private_key.byte_size - 11: + raise ValueError(pretty_message( + ''' + data must be 11 bytes shorter than the key size when + hash_algorithm is "raw" - key size is %s bytes, but + data is %s bytes long + ''', + private_key.byte_size, + len(data) + )) + + key_length = private_key.byte_size + buffer = buffer_from_bytes(key_length) + output_length = new(Security, 'size_t *', key_length) + result = Security.SecKeyRawSign( + private_key.sec_key_ref, + SecurityConst.kSecPaddingPKCS1, + data, + len(data), + buffer, + output_length + ) + handle_sec_error(result) + + return bytes_from_buffer(buffer, deref(output_length)) + + cf_signature = None + cf_data = None + cf_hash_length = None + sec_transform = None + + try: + error_pointer = new(CoreFoundation, 'CFErrorRef *') + sec_transform = Security.SecSignTransformCreate(private_key.sec_key_ref, error_pointer) + handle_cf_error(error_pointer) + + hash_constant = { + 'md5': Security.kSecDigestMD5, + 'sha1': Security.kSecDigestSHA1, + 'sha224': Security.kSecDigestSHA2, + 'sha256': Security.kSecDigestSHA2, + 'sha384': Security.kSecDigestSHA2, + 'sha512': Security.kSecDigestSHA2 + }[hash_algorithm] + + Security.SecTransformSetAttribute( + sec_transform, + Security.kSecDigestTypeAttribute, + hash_constant, + error_pointer + ) + handle_cf_error(error_pointer) + + if hash_algorithm in set(['sha224', 'sha256', 'sha384', 'sha512']): + hash_length = { + 'sha224': 224, + 'sha256': 256, + 'sha384': 384, + 'sha512': 512 + }[hash_algorithm] + + cf_hash_length = CFHelpers.cf_number_from_integer(hash_length) + + Security.SecTransformSetAttribute( + sec_transform, + Security.kSecDigestLengthAttribute, + cf_hash_length, + error_pointer + ) + handle_cf_error(error_pointer) + + if private_key.algorithm == 'rsa': + Security.SecTransformSetAttribute( + sec_transform, + Security.kSecPaddingKey, + Security.kSecPaddingPKCS1Key, + error_pointer + ) + handle_cf_error(error_pointer) + + cf_data = CFHelpers.cf_data_from_bytes(data) + Security.SecTransformSetAttribute( + sec_transform, + Security.kSecTransformInputAttributeName, + cf_data, + error_pointer + ) + handle_cf_error(error_pointer) + + cf_signature = Security.SecTransformExecute(sec_transform, error_pointer) + handle_cf_error(error_pointer) + + return CFHelpers.cf_data_to_bytes(cf_signature) + + finally: + if sec_transform: + CoreFoundation.CFRelease(sec_transform) + if cf_signature: + CoreFoundation.CFRelease(cf_signature) + if cf_data: + CoreFoundation.CFRelease(cf_data) + if cf_hash_length: + CoreFoundation.CFRelease(cf_hash_length) diff --git a/tasks/lib/package_control/deps/oscrypto/_mac/symmetric.py b/tasks/lib/package_control/deps/oscrypto/_mac/symmetric.py new file mode 100644 index 0000000..431fef2 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_mac/symmetric.py @@ -0,0 +1,757 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from .._errors import pretty_message +from .._ffi import new, null +from ._core_foundation import CoreFoundation, CFHelpers, handle_cf_error +from ._security import Security +from .util import rand_bytes +from .._types import type_name, byte_cls + + +__all__ = [ + 'aes_cbc_no_padding_decrypt', + 'aes_cbc_no_padding_encrypt', + 'aes_cbc_pkcs7_decrypt', + 'aes_cbc_pkcs7_encrypt', + 'des_cbc_pkcs5_decrypt', + 'des_cbc_pkcs5_encrypt', + 'rc2_cbc_pkcs5_decrypt', + 'rc2_cbc_pkcs5_encrypt', + 'rc4_decrypt', + 'rc4_encrypt', + 'tripledes_cbc_pkcs5_decrypt', + 'tripledes_cbc_pkcs5_encrypt', +] + + +def aes_cbc_no_padding_encrypt(key, data, iv): + """ + Encrypts plaintext using AES in CBC mode with a 128, 192 or 256 bit key and + no padding. This means the ciphertext must be an exact multiple of 16 bytes + long. + + :param key: + The encryption key - a byte string either 16, 24 or 32 bytes long + + :param data: + The plaintext - a byte string + + :param iv: + The initialization vector - either a byte string 16-bytes long or None + to generate an IV + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A tuple of two byte strings (iv, ciphertext) + """ + + if len(key) not in [16, 24, 32]: + raise ValueError(pretty_message( + ''' + key must be either 16, 24 or 32 bytes (128, 192 or 256 bits) long - is %s + ''', + len(key) + )) + + if not iv: + iv = rand_bytes(16) + elif len(iv) != 16: + raise ValueError(pretty_message( + ''' + iv must be 16 bytes long - is %s + ''', + len(iv) + )) + + if len(data) % 16 != 0: + raise ValueError(pretty_message( + ''' + data must be a multiple of 16 bytes long - is %s + ''', + len(data) + )) + + return (iv, _encrypt(Security.kSecAttrKeyTypeAES, key, data, iv, Security.kSecPaddingNoneKey)) + + +def aes_cbc_no_padding_decrypt(key, data, iv): + """ + Decrypts AES ciphertext in CBC mode using a 128, 192 or 256 bit key and no + padding. + + :param key: + The encryption key - a byte string either 16, 24 or 32 bytes long + + :param data: + The ciphertext - a byte string + + :param iv: + The initialization vector - a byte string 16-bytes long + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the plaintext + """ + + if len(key) not in [16, 24, 32]: + raise ValueError(pretty_message( + ''' + key must be either 16, 24 or 32 bytes (128, 192 or 256 bits) long - is %s + ''', + len(key) + )) + + if len(iv) != 16: + raise ValueError(pretty_message( + ''' + iv must be 16 bytes long - is %s + ''', + len(iv) + )) + + return _decrypt(Security.kSecAttrKeyTypeAES, key, data, iv, Security.kSecPaddingNoneKey) + + +def aes_cbc_pkcs7_encrypt(key, data, iv): + """ + Encrypts plaintext using AES in CBC mode with a 128, 192 or 256 bit key and + PKCS#7 padding. + + :param key: + The encryption key - a byte string either 16, 24 or 32 bytes long + + :param data: + The plaintext - a byte string + + :param iv: + The initialization vector - either a byte string 16-bytes long or None + to generate an IV + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A tuple of two byte strings (iv, ciphertext) + """ + + if len(key) not in [16, 24, 32]: + raise ValueError(pretty_message( + ''' + key must be either 16, 24 or 32 bytes (128, 192 or 256 bits) long - is %s + ''', + len(key) + )) + + if not iv: + iv = rand_bytes(16) + elif len(iv) != 16: + raise ValueError(pretty_message( + ''' + iv must be 16 bytes long - is %s + ''', + len(iv) + )) + + return (iv, _encrypt(Security.kSecAttrKeyTypeAES, key, data, iv, Security.kSecPaddingPKCS7Key)) + + +def aes_cbc_pkcs7_decrypt(key, data, iv): + """ + Decrypts AES ciphertext in CBC mode using a 128, 192 or 256 bit key + + :param key: + The encryption key - a byte string either 16, 24 or 32 bytes long + + :param data: + The ciphertext - a byte string + + :param iv: + The initialization vector - a byte string 16-bytes long + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the plaintext + """ + + if len(key) not in [16, 24, 32]: + raise ValueError(pretty_message( + ''' + key must be either 16, 24 or 32 bytes (128, 192 or 256 bits) + long - is %s + ''', + len(key) + )) + + if len(iv) != 16: + raise ValueError(pretty_message( + ''' + iv must be 16 bytes long - is %s + ''', + len(iv) + )) + + return _decrypt(Security.kSecAttrKeyTypeAES, key, data, iv, Security.kSecPaddingPKCS7Key) + + +def rc4_encrypt(key, data): + """ + Encrypts plaintext using RC4 with a 40-128 bit key + + :param key: + The encryption key - a byte string 5-16 bytes long + + :param data: + The plaintext - a byte string + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the ciphertext + """ + + if len(key) < 5 or len(key) > 16: + raise ValueError(pretty_message( + ''' + key must be 5 to 16 bytes (40 to 128 bits) long - is %s + ''', + len(key) + )) + + return _encrypt(Security.kSecAttrKeyTypeRC4, key, data, None, None) + + +def rc4_decrypt(key, data): + """ + Decrypts RC4 ciphertext using a 40-128 bit key + + :param key: + The encryption key - a byte string 5-16 bytes long + + :param data: + The ciphertext - a byte string + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the plaintext + """ + + if len(key) < 5 or len(key) > 16: + raise ValueError(pretty_message( + ''' + key must be 5 to 16 bytes (40 to 128 bits) long - is %s + ''', + len(key) + )) + + return _decrypt(Security.kSecAttrKeyTypeRC4, key, data, None, None) + + +def rc2_cbc_pkcs5_encrypt(key, data, iv): + """ + Encrypts plaintext using RC2 with a 64 bit key + + :param key: + The encryption key - a byte string 8 bytes long + + :param data: + The plaintext - a byte string + + :param iv: + The 8-byte initialization vector to use - a byte string - set as None + to generate an appropriate one + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A tuple of two byte strings (iv, ciphertext) + """ + + if len(key) < 5 or len(key) > 16: + raise ValueError(pretty_message( + ''' + key must be 5 to 16 bytes (40 to 128 bits) long - is %s + ''', + len(key) + )) + + if not iv: + iv = rand_bytes(8) + elif len(iv) != 8: + raise ValueError(pretty_message( + ''' + iv must be 8 bytes long - is %s + ''', + len(iv) + )) + + return (iv, _encrypt(Security.kSecAttrKeyTypeRC2, key, data, iv, Security.kSecPaddingPKCS5Key)) + + +def rc2_cbc_pkcs5_decrypt(key, data, iv): + """ + Decrypts RC2 ciphertext using a 64 bit key + + :param key: + The encryption key - a byte string 8 bytes long + + :param data: + The ciphertext - a byte string + + :param iv: + The initialization vector used for encryption - a byte string + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the plaintext + """ + + if len(key) < 5 or len(key) > 16: + raise ValueError(pretty_message( + ''' + key must be 5 to 16 bytes (40 to 128 bits) long - is %s + ''', + len(key) + )) + + if len(iv) != 8: + raise ValueError(pretty_message( + ''' + iv must be 8 bytes long - is %s + ''', + len(iv) + )) + + return _decrypt(Security.kSecAttrKeyTypeRC2, key, data, iv, Security.kSecPaddingPKCS5Key) + + +def tripledes_cbc_pkcs5_encrypt(key, data, iv): + """ + Encrypts plaintext using 3DES in either 2 or 3 key mode + + :param key: + The encryption key - a byte string 16 or 24 bytes long (2 or 3 key mode) + + :param data: + The plaintext - a byte string + + :param iv: + The 8-byte initialization vector to use - a byte string - set as None + to generate an appropriate one + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A tuple of two byte strings (iv, ciphertext) + """ + + if len(key) != 16 and len(key) != 24: + raise ValueError(pretty_message( + ''' + key must be 16 bytes (2 key) or 24 bytes (3 key) long - %s + ''', + len(key) + )) + + if not iv: + iv = rand_bytes(8) + elif len(iv) != 8: + raise ValueError(pretty_message( + ''' + iv must be 8 bytes long - %s + ''', + len(iv) + )) + + # Expand 2-key to actual 24 byte byte string used by cipher + if len(key) == 16: + key = key + key[0:8] + + return (iv, _encrypt(Security.kSecAttrKeyType3DES, key, data, iv, Security.kSecPaddingPKCS5Key)) + + +def tripledes_cbc_pkcs5_decrypt(key, data, iv): + """ + Decrypts 3DES ciphertext in either 2 or 3 key mode + + :param key: + The encryption key - a byte string 16 or 24 bytes long (2 or 3 key mode) + + :param data: + The ciphertext - a byte string + + :param iv: + The initialization vector used for encryption - a byte string + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the plaintext + """ + + if len(key) != 16 and len(key) != 24: + raise ValueError(pretty_message( + ''' + key must be 16 bytes (2 key) or 24 bytes (3 key) long - is %s + ''', + len(key) + )) + + if len(iv) != 8: + raise ValueError(pretty_message( + ''' + iv must be 8 bytes long - is %s + ''', + len(iv) + )) + + # Expand 2-key to actual 24 byte byte string used by cipher + if len(key) == 16: + key = key + key[0:8] + + return _decrypt(Security.kSecAttrKeyType3DES, key, data, iv, Security.kSecPaddingPKCS5Key) + + +def des_cbc_pkcs5_encrypt(key, data, iv): + """ + Encrypts plaintext using DES with a 56 bit key + + :param key: + The encryption key - a byte string 8 bytes long (includes error correction bits) + + :param data: + The plaintext - a byte string + + :param iv: + The 8-byte initialization vector to use - a byte string - set as None + to generate an appropriate one + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A tuple of two byte strings (iv, ciphertext) + """ + + if len(key) != 8: + raise ValueError(pretty_message( + ''' + key must be 8 bytes (56 bits + 8 parity bits) long - is %s + ''', + len(key) + )) + + if not iv: + iv = rand_bytes(8) + elif len(iv) != 8: + raise ValueError(pretty_message( + ''' + iv must be 8 bytes long - is %s + ''', + len(iv) + )) + + return (iv, _encrypt(Security.kSecAttrKeyTypeDES, key, data, iv, Security.kSecPaddingPKCS5Key)) + + +def des_cbc_pkcs5_decrypt(key, data, iv): + """ + Decrypts DES ciphertext using a 56 bit key + + :param key: + The encryption key - a byte string 8 bytes long (includes error correction bits) + + :param data: + The ciphertext - a byte string + + :param iv: + The initialization vector used for encryption - a byte string + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the plaintext + """ + + if len(key) != 8: + raise ValueError(pretty_message( + ''' + key must be 8 bytes (56 bits + 8 parity bits) long - is %s + ''', + len(key) + )) + + if len(iv) != 8: + raise ValueError(pretty_message( + ''' + iv must be 8 bytes long - is %s + ''', + len(iv) + )) + + return _decrypt(Security.kSecAttrKeyTypeDES, key, data, iv, Security.kSecPaddingPKCS5Key) + + +def _encrypt(cipher, key, data, iv, padding): + """ + Encrypts plaintext + + :param cipher: + A kSecAttrKeyType* value that specifies the cipher to use + + :param key: + The encryption key - a byte string 5-16 bytes long + + :param data: + The plaintext - a byte string + + :param iv: + The initialization vector - a byte string - unused for RC4 + + :param padding: + The padding mode to use, specified as a kSecPadding*Key value - unused for RC4 + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the ciphertext + """ + + if not isinstance(key, byte_cls): + raise TypeError(pretty_message( + ''' + key must be a byte string, not %s + ''', + type_name(key) + )) + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + if cipher != Security.kSecAttrKeyTypeRC4 and not isinstance(iv, byte_cls): + raise TypeError(pretty_message( + ''' + iv must be a byte string, not %s + ''', + type_name(iv) + )) + + if cipher != Security.kSecAttrKeyTypeRC4 and not padding: + raise ValueError('padding must be specified') + + cf_dict = None + cf_key = None + cf_data = None + cf_iv = None + sec_key = None + sec_transform = None + + try: + cf_dict = CFHelpers.cf_dictionary_from_pairs([(Security.kSecAttrKeyType, cipher)]) + cf_key = CFHelpers.cf_data_from_bytes(key) + cf_data = CFHelpers.cf_data_from_bytes(data) + + error_pointer = new(CoreFoundation, 'CFErrorRef *') + sec_key = Security.SecKeyCreateFromData(cf_dict, cf_key, error_pointer) + handle_cf_error(error_pointer) + + sec_transform = Security.SecEncryptTransformCreate(sec_key, error_pointer) + handle_cf_error(error_pointer) + + if cipher != Security.kSecAttrKeyTypeRC4: + Security.SecTransformSetAttribute(sec_transform, Security.kSecModeCBCKey, null(), error_pointer) + handle_cf_error(error_pointer) + + Security.SecTransformSetAttribute(sec_transform, Security.kSecPaddingKey, padding, error_pointer) + handle_cf_error(error_pointer) + + cf_iv = CFHelpers.cf_data_from_bytes(iv) + Security.SecTransformSetAttribute(sec_transform, Security.kSecIVKey, cf_iv, error_pointer) + handle_cf_error(error_pointer) + + Security.SecTransformSetAttribute( + sec_transform, + Security.kSecTransformInputAttributeName, + cf_data, + error_pointer + ) + handle_cf_error(error_pointer) + + ciphertext = Security.SecTransformExecute(sec_transform, error_pointer) + handle_cf_error(error_pointer) + + return CFHelpers.cf_data_to_bytes(ciphertext) + + finally: + if cf_dict: + CoreFoundation.CFRelease(cf_dict) + if cf_key: + CoreFoundation.CFRelease(cf_key) + if cf_data: + CoreFoundation.CFRelease(cf_data) + if cf_iv: + CoreFoundation.CFRelease(cf_iv) + if sec_key: + CoreFoundation.CFRelease(sec_key) + if sec_transform: + CoreFoundation.CFRelease(sec_transform) + + +def _decrypt(cipher, key, data, iv, padding): + """ + Decrypts AES/RC4/RC2/3DES/DES ciphertext + + :param cipher: + A kSecAttrKeyType* value that specifies the cipher to use + + :param key: + The encryption key - a byte string 5-16 bytes long + + :param data: + The ciphertext - a byte string + + :param iv: + The initialization vector - a byte string - unused for RC4 + + :param padding: + The padding mode to use, specified as a kSecPadding*Key value - unused for RC4 + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the plaintext + """ + + if not isinstance(key, byte_cls): + raise TypeError(pretty_message( + ''' + key must be a byte string, not %s + ''', + type_name(key) + )) + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + if cipher != Security.kSecAttrKeyTypeRC4 and not isinstance(iv, byte_cls): + raise TypeError(pretty_message( + ''' + iv must be a byte string, not %s + ''', + type_name(iv) + )) + + if cipher != Security.kSecAttrKeyTypeRC4 and not padding: + raise ValueError('padding must be specified') + + cf_dict = None + cf_key = None + cf_data = None + cf_iv = None + sec_key = None + sec_transform = None + + try: + cf_dict = CFHelpers.cf_dictionary_from_pairs([(Security.kSecAttrKeyType, cipher)]) + cf_key = CFHelpers.cf_data_from_bytes(key) + cf_data = CFHelpers.cf_data_from_bytes(data) + + error_pointer = new(CoreFoundation, 'CFErrorRef *') + sec_key = Security.SecKeyCreateFromData(cf_dict, cf_key, error_pointer) + handle_cf_error(error_pointer) + + sec_transform = Security.SecDecryptTransformCreate(sec_key, error_pointer) + handle_cf_error(error_pointer) + + if cipher != Security.kSecAttrKeyTypeRC4: + Security.SecTransformSetAttribute(sec_transform, Security.kSecModeCBCKey, null(), error_pointer) + handle_cf_error(error_pointer) + + Security.SecTransformSetAttribute(sec_transform, Security.kSecPaddingKey, padding, error_pointer) + handle_cf_error(error_pointer) + + cf_iv = CFHelpers.cf_data_from_bytes(iv) + Security.SecTransformSetAttribute(sec_transform, Security.kSecIVKey, cf_iv, error_pointer) + handle_cf_error(error_pointer) + + Security.SecTransformSetAttribute( + sec_transform, + Security.kSecTransformInputAttributeName, + cf_data, + error_pointer + ) + handle_cf_error(error_pointer) + + plaintext = Security.SecTransformExecute(sec_transform, error_pointer) + handle_cf_error(error_pointer) + + return CFHelpers.cf_data_to_bytes(plaintext) + + finally: + if cf_dict: + CoreFoundation.CFRelease(cf_dict) + if cf_key: + CoreFoundation.CFRelease(cf_key) + if cf_data: + CoreFoundation.CFRelease(cf_data) + if cf_iv: + CoreFoundation.CFRelease(cf_iv) + if sec_key: + CoreFoundation.CFRelease(sec_key) + if sec_transform: + CoreFoundation.CFRelease(sec_transform) diff --git a/tasks/lib/package_control/deps/oscrypto/_mac/tls.py b/tasks/lib/package_control/deps/oscrypto/_mac/tls.py new file mode 100644 index 0000000..f936407 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_mac/tls.py @@ -0,0 +1,1518 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import datetime +import sys +import re +import socket as socket_ +import select +import numbers +import errno +import weakref + +from ._security import Security, osx_version_info, handle_sec_error, SecurityConst +from ._core_foundation import CoreFoundation, handle_cf_error, CFHelpers +from .._asn1 import ( + Certificate as Asn1Certificate, + int_to_bytes, + timezone, +) +from .._errors import pretty_message +from .._ffi import ( + array_from_pointer, + array_set, + buffer_from_bytes, + bytes_from_buffer, + callback, + cast, + deref, + new, + null, + pointer_set, + struct, + struct_bytes, + unwrap, + write_to_buffer, +) +from .._types import type_name, str_cls, byte_cls, int_types +from .._cipher_suites import CIPHER_SUITE_MAP +from .util import rand_bytes +from ..errors import TLSError, TLSDisconnectError, TLSGracefulDisconnectError +from .._tls import ( + detect_client_auth_request, + detect_other_protocol, + extract_chain, + get_dh_params_length, + parse_session_info, + raise_client_auth, + raise_dh_params, + raise_disconnection, + raise_expired_not_yet_valid, + raise_handshake, + raise_hostname, + raise_lifetime_too_long, + raise_no_issuer, + raise_protocol_error, + raise_protocol_version, + raise_revoked, + raise_self_signed, + raise_verification, + raise_weak_signature, +) +from .asymmetric import load_certificate, Certificate +from ..keys import parse_certificate + +if sys.version_info < (3,): + range = xrange # noqa + +if sys.version_info < (3, 7): + Pattern = re._pattern_type +else: + Pattern = re.Pattern + + +__all__ = [ + 'TLSSession', + 'TLSSocket', +] + + +_PROTOCOL_STRING_CONST_MAP = { + 'SSLv2': SecurityConst.kSSLProtocol2, + 'SSLv3': SecurityConst.kSSLProtocol3, + 'TLSv1': SecurityConst.kTLSProtocol1, + 'TLSv1.1': SecurityConst.kTLSProtocol11, + 'TLSv1.2': SecurityConst.kTLSProtocol12, +} + +_PROTOCOL_CONST_STRING_MAP = { + SecurityConst.kSSLProtocol2: 'SSLv2', + SecurityConst.kSSLProtocol3: 'SSLv3', + SecurityConst.kTLSProtocol1: 'TLSv1', + SecurityConst.kTLSProtocol11: 'TLSv1.1', + SecurityConst.kTLSProtocol12: 'TLSv1.2', +} + +_line_regex = re.compile(b'(\r\n|\r|\n)') +_cipher_blacklist_regex = re.compile('anon|PSK|SEED|RC4|MD5|NULL|CAMELLIA|ARIA|SRP|KRB5|EXPORT|(? 0.0: + read_ready, _, _ = select.select([socket], [], [], timeout) + if len(read_ready) == 0: + raise socket_.error(errno.EAGAIN, 'timed out') + chunk = socket.recv(bytes_requested - len(data)) + data += chunk + if chunk == b'': + if len(data) == 0: + if timeout is None: + return SecurityConst.errSSLClosedNoNotify + return SecurityConst.errSSLClosedAbort + break + except (socket_.error) as e: + error = e.errno + + if error is not None and error != errno.EAGAIN: + if error == errno.ECONNRESET or error == errno.EPIPE: + return SecurityConst.errSSLClosedNoNotify + return SecurityConst.errSSLClosedAbort + + if self and not self._done_handshake: + # SecureTransport doesn't bother to check if the TLS record header + # is valid before asking to read more data, which can result in + # connection hangs. Here we do basic checks to get around the issue. + if len(data) >= 3 and len(self._server_hello) == 0: + # Check to ensure it is an alert or handshake first + valid_record_type = data[0:1] in set([b'\x15', b'\x16']) + # Check if the protocol version is SSL 3.0 or TLS 1.0-1.3 + valid_protocol_version = data[1:3] in set([ + b'\x03\x00', + b'\x03\x01', + b'\x03\x02', + b'\x03\x03', + b'\x03\x04' + ]) + if not valid_record_type or not valid_protocol_version: + self._server_hello += data + _read_remaining(socket) + return SecurityConst.errSSLProtocol + self._server_hello += data + + write_to_buffer(data_buffer, data) + pointer_set(data_length_pointer, len(data)) + + if len(data) != bytes_requested: + return SecurityConst.errSSLWouldBlock + + return 0 + except (KeyboardInterrupt) as e: + if self: + self._exception = e + return SecurityConst.errSSLClosedAbort + + +def _read_remaining(socket): + """ + Reads everything available from the socket - used for debugging when there + is a protocol error + + :param socket: + The socket to read from + + :return: + A byte string of the remaining data + """ + + output = b'' + old_timeout = socket.gettimeout() + try: + socket.settimeout(0.0) + output += socket.recv(8192) + except (socket_.error): + pass + finally: + socket.settimeout(old_timeout) + return output + + +def _write_callback(connection_id, data_buffer, data_length_pointer): + """ + Callback called by Secure Transport to actually write to the socket + + :param connection_id: + An integer identifying the connection + + :param data_buffer: + A char pointer FFI type containing the data to write + + :param data_length_pointer: + A size_t pointer FFI type of the amount of data to write. Will be + overwritten with the amount of data actually written on return. + + :return: + An integer status code of the result - 0 for success + """ + + try: + self = _connection_refs.get(connection_id) + if not self: + socket = _socket_refs.get(connection_id) + else: + socket = self._socket + + if not self and not socket: + return 0 + + data_length = deref(data_length_pointer) + data = bytes_from_buffer(data_buffer, data_length) + + if self and not self._done_handshake: + self._client_hello += data + + error = None + try: + sent = socket.send(data) + except (socket_.error) as e: + error = e.errno + + if error is not None and error != errno.EAGAIN: + if error == errno.ECONNRESET or error == errno.EPIPE: + return SecurityConst.errSSLClosedNoNotify + return SecurityConst.errSSLClosedAbort + + if sent != data_length: + pointer_set(data_length_pointer, sent) + return SecurityConst.errSSLWouldBlock + + return 0 + except (KeyboardInterrupt) as e: + self._exception = e + return SecurityConst.errSSLPeerUserCancelled + + +_read_callback_pointer = callback(Security, 'SSLReadFunc', _read_callback) +_write_callback_pointer = callback(Security, 'SSLWriteFunc', _write_callback) + + +class TLSSession(object): + """ + A TLS session object that multiple TLSSocket objects can share for the + sake of session reuse + """ + + _protocols = None + _ciphers = None + _manual_validation = None + _extra_trust_roots = None + _peer_id = None + + def __init__(self, protocol=None, manual_validation=False, extra_trust_roots=None): + """ + :param protocol: + A unicode string or set of unicode strings representing allowable + protocols to negotiate with the server: + + - "TLSv1.2" + - "TLSv1.1" + - "TLSv1" + - "SSLv3" + + Default is: {"TLSv1", "TLSv1.1", "TLSv1.2"} + + :param manual_validation: + If certificate and certificate path validation should be skipped + and left to the developer to implement + + :param extra_trust_roots: + A list containing one or more certificates to be treated as trust + roots, in one of the following formats: + - A byte string of the DER encoded certificate + - A unicode string of the certificate filename + - An asn1crypto.x509.Certificate object + - An oscrypto.asymmetric.Certificate object + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + if not isinstance(manual_validation, bool): + raise TypeError(pretty_message( + ''' + manual_validation must be a boolean, not %s + ''', + type_name(manual_validation) + )) + + self._manual_validation = manual_validation + + if protocol is None: + protocol = set(['TLSv1', 'TLSv1.1', 'TLSv1.2']) + + if isinstance(protocol, str_cls): + protocol = set([protocol]) + elif not isinstance(protocol, set): + raise TypeError(pretty_message( + ''' + protocol must be a unicode string or set of unicode strings, + not %s + ''', + type_name(protocol) + )) + + unsupported_protocols = protocol - set(['SSLv3', 'TLSv1', 'TLSv1.1', 'TLSv1.2']) + if unsupported_protocols: + raise ValueError(pretty_message( + ''' + protocol must contain only the unicode strings "SSLv3", "TLSv1", + "TLSv1.1", "TLSv1.2", not %s + ''', + repr(unsupported_protocols) + )) + + self._protocols = protocol + + self._extra_trust_roots = [] + if extra_trust_roots: + for extra_trust_root in extra_trust_roots: + if isinstance(extra_trust_root, Certificate): + extra_trust_root = extra_trust_root.asn1 + elif isinstance(extra_trust_root, byte_cls): + extra_trust_root = parse_certificate(extra_trust_root) + elif isinstance(extra_trust_root, str_cls): + with open(extra_trust_root, 'rb') as f: + extra_trust_root = parse_certificate(f.read()) + elif not isinstance(extra_trust_root, Asn1Certificate): + raise TypeError(pretty_message( + ''' + extra_trust_roots must be a list of byte strings, unicode + strings, asn1crypto.x509.Certificate objects or + oscrypto.asymmetric.Certificate objects, not %s + ''', + type_name(extra_trust_root) + )) + self._extra_trust_roots.append(extra_trust_root) + + self._peer_id = rand_bytes(8) + + +class TLSSocket(object): + """ + A wrapper around a socket.socket that adds TLS + """ + + _socket = None + _session = None + _exception = None + + _session_context = None + + _decrypted_bytes = None + + _hostname = None + + _certificate = None + _intermediates = None + + _protocol = None + _cipher_suite = None + _compression = None + _session_id = None + _session_ticket = None + + _done_handshake = None + _server_hello = None + _client_hello = None + + _local_closed = False + _gracefully_closed = False + + _connection_id = None + + @classmethod + def wrap(cls, socket, hostname, session=None): + """ + Takes an existing socket and adds TLS + + :param socket: + A socket.socket object to wrap with TLS + + :param hostname: + A unicode string of the hostname or IP the socket is connected to + + :param session: + An existing TLSSession object to allow for session reuse, specific + protocol or manual certificate validation + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + if not isinstance(socket, socket_.socket): + raise TypeError(pretty_message( + ''' + socket must be an instance of socket.socket, not %s + ''', + type_name(socket) + )) + + if not isinstance(hostname, str_cls): + raise TypeError(pretty_message( + ''' + hostname must be a unicode string, not %s + ''', + type_name(hostname) + )) + + if session is not None and not isinstance(session, TLSSession): + raise TypeError(pretty_message( + ''' + session must be an instance of oscrypto.tls.TLSSession, not %s + ''', + type_name(session) + )) + + new_socket = cls(None, None, session=session) + new_socket._socket = socket + new_socket._hostname = hostname + new_socket._handshake() + + return new_socket + + def __init__(self, address, port, timeout=10, session=None): + """ + :param address: + A unicode string of the domain name or IP address to connect to + + :param port: + An integer of the port number to connect to + + :param timeout: + An integer timeout to use for the socket + + :param session: + An oscrypto.tls.TLSSession object to allow for session reuse and + controlling the protocols and validation performed + """ + + self._done_handshake = False + self._server_hello = b'' + self._client_hello = b'' + + self._decrypted_bytes = b'' + + if address is None and port is None: + self._socket = None + + else: + if not isinstance(address, str_cls): + raise TypeError(pretty_message( + ''' + address must be a unicode string, not %s + ''', + type_name(address) + )) + + if not isinstance(port, int_types): + raise TypeError(pretty_message( + ''' + port must be an integer, not %s + ''', + type_name(port) + )) + + if timeout is not None and not isinstance(timeout, numbers.Number): + raise TypeError(pretty_message( + ''' + timeout must be a number, not %s + ''', + type_name(timeout) + )) + + self._socket = socket_.create_connection((address, port), timeout) + self._socket.settimeout(timeout) + + if session is None: + session = TLSSession() + + elif not isinstance(session, TLSSession): + raise TypeError(pretty_message( + ''' + session must be an instance of oscrypto.tls.TLSSession, not %s + ''', + type_name(session) + )) + + self._session = session + + if self._socket: + self._hostname = address + self._handshake() + + def _handshake(self): + """ + Perform an initial TLS handshake + """ + + session_context = None + ssl_policy_ref = None + crl_search_ref = None + crl_policy_ref = None + ocsp_search_ref = None + ocsp_policy_ref = None + policy_array_ref = None + trust_ref = None + + try: + if osx_version_info < (10, 8): + session_context_pointer = new(Security, 'SSLContextRef *') + result = Security.SSLNewContext(False, session_context_pointer) + handle_sec_error(result) + session_context = unwrap(session_context_pointer) + + else: + session_context = Security.SSLCreateContext( + null(), + SecurityConst.kSSLClientSide, + SecurityConst.kSSLStreamType + ) + + result = Security.SSLSetIOFuncs( + session_context, + _read_callback_pointer, + _write_callback_pointer + ) + handle_sec_error(result) + + self._connection_id = id(self) % 2147483647 + _connection_refs[self._connection_id] = self + _socket_refs[self._connection_id] = self._socket + result = Security.SSLSetConnection(session_context, self._connection_id) + handle_sec_error(result) + + utf8_domain = self._hostname.encode('utf-8') + result = Security.SSLSetPeerDomainName( + session_context, + utf8_domain, + len(utf8_domain) + ) + handle_sec_error(result) + + if osx_version_info >= (10, 10): + disable_auto_validation = self._session._manual_validation or self._session._extra_trust_roots + explicit_validation = (not self._session._manual_validation) and self._session._extra_trust_roots + else: + disable_auto_validation = True + explicit_validation = not self._session._manual_validation + + # Ensure requested protocol support is set for the session + if osx_version_info < (10, 8): + for protocol in ['SSLv2', 'SSLv3', 'TLSv1']: + protocol_const = _PROTOCOL_STRING_CONST_MAP[protocol] + enabled = protocol in self._session._protocols + result = Security.SSLSetProtocolVersionEnabled( + session_context, + protocol_const, + enabled + ) + handle_sec_error(result) + + if disable_auto_validation: + result = Security.SSLSetEnableCertVerify(session_context, False) + handle_sec_error(result) + + else: + protocol_consts = [_PROTOCOL_STRING_CONST_MAP[protocol] for protocol in self._session._protocols] + min_protocol = min(protocol_consts) + max_protocol = max(protocol_consts) + result = Security.SSLSetProtocolVersionMin( + session_context, + min_protocol + ) + handle_sec_error(result) + result = Security.SSLSetProtocolVersionMax( + session_context, + max_protocol + ) + handle_sec_error(result) + + if disable_auto_validation: + result = Security.SSLSetSessionOption( + session_context, + SecurityConst.kSSLSessionOptionBreakOnServerAuth, + True + ) + handle_sec_error(result) + + # Disable all sorts of bad cipher suites + supported_ciphers_pointer = new(Security, 'size_t *') + result = Security.SSLGetNumberSupportedCiphers(session_context, supported_ciphers_pointer) + handle_sec_error(result) + + supported_ciphers = deref(supported_ciphers_pointer) + + cipher_buffer = buffer_from_bytes(supported_ciphers * 4) + supported_cipher_suites_pointer = cast(Security, 'uint32_t *', cipher_buffer) + result = Security.SSLGetSupportedCiphers( + session_context, + supported_cipher_suites_pointer, + supported_ciphers_pointer + ) + handle_sec_error(result) + + supported_ciphers = deref(supported_ciphers_pointer) + supported_cipher_suites = array_from_pointer( + Security, + 'uint32_t', + supported_cipher_suites_pointer, + supported_ciphers + ) + good_ciphers = [] + for supported_cipher_suite in supported_cipher_suites: + cipher_suite = int_to_bytes(supported_cipher_suite, width=2) + cipher_suite_name = CIPHER_SUITE_MAP.get(cipher_suite, cipher_suite) + good_cipher = _cipher_blacklist_regex.search(cipher_suite_name) is None + if good_cipher: + good_ciphers.append(supported_cipher_suite) + + num_good_ciphers = len(good_ciphers) + good_ciphers_array = new(Security, 'uint32_t[]', num_good_ciphers) + array_set(good_ciphers_array, good_ciphers) + good_ciphers_pointer = cast(Security, 'uint32_t *', good_ciphers_array) + result = Security.SSLSetEnabledCiphers( + session_context, + good_ciphers_pointer, + num_good_ciphers + ) + handle_sec_error(result) + + # Set a peer id from the session to allow for session reuse, the hostname + # is appended to prevent a bug on OS X 10.7 where it tries to reuse a + # connection even if the hostnames are different. + peer_id = self._session._peer_id + self._hostname.encode('utf-8') + result = Security.SSLSetPeerID(session_context, peer_id, len(peer_id)) + handle_sec_error(result) + + handshake_result = Security.SSLHandshake(session_context) + if self._exception is not None: + exception = self._exception + self._exception = None + raise exception + while handshake_result == SecurityConst.errSSLWouldBlock: + handshake_result = Security.SSLHandshake(session_context) + if self._exception is not None: + exception = self._exception + self._exception = None + raise exception + + if osx_version_info < (10, 8) and osx_version_info >= (10, 7): + do_validation = explicit_validation and handshake_result == 0 + else: + do_validation = explicit_validation and handshake_result == SecurityConst.errSSLServerAuthCompleted + + if do_validation: + trust_ref_pointer = new(Security, 'SecTrustRef *') + result = Security.SSLCopyPeerTrust( + session_context, + trust_ref_pointer + ) + handle_sec_error(result) + trust_ref = unwrap(trust_ref_pointer) + + cf_string_hostname = CFHelpers.cf_string_from_unicode(self._hostname) + ssl_policy_ref = Security.SecPolicyCreateSSL(True, cf_string_hostname) + result = CoreFoundation.CFRelease(cf_string_hostname) + handle_cf_error(result) + + # Create a new policy for OCSP checking to disable it + ocsp_oid_pointer = struct(Security, 'CSSM_OID') + ocsp_oid = unwrap(ocsp_oid_pointer) + ocsp_oid.Length = len(SecurityConst.APPLE_TP_REVOCATION_OCSP) + ocsp_oid_buffer = buffer_from_bytes(SecurityConst.APPLE_TP_REVOCATION_OCSP) + ocsp_oid.Data = cast(Security, 'char *', ocsp_oid_buffer) + + ocsp_search_ref_pointer = new(Security, 'SecPolicySearchRef *') + result = Security.SecPolicySearchCreate( + SecurityConst.CSSM_CERT_X_509v3, + ocsp_oid_pointer, + null(), + ocsp_search_ref_pointer + ) + handle_sec_error(result) + ocsp_search_ref = unwrap(ocsp_search_ref_pointer) + + ocsp_policy_ref_pointer = new(Security, 'SecPolicyRef *') + result = Security.SecPolicySearchCopyNext(ocsp_search_ref, ocsp_policy_ref_pointer) + handle_sec_error(result) + ocsp_policy_ref = unwrap(ocsp_policy_ref_pointer) + + ocsp_struct_pointer = struct(Security, 'CSSM_APPLE_TP_OCSP_OPTIONS') + ocsp_struct = unwrap(ocsp_struct_pointer) + ocsp_struct.Version = SecurityConst.CSSM_APPLE_TP_OCSP_OPTS_VERSION + ocsp_struct.Flags = ( + SecurityConst.CSSM_TP_ACTION_OCSP_DISABLE_NET | + SecurityConst.CSSM_TP_ACTION_OCSP_CACHE_READ_DISABLE + ) + ocsp_struct_bytes = struct_bytes(ocsp_struct_pointer) + + cssm_data_pointer = struct(Security, 'CSSM_DATA') + cssm_data = unwrap(cssm_data_pointer) + cssm_data.Length = len(ocsp_struct_bytes) + ocsp_struct_buffer = buffer_from_bytes(ocsp_struct_bytes) + cssm_data.Data = cast(Security, 'char *', ocsp_struct_buffer) + + result = Security.SecPolicySetValue(ocsp_policy_ref, cssm_data_pointer) + handle_sec_error(result) + + # Create a new policy for CRL checking to disable it + crl_oid_pointer = struct(Security, 'CSSM_OID') + crl_oid = unwrap(crl_oid_pointer) + crl_oid.Length = len(SecurityConst.APPLE_TP_REVOCATION_CRL) + crl_oid_buffer = buffer_from_bytes(SecurityConst.APPLE_TP_REVOCATION_CRL) + crl_oid.Data = cast(Security, 'char *', crl_oid_buffer) + + crl_search_ref_pointer = new(Security, 'SecPolicySearchRef *') + result = Security.SecPolicySearchCreate( + SecurityConst.CSSM_CERT_X_509v3, + crl_oid_pointer, + null(), + crl_search_ref_pointer + ) + handle_sec_error(result) + crl_search_ref = unwrap(crl_search_ref_pointer) + + crl_policy_ref_pointer = new(Security, 'SecPolicyRef *') + result = Security.SecPolicySearchCopyNext(crl_search_ref, crl_policy_ref_pointer) + handle_sec_error(result) + crl_policy_ref = unwrap(crl_policy_ref_pointer) + + crl_struct_pointer = struct(Security, 'CSSM_APPLE_TP_CRL_OPTIONS') + crl_struct = unwrap(crl_struct_pointer) + crl_struct.Version = SecurityConst.CSSM_APPLE_TP_CRL_OPTS_VERSION + crl_struct.CrlFlags = 0 + crl_struct_bytes = struct_bytes(crl_struct_pointer) + + cssm_data_pointer = struct(Security, 'CSSM_DATA') + cssm_data = unwrap(cssm_data_pointer) + cssm_data.Length = len(crl_struct_bytes) + crl_struct_buffer = buffer_from_bytes(crl_struct_bytes) + cssm_data.Data = cast(Security, 'char *', crl_struct_buffer) + + result = Security.SecPolicySetValue(crl_policy_ref, cssm_data_pointer) + handle_sec_error(result) + + policy_array_ref = CFHelpers.cf_array_from_list([ + ssl_policy_ref, + crl_policy_ref, + ocsp_policy_ref + ]) + + result = Security.SecTrustSetPolicies(trust_ref, policy_array_ref) + handle_sec_error(result) + + if self._session._extra_trust_roots: + ca_cert_refs = [] + ca_certs = [] + for cert in self._session._extra_trust_roots: + ca_cert = load_certificate(cert) + ca_certs.append(ca_cert) + ca_cert_refs.append(ca_cert.sec_certificate_ref) + + result = Security.SecTrustSetAnchorCertificatesOnly(trust_ref, False) + handle_sec_error(result) + + array_ref = CFHelpers.cf_array_from_list(ca_cert_refs) + result = Security.SecTrustSetAnchorCertificates(trust_ref, array_ref) + handle_sec_error(result) + + result_pointer = new(Security, 'SecTrustResultType *') + result = Security.SecTrustEvaluate(trust_ref, result_pointer) + handle_sec_error(result) + + trust_result_code = deref(result_pointer) + invalid_chain_error_codes = set([ + SecurityConst.kSecTrustResultProceed, + SecurityConst.kSecTrustResultUnspecified + ]) + if trust_result_code not in invalid_chain_error_codes: + handshake_result = SecurityConst.errSSLXCertChainInvalid + else: + handshake_result = Security.SSLHandshake(session_context) + while handshake_result == SecurityConst.errSSLWouldBlock: + handshake_result = Security.SSLHandshake(session_context) + + self._done_handshake = True + + handshake_error_codes = set([ + SecurityConst.errSSLXCertChainInvalid, + SecurityConst.errSSLCertExpired, + SecurityConst.errSSLCertNotYetValid, + SecurityConst.errSSLUnknownRootCert, + SecurityConst.errSSLNoRootCert, + SecurityConst.errSSLHostNameMismatch, + SecurityConst.errSSLInternal, + ]) + + # In testing, only errSSLXCertChainInvalid was ever returned for + # all of these different situations, however we include the others + # for completeness. To get the real reason we have to use the + # certificate from the handshake and use the deprecated function + # SecTrustGetCssmResultCode(). + if handshake_result in handshake_error_codes: + if trust_ref: + CoreFoundation.CFRelease(trust_ref) + trust_ref = None + + trust_ref_pointer = new(Security, 'SecTrustRef *') + result = Security.SSLCopyPeerTrust( + session_context, + trust_ref_pointer + ) + handle_sec_error(result) + trust_ref = unwrap(trust_ref_pointer) + + result_code_pointer = new(Security, 'OSStatus *') + result = Security.SecTrustGetCssmResultCode(trust_ref, result_code_pointer) + result_code = deref(result_code_pointer) + + chain = extract_chain(self._server_hello) + + self_signed = False + revoked = False + expired = False + not_yet_valid = False + no_issuer = False + cert = None + bad_hostname = False + + if chain: + cert = chain[0] + oscrypto_cert = load_certificate(cert) + self_signed = oscrypto_cert.self_signed + revoked = result_code == SecurityConst.CSSMERR_TP_CERT_REVOKED + no_issuer = not self_signed and result_code == SecurityConst.CSSMERR_TP_NOT_TRUSTED + expired = result_code == SecurityConst.CSSMERR_TP_CERT_EXPIRED + not_yet_valid = result_code == SecurityConst.CSSMERR_TP_CERT_NOT_VALID_YET + bad_hostname = result_code == SecurityConst.CSSMERR_APPLETP_HOSTNAME_MISMATCH + validity_too_long = result_code == SecurityConst.CSSMERR_TP_CERT_SUSPENDED + + # On macOS 10.12, some expired certificates return errSSLInternal + if osx_version_info >= (10, 12): + validity = cert['tbs_certificate']['validity'] + not_before = validity['not_before'].chosen.native + not_after = validity['not_after'].chosen.native + utcnow = datetime.datetime.now(timezone.utc) + expired = not_after < utcnow + not_yet_valid = not_before > utcnow + + if chain and chain[0].hash_algo in set(['md5', 'md2']): + raise_weak_signature(chain[0]) + + if revoked: + raise_revoked(cert) + + if bad_hostname: + raise_hostname(cert, self._hostname) + + elif expired or not_yet_valid: + raise_expired_not_yet_valid(cert) + + elif no_issuer: + raise_no_issuer(cert) + + elif self_signed: + raise_self_signed(cert) + + elif validity_too_long: + raise_lifetime_too_long(cert) + + if detect_client_auth_request(self._server_hello): + raise_client_auth() + + raise_verification(cert) + + if handshake_result == SecurityConst.errSSLPeerHandshakeFail: + if detect_client_auth_request(self._server_hello): + raise_client_auth() + raise_handshake() + + if handshake_result == SecurityConst.errSSLWeakPeerEphemeralDHKey: + raise_dh_params() + + if handshake_result == SecurityConst.errSSLPeerProtocolVersion: + raise_protocol_version() + + if handshake_result in set([SecurityConst.errSSLRecordOverflow, SecurityConst.errSSLProtocol]): + self._server_hello += _read_remaining(self._socket) + raise_protocol_error(self._server_hello) + + if handshake_result in set([SecurityConst.errSSLClosedNoNotify, SecurityConst.errSSLClosedAbort]): + if not self._done_handshake: + self._server_hello += _read_remaining(self._socket) + if detect_other_protocol(self._server_hello): + raise_protocol_error(self._server_hello) + raise_disconnection() + + if osx_version_info < (10, 10): + dh_params_length = get_dh_params_length(self._server_hello) + if dh_params_length is not None and dh_params_length < 1024: + raise_dh_params() + + would_block = handshake_result == SecurityConst.errSSLWouldBlock + server_auth_complete = handshake_result == SecurityConst.errSSLServerAuthCompleted + manual_validation = self._session._manual_validation and server_auth_complete + if not would_block and not manual_validation: + handle_sec_error(handshake_result, TLSError) + + self._session_context = session_context + + protocol_const_pointer = new(Security, 'SSLProtocol *') + result = Security.SSLGetNegotiatedProtocolVersion( + session_context, + protocol_const_pointer + ) + handle_sec_error(result) + protocol_const = deref(protocol_const_pointer) + + self._protocol = _PROTOCOL_CONST_STRING_MAP[protocol_const] + + cipher_int_pointer = new(Security, 'SSLCipherSuite *') + result = Security.SSLGetNegotiatedCipher( + session_context, + cipher_int_pointer + ) + handle_sec_error(result) + cipher_int = deref(cipher_int_pointer) + + cipher_bytes = int_to_bytes(cipher_int, width=2) + self._cipher_suite = CIPHER_SUITE_MAP.get(cipher_bytes, cipher_bytes) + + session_info = parse_session_info( + self._server_hello, + self._client_hello + ) + self._compression = session_info['compression'] + self._session_id = session_info['session_id'] + self._session_ticket = session_info['session_ticket'] + + except (OSError, socket_.error): + if session_context: + if osx_version_info < (10, 8): + result = Security.SSLDisposeContext(session_context) + handle_sec_error(result) + else: + result = CoreFoundation.CFRelease(session_context) + handle_cf_error(result) + + self._session_context = None + self.close() + + raise + + finally: + # Trying to release crl_search_ref or ocsp_search_ref results in + # a segmentation fault, so we do not do that + + if ssl_policy_ref: + result = CoreFoundation.CFRelease(ssl_policy_ref) + handle_cf_error(result) + ssl_policy_ref = None + + if crl_policy_ref: + result = CoreFoundation.CFRelease(crl_policy_ref) + handle_cf_error(result) + crl_policy_ref = None + + if ocsp_policy_ref: + result = CoreFoundation.CFRelease(ocsp_policy_ref) + handle_cf_error(result) + ocsp_policy_ref = None + + if policy_array_ref: + result = CoreFoundation.CFRelease(policy_array_ref) + handle_cf_error(result) + policy_array_ref = None + + if trust_ref: + CoreFoundation.CFRelease(trust_ref) + trust_ref = None + + def read(self, max_length): + """ + Reads data from the TLS-wrapped socket + + :param max_length: + The number of bytes to read - output may be less than this + + :raises: + socket.socket - when a non-TLS socket error occurs + oscrypto.errors.TLSError - when a TLS-related error occurs + oscrypto.errors.TLSDisconnectError - when the connection disconnects + oscrypto.errors.TLSGracefulDisconnectError - when the remote end gracefully closed the connection + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the data read + """ + + if not isinstance(max_length, int_types): + raise TypeError(pretty_message( + ''' + max_length must be an integer, not %s + ''', + type_name(max_length) + )) + + if self._session_context is None: + # Even if the session is closed, we can use + # buffered data to respond to read requests + if self._decrypted_bytes != b'': + output = self._decrypted_bytes + self._decrypted_bytes = b'' + return output + + self._raise_closed() + + buffered_length = len(self._decrypted_bytes) + + # If we already have enough buffered data, just use that + if buffered_length >= max_length: + output = self._decrypted_bytes[0:max_length] + self._decrypted_bytes = self._decrypted_bytes[max_length:] + return output + + # Don't block if we have buffered data available, since it is ok to + # return less than the max_length + if buffered_length > 0 and not self.select_read(0): + output = self._decrypted_bytes + self._decrypted_bytes = b'' + return output + + # Only read enough to get the requested amount when + # combined with buffered data + to_read = max_length - len(self._decrypted_bytes) + + read_buffer = buffer_from_bytes(to_read) + processed_pointer = new(Security, 'size_t *') + result = Security.SSLRead( + self._session_context, + read_buffer, + to_read, + processed_pointer + ) + if self._exception is not None: + exception = self._exception + self._exception = None + raise exception + if result and result not in set([SecurityConst.errSSLWouldBlock, SecurityConst.errSSLClosedGraceful]): + handle_sec_error(result, TLSError) + + if result and result == SecurityConst.errSSLClosedGraceful: + self._gracefully_closed = True + self._shutdown(False) + self._raise_closed() + + bytes_read = deref(processed_pointer) + output = self._decrypted_bytes + bytes_from_buffer(read_buffer, bytes_read) + + self._decrypted_bytes = output[max_length:] + return output[0:max_length] + + def select_read(self, timeout=None): + """ + Blocks until the socket is ready to be read from, or the timeout is hit + + :param timeout: + A float - the period of time to wait for data to be read. None for + no time limit. + + :return: + A boolean - if data is ready to be read. Will only be False if + timeout is not None. + """ + + # If we have buffered data, we consider a read possible + if len(self._decrypted_bytes) > 0: + return True + + read_ready, _, _ = select.select([self._socket], [], [], timeout) + return len(read_ready) > 0 + + def read_until(self, marker): + """ + Reads data from the socket until a marker is found. Data read includes + the marker. + + :param marker: + A byte string or regex object from re.compile(). Used to determine + when to stop reading. Regex objects are more inefficient since + they must scan the entire byte string of read data each time data + is read off the socket. + + :return: + A byte string of the data read, including the marker + """ + + if not isinstance(marker, byte_cls) and not isinstance(marker, Pattern): + raise TypeError(pretty_message( + ''' + marker must be a byte string or compiled regex object, not %s + ''', + type_name(marker) + )) + + output = b'' + + is_regex = isinstance(marker, Pattern) + + while True: + if len(self._decrypted_bytes) > 0: + chunk = self._decrypted_bytes + self._decrypted_bytes = b'' + else: + to_read = self._os_buffered_size() or 8192 + chunk = self.read(to_read) + + offset = len(output) + output += chunk + + if is_regex: + match = marker.search(output) + if match is not None: + end = match.end() + break + else: + # If the marker was not found last time, we have to start + # at a position where the marker would have its final char + # in the newly read chunk + start = max(0, offset - len(marker) - 1) + match = output.find(marker, start) + if match != -1: + end = match + len(marker) + break + + self._decrypted_bytes = output[end:] + self._decrypted_bytes + return output[0:end] + + def _os_buffered_size(self): + """ + Returns the number of bytes of decrypted data stored in the Secure + Transport read buffer. This amount of data can be read from SSLRead() + without calling self._socket.recv(). + + :return: + An integer - the number of available bytes + """ + + num_bytes_pointer = new(Security, 'size_t *') + result = Security.SSLGetBufferedReadSize( + self._session_context, + num_bytes_pointer + ) + handle_sec_error(result) + + return deref(num_bytes_pointer) + + def read_line(self): + r""" + Reads a line from the socket, including the line ending of "\r\n", "\r", + or "\n" + + :return: + A byte string of the next line from the socket + """ + + return self.read_until(_line_regex) + + def read_exactly(self, num_bytes): + """ + Reads exactly the specified number of bytes from the socket + + :param num_bytes: + An integer - the exact number of bytes to read + + :return: + A byte string of the data that was read + """ + + output = b'' + remaining = num_bytes + while remaining > 0: + output += self.read(remaining) + remaining = num_bytes - len(output) + + return output + + def write(self, data): + """ + Writes data to the TLS-wrapped socket + + :param data: + A byte string to write to the socket + + :raises: + socket.socket - when a non-TLS socket error occurs + oscrypto.errors.TLSError - when a TLS-related error occurs + oscrypto.errors.TLSDisconnectError - when the connection disconnects + oscrypto.errors.TLSGracefulDisconnectError - when the remote end gracefully closed the connection + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + if self._session_context is None: + self._raise_closed() + + processed_pointer = new(Security, 'size_t *') + + data_len = len(data) + while data_len: + write_buffer = buffer_from_bytes(data) + result = Security.SSLWrite( + self._session_context, + write_buffer, + data_len, + processed_pointer + ) + if self._exception is not None: + exception = self._exception + self._exception = None + raise exception + handle_sec_error(result, TLSError) + + bytes_written = deref(processed_pointer) + data = data[bytes_written:] + data_len = len(data) + if data_len > 0: + self.select_write() + + def select_write(self, timeout=None): + """ + Blocks until the socket is ready to be written to, or the timeout is hit + + :param timeout: + A float - the period of time to wait for the socket to be ready to + written to. None for no time limit. + + :return: + A boolean - if the socket is ready for writing. Will only be False + if timeout is not None. + """ + + _, write_ready, _ = select.select([], [self._socket], [], timeout) + return len(write_ready) > 0 + + def _shutdown(self, manual): + """ + Shuts down the TLS session and then shuts down the underlying socket + + :param manual: + A boolean if the connection was manually shutdown + """ + + if self._session_context is None: + return + + # Ignore error during close in case other end closed already + result = Security.SSLClose(self._session_context) + + if osx_version_info < (10, 8): + result = Security.SSLDisposeContext(self._session_context) + handle_sec_error(result) + else: + result = CoreFoundation.CFRelease(self._session_context) + handle_cf_error(result) + + self._session_context = None + + if manual: + self._local_closed = True + + try: + self._socket.shutdown(socket_.SHUT_RDWR) + except (socket_.error): + pass + + def shutdown(self): + """ + Shuts down the TLS session and then shuts down the underlying socket + """ + + self._shutdown(True) + + def close(self): + """ + Shuts down the TLS session and socket and forcibly closes it + """ + + try: + self.shutdown() + + finally: + if self._socket: + try: + self._socket.close() + except (socket_.error): + pass + self._socket = None + + if self._connection_id in _socket_refs: + del _socket_refs[self._connection_id] + + def _read_certificates(self): + """ + Reads end-entity and intermediate certificate information from the + TLS session + """ + + trust_ref = None + cf_data_ref = None + result = None + + try: + trust_ref_pointer = new(Security, 'SecTrustRef *') + result = Security.SSLCopyPeerTrust( + self._session_context, + trust_ref_pointer + ) + handle_sec_error(result) + + trust_ref = unwrap(trust_ref_pointer) + + number_certs = Security.SecTrustGetCertificateCount(trust_ref) + + self._intermediates = [] + + for index in range(0, number_certs): + sec_certificate_ref = Security.SecTrustGetCertificateAtIndex( + trust_ref, + index + ) + cf_data_ref = Security.SecCertificateCopyData(sec_certificate_ref) + + cert_data = CFHelpers.cf_data_to_bytes(cf_data_ref) + + result = CoreFoundation.CFRelease(cf_data_ref) + handle_cf_error(result) + cf_data_ref = None + + cert = Asn1Certificate.load(cert_data) + + if index == 0: + self._certificate = cert + else: + self._intermediates.append(cert) + + finally: + if trust_ref: + result = CoreFoundation.CFRelease(trust_ref) + handle_cf_error(result) + if cf_data_ref: + result = CoreFoundation.CFRelease(cf_data_ref) + handle_cf_error(result) + + def _raise_closed(self): + """ + Raises an exception describing if the local or remote end closed the + connection + """ + + if self._local_closed: + raise TLSDisconnectError('The connection was already closed') + elif self._gracefully_closed: + raise TLSGracefulDisconnectError('The remote end closed the connection') + else: + raise TLSDisconnectError('The connection was closed') + + @property + def certificate(self): + """ + An asn1crypto.x509.Certificate object of the end-entity certificate + presented by the server + """ + + if self._session_context is None: + self._raise_closed() + + if self._certificate is None: + self._read_certificates() + + return self._certificate + + @property + def intermediates(self): + """ + A list of asn1crypto.x509.Certificate objects that were presented as + intermediates by the server + """ + + if self._session_context is None: + self._raise_closed() + + if self._certificate is None: + self._read_certificates() + + return self._intermediates + + @property + def cipher_suite(self): + """ + A unicode string of the IANA cipher suite name of the negotiated + cipher suite + """ + + return self._cipher_suite + + @property + def protocol(self): + """ + A unicode string of: "TLSv1.2", "TLSv1.1", "TLSv1", "SSLv3" + """ + + return self._protocol + + @property + def compression(self): + """ + A boolean if compression is enabled + """ + + return self._compression + + @property + def session_id(self): + """ + A unicode string of "new" or "reused" or None for no ticket + """ + + return self._session_id + + @property + def session_ticket(self): + """ + A unicode string of "new" or "reused" or None for no ticket + """ + + return self._session_ticket + + @property + def session(self): + """ + The oscrypto.tls.TLSSession object used for this connection + """ + + return self._session + + @property + def hostname(self): + """ + A unicode string of the TLS server domain name or IP address + """ + + return self._hostname + + @property + def port(self): + """ + An integer of the port number the socket is connected to + """ + + return self.socket.getpeername()[1] + + @property + def socket(self): + """ + The underlying socket.socket connection + """ + + if self._session_context is None: + self._raise_closed() + + return self._socket + + def __del__(self): + self.close() diff --git a/tasks/lib/package_control/deps/oscrypto/_mac/trust_list.py b/tasks/lib/package_control/deps/oscrypto/_mac/trust_list.py new file mode 100644 index 0000000..93943f0 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_mac/trust_list.py @@ -0,0 +1,208 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import hashlib +import sys + +from .._asn1 import Certificate +from .._ffi import new, unwrap +from ._core_foundation import CoreFoundation, CFHelpers +from ._security import Security, SecurityConst, handle_sec_error + +if sys.version_info < (3,): + range = xrange # noqa + + +__all__ = [ + 'extract_from_system', + 'system_path', +] + + +def system_path(): + return None + + +def extract_from_system(cert_callback=None, callback_only_on_failure=False): + """ + Extracts trusted CA certificates from the OS X trusted root keychain. + + :param cert_callback: + A callback that is called once for each certificate in the trust store. + It should accept two parameters: an asn1crypto.x509.Certificate object, + and a reason. The reason will be None if the certificate is being + exported, otherwise it will be a unicode string of the reason it won't. + + :param callback_only_on_failure: + A boolean - if the callback should only be called when a certificate is + not exported. + + :raises: + OSError - when an error is returned by the OS crypto library + + :return: + A list of 3-element tuples: + - 0: a byte string of a DER-encoded certificate + - 1: a set of unicode strings that are OIDs of purposes to trust the + certificate for + - 2: a set of unicode strings that are OIDs of purposes to reject the + certificate for + """ + + certs_pointer_pointer = new(CoreFoundation, 'CFArrayRef *') + res = Security.SecTrustCopyAnchorCertificates(certs_pointer_pointer) + handle_sec_error(res) + + certs_pointer = unwrap(certs_pointer_pointer) + + certificates = {} + trust_info = {} + + all_purposes = '2.5.29.37.0' + default_trust = (set(), set()) + + length = CoreFoundation.CFArrayGetCount(certs_pointer) + for index in range(0, length): + cert_pointer = CoreFoundation.CFArrayGetValueAtIndex(certs_pointer, index) + der_cert, cert_hash = _cert_details(cert_pointer) + certificates[cert_hash] = der_cert + + CoreFoundation.CFRelease(certs_pointer) + + for domain in [SecurityConst.kSecTrustSettingsDomainUser, SecurityConst.kSecTrustSettingsDomainAdmin]: + cert_trust_settings_pointer_pointer = new(CoreFoundation, 'CFArrayRef *') + res = Security.SecTrustSettingsCopyCertificates(domain, cert_trust_settings_pointer_pointer) + if res == SecurityConst.errSecNoTrustSettings: + continue + handle_sec_error(res) + + cert_trust_settings_pointer = unwrap(cert_trust_settings_pointer_pointer) + + length = CoreFoundation.CFArrayGetCount(cert_trust_settings_pointer) + for index in range(0, length): + cert_pointer = CoreFoundation.CFArrayGetValueAtIndex(cert_trust_settings_pointer, index) + + trust_settings_pointer_pointer = new(CoreFoundation, 'CFArrayRef *') + res = Security.SecTrustSettingsCopyTrustSettings(cert_pointer, domain, trust_settings_pointer_pointer) + + # In OS X 10.11, this value started being seen. From the comments in + # the Security Framework Reference, the lack of any settings should + # indicate "always trust this certificate" + if res == SecurityConst.errSecItemNotFound: + continue + + # If the trust settings for a certificate are invalid, we need to + # assume the certificate should not be trusted + if res == SecurityConst.errSecInvalidTrustSettings: + der_cert, cert_hash = _cert_details(cert_pointer) + if cert_hash in certificates: + _cert_callback( + cert_callback, + certificates[cert_hash], + 'invalid trust settings' + ) + del certificates[cert_hash] + continue + + handle_sec_error(res) + + trust_settings_pointer = unwrap(trust_settings_pointer_pointer) + + trust_oids = set() + reject_oids = set() + settings_length = CoreFoundation.CFArrayGetCount(trust_settings_pointer) + for settings_index in range(0, settings_length): + settings_dict_entry = CoreFoundation.CFArrayGetValueAtIndex(trust_settings_pointer, settings_index) + settings_dict = CFHelpers.cf_dictionary_to_dict(settings_dict_entry) + + # No policy OID means the trust result is for all purposes + policy_oid = settings_dict.get('kSecTrustSettingsPolicy', {}).get('SecPolicyOid', all_purposes) + + # 0 = kSecTrustSettingsResultInvalid + # 1 = kSecTrustSettingsResultTrustRoot + # 2 = kSecTrustSettingsResultTrustAsRoot + # 3 = kSecTrustSettingsResultDeny + # 4 = kSecTrustSettingsResultUnspecified + trust_result = settings_dict.get('kSecTrustSettingsResult', 1) + should_trust = trust_result != 0 and trust_result != 3 + + if should_trust: + trust_oids.add(policy_oid) + else: + reject_oids.add(policy_oid) + + der_cert, cert_hash = _cert_details(cert_pointer) + + # If rejected for all purposes, we don't export the certificate + if all_purposes in reject_oids: + if cert_hash in certificates: + _cert_callback( + cert_callback, + certificates[cert_hash], + 'explicitly distrusted' + ) + del certificates[cert_hash] + else: + if all_purposes in trust_oids: + trust_oids = set([all_purposes]) + trust_info[cert_hash] = (trust_oids, reject_oids) + + CoreFoundation.CFRelease(trust_settings_pointer) + + CoreFoundation.CFRelease(cert_trust_settings_pointer) + + output = [] + for cert_hash in certificates: + if not callback_only_on_failure: + _cert_callback(cert_callback, certificates[cert_hash], None) + cert_trust_info = trust_info.get(cert_hash, default_trust) + output.append((certificates[cert_hash], cert_trust_info[0], cert_trust_info[1])) + return output + + +def _cert_callback(callback, der_cert, reason): + """ + Constructs an asn1crypto.x509.Certificate object and calls the export + callback + + :param callback: + The callback to call + + :param der_cert: + A byte string of the DER-encoded certificate + + :param reason: + None if cert is being exported, or a unicode string of the reason it + is not being exported + """ + + if not callback: + return + callback(Certificate.load(der_cert), reason) + + +def _cert_details(cert_pointer): + """ + Return the certificate and a hash of it + + :param cert_pointer: + A SecCertificateRef + + :return: + A 2-element tuple: + - [0]: A byte string of the SHA1 hash of the cert + - [1]: A byte string of the DER-encoded contents of the cert + """ + + data_pointer = None + + try: + data_pointer = Security.SecCertificateCopyData(cert_pointer) + der_cert = CFHelpers.cf_data_to_bytes(data_pointer) + cert_hash = hashlib.sha1(der_cert).digest() + + return (der_cert, cert_hash) + + finally: + if data_pointer is not None: + CoreFoundation.CFRelease(data_pointer) diff --git a/tasks/lib/package_control/deps/oscrypto/_mac/util.py b/tasks/lib/package_control/deps/oscrypto/_mac/util.py new file mode 100644 index 0000000..34cc8c0 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_mac/util.py @@ -0,0 +1,353 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import os + +from .._errors import pretty_message +from .._ffi import buffer_from_bytes, bytes_from_buffer, errno, byte_string_from_buffer +from .._types import type_name, str_cls, byte_cls, int_types +from ..errors import LibraryNotFoundError +from ._common_crypto import CommonCrypto, CommonCryptoConst +from ._security import Security + + +__all__ = [ + 'pbkdf2', + 'pkcs12_kdf', + 'rand_bytes', +] + + +_encoding = 'utf-8' +_fallback_encodings = ['utf-8', 'cp1252'] + + +def _try_decode(value): + + try: + return str_cls(value, _encoding) + + # If the "correct" encoding did not work, try some defaults, and then just + # obliterate characters that we can't seen to decode properly + except (UnicodeDecodeError): + for encoding in _fallback_encodings: + try: + return str_cls(value, encoding, errors='strict') + except (UnicodeDecodeError): + pass + + return str_cls(value, errors='replace') + + +def _extract_error(): + """ + Extracts the last OS error message into a python unicode string + + :return: + A unicode string error message + """ + + error_num = errno() + + try: + error_string = os.strerror(error_num) + except (ValueError): + return str_cls(error_num) + + if isinstance(error_string, str_cls): + return error_string + + return _try_decode(error_string) + + +def pbkdf2(hash_algorithm, password, salt, iterations, key_length): + """ + PBKDF2 from PKCS#5 + + :param hash_algorithm: + The string name of the hash algorithm to use: "sha1", "sha224", "sha256", "sha384", "sha512" + + :param password: + A byte string of the password to use an input to the KDF + + :param salt: + A cryptographic random byte string + + :param iterations: + The numbers of iterations to use when deriving the key + + :param key_length: + The length of the desired key in bytes + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + The derived key as a byte string + """ + + if not isinstance(password, byte_cls): + raise TypeError(pretty_message( + ''' + password must be a byte string, not %s + ''', + type_name(password) + )) + + if not isinstance(salt, byte_cls): + raise TypeError(pretty_message( + ''' + salt must be a byte string, not %s + ''', + type_name(salt) + )) + + if not isinstance(iterations, int_types): + raise TypeError(pretty_message( + ''' + iterations must be an integer, not %s + ''', + type_name(iterations) + )) + + if iterations < 1: + raise ValueError('iterations must be greater than 0') + + if not isinstance(key_length, int_types): + raise TypeError(pretty_message( + ''' + key_length must be an integer, not %s + ''', + type_name(key_length) + )) + + if key_length < 1: + raise ValueError('key_length must be greater than 0') + + if hash_algorithm not in set(['sha1', 'sha224', 'sha256', 'sha384', 'sha512']): + raise ValueError(pretty_message( + ''' + hash_algorithm must be one of "sha1", "sha224", "sha256", "sha384", + "sha512", not %s + ''', + repr(hash_algorithm) + )) + + algo = { + 'sha1': CommonCryptoConst.kCCPRFHmacAlgSHA1, + 'sha224': CommonCryptoConst.kCCPRFHmacAlgSHA224, + 'sha256': CommonCryptoConst.kCCPRFHmacAlgSHA256, + 'sha384': CommonCryptoConst.kCCPRFHmacAlgSHA384, + 'sha512': CommonCryptoConst.kCCPRFHmacAlgSHA512 + }[hash_algorithm] + + output_buffer = buffer_from_bytes(key_length) + result = CommonCrypto.CCKeyDerivationPBKDF( + CommonCryptoConst.kCCPBKDF2, + password, + len(password), + salt, + len(salt), + algo, + iterations, + output_buffer, + key_length + ) + if result != 0: + raise OSError(_extract_error()) + + return bytes_from_buffer(output_buffer) + + +pbkdf2.pure_python = False + + +def rand_bytes(length): + """ + Returns a number of random bytes suitable for cryptographic purposes + + :param length: + The desired number of bytes + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string + """ + + if not isinstance(length, int_types): + raise TypeError(pretty_message( + ''' + length must be an integer, not %s + ''', + type_name(length) + )) + + if length < 1: + raise ValueError('length must be greater than 0') + + if length > 1024: + raise ValueError('length must not be greater than 1024') + + buffer = buffer_from_bytes(length) + result = Security.SecRandomCopyBytes(Security.kSecRandomDefault, length, buffer) + if result != 0: + raise OSError(_extract_error()) + + return bytes_from_buffer(buffer) + + +# If in a future version of OS X they remove OpenSSL, this try/except block +# will fall back to the pure Python implementation, which is just slower +try: + from .._openssl._libcrypto import libcrypto + + def _extract_openssl_error(): + """ + Extracts the last OpenSSL error message into a python unicode string + + :return: + A unicode string error message + """ + + error_num = libcrypto.ERR_get_error() + buffer = buffer_from_bytes(120) + libcrypto.ERR_error_string(error_num, buffer) + + # Since we are dealing with a string, it is NULL terminated + error_string = byte_string_from_buffer(buffer) + + return _try_decode(error_string) + + def pkcs12_kdf(hash_algorithm, password, salt, iterations, key_length, id_): + """ + KDF from RFC7292 appendix B.2 - https://tools.ietf.org/html/rfc7292#page-19 + + :param hash_algorithm: + The string name of the hash algorithm to use: "md5", "sha1", "sha224", "sha256", "sha384", "sha512" + + :param password: + A byte string of the password to use an input to the KDF + + :param salt: + A cryptographic random byte string + + :param iterations: + The numbers of iterations to use when deriving the key + + :param key_length: + The length of the desired key in bytes + + :param id_: + The ID of the usage - 1 for key, 2 for iv, 3 for mac + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + The derived key as a byte string + """ + + if not isinstance(password, byte_cls): + raise TypeError(pretty_message( + ''' + password must be a byte string, not %s + ''', + type_name(password) + )) + + if not isinstance(salt, byte_cls): + raise TypeError(pretty_message( + ''' + salt must be a byte string, not %s + ''', + type_name(salt) + )) + + if not isinstance(iterations, int_types): + raise TypeError(pretty_message( + ''' + iterations must be an integer, not %s + ''', + type_name(iterations) + )) + + if iterations < 1: + raise ValueError(pretty_message( + ''' + iterations must be greater than 0 - is %s + ''', + repr(iterations) + )) + + if not isinstance(key_length, int_types): + raise TypeError(pretty_message( + ''' + key_length must be an integer, not %s + ''', + type_name(key_length) + )) + + if key_length < 1: + raise ValueError(pretty_message( + ''' + key_length must be greater than 0 - is %s + ''', + repr(key_length) + )) + + if hash_algorithm not in set(['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']): + raise ValueError(pretty_message( + ''' + hash_algorithm must be one of "md5", "sha1", "sha224", "sha256", + "sha384", "sha512", not %s + ''', + repr(hash_algorithm) + )) + + if id_ not in set([1, 2, 3]): + raise ValueError(pretty_message( + ''' + id_ must be one of 1, 2, 3, not %s + ''', + repr(id_) + )) + + utf16_password = password.decode('utf-8').encode('utf-16be') + b'\x00\x00' + + digest_type = { + 'md5': libcrypto.EVP_md5, + 'sha1': libcrypto.EVP_sha1, + 'sha224': libcrypto.EVP_sha224, + 'sha256': libcrypto.EVP_sha256, + 'sha384': libcrypto.EVP_sha384, + 'sha512': libcrypto.EVP_sha512, + }[hash_algorithm]() + + output_buffer = buffer_from_bytes(key_length) + result = libcrypto.PKCS12_key_gen_uni( + utf16_password, + len(utf16_password), + salt, + len(salt), + id_, + iterations, + key_length, + output_buffer, + digest_type + ) + if result != 1: + raise OSError(_extract_openssl_error()) + + return bytes_from_buffer(output_buffer) + +except (LibraryNotFoundError): + + from .._pkcs12 import pkcs12_kdf diff --git a/tasks/lib/package_control/deps/oscrypto/_openssl/__init__.py b/tasks/lib/package_control/deps/oscrypto/_openssl/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tasks/lib/package_control/deps/oscrypto/_openssl/_libcrypto.py b/tasks/lib/package_control/deps/oscrypto/_openssl/_libcrypto.py new file mode 100644 index 0000000..1c52488 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_openssl/_libcrypto.py @@ -0,0 +1,149 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from .. import ffi +from .._ffi import buffer_from_bytes, byte_string_from_buffer, null +from .._types import str_cls + +if ffi() == 'cffi': + from ._libcrypto_cffi import ( + libcrypto, + version as libcrypto_version, + version_info as libcrypto_version_info + ) +else: + from ._libcrypto_ctypes import ( + libcrypto, + version as libcrypto_version, + version_info as libcrypto_version_info + ) + + +__all__ = [ + 'handle_openssl_error', + 'libcrypto', + 'libcrypto_legacy_support', + 'libcrypto_version', + 'libcrypto_version_info', + 'LibcryptoConst', + 'peek_openssl_error', +] + + +_encoding = 'utf-8' +_fallback_encodings = ['utf-8', 'cp1252'] + + +if libcrypto_version_info < (1, 1): + libcrypto.ERR_load_crypto_strings() +libcrypto.OPENSSL_config(null()) + + +# This enables legacy algorithms in OpenSSL 3.0, such as RC2, etc +# which are used by various tests and some old protocols and things +# like PKCS12 +libcrypto_legacy_support = True +if libcrypto_version_info >= (3, ): + if libcrypto.OSSL_PROVIDER_available(null(), "legacy".encode("ascii")): + libcrypto.OSSL_PROVIDER_load(null(), "legacy".encode("ascii")) + else: + libcrypto_legacy_support = False + + +def _try_decode(value): + + try: + return str_cls(value, _encoding) + + # If the "correct" encoding did not work, try some defaults, and then just + # obliterate characters that we can't seen to decode properly + except (UnicodeDecodeError): + for encoding in _fallback_encodings: + try: + return str_cls(value, encoding, errors='strict') + except (UnicodeDecodeError): + pass + + return str_cls(value, errors='replace') + + +def handle_openssl_error(result, exception_class=None): + """ + Checks if an error occurred, and if so throws an OSError containing the + last OpenSSL error message + + :param result: + An integer result code - 1 or greater indicates success + + :param exception_class: + The exception class to use for the exception if an error occurred + + :raises: + OSError - when an OpenSSL error occurs + """ + + if result > 0: + return + + if exception_class is None: + exception_class = OSError + + error_num = libcrypto.ERR_get_error() + buffer = buffer_from_bytes(120) + libcrypto.ERR_error_string(error_num, buffer) + + # Since we are dealing with a string, it is NULL terminated + error_string = byte_string_from_buffer(buffer) + + raise exception_class(_try_decode(error_string)) + + +def peek_openssl_error(): + """ + Peeks into the error stack and pulls out the lib, func and reason + + :return: + A three-element tuple of integers (lib, func, reason) + """ + + error = libcrypto.ERR_peek_error() + if libcrypto_version_info < (3, 0): + lib = int((error >> 24) & 0xff) + func = int((error >> 12) & 0xfff) + reason = int(error & 0xfff) + else: + lib = int((error >> 23) & 0xff) + # OpenSSL 3.0 removed ERR_GET_FUNC() + func = 0 + reason = int(error & 0x7fffff) + + return (lib, func, reason) + + +class LibcryptoConst(): + EVP_CTRL_SET_RC2_KEY_BITS = 3 + + SSLEAY_VERSION = 0 + + RSA_PKCS1_PADDING = 1 + RSA_NO_PADDING = 3 + RSA_PKCS1_OAEP_PADDING = 4 + + # OpenSSL 0.9.x + EVP_MD_CTX_FLAG_PSS_MDLEN = -1 + + # OpenSSL 1.x.x + EVP_PKEY_CTRL_RSA_PADDING = 0x1001 + RSA_PKCS1_PSS_PADDING = 6 + EVP_PKEY_CTRL_RSA_PSS_SALTLEN = 0x1002 + EVP_PKEY_RSA = 6 + EVP_PKEY_OP_SIGN = 1 << 3 + EVP_PKEY_OP_VERIFY = 1 << 4 + + NID_X9_62_prime256v1 = 415 + NID_secp384r1 = 715 + NID_secp521r1 = 716 + + OPENSSL_EC_NAMED_CURVE = 1 + + DH_GENERATOR_2 = 2 diff --git a/tasks/lib/package_control/deps/oscrypto/_openssl/_libcrypto_cffi.py b/tasks/lib/package_control/deps/oscrypto/_openssl/_libcrypto_cffi.py new file mode 100644 index 0000000..8aed03e --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_openssl/_libcrypto_cffi.py @@ -0,0 +1,278 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import re + +from .. import _backend_config +from .._errors import pretty_message +from .._ffi import get_library, register_ffi +from ..errors import LibraryNotFoundError + +from cffi import FFI + + +__all__ = [ + 'is_libressl', + 'libcrypto', + 'libressl_version', + 'libressl_version_info', + 'version', + 'version_info', +] + +libcrypto_path = _backend_config().get('libcrypto_path') +if libcrypto_path is None: + libcrypto_path = get_library('crypto', 'libcrypto.dylib', '42') +if not libcrypto_path: + raise LibraryNotFoundError('The library libcrypto could not be found') + +try: + vffi = FFI() + vffi.cdef("const char *SSLeay_version(int type);") + version_string = vffi.string(vffi.dlopen(libcrypto_path).SSLeay_version(0)).decode('utf-8') +except (AttributeError): + vffi = FFI() + vffi.cdef("const char *OpenSSL_version(int type);") + version_string = vffi.string(vffi.dlopen(libcrypto_path).OpenSSL_version(0)).decode('utf-8') + +is_libressl = 'LibreSSL' in version_string + +version_match = re.search('\\b(\\d\\.\\d\\.\\d[a-z]*)\\b', version_string) +if not version_match: + version_match = re.search('(?<=LibreSSL )(\\d\\.\\d(\\.\\d)?)\\b', version_string) +if not version_match: + raise LibraryNotFoundError('Error detecting the version of libcrypto') +version = version_match.group(1) +version_parts = re.sub('(\\d)([a-z]+)', '\\1.\\2', version).split('.') +version_info = tuple(int(part) if part.isdigit() else part for part in version_parts) + +# LibreSSL is compatible with libcrypto from OpenSSL 1.0.1 +libressl_version = '' +libressl_version_info = tuple() +if is_libressl: + libressl_version = version + libressl_version_info = version_info + version = '1.0.1' + version_info = (1, 0, 1) + +ffi = FFI() + +libcrypto = ffi.dlopen(libcrypto_path) +register_ffi(libcrypto, ffi) + +if version_info < (0, 9, 8): + raise LibraryNotFoundError(pretty_message( + ''' + OpenSSL versions older than 0.9.8 are not supported - found version %s + ''', + version + )) + +if version_info < (1, 1): + ffi.cdef(""" + void ERR_load_crypto_strings(void); + void ERR_free_strings(void); + """) + + +if version_info >= (3, ): + ffi.cdef(""" + typedef ... OSSL_LIB_CTX; + typedef ... OSSL_PROVIDER; + + int OSSL_PROVIDER_available(OSSL_LIB_CTX *libctx, const char *name); + OSSL_PROVIDER *OSSL_PROVIDER_load(OSSL_LIB_CTX *libctx, const char *name); + """) + +# The typedef uintptr_t lines here allow us to check for a NULL pointer, +# without having to redefine the structs in our code. This is kind of a hack, +# but it should cause problems since we treat these as opaque. +ffi.cdef(""" + typedef ... EVP_MD; + typedef uintptr_t EVP_CIPHER_CTX; + typedef ... EVP_CIPHER; + typedef ... ENGINE; + typedef uintptr_t EVP_PKEY; + typedef uintptr_t X509; + typedef uintptr_t DH; + typedef uintptr_t RSA; + typedef uintptr_t DSA; + typedef uintptr_t EC_KEY; + typedef ... EVP_MD_CTX; + typedef ... EVP_PKEY_CTX; + typedef ... BN_GENCB; + typedef ... BIGNUM; + + unsigned long ERR_get_error(void); + char *ERR_error_string(unsigned long e, char *buf); + unsigned long ERR_peek_error(void); + + void OPENSSL_config(const char *config_name); + + EVP_CIPHER_CTX *EVP_CIPHER_CTX_new(void); + void EVP_CIPHER_CTX_free(EVP_CIPHER_CTX *ctx); + + int EVP_CIPHER_CTX_set_key_length(EVP_CIPHER_CTX *x, int keylen); + int EVP_CIPHER_CTX_set_padding(EVP_CIPHER_CTX *x, int padding); + int EVP_CIPHER_CTX_ctrl(EVP_CIPHER_CTX *ctx, int type, int arg, void *ptr); + + const EVP_CIPHER *EVP_aes_128_cbc(void); + const EVP_CIPHER *EVP_aes_192_cbc(void); + const EVP_CIPHER *EVP_aes_256_cbc(void); + const EVP_CIPHER *EVP_des_cbc(void); + const EVP_CIPHER *EVP_des_ede_cbc(void); + const EVP_CIPHER *EVP_des_ede3_cbc(void); + const EVP_CIPHER *EVP_rc4(void); + const EVP_CIPHER *EVP_rc2_cbc(void); + + int EVP_EncryptInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *cipher, + ENGINE *impl, const char *key, + const char *iv); + int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, char *out, int *outl, + const char *in, int inl); + int EVP_EncryptFinal_ex(EVP_CIPHER_CTX *ctx, char *out, int *outl); + + int EVP_DecryptInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *cipher, + ENGINE *impl, const char *key, + const char *iv); + int EVP_DecryptUpdate(EVP_CIPHER_CTX *ctx, char *out, int *outl, + const char *in, int inl); + int EVP_DecryptFinal_ex(EVP_CIPHER_CTX *ctx, char *out, int *outl); + + EVP_PKEY *d2i_AutoPrivateKey(EVP_PKEY **a, const char **pp, + long length); + EVP_PKEY *d2i_PUBKEY(EVP_PKEY **a, const char **pp, long length); + int i2d_PUBKEY(EVP_PKEY *a, char **pp); + void EVP_PKEY_free(EVP_PKEY *key); + + X509 *d2i_X509(X509 **px, const char **in, int len); + int i2d_X509(X509 *x, char **out); + EVP_PKEY *X509_get_pubkey(X509 *x); + void X509_free(X509 *a); + + RSA *EVP_PKEY_get1_RSA(EVP_PKEY *pkey); + void RSA_free(RSA *r); + + int RSA_public_encrypt(int flen, const char *from, + char *to, RSA *rsa, int padding); + int RSA_private_encrypt(int flen, const char *from, + char *to, RSA *rsa, int padding); + int RSA_public_decrypt(int flen, const char *from, + char *to, RSA *rsa, int padding); + int RSA_private_decrypt(int flen, const char *from, + char *to, RSA *rsa, int padding); + + int EVP_DigestUpdate(EVP_MD_CTX *ctx, const void *d, unsigned int cnt); + + const EVP_MD *EVP_md5(void); + const EVP_MD *EVP_sha1(void); + const EVP_MD *EVP_sha224(void); + const EVP_MD *EVP_sha256(void); + const EVP_MD *EVP_sha384(void); + const EVP_MD *EVP_sha512(void); + + int PKCS12_key_gen_uni(char *pass, int passlen, char *salt, + int saltlen, int id, int iter, int n, + char *out, const EVP_MD *md_type); + + void BN_free(BIGNUM *a); + int BN_dec2bn(BIGNUM **a, const char *str); + + DH *DH_new(void); + int DH_generate_parameters_ex(DH *dh, int prime_len, int generator, BN_GENCB *cb); + int i2d_DHparams(const DH *a, char **pp); + void DH_free(DH *dh); + + RSA *RSA_new(void); + int RSA_generate_key_ex(RSA *rsa, int bits, BIGNUM *e, BN_GENCB *cb); + int i2d_RSAPublicKey(RSA *a, char **pp); + int i2d_RSAPrivateKey(RSA *a, char **pp); + + DSA *DSA_new(void); + int DSA_generate_parameters_ex(DSA *dsa, int bits, + const char *seed, int seed_len, int *counter_ret, + unsigned long *h_ret, BN_GENCB *cb); + int DSA_generate_key(DSA *a); + int i2d_DSA_PUBKEY(const DSA *a, char **pp); + int i2d_DSAPrivateKey(const DSA *a, char **pp); + void DSA_free(DSA *dsa); + + EC_KEY *EC_KEY_new_by_curve_name(int nid); + int EC_KEY_generate_key(EC_KEY *key); + void EC_KEY_set_asn1_flag(EC_KEY *, int); + int i2d_ECPrivateKey(EC_KEY *key, char **out); + int i2o_ECPublicKey(EC_KEY *key, char **out); + void EC_KEY_free(EC_KEY *key); +""") + +if version_info < (3, ): + ffi.cdef(""" + int EVP_PKEY_size(EVP_PKEY *pkey); + """) +else: + ffi.cdef(""" + int EVP_PKEY_get_size(EVP_PKEY *pkey); + """) + +if version_info < (1, 1): + ffi.cdef(""" + EVP_MD_CTX *EVP_MD_CTX_create(void); + void EVP_MD_CTX_destroy(EVP_MD_CTX *ctx); + """) +else: + ffi.cdef(""" + EVP_MD_CTX *EVP_MD_CTX_new(void); + void EVP_MD_CTX_free(EVP_MD_CTX *ctx); + """) + +if version_info < (1,): + ffi.cdef(""" + typedef ... *DSA_SIG; + typedef ... *ECDSA_SIG; + + DSA_SIG *DSA_do_sign(const char *dgst, int dlen, DSA *dsa); + ECDSA_SIG *ECDSA_do_sign(const char *dgst, int dgst_len, EC_KEY *eckey); + + DSA_SIG *d2i_DSA_SIG(DSA_SIG **v, const char **pp, long length); + ECDSA_SIG *d2i_ECDSA_SIG(ECDSA_SIG **v, const char **pp, long len); + + int i2d_DSA_SIG(const DSA_SIG *a, char **pp); + int i2d_ECDSA_SIG(const ECDSA_SIG *a, char **pp); + + int DSA_do_verify(const char *dgst, int dgst_len, DSA_SIG *sig, DSA *dsa); + int ECDSA_do_verify(const char *dgst, int dgst_len, const ECDSA_SIG *sig, EC_KEY *eckey); + + void DSA_SIG_free(DSA_SIG *a); + void ECDSA_SIG_free(ECDSA_SIG *a); + + DSA *EVP_PKEY_get1_DSA(EVP_PKEY *pkey); + EC_KEY *EVP_PKEY_get1_EC_KEY(EVP_PKEY *pkey); + + int RSA_verify_PKCS1_PSS(RSA *rsa, const char *mHash, + const EVP_MD *Hash, const char *EM, + int sLen); + int RSA_padding_add_PKCS1_PSS(RSA *rsa, char *EM, + const char *mHash, const EVP_MD *Hash, + int sLen); + + int EVP_DigestInit_ex(EVP_MD_CTX *ctx, const EVP_MD *type, ENGINE *impl); + int EVP_SignFinal(EVP_MD_CTX *ctx, char *sig, unsigned int *s, EVP_PKEY *pkey); + int EVP_VerifyFinal(EVP_MD_CTX *ctx, char *sigbuf, unsigned int siglen, EVP_PKEY *pkey); + + void EVP_MD_CTX_set_flags(EVP_MD_CTX *ctx, int flags); + """) +else: + ffi.cdef(""" + int PKCS5_PBKDF2_HMAC(const char *pass, int passlen, + const char *salt, int saltlen, int iter, + const EVP_MD *digest, + int keylen, char *out); + + int EVP_DigestSignInit(EVP_MD_CTX *ctx, EVP_PKEY_CTX **pctx, const EVP_MD *type, ENGINE *e, EVP_PKEY *pkey); + int EVP_DigestSignFinal(EVP_MD_CTX *ctx, char *sig, size_t *siglen); + + int EVP_DigestVerifyInit(EVP_MD_CTX *ctx, EVP_PKEY_CTX **pctx, const EVP_MD *type, ENGINE *e, EVP_PKEY *pkey); + int EVP_DigestVerifyFinal(EVP_MD_CTX *ctx, const char *sig, size_t siglen); + + int EVP_PKEY_CTX_ctrl(EVP_PKEY_CTX *ctx, int keytype, int optype, int cmd, int p1, void *p2); + """) diff --git a/tasks/lib/package_control/deps/oscrypto/_openssl/_libcrypto_ctypes.py b/tasks/lib/package_control/deps/oscrypto/_openssl/_libcrypto_ctypes.py new file mode 100644 index 0000000..e33ebbc --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_openssl/_libcrypto_ctypes.py @@ -0,0 +1,707 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import re + +from ctypes import CDLL, c_void_p, c_char_p, c_int, c_ulong, c_uint, c_long, c_size_t, POINTER + +from .. import _backend_config +from .._errors import pretty_message +from .._ffi import FFIEngineError, get_library +from ..errors import LibraryNotFoundError + + +__all__ = [ + 'is_libressl', + 'libcrypto', + 'libressl_version', + 'libressl_version_info', + 'version', + 'version_info', +] + + +libcrypto_path = _backend_config().get('libcrypto_path') +if libcrypto_path is None: + libcrypto_path = get_library('crypto', 'libcrypto.dylib', '42') +if not libcrypto_path: + raise LibraryNotFoundError('The library libcrypto could not be found') + +libcrypto = CDLL(libcrypto_path, use_errno=True) + +try: + libcrypto.SSLeay_version.argtypes = [c_int] + libcrypto.SSLeay_version.restype = c_char_p + version_string = libcrypto.SSLeay_version(0).decode('utf-8') +except (AttributeError): + libcrypto.OpenSSL_version.argtypes = [c_int] + libcrypto.OpenSSL_version.restype = c_char_p + version_string = libcrypto.OpenSSL_version(0).decode('utf-8') + +is_libressl = 'LibreSSL' in version_string + +version_match = re.search('\\b(\\d\\.\\d\\.\\d[a-z]*)\\b', version_string) +if not version_match: + version_match = re.search('(?<=LibreSSL )(\\d\\.\\d(\\.\\d)?)\\b', version_string) +if not version_match: + raise LibraryNotFoundError('Error detecting the version of libcrypto') +version = version_match.group(1) +version_parts = re.sub('(\\d)([a-z]+)', '\\1.\\2', version).split('.') +version_info = tuple(int(part) if part.isdigit() else part for part in version_parts) + +# LibreSSL is compatible with libcrypto from OpenSSL 1.0.1 +libressl_version = '' +libressl_version_info = tuple() +if is_libressl: + libressl_version = version + libressl_version_info = version_info + version = '1.0.1' + version_info = (1, 0, 1) + +if version_info < (0, 9, 8): + raise LibraryNotFoundError(pretty_message( + ''' + OpenSSL versions older than 0.9.8 are not supported - found version %s + ''', + version + )) + +P_EVP_CIPHER_CTX = c_void_p +P_EVP_CIPHER = c_void_p + +P_EVP_MD_CTX = c_void_p +P_EVP_MD = c_void_p + +P_ENGINE = c_void_p +OSSL_PROVIDER = c_void_p +OSSL_LIB_CTX = c_void_p + +P_EVP_PKEY = c_void_p +EVP_PKEY_CTX = c_void_p +P_EVP_PKEY_CTX = POINTER(c_void_p) +P_X509 = POINTER(c_void_p) +P_DH = c_void_p +P_RSA = c_void_p +P_DSA = c_void_p +P_EC_KEY = c_void_p +P_BN_GENCB = c_void_p +BIGNUM = c_void_p +P_BIGNUM = POINTER(BIGNUM) + +p_int = POINTER(c_int) +p_uint = POINTER(c_uint) + +try: + if version_info < (1, 1): + libcrypto.ERR_load_crypto_strings.argtypes = [] + libcrypto.ERR_load_crypto_strings.restype = None + + libcrypto.ERR_free_strings.argtypes = [] + libcrypto.ERR_free_strings.restype = None + + if version_info >= (3, ): + libcrypto.OSSL_PROVIDER_available.argtypes = [OSSL_LIB_CTX, c_char_p] + libcrypto.OSSL_PROVIDER_available.restype = c_int + + libcrypto.OSSL_PROVIDER_load.argtypes = [OSSL_LIB_CTX, c_char_p] + libcrypto.OSSL_PROVIDER_load.restype = POINTER(OSSL_PROVIDER) + + libcrypto.ERR_get_error.argtypes = [] + libcrypto.ERR_get_error.restype = c_ulong + + libcrypto.ERR_peek_error.argtypes = [] + libcrypto.ERR_peek_error.restype = c_ulong + + libcrypto.ERR_error_string.argtypes = [ + c_ulong, + c_char_p + ] + libcrypto.ERR_error_string.restype = c_char_p + + libcrypto.OPENSSL_config.argtypes = [ + c_char_p + ] + libcrypto.OPENSSL_config.restype = None + + # This allocates the memory and inits + libcrypto.EVP_CIPHER_CTX_new.argtype = [] + libcrypto.EVP_CIPHER_CTX_new.restype = P_EVP_CIPHER_CTX + + libcrypto.EVP_CIPHER_CTX_set_key_length.argtypes = [ + P_EVP_CIPHER_CTX, + c_int + ] + libcrypto.EVP_CIPHER_CTX_set_key_length.restype = c_int + + libcrypto.EVP_CIPHER_CTX_set_padding.argtypes = [ + P_EVP_CIPHER_CTX, + c_int + ] + libcrypto.EVP_CIPHER_CTX_set_padding.restype = c_int + + libcrypto.EVP_CIPHER_CTX_ctrl.argtypes = [ + P_EVP_CIPHER_CTX, + c_int, + c_int, + c_void_p + ] + libcrypto.EVP_CIPHER_CTX_ctrl.restype = c_int + + # This cleans up and frees + libcrypto.EVP_CIPHER_CTX_free.argtypes = [ + P_EVP_CIPHER_CTX + ] + libcrypto.EVP_CIPHER_CTX_free.restype = None + + libcrypto.EVP_aes_128_cbc.argtypes = [] + libcrypto.EVP_aes_128_cbc.restype = P_EVP_CIPHER + + libcrypto.EVP_aes_192_cbc.argtypes = [] + libcrypto.EVP_aes_192_cbc.restype = P_EVP_CIPHER + + libcrypto.EVP_aes_256_cbc.argtypes = [] + libcrypto.EVP_aes_256_cbc.restype = P_EVP_CIPHER + + libcrypto.EVP_des_cbc.argtypes = [] + libcrypto.EVP_des_cbc.restype = P_EVP_CIPHER + + libcrypto.EVP_des_ede_cbc.argtypes = [] + libcrypto.EVP_des_ede_cbc.restype = P_EVP_CIPHER + + libcrypto.EVP_des_ede3_cbc.argtypes = [] + libcrypto.EVP_des_ede3_cbc.restype = P_EVP_CIPHER + + libcrypto.EVP_rc4.argtypes = [] + libcrypto.EVP_rc4.restype = P_EVP_CIPHER + + libcrypto.EVP_rc2_cbc.argtypes = [] + libcrypto.EVP_rc2_cbc.restype = P_EVP_CIPHER + + libcrypto.EVP_EncryptInit_ex.argtypes = [ + P_EVP_CIPHER_CTX, + P_EVP_CIPHER, + P_ENGINE, + c_char_p, + c_char_p + ] + libcrypto.EVP_EncryptInit_ex.restype = c_int + + libcrypto.EVP_EncryptUpdate.argtypes = [ + P_EVP_CIPHER_CTX, + c_char_p, + p_int, + c_char_p, + c_int + ] + libcrypto.EVP_EncryptUpdate.restype = c_int + + libcrypto.EVP_EncryptFinal_ex.argtypes = [ + P_EVP_CIPHER_CTX, + c_char_p, + p_int + ] + libcrypto.EVP_EncryptFinal_ex.restype = c_int + + libcrypto.EVP_DecryptInit_ex.argtypes = [ + P_EVP_CIPHER_CTX, + P_EVP_CIPHER, + P_ENGINE, + c_char_p, + c_char_p + ] + libcrypto.EVP_DecryptInit_ex.restype = c_int + + libcrypto.EVP_DecryptUpdate.argtypes = [ + P_EVP_CIPHER_CTX, + c_char_p, + p_int, + c_char_p, + c_int + ] + libcrypto.EVP_DecryptUpdate.restype = c_int + + libcrypto.EVP_DecryptFinal_ex.argtypes = [ + P_EVP_CIPHER_CTX, + c_char_p, + p_int + ] + libcrypto.EVP_DecryptFinal_ex.restype = c_int + + libcrypto.d2i_AutoPrivateKey.argtypes = [ + POINTER(P_EVP_PKEY), + POINTER(c_char_p), + c_int + ] + libcrypto.d2i_AutoPrivateKey.restype = P_EVP_PKEY + + libcrypto.d2i_PUBKEY.argtypes = [ + POINTER(P_EVP_PKEY), + POINTER(c_char_p), + c_int + ] + libcrypto.d2i_PUBKEY.restype = P_EVP_PKEY + + libcrypto.i2d_PUBKEY.argtypes = [ + P_EVP_PKEY, + POINTER(c_char_p) + ] + libcrypto.i2d_PUBKEY.restype = c_int + + libcrypto.d2i_X509.argtypes = [ + POINTER(P_X509), + POINTER(c_char_p), + c_int + ] + libcrypto.d2i_X509.restype = P_X509 + + libcrypto.i2d_X509.argtypes = [ + P_X509, + POINTER(c_char_p) + ] + libcrypto.i2d_X509.restype = c_int + + libcrypto.X509_get_pubkey.argtypes = [ + P_X509 + ] + libcrypto.X509_get_pubkey.restype = P_EVP_PKEY + + libcrypto.X509_free.argtypes = [ + P_X509 + ] + libcrypto.X509_free.restype = None + + libcrypto.EVP_PKEY_free.argtypes = [ + P_EVP_PKEY + ] + libcrypto.EVP_PKEY_free.restype = None + + if version_info < (1, 1): + libcrypto.EVP_MD_CTX_create.argtypes = [] + libcrypto.EVP_MD_CTX_create.restype = P_EVP_MD_CTX + + libcrypto.EVP_MD_CTX_destroy.argtypes = [ + P_EVP_MD_CTX + ] + libcrypto.EVP_MD_CTX_destroy.restype = None + else: + libcrypto.EVP_MD_CTX_new.argtypes = [] + libcrypto.EVP_MD_CTX_new.restype = P_EVP_MD_CTX + + libcrypto.EVP_MD_CTX_free.argtypes = [ + P_EVP_MD_CTX + ] + libcrypto.EVP_MD_CTX_free.restype = None + + libcrypto.EVP_md5.argtypes = [] + libcrypto.EVP_md5.restype = P_EVP_MD + + libcrypto.EVP_sha1.argtypes = [] + libcrypto.EVP_sha1.restype = P_EVP_MD + + libcrypto.EVP_sha224.argtypes = [] + libcrypto.EVP_sha224.restype = P_EVP_MD + + libcrypto.EVP_sha256.argtypes = [] + libcrypto.EVP_sha256.restype = P_EVP_MD + + libcrypto.EVP_sha384.argtypes = [] + libcrypto.EVP_sha384.restype = P_EVP_MD + + libcrypto.EVP_sha512.argtypes = [] + libcrypto.EVP_sha512.restype = P_EVP_MD + + if version_info < (3, 0): + libcrypto.EVP_PKEY_size.argtypes = [ + P_EVP_PKEY + ] + libcrypto.EVP_PKEY_size.restype = c_int + else: + libcrypto.EVP_PKEY_get_size.argtypes = [ + P_EVP_PKEY + ] + libcrypto.EVP_PKEY_get_size.restype = c_int + + libcrypto.EVP_PKEY_get1_RSA.argtypes = [ + P_EVP_PKEY + ] + libcrypto.EVP_PKEY_get1_RSA.restype = P_RSA + + libcrypto.RSA_free.argtypes = [ + P_RSA + ] + libcrypto.RSA_free.restype = None + + libcrypto.RSA_public_encrypt.argtypes = [ + c_int, + c_char_p, + c_char_p, + P_RSA, + c_int + ] + libcrypto.RSA_public_encrypt.restype = c_int + + libcrypto.RSA_private_encrypt.argtypes = [ + c_int, + c_char_p, + c_char_p, + P_RSA, + c_int + ] + libcrypto.RSA_private_encrypt.restype = c_int + + libcrypto.RSA_public_decrypt.argtypes = [ + c_int, + c_char_p, + c_char_p, + P_RSA, + c_int + ] + libcrypto.RSA_public_decrypt.restype = c_int + + libcrypto.RSA_private_decrypt.argtypes = [ + c_int, + c_char_p, + c_char_p, + P_RSA, + c_int + ] + libcrypto.RSA_private_decrypt.restype = c_int + + libcrypto.EVP_DigestUpdate.argtypes = [ + P_EVP_MD_CTX, + c_char_p, + c_uint + ] + libcrypto.EVP_DigestUpdate.restype = c_int + + libcrypto.PKCS12_key_gen_uni.argtypes = [ + c_char_p, + c_int, + c_char_p, + c_int, + c_int, + c_int, + c_int, + c_char_p, + c_void_p + ] + libcrypto.PKCS12_key_gen_uni.restype = c_int + + libcrypto.BN_free.argtypes = [ + P_BIGNUM + ] + libcrypto.BN_free.restype = None + + libcrypto.BN_dec2bn.argtypes = [ + POINTER(P_BIGNUM), + c_char_p + ] + libcrypto.BN_dec2bn.restype = c_int + + libcrypto.DH_new.argtypes = [] + libcrypto.DH_new.restype = P_DH + + libcrypto.DH_generate_parameters_ex.argtypes = [ + P_DH, + c_int, + c_int, + P_BN_GENCB + ] + libcrypto.DH_generate_parameters_ex.restype = c_int + + libcrypto.i2d_DHparams.argtypes = [ + P_DH, + POINTER(c_char_p) + ] + libcrypto.i2d_DHparams.restype = c_int + + libcrypto.DH_free.argtypes = [ + P_DH + ] + libcrypto.DH_free.restype = None + + libcrypto.RSA_new.argtypes = [] + libcrypto.RSA_new.restype = P_RSA + + libcrypto.RSA_generate_key_ex.argtypes = [ + P_RSA, + c_int, + P_BIGNUM, + P_BN_GENCB + ] + libcrypto.RSA_generate_key_ex.restype = c_int + + libcrypto.i2d_RSAPublicKey.argtypes = [ + P_RSA, + POINTER(c_char_p) + ] + libcrypto.i2d_RSAPublicKey.restype = c_int + + libcrypto.i2d_RSAPrivateKey.argtypes = [ + P_RSA, + POINTER(c_char_p) + ] + libcrypto.i2d_RSAPrivateKey.restype = c_int + + libcrypto.RSA_free.argtypes = [ + P_RSA + ] + libcrypto.RSA_free.restype = None + + libcrypto.DSA_new.argtypes = [] + libcrypto.DSA_new.restype = P_DSA + + libcrypto.DSA_generate_parameters_ex.argtypes = [ + P_DSA, + c_int, + c_char_p, + c_int, + POINTER(c_int), + POINTER(c_ulong), + P_BN_GENCB + ] + libcrypto.DSA_generate_parameters_ex.restype = c_int + + libcrypto.DSA_generate_key.argtypes = [ + P_DSA + ] + libcrypto.DSA_generate_key.restype = c_int + + libcrypto.i2d_DSA_PUBKEY.argtypes = [ + P_DSA, + POINTER(c_char_p) + ] + libcrypto.i2d_DSA_PUBKEY.restype = c_int + + libcrypto.i2d_DSAPrivateKey.argtypes = [ + P_DSA, + POINTER(c_char_p) + ] + libcrypto.i2d_DSAPrivateKey.restype = c_int + + libcrypto.DSA_free.argtypes = [ + P_DSA + ] + libcrypto.DSA_free.restype = None + + libcrypto.EC_KEY_new_by_curve_name.argtypes = [ + c_int + ] + libcrypto.EC_KEY_new_by_curve_name.restype = P_EC_KEY + + libcrypto.EC_KEY_generate_key.argtypes = [ + P_EC_KEY + ] + libcrypto.EC_KEY_generate_key.restype = c_int + + libcrypto.EC_KEY_set_asn1_flag.argtypes = [ + P_EC_KEY, + c_int + ] + libcrypto.EC_KEY_set_asn1_flag.restype = None + + libcrypto.i2d_ECPrivateKey.argtypes = [ + P_EC_KEY, + POINTER(c_char_p) + ] + libcrypto.i2d_ECPrivateKey.restype = c_int + + libcrypto.i2o_ECPublicKey.argtypes = [ + P_EC_KEY, + POINTER(c_char_p) + ] + libcrypto.i2o_ECPublicKey.restype = c_int + + libcrypto.EC_KEY_free.argtypes = [ + P_EC_KEY + ] + libcrypto.EC_KEY_free.restype = None + + if version_info < (1,): + P_DSA_SIG = c_void_p + P_ECDSA_SIG = c_void_p + + libcrypto.DSA_do_sign.argtypes = [ + c_char_p, + c_int, + P_DSA + ] + libcrypto.DSA_do_sign.restype = P_DSA_SIG + + libcrypto.ECDSA_do_sign.argtypes = [ + c_char_p, + c_int, + P_EC_KEY + ] + libcrypto.ECDSA_do_sign.restype = P_ECDSA_SIG + + libcrypto.d2i_DSA_SIG.argtypes = [ + POINTER(P_DSA_SIG), + POINTER(c_char_p), + c_long + ] + libcrypto.d2i_DSA_SIG.restype = P_DSA_SIG + + libcrypto.d2i_ECDSA_SIG.argtypes = [ + POINTER(P_ECDSA_SIG), + POINTER(c_char_p), + c_long + ] + libcrypto.d2i_ECDSA_SIG.restype = P_ECDSA_SIG + + libcrypto.i2d_DSA_SIG.argtypes = [ + P_DSA_SIG, + POINTER(c_char_p) + ] + libcrypto.i2d_DSA_SIG.restype = c_int + + libcrypto.i2d_ECDSA_SIG.argtypes = [ + P_ECDSA_SIG, + POINTER(c_char_p) + ] + libcrypto.i2d_ECDSA_SIG.restype = c_int + + libcrypto.DSA_do_verify.argtypes = [ + c_char_p, + c_int, + P_DSA_SIG, + P_DSA + ] + libcrypto.DSA_do_verify.restype = c_int + + libcrypto.ECDSA_do_verify.argtypes = [ + c_char_p, + c_int, + P_ECDSA_SIG, + P_EC_KEY + ] + libcrypto.ECDSA_do_verify.restype = c_int + + libcrypto.DSA_SIG_free.argtypes = [ + P_DSA_SIG + ] + libcrypto.DSA_SIG_free.restype = None + + libcrypto.ECDSA_SIG_free.argtypes = [ + P_ECDSA_SIG + ] + libcrypto.ECDSA_SIG_free.restype = None + + libcrypto.EVP_PKEY_get1_DSA.argtypes = [ + P_EVP_PKEY + ] + libcrypto.EVP_PKEY_get1_DSA.restype = P_DSA + + libcrypto.EVP_PKEY_get1_EC_KEY.argtypes = [ + P_EVP_PKEY + ] + libcrypto.EVP_PKEY_get1_EC_KEY.restype = P_EC_KEY + + libcrypto.RSA_verify_PKCS1_PSS.argtypes = [ + P_RSA, + c_char_p, + P_EVP_MD, + c_char_p, + c_int + ] + libcrypto.RSA_verify_PKCS1_PSS.restype = c_int + + libcrypto.RSA_padding_add_PKCS1_PSS.argtypes = [ + P_RSA, + c_char_p, + c_char_p, + P_EVP_MD, + c_int + ] + libcrypto.RSA_padding_add_PKCS1_PSS.restype = c_int + + libcrypto.EVP_DigestInit_ex.argtypes = [ + P_EVP_MD_CTX, + P_EVP_MD, + P_ENGINE + ] + libcrypto.EVP_DigestInit_ex.restype = c_int + + libcrypto.EVP_SignFinal.argtypes = [ + P_EVP_MD_CTX, + c_char_p, + p_uint, + P_EVP_PKEY + ] + libcrypto.EVP_SignFinal.restype = c_int + + libcrypto.EVP_VerifyFinal.argtypes = [ + P_EVP_MD_CTX, + c_char_p, + c_uint, + P_EVP_PKEY + ] + libcrypto.EVP_VerifyFinal.restype = c_int + + libcrypto.EVP_MD_CTX_set_flags.argtypes = [ + P_EVP_MD_CTX, + c_int + ] + libcrypto.EVP_MD_CTX_set_flags.restype = None + + else: + libcrypto.PKCS5_PBKDF2_HMAC.argtypes = [ + c_char_p, + c_int, + c_char_p, + c_int, + c_int, + P_EVP_MD, + c_int, + c_char_p + ] + libcrypto.PKCS5_PBKDF2_HMAC.restype = c_int + + libcrypto.EVP_DigestSignInit.argtypes = [ + P_EVP_MD_CTX, + POINTER(P_EVP_PKEY_CTX), + P_EVP_MD, + P_ENGINE, + P_EVP_PKEY + ] + libcrypto.EVP_DigestSignInit.restype = c_int + + libcrypto.EVP_DigestSignFinal.argtypes = [ + P_EVP_MD_CTX, + c_char_p, + POINTER(c_size_t) + ] + libcrypto.EVP_DigestSignFinal.restype = c_int + + libcrypto.EVP_DigestVerifyInit.argtypes = [ + P_EVP_MD_CTX, + POINTER(P_EVP_PKEY_CTX), + P_EVP_MD, + P_ENGINE, + P_EVP_PKEY + ] + libcrypto.EVP_DigestVerifyInit.restype = c_int + + libcrypto.EVP_DigestVerifyFinal.argtypes = [ + P_EVP_MD_CTX, + c_char_p, + c_size_t + ] + libcrypto.EVP_DigestVerifyFinal.restype = c_int + + libcrypto.EVP_PKEY_CTX_ctrl.argtypes = [ + P_EVP_PKEY_CTX, + c_int, + c_int, + c_int, + c_int, + c_void_p + ] + libcrypto.EVP_PKEY_CTX_ctrl.restype = c_int + +except (AttributeError): + raise FFIEngineError('Error initializing ctypes') + + +setattr(libcrypto, 'EVP_PKEY_CTX', EVP_PKEY_CTX) +setattr(libcrypto, 'BIGNUM', BIGNUM) diff --git a/tasks/lib/package_control/deps/oscrypto/_openssl/_libssl.py b/tasks/lib/package_control/deps/oscrypto/_openssl/_libssl.py new file mode 100644 index 0000000..2fa2bce --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_openssl/_libssl.py @@ -0,0 +1,89 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from .. import ffi + +# Initialize OpenSSL +from ._libcrypto import libcrypto_version_info + +if ffi() == 'cffi': + from ._libssl_cffi import libssl +else: + from ._libssl_ctypes import libssl + + +__all__ = [ + 'libssl', + 'LibsslConst', +] + + +if libcrypto_version_info < (1, 1): + libssl.SSL_library_init() +# Enables SHA2 algorithms on 0.9.8n and older +if libcrypto_version_info < (1, 0): + libssl.OPENSSL_add_all_algorithms_noconf() + + +class LibsslConst(): + ERR_LIB_ASN1 = 13 + ERR_LIB_SSL = 20 + + SSL_CTRL_OPTIONS = 32 + SSL_CTRL_SET_SESS_CACHE_MODE = 44 + + SSL_VERIFY_NONE = 0 + SSL_VERIFY_PEER = 1 + + SSL_ST_OK = 3 + + SSL_ERROR_WANT_READ = 2 + SSL_ERROR_WANT_WRITE = 3 + SSL_ERROR_ZERO_RETURN = 6 + + SSL_OP_NO_SSLv2 = 0x01000000 + SSL_OP_NO_SSLv3 = 0x02000000 + SSL_OP_NO_TLSv1 = 0x04000000 + SSL_OP_NO_TLSv1_2 = 0x08000000 + SSL_OP_NO_TLSv1_1 = 0x10000000 + + SSL_SESS_CACHE_CLIENT = 0x0001 + + SSL_R_NO_SHARED_CIPHER = 193 + + SSL_F_SSL3_CHECK_CERT_AND_ALGORITHM = 130 + SSL_F_SSL3_GET_KEY_EXCHANGE = 141 + SSL_F_SSL3_GET_SERVER_CERTIFICATE = 144 + SSL_R_BAD_DH_P_LENGTH = 110 + SSL_R_CERTIFICATE_VERIFY_FAILED = 134 + SSL_R_UNKNOWN_PROTOCOL = 252 + SSL_R_DH_KEY_TOO_SMALL = 372 + + # OpenSSL 1.1.0 + SSL_F_TLS_PROCESS_SKE_DHE = 419 + SSL_F_SSL3_GET_RECORD = 143 + SSL_R_WRONG_VERSION_NUMBER = 267 + SSL_F_TLS_PROCESS_SERVER_CERTIFICATE = 367 + + # OpenSSL < 1.1.0 + SSL_F_SSL23_GET_SERVER_HELLO = 119 + SSL_F_SSL3_READ_BYTES = 148 + SSL_R_SSLV3_ALERT_HANDSHAKE_FAILURE = 1040 + SSL_R_TLSV1_ALERT_PROTOCOL_VERSION = 1070 + + SSL_CTRL_SET_TLSEXT_HOSTNAME = 55 + TLSEXT_NAMETYPE_host_name = 0 + + X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY = 20 + X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN = 19 + X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT = 18 + + X509_V_ERR_CERT_NOT_YET_VALID = 9 + X509_V_ERR_CERT_HAS_EXPIRED = 10 + + ASN1_F_ASN1_ITEM_VERIFY = 197 + ASN1_R_UNKNOWN_MESSAGE_DIGEST_ALGORITHM = 161 + + +if libcrypto_version_info >= (1, 1, 0): + LibsslConst.SSL_R_DH_KEY_TOO_SMALL = 394 diff --git a/tasks/lib/package_control/deps/oscrypto/_openssl/_libssl_cffi.py b/tasks/lib/package_control/deps/oscrypto/_openssl/_libssl_cffi.py new file mode 100644 index 0000000..611f50c --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_openssl/_libssl_cffi.py @@ -0,0 +1,99 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from .. import _backend_config +from .._ffi import get_library, register_ffi +from ..errors import LibraryNotFoundError +from ._libcrypto import libcrypto_version_info + +from cffi import FFI + + +__all__ = [ + 'libssl', +] + + +ffi = FFI() + +libssl_path = _backend_config().get('libssl_path') +if libssl_path is None: + libssl_path = get_library('ssl', 'libssl', '44') +if not libssl_path: + raise LibraryNotFoundError('The library libssl could not be found') + +libssl = ffi.dlopen(libssl_path) +register_ffi(libssl, ffi) + +ffi.cdef(""" + typedef ... SSL_METHOD; + typedef uintptr_t SSL_CTX; + typedef ... SSL_SESSION; + typedef uintptr_t SSL; + typedef ... BIO_METHOD; + typedef uintptr_t BIO; + typedef uintptr_t X509; + typedef ... X509_STORE; + typedef ... X509_STORE_CTX; + typedef uintptr_t _STACK; + + BIO_METHOD *BIO_s_mem(void); + BIO *BIO_new(BIO_METHOD *type); + int BIO_free(BIO *a); + int BIO_read(BIO *b, void *buf, int len); + int BIO_write(BIO *b, const void *buf, int len); + size_t BIO_ctrl_pending(BIO *b); + + SSL_CTX *SSL_CTX_new(const SSL_METHOD *method); + long SSL_CTX_set_timeout(SSL_CTX *ctx, long t); + void SSL_CTX_set_verify(SSL_CTX *ctx, int mode, + int (*verify_callback)(int, X509_STORE_CTX *)); + int SSL_CTX_set_default_verify_paths(SSL_CTX *ctx); + int SSL_CTX_load_verify_locations(SSL_CTX *ctx, const char *CAfile, + const char *CApath); + long SSL_get_verify_result(const SSL *ssl); + X509_STORE *SSL_CTX_get_cert_store(const SSL_CTX *ctx); + int X509_STORE_add_cert(X509_STORE *ctx, X509 *x); + int SSL_CTX_set_cipher_list(SSL_CTX *ctx, const char *str); + long SSL_CTX_ctrl(SSL_CTX *ctx, int cmd, long larg, void *parg); + void SSL_CTX_free(SSL_CTX *a); + + SSL *SSL_new(SSL_CTX *ctx); + void SSL_free(SSL *ssl); + void SSL_set_bio(SSL *ssl, BIO *rbio, BIO *wbio); + long SSL_ctrl(SSL *ssl, int cmd, long larg, void *parg); + _STACK *SSL_get_peer_cert_chain(const SSL *s); + + SSL_SESSION *SSL_get1_session(const SSL *ssl); + int SSL_set_session(SSL *ssl, SSL_SESSION *session); + void SSL_SESSION_free(SSL_SESSION *session); + + void SSL_set_connect_state(SSL *ssl); + int SSL_do_handshake(SSL *ssl); + int SSL_get_error(const SSL *ssl, int ret); + const char *SSL_get_version(const SSL *ssl); + + int SSL_read(SSL *ssl, void *buf, int num); + int SSL_write(SSL *ssl, const void *buf, int num); + int SSL_pending(const SSL *ssl); + + int SSL_shutdown(SSL *ssl); +""") + +if libcrypto_version_info < (1, 1): + ffi.cdef(""" + int sk_num(const _STACK *); + X509 *sk_value(const _STACK *, int); + + int SSL_library_init(void); + void OPENSSL_add_all_algorithms_noconf(void); + + SSL_METHOD *SSLv23_method(void); + """) +else: + ffi.cdef(""" + int OPENSSL_sk_num(const _STACK *); + X509 *OPENSSL_sk_value(const _STACK *, int); + + SSL_METHOD *TLS_method(void); + """) diff --git a/tasks/lib/package_control/deps/oscrypto/_openssl/_libssl_ctypes.py b/tasks/lib/package_control/deps/oscrypto/_openssl/_libssl_ctypes.py new file mode 100644 index 0000000..a0ffbc0 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_openssl/_libssl_ctypes.py @@ -0,0 +1,260 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from ctypes import CDLL, CFUNCTYPE, POINTER, c_void_p, c_char_p, c_int, c_size_t, c_long + +from .. import _backend_config +from .._ffi import FFIEngineError, get_library +from ..errors import LibraryNotFoundError +from ._libcrypto import libcrypto_version_info + + +__all__ = [ + 'libssl', +] + + +libssl_path = _backend_config().get('libssl_path') +if libssl_path is None: + libssl_path = get_library('ssl', 'libssl', '44') +if not libssl_path: + raise LibraryNotFoundError('The library libssl could not be found') + +libssl = CDLL(libssl_path, use_errno=True) + +P_SSL_METHOD = POINTER(c_void_p) +P_SSL_CTX = POINTER(c_void_p) +P_SSL_SESSION = POINTER(c_void_p) +P_SSL = POINTER(c_void_p) +P_BIO_METHOD = POINTER(c_void_p) +P_BIO = POINTER(c_void_p) +X509 = c_void_p +P_X509 = POINTER(X509) +P_X509_STORE = POINTER(c_void_p) +P_X509_STORE_CTX = POINTER(c_void_p) +_STACK = c_void_p +P_STACK = POINTER(_STACK) + +try: + if libcrypto_version_info < (1, 1): + libssl.sk_num.argtypes = [P_STACK] + libssl.sk_num.restype = c_int + + libssl.sk_value.argtypes = [P_STACK, c_int] + libssl.sk_value.restype = P_X509 + + libssl.SSL_library_init.argtypes = [] + libssl.SSL_library_init.restype = c_int + + libssl.OPENSSL_add_all_algorithms_noconf.argtypes = [] + libssl.OPENSSL_add_all_algorithms_noconf.restype = None + + libssl.SSLv23_method.argtypes = [] + libssl.SSLv23_method.restype = P_SSL_METHOD + + else: + libssl.OPENSSL_sk_num.argtypes = [P_STACK] + libssl.OPENSSL_sk_num.restype = c_int + + libssl.OPENSSL_sk_value.argtypes = [P_STACK, c_int] + libssl.OPENSSL_sk_value.restype = P_X509 + + libssl.TLS_method.argtypes = [] + libssl.TLS_method.restype = P_SSL_METHOD + + libssl.BIO_s_mem.argtypes = [] + libssl.BIO_s_mem.restype = P_BIO_METHOD + + libssl.BIO_new.argtypes = [ + P_BIO_METHOD + ] + libssl.BIO_new.restype = P_BIO + + libssl.BIO_free.argtypes = [ + P_BIO + ] + libssl.BIO_free.restype = c_int + + libssl.BIO_read.argtypes = [ + P_BIO, + c_char_p, + c_int + ] + libssl.BIO_read.restype = c_int + + libssl.BIO_write.argtypes = [ + P_BIO, + c_char_p, + c_int + ] + libssl.BIO_write.restype = c_int + + libssl.BIO_ctrl_pending.argtypes = [ + P_BIO + ] + libssl.BIO_ctrl_pending.restype = c_size_t + + libssl.SSL_CTX_new.argtypes = [ + P_SSL_METHOD + ] + libssl.SSL_CTX_new.restype = P_SSL_CTX + + libssl.SSL_CTX_set_timeout.argtypes = [ + P_SSL_CTX, + c_long + ] + libssl.SSL_CTX_set_timeout.restype = c_long + + verify_callback = CFUNCTYPE(c_int, c_int, P_X509_STORE_CTX) + setattr(libssl, 'verify_callback', verify_callback) + + libssl.SSL_CTX_set_verify.argtypes = [ + P_SSL_CTX, + c_int, + POINTER(verify_callback) + ] + libssl.SSL_CTX_set_verify.restype = None + + libssl.SSL_CTX_set_default_verify_paths.argtypes = [ + P_SSL_CTX + ] + libssl.SSL_CTX_set_default_verify_paths.restype = c_int + + libssl.SSL_CTX_load_verify_locations.argtypes = [ + P_SSL_CTX, + c_char_p, + c_char_p + ] + libssl.SSL_CTX_load_verify_locations.restype = c_int + + libssl.SSL_get_verify_result.argtypes = [ + P_SSL + ] + libssl.SSL_get_verify_result.restype = c_long + + libssl.SSL_CTX_get_cert_store.argtypes = [ + P_SSL_CTX + ] + libssl.SSL_CTX_get_cert_store.restype = P_X509_STORE + + libssl.X509_STORE_add_cert.argtypes = [ + P_X509_STORE, + P_X509 + ] + libssl.X509_STORE_add_cert.restype = c_int + + libssl.SSL_CTX_set_cipher_list.argtypes = [ + P_SSL_CTX, + c_char_p + ] + libssl.SSL_CTX_set_cipher_list.restype = c_int + + libssl.SSL_CTX_ctrl.arg_types = [ + P_SSL_CTX, + c_int, + c_long, + c_void_p + ] + libssl.SSL_CTX_ctrl.restype = c_long + + libssl.SSL_CTX_free.argtypes = [ + P_SSL_CTX + ] + libssl.SSL_CTX_free.restype = None + + libssl.SSL_new.argtypes = [ + P_SSL_CTX + ] + libssl.SSL_new.restype = P_SSL + + libssl.SSL_free.argtypes = [ + P_SSL + ] + libssl.SSL_free.restype = None + + libssl.SSL_set_bio.argtypes = [ + P_SSL, + P_BIO, + P_BIO + ] + libssl.SSL_set_bio.restype = None + + libssl.SSL_ctrl.arg_types = [ + P_SSL, + c_int, + c_long, + c_void_p + ] + libssl.SSL_ctrl.restype = c_long + + libssl.SSL_get_peer_cert_chain.argtypes = [ + P_SSL + ] + libssl.SSL_get_peer_cert_chain.restype = P_STACK + + libssl.SSL_get1_session.argtypes = [ + P_SSL + ] + libssl.SSL_get1_session.restype = P_SSL_SESSION + + libssl.SSL_set_session.argtypes = [ + P_SSL, + P_SSL_SESSION + ] + libssl.SSL_set_session.restype = c_int + + libssl.SSL_SESSION_free.argtypes = [ + P_SSL_SESSION + ] + libssl.SSL_SESSION_free.restype = None + + libssl.SSL_set_connect_state.argtypes = [ + P_SSL + ] + libssl.SSL_set_connect_state.restype = None + + libssl.SSL_do_handshake.argtypes = [ + P_SSL + ] + libssl.SSL_do_handshake.restype = c_int + + libssl.SSL_get_error.argtypes = [ + P_SSL, + c_int + ] + libssl.SSL_get_error.restype = c_int + + libssl.SSL_get_version.argtypes = [ + P_SSL + ] + libssl.SSL_get_version.restype = c_char_p + + libssl.SSL_read.argtypes = [ + P_SSL, + c_char_p, + c_int + ] + libssl.SSL_read.restype = c_int + + libssl.SSL_write.argtypes = [ + P_SSL, + c_char_p, + c_int + ] + libssl.SSL_write.restype = c_int + + libssl.SSL_pending.argtypes = [ + P_SSL + ] + libssl.SSL_pending.restype = c_int + + libssl.SSL_shutdown.argtypes = [ + P_SSL + ] + libssl.SSL_shutdown.restype = c_int + +except (AttributeError): + raise FFIEngineError('Error initializing ctypes') + +setattr(libssl, '_STACK', _STACK) +setattr(libssl, 'X509', X509) diff --git a/tasks/lib/package_control/deps/oscrypto/_openssl/asymmetric.py b/tasks/lib/package_control/deps/oscrypto/_openssl/asymmetric.py new file mode 100644 index 0000000..a823bca --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_openssl/asymmetric.py @@ -0,0 +1,1926 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import hashlib + +from .._asn1 import ( + Certificate as Asn1Certificate, + DHParameters, + ECDomainParameters, + PrivateKeyInfo, + PublicKeyAlgorithm, + PublicKeyInfo, +) +from .._asymmetric import ( + _CertificateBase, + _fingerprint, + _parse_pkcs12, + _PrivateKeyBase, + _PublicKeyBase, + _unwrap_private_key_info, + parse_certificate, + parse_private, + parse_public, +) +from .._errors import pretty_message +from .._ffi import ( + buffer_from_bytes, + buffer_pointer, + bytes_from_buffer, + deref, + is_null, + new, + null, + unwrap, + write_to_buffer, +) +from ._libcrypto import libcrypto, LibcryptoConst, libcrypto_version_info, handle_openssl_error +from ..errors import AsymmetricKeyError, IncompleteAsymmetricKeyError, SignatureError +from .._types import type_name, str_cls, byte_cls, int_types +from ..util import constant_compare + + +__all__ = [ + 'Certificate', + 'dsa_sign', + 'dsa_verify', + 'ecdsa_sign', + 'ecdsa_verify', + 'generate_pair', + 'load_certificate', + 'load_pkcs12', + 'load_private_key', + 'load_public_key', + 'parse_pkcs12', + 'PrivateKey', + 'PublicKey', + 'rsa_oaep_decrypt', + 'rsa_oaep_encrypt', + 'rsa_pkcs1v15_decrypt', + 'rsa_pkcs1v15_encrypt', + 'rsa_pkcs1v15_sign', + 'rsa_pkcs1v15_verify', + 'rsa_pss_sign', + 'rsa_pss_verify', +] + + +class PrivateKey(_PrivateKeyBase): + """ + Container for the OpenSSL representation of a private key + """ + + evp_pkey = None + _public_key = None + + # A reference to the library used in the destructor to make sure it hasn't + # been garbage collected by the time this object is garbage collected + _lib = None + + def __init__(self, evp_pkey, asn1): + """ + :param evp_pkey: + An OpenSSL EVP_PKEY value from loading/importing the key + + :param asn1: + An asn1crypto.keys.PrivateKeyInfo object + """ + + self.evp_pkey = evp_pkey + self.asn1 = asn1 + self._lib = libcrypto + + @property + def public_key(self): + """ + :return: + A PublicKey object corresponding to this private key. + """ + + if self._public_key is None: + buffer_size = libcrypto.i2d_PUBKEY(self.evp_pkey, null()) + pubkey_buffer = buffer_from_bytes(buffer_size) + pubkey_pointer = buffer_pointer(pubkey_buffer) + pubkey_length = libcrypto.i2d_PUBKEY(self.evp_pkey, pubkey_pointer) + handle_openssl_error(pubkey_length) + pubkey_data = bytes_from_buffer(pubkey_buffer, pubkey_length) + + asn1 = PublicKeyInfo.load(pubkey_data) + + # OpenSSL 1.x suffers from issues trying to use RSASSA-PSS keys, so we + # masquerade it as a normal RSA key so the OID checks work + if libcrypto_version_info < (3,) and asn1.algorithm == 'rsassa_pss': + temp_asn1 = asn1.copy() + temp_asn1['algorithm']['algorithm'] = 'rsa' + temp_data = temp_asn1.dump() + write_to_buffer(pubkey_buffer, temp_data) + pubkey_length = len(temp_data) + + pub_evp_pkey = libcrypto.d2i_PUBKEY(null(), buffer_pointer(pubkey_buffer), pubkey_length) + if is_null(pub_evp_pkey): + handle_openssl_error(0) + + self._public_key = PublicKey(pub_evp_pkey, asn1) + + return self._public_key + + @property + def fingerprint(self): + """ + Creates a fingerprint that can be compared with a public key to see if + the two form a pair. + + This fingerprint is not compatible with fingerprints generated by any + other software. + + :return: + A byte string that is a sha256 hash of selected components (based + on the key type) + """ + + if self._fingerprint is None: + self._fingerprint = _fingerprint(self.asn1, load_private_key) + return self._fingerprint + + def __del__(self): + if self.evp_pkey: + self._lib.EVP_PKEY_free(self.evp_pkey) + self._lib = None + self.evp_pkey = None + + +class PublicKey(_PublicKeyBase): + """ + Container for the OpenSSL representation of a public key + """ + + evp_pkey = None + + # A reference to the library used in the destructor to make sure it hasn't + # been garbage collected by the time this object is garbage collected + _lib = None + + def __init__(self, evp_pkey, asn1): + """ + :param evp_pkey: + An OpenSSL EVP_PKEY value from loading/importing the key + + :param asn1: + An asn1crypto.keys.PublicKeyInfo object + """ + + self.evp_pkey = evp_pkey + self.asn1 = asn1 + self._lib = libcrypto + + def __del__(self): + if self.evp_pkey: + self._lib.EVP_PKEY_free(self.evp_pkey) + self._lib = None + self.evp_pkey = None + + +class Certificate(_CertificateBase): + """ + Container for the OpenSSL representation of a certificate + """ + + x509 = None + _public_key = None + _self_signed = None + + # A reference to the library used in the destructor to make sure it hasn't + # been garbage collected by the time this object is garbage collected + _lib = None + + def __init__(self, x509, asn1): + """ + :param x509: + An OpenSSL X509 value from loading/importing the certificate + + :param asn1: + An asn1crypto.x509.Certificate object + """ + + self.x509 = x509 + self.asn1 = asn1 + self._lib = libcrypto + + @property + def evp_pkey(self): + """ + :return: + The EVP_PKEY of the public key this certificate contains + """ + + return self.public_key.evp_pkey + + @property + def public_key(self): + """ + :return: + The PublicKey object for the public key this certificate contains + """ + + if not self._public_key and self.x509: + # OpenSSL 1.x suffers from issues trying to use RSASSA-PSS keys, so we + # masquerade it as a normal RSA key so the OID checks work + if libcrypto_version_info < (3,) and self.asn1.public_key.algorithm == 'rsassa_pss': + self._public_key = load_public_key(self.asn1.public_key) + else: + evp_pkey = libcrypto.X509_get_pubkey(self.x509) + self._public_key = PublicKey(evp_pkey, self.asn1.public_key) + + return self._public_key + + @property + def self_signed(self): + """ + :return: + A boolean - if the certificate is self-signed + """ + + if self._self_signed is None: + self._self_signed = False + if self.asn1.self_signed in set(['yes', 'maybe']): + + signature_algo = self.asn1['signature_algorithm'].signature_algo + hash_algo = self.asn1['signature_algorithm'].hash_algo + + if signature_algo == 'rsassa_pkcs1v15': + verify_func = rsa_pkcs1v15_verify + elif signature_algo == 'rsassa_pss': + verify_func = rsa_pss_verify + elif signature_algo == 'dsa': + verify_func = dsa_verify + elif signature_algo == 'ecdsa': + verify_func = ecdsa_verify + else: + raise OSError(pretty_message( + ''' + Unable to verify the signature of the certificate since + it uses the unsupported algorithm %s + ''', + signature_algo + )) + + try: + verify_func( + self.public_key, + self.asn1['signature_value'].native, + self.asn1['tbs_certificate'].dump(), + hash_algo + ) + self._self_signed = True + except (SignatureError): + pass + + return self._self_signed + + def __del__(self): + if self._public_key: + self._public_key.__del__() + self._public_key = None + + if self.x509: + self._lib.X509_free(self.x509) + self._lib = None + self.x509 = None + + +def generate_pair(algorithm, bit_size=None, curve=None): + """ + Generates a public/private key pair + + :param algorithm: + The key algorithm - "rsa", "dsa" or "ec" + + :param bit_size: + An integer - used for "rsa" and "dsa". For "rsa" the value maye be 1024, + 2048, 3072 or 4096. For "dsa" the value may be 1024, plus 2048 or 3072 + if OpenSSL 1.0.0 or newer is available. + + :param curve: + A unicode string - used for "ec" keys. Valid values include "secp256r1", + "secp384r1" and "secp521r1". + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A 2-element tuple of (PublicKey, PrivateKey). The contents of each key + may be saved by calling .asn1.dump(). + """ + + if algorithm not in set(['rsa', 'dsa', 'ec']): + raise ValueError(pretty_message( + ''' + algorithm must be one of "rsa", "dsa", "ec", not %s + ''', + repr(algorithm) + )) + + if algorithm == 'rsa': + if bit_size not in set([1024, 2048, 3072, 4096]): + raise ValueError(pretty_message( + ''' + bit_size must be one of 1024, 2048, 3072, 4096, not %s + ''', + repr(bit_size) + )) + + elif algorithm == 'dsa': + if libcrypto_version_info < (1,): + if bit_size != 1024: + raise ValueError(pretty_message( + ''' + bit_size must be 1024, not %s + ''', + repr(bit_size) + )) + else: + if bit_size not in set([1024, 2048, 3072]): + raise ValueError(pretty_message( + ''' + bit_size must be one of 1024, 2048, 3072, not %s + ''', + repr(bit_size) + )) + + elif algorithm == 'ec': + if curve not in set(['secp256r1', 'secp384r1', 'secp521r1']): + raise ValueError(pretty_message( + ''' + curve must be one of "secp256r1", "secp384r1", "secp521r1", + not %s + ''', + repr(curve) + )) + + if algorithm == 'rsa': + rsa = None + exponent = None + + try: + rsa = libcrypto.RSA_new() + if is_null(rsa): + handle_openssl_error(0) + + exponent_pointer = new(libcrypto, 'BIGNUM **') + result = libcrypto.BN_dec2bn(exponent_pointer, b'65537') + handle_openssl_error(result) + exponent = unwrap(exponent_pointer) + + result = libcrypto.RSA_generate_key_ex(rsa, bit_size, exponent, null()) + handle_openssl_error(result) + + buffer_length = libcrypto.i2d_RSAPublicKey(rsa, null()) + if buffer_length < 0: + handle_openssl_error(buffer_length) + buffer = buffer_from_bytes(buffer_length) + result = libcrypto.i2d_RSAPublicKey(rsa, buffer_pointer(buffer)) + if result < 0: + handle_openssl_error(result) + public_key_bytes = bytes_from_buffer(buffer, buffer_length) + + buffer_length = libcrypto.i2d_RSAPrivateKey(rsa, null()) + if buffer_length < 0: + handle_openssl_error(buffer_length) + buffer = buffer_from_bytes(buffer_length) + result = libcrypto.i2d_RSAPrivateKey(rsa, buffer_pointer(buffer)) + if result < 0: + handle_openssl_error(result) + private_key_bytes = bytes_from_buffer(buffer, buffer_length) + + finally: + if rsa: + libcrypto.RSA_free(rsa) + if exponent: + libcrypto.BN_free(exponent) + + elif algorithm == 'dsa': + dsa = None + + try: + dsa = libcrypto.DSA_new() + if is_null(dsa): + handle_openssl_error(0) + + result = libcrypto.DSA_generate_parameters_ex(dsa, bit_size, null(), 0, null(), null(), null()) + handle_openssl_error(result) + + result = libcrypto.DSA_generate_key(dsa) + handle_openssl_error(result) + + buffer_length = libcrypto.i2d_DSA_PUBKEY(dsa, null()) + if buffer_length < 0: + handle_openssl_error(buffer_length) + buffer = buffer_from_bytes(buffer_length) + result = libcrypto.i2d_DSA_PUBKEY(dsa, buffer_pointer(buffer)) + if result < 0: + handle_openssl_error(result) + public_key_bytes = bytes_from_buffer(buffer, buffer_length) + + buffer_length = libcrypto.i2d_DSAPrivateKey(dsa, null()) + if buffer_length < 0: + handle_openssl_error(buffer_length) + buffer = buffer_from_bytes(buffer_length) + result = libcrypto.i2d_DSAPrivateKey(dsa, buffer_pointer(buffer)) + if result < 0: + handle_openssl_error(result) + private_key_bytes = bytes_from_buffer(buffer, buffer_length) + + finally: + if dsa: + libcrypto.DSA_free(dsa) + + elif algorithm == 'ec': + ec_key = None + + try: + curve_id = { + 'secp256r1': LibcryptoConst.NID_X9_62_prime256v1, + 'secp384r1': LibcryptoConst.NID_secp384r1, + 'secp521r1': LibcryptoConst.NID_secp521r1, + }[curve] + + ec_key = libcrypto.EC_KEY_new_by_curve_name(curve_id) + if is_null(ec_key): + handle_openssl_error(0) + + result = libcrypto.EC_KEY_generate_key(ec_key) + handle_openssl_error(result) + + libcrypto.EC_KEY_set_asn1_flag(ec_key, LibcryptoConst.OPENSSL_EC_NAMED_CURVE) + + buffer_length = libcrypto.i2o_ECPublicKey(ec_key, null()) + if buffer_length < 0: + handle_openssl_error(buffer_length) + buffer = buffer_from_bytes(buffer_length) + result = libcrypto.i2o_ECPublicKey(ec_key, buffer_pointer(buffer)) + if result < 0: + handle_openssl_error(result) + public_key_point_bytes = bytes_from_buffer(buffer, buffer_length) + + # i2o_ECPublicKey only returns the ECPoint bytes, so we have to + # manually wrap it in a PublicKeyInfo structure to get it to parse + public_key = PublicKeyInfo({ + 'algorithm': PublicKeyAlgorithm({ + 'algorithm': 'ec', + 'parameters': ECDomainParameters( + name='named', + value=curve + ) + }), + 'public_key': public_key_point_bytes + }) + public_key_bytes = public_key.dump() + + buffer_length = libcrypto.i2d_ECPrivateKey(ec_key, null()) + if buffer_length < 0: + handle_openssl_error(buffer_length) + buffer = buffer_from_bytes(buffer_length) + result = libcrypto.i2d_ECPrivateKey(ec_key, buffer_pointer(buffer)) + if result < 0: + handle_openssl_error(result) + private_key_bytes = bytes_from_buffer(buffer, buffer_length) + + finally: + if ec_key: + libcrypto.EC_KEY_free(ec_key) + + return (load_public_key(public_key_bytes), load_private_key(private_key_bytes)) + + +def generate_dh_parameters(bit_size): + """ + Generates DH parameters for use with Diffie-Hellman key exchange. Returns + a structure in the format of DHParameter defined in PKCS#3, which is also + used by the OpenSSL dhparam tool. + + THIS CAN BE VERY TIME CONSUMING! + + :param bit_size: + The integer bit size of the parameters to generate. Must be between 512 + and 4096, and divisible by 64. Recommended secure value as of early 2016 + is 2048, with an absolute minimum of 1024. + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + An asn1crypto.algos.DHParameters object. Use + oscrypto.asymmetric.dump_dh_parameters() to save to disk for usage with + web servers. + """ + + if not isinstance(bit_size, int_types): + raise TypeError(pretty_message( + ''' + bit_size must be an integer, not %s + ''', + type_name(bit_size) + )) + + if bit_size < 512: + raise ValueError('bit_size must be greater than or equal to 512') + + if bit_size > 4096: + raise ValueError('bit_size must be less than or equal to 4096') + + if bit_size % 64 != 0: + raise ValueError('bit_size must be a multiple of 64') + + dh = None + + try: + dh = libcrypto.DH_new() + if is_null(dh): + handle_openssl_error(0) + + result = libcrypto.DH_generate_parameters_ex(dh, bit_size, LibcryptoConst.DH_GENERATOR_2, null()) + handle_openssl_error(result) + + buffer_length = libcrypto.i2d_DHparams(dh, null()) + if buffer_length < 0: + handle_openssl_error(buffer_length) + buffer = buffer_from_bytes(buffer_length) + result = libcrypto.i2d_DHparams(dh, buffer_pointer(buffer)) + if result < 0: + handle_openssl_error(result) + dh_params_bytes = bytes_from_buffer(buffer, buffer_length) + + return DHParameters.load(dh_params_bytes) + + finally: + if dh: + libcrypto.DH_free(dh) + + +def load_certificate(source): + """ + Loads an x509 certificate into a Certificate object + + :param source: + A byte string of file contents, a unicode string filename or an + asn1crypto.x509.Certificate object + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A Certificate object + """ + + if isinstance(source, Asn1Certificate): + certificate = source + + elif isinstance(source, byte_cls): + certificate = parse_certificate(source) + + elif isinstance(source, str_cls): + with open(source, 'rb') as f: + certificate = parse_certificate(f.read()) + + else: + raise TypeError(pretty_message( + ''' + source must be a byte string, unicode string or + asn1crypto.x509.Certificate object, not %s + ''', + type_name(source) + )) + + return _load_x509(certificate) + + +def _load_x509(certificate): + """ + Loads an ASN.1 object of an x509 certificate into a Certificate object + + :param certificate: + An asn1crypto.x509.Certificate object + + :return: + A Certificate object + """ + + source = certificate.dump() + + buffer = buffer_from_bytes(source) + evp_pkey = libcrypto.d2i_X509(null(), buffer_pointer(buffer), len(source)) + if is_null(evp_pkey): + handle_openssl_error(0) + return Certificate(evp_pkey, certificate) + + +def load_private_key(source, password=None): + """ + Loads a private key into a PrivateKey object + + :param source: + A byte string of file contents, a unicode string filename or an + asn1crypto.keys.PrivateKeyInfo object + + :param password: + A byte or unicode string to decrypt the private key file. Unicode + strings will be encoded using UTF-8. Not used is the source is a + PrivateKeyInfo object. + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + oscrypto.errors.AsymmetricKeyError - when the private key is incompatible with the OS crypto library + OSError - when an error is returned by the OS crypto library + + :return: + A PrivateKey object + """ + + if isinstance(source, PrivateKeyInfo): + private_object = source + + else: + if password is not None: + if isinstance(password, str_cls): + password = password.encode('utf-8') + if not isinstance(password, byte_cls): + raise TypeError(pretty_message( + ''' + password must be a byte string, not %s + ''', + type_name(password) + )) + + if isinstance(source, str_cls): + with open(source, 'rb') as f: + source = f.read() + + elif not isinstance(source, byte_cls): + raise TypeError(pretty_message( + ''' + source must be a byte string, unicode string or + asn1crypto.keys.PrivateKeyInfo object, not %s + ''', + type_name(source) + )) + + private_object = parse_private(source, password) + + return _load_key(private_object) + + +def load_public_key(source): + """ + Loads a public key into a PublicKey object + + :param source: + A byte string of file contents, a unicode string filename or an + asn1crypto.keys.PublicKeyInfo object + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + oscrypto.errors.AsymmetricKeyError - when the public key is incompatible with the OS crypto library + OSError - when an error is returned by the OS crypto library + + :return: + A PublicKey object + """ + + if isinstance(source, PublicKeyInfo): + public_key = source + + elif isinstance(source, byte_cls): + public_key = parse_public(source) + + elif isinstance(source, str_cls): + with open(source, 'rb') as f: + public_key = parse_public(f.read()) + + else: + raise TypeError(pretty_message( + ''' + source must be a byte string, unicode string or + asn1crypto.keys.PublicKeyInfo object, not %s + ''', + type_name(source) + )) + + if public_key.algorithm == 'dsa': + if libcrypto_version_info < (1,) and public_key.hash_algo == 'sha2': + raise AsymmetricKeyError(pretty_message( + ''' + OpenSSL 0.9.8 only supports DSA keys based on SHA1 (2048 bits or + less) - this key is based on SHA2 and is %s bits + ''', + public_key.bit_size + )) + elif public_key.hash_algo is None: + raise IncompleteAsymmetricKeyError(pretty_message( + ''' + The DSA key does not contain the necessary p, q and g + parameters and can not be used + ''' + )) + + # OpenSSL 1.x suffers from issues trying to use RSASSA-PSS keys, so we + # masquerade it as a normal RSA key so the OID checks work + if libcrypto_version_info < (3,) and public_key.algorithm == 'rsassa_pss': + temp_key = public_key.copy() + temp_key['algorithm']['algorithm'] = 'rsa' + data = temp_key.dump() + else: + data = public_key.dump() + + buffer = buffer_from_bytes(data) + evp_pkey = libcrypto.d2i_PUBKEY(null(), buffer_pointer(buffer), len(data)) + if is_null(evp_pkey): + handle_openssl_error(0) + return PublicKey(evp_pkey, public_key) + + +def _load_key(private_object): + """ + Loads a private key into a PrivateKey object + + :param private_object: + An asn1crypto.keys.PrivateKeyInfo object + + :return: + A PrivateKey object + """ + + if libcrypto_version_info < (1,) and private_object.algorithm == 'dsa' and private_object.hash_algo == 'sha2': + raise AsymmetricKeyError(pretty_message( + ''' + OpenSSL 0.9.8 only supports DSA keys based on SHA1 (2048 bits or + less) - this key is based on SHA2 and is %s bits + ''', + private_object.bit_size + )) + + source = _unwrap_private_key_info(private_object).dump() + + buffer = buffer_from_bytes(source) + evp_pkey = libcrypto.d2i_AutoPrivateKey(null(), buffer_pointer(buffer), len(source)) + if is_null(evp_pkey): + handle_openssl_error(0) + return PrivateKey(evp_pkey, private_object) + + +def parse_pkcs12(data, password=None): + """ + Parses a PKCS#12 ANS.1 DER-encoded structure and extracts certs and keys + + :param data: + A byte string of a DER-encoded PKCS#12 file + + :param password: + A byte string of the password to any encrypted data + + :raises: + ValueError - when any of the parameters are of the wrong type or value + OSError - when an error is returned by one of the OS decryption functions + + :return: + A three-element tuple of: + 1. An asn1crypto.keys.PrivateKeyInfo object + 2. An asn1crypto.x509.Certificate object + 3. A list of zero or more asn1crypto.x509.Certificate objects that are + "extra" certificates, possibly intermediates from the cert chain + """ + + return _parse_pkcs12(data, password, load_private_key) + + +def load_pkcs12(source, password=None): + """ + Loads a .p12 or .pfx file into a PrivateKey object and one or more + Certificates objects + + :param source: + A byte string of file contents or a unicode string filename + + :param password: + A byte or unicode string to decrypt the PKCS12 file. Unicode strings + will be encoded using UTF-8. + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + oscrypto.errors.AsymmetricKeyError - when a contained key is incompatible with the OS crypto library + OSError - when an error is returned by the OS crypto library + + :return: + A three-element tuple containing (PrivateKey, Certificate, [Certificate, ...]) + """ + + if password is not None: + if isinstance(password, str_cls): + password = password.encode('utf-8') + if not isinstance(password, byte_cls): + raise TypeError(pretty_message( + ''' + password must be a byte string, not %s + ''', + type_name(password) + )) + + if isinstance(source, str_cls): + with open(source, 'rb') as f: + source = f.read() + + elif not isinstance(source, byte_cls): + raise TypeError(pretty_message( + ''' + source must be a byte string or a unicode string, not %s + ''', + type_name(source) + )) + + key_info, cert_info, extra_certs_info = parse_pkcs12(source, password) + + key = None + cert = None + + if key_info: + key = _load_key(key_info) + + if cert_info: + cert = _load_x509(cert_info) + + extra_certs = [_load_x509(info) for info in extra_certs_info] + + return (key, cert, extra_certs) + + +def rsa_pkcs1v15_encrypt(certificate_or_public_key, data): + """ + Encrypts a byte string using an RSA public key or certificate. Uses PKCS#1 + v1.5 padding. + + :param certificate_or_public_key: + A PublicKey or Certificate object + + :param data: + A byte string, with a maximum length 11 bytes less than the key length + (in bytes) + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the encrypted data + """ + + return _encrypt(certificate_or_public_key, data, LibcryptoConst.RSA_PKCS1_PADDING) + + +def rsa_pkcs1v15_decrypt(private_key, ciphertext): + """ + Decrypts a byte string using an RSA private key. Uses PKCS#1 v1.5 padding. + + :param private_key: + A PrivateKey object + + :param ciphertext: + A byte string of the encrypted data + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the original plaintext + """ + + return _decrypt(private_key, ciphertext, LibcryptoConst.RSA_PKCS1_PADDING) + + +def rsa_oaep_encrypt(certificate_or_public_key, data): + """ + Encrypts a byte string using an RSA public key or certificate. Uses PKCS#1 + OAEP padding with SHA1. + + :param certificate_or_public_key: + A PublicKey or Certificate object + + :param data: + A byte string, with a maximum length 41 bytes (or more) less than the + key length (in bytes) + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the encrypted data + """ + + return _encrypt(certificate_or_public_key, data, LibcryptoConst.RSA_PKCS1_OAEP_PADDING) + + +def rsa_oaep_decrypt(private_key, ciphertext): + """ + Decrypts a byte string using an RSA private key. Uses PKCS#1 OAEP padding + with SHA1. + + :param private_key: + A PrivateKey object + + :param ciphertext: + A byte string of the encrypted data + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the original plaintext + """ + + return _decrypt(private_key, ciphertext, LibcryptoConst.RSA_PKCS1_OAEP_PADDING) + + +def _evp_pkey_get_size(evp_pkey): + """ + Handles the function name change from OpenSSL 1.1 -> 3.0 + + :param evp_pkey: + The EVP_PKEY of the Certificte or PublicKey to get the size of + + :return: + An int of the number of bytes necessary for the key + """ + + if libcrypto_version_info < (3, ): + return libcrypto.EVP_PKEY_size(evp_pkey) + return libcrypto.EVP_PKEY_get_size(evp_pkey) + + +def _encrypt(certificate_or_public_key, data, padding): + """ + Encrypts plaintext using an RSA public key or certificate + + :param certificate_or_public_key: + A PublicKey, Certificate or PrivateKey object + + :param data: + The byte string to encrypt + + :param padding: + The padding mode to use + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the encrypted data + """ + + if not isinstance(certificate_or_public_key, (Certificate, PublicKey)): + raise TypeError(pretty_message( + ''' + certificate_or_public_key must be an instance of the Certificate or + PublicKey class, not %s + ''', + type_name(certificate_or_public_key) + )) + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + rsa = None + + try: + buffer_size = _evp_pkey_get_size(certificate_or_public_key.evp_pkey) + buffer = buffer_from_bytes(buffer_size) + + rsa = libcrypto.EVP_PKEY_get1_RSA(certificate_or_public_key.evp_pkey) + res = libcrypto.RSA_public_encrypt(len(data), data, buffer, rsa, padding) + handle_openssl_error(res) + + return bytes_from_buffer(buffer, res) + + finally: + if rsa: + libcrypto.RSA_free(rsa) + + +def _decrypt(private_key, ciphertext, padding): + """ + Decrypts RSA ciphertext using a private key + + :param private_key: + A PrivateKey object + + :param ciphertext: + The ciphertext - a byte string + + :param padding: + The padding mode to use + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the plaintext + """ + + if not isinstance(private_key, PrivateKey): + raise TypeError(pretty_message( + ''' + private_key must be an instance of the PrivateKey class, not %s + ''', + type_name(private_key) + )) + + if not isinstance(ciphertext, byte_cls): + raise TypeError(pretty_message( + ''' + ciphertext must be a byte string, not %s + ''', + type_name(ciphertext) + )) + + rsa = None + + try: + buffer_size = _evp_pkey_get_size(private_key.evp_pkey) + buffer = buffer_from_bytes(buffer_size) + + rsa = libcrypto.EVP_PKEY_get1_RSA(private_key.evp_pkey) + res = libcrypto.RSA_private_decrypt(len(ciphertext), ciphertext, buffer, rsa, padding) + handle_openssl_error(res) + + return bytes_from_buffer(buffer, res) + + finally: + if rsa: + libcrypto.RSA_free(rsa) + + +def rsa_pkcs1v15_verify(certificate_or_public_key, signature, data, hash_algorithm): + """ + Verifies an RSASSA-PKCS-v1.5 signature. + + When the hash_algorithm is "raw", the operation is identical to RSA + public key decryption. That is: the data is not hashed and no ASN.1 + structure with an algorithm identifier of the hash algorithm is placed in + the encrypted byte string. + + :param certificate_or_public_key: + A Certificate or PublicKey instance to verify the signature with + + :param signature: + A byte string of the signature to verify + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha224", "sha256", "sha384", + "sha512" or "raw" + + :raises: + oscrypto.errors.SignatureError - when the signature is determined to be invalid + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + if certificate_or_public_key.algorithm != 'rsa': + raise ValueError(pretty_message( + ''' + The key specified is not an RSA public key, but %s + ''', + certificate_or_public_key.algorithm.upper() + )) + + return _verify(certificate_or_public_key, signature, data, hash_algorithm) + + +def rsa_pss_verify(certificate_or_public_key, signature, data, hash_algorithm): + """ + Verifies an RSASSA-PSS signature. For the PSS padding the mask gen algorithm + will be mgf1 using the same hash algorithm as the signature. The salt length + with be the length of the hash algorithm, and the trailer field with be the + standard 0xBC byte. + + :param certificate_or_public_key: + A Certificate or PublicKey instance to verify the signature with + + :param signature: + A byte string of the signature to verify + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or "sha512" + + :raises: + oscrypto.errors.SignatureError - when the signature is determined to be invalid + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + cp_alg = certificate_or_public_key.algorithm + + if cp_alg != 'rsa' and cp_alg != 'rsassa_pss': + raise ValueError(pretty_message( + ''' + The key specified is not an RSA public key, but %s + ''', + certificate_or_public_key.algorithm.upper() + )) + + return _verify(certificate_or_public_key, signature, data, hash_algorithm, rsa_pss_padding=True) + + +def dsa_verify(certificate_or_public_key, signature, data, hash_algorithm): + """ + Verifies a DSA signature + + :param certificate_or_public_key: + A Certificate or PublicKey instance to verify the signature with + + :param signature: + A byte string of the signature to verify + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or "sha512" + + :raises: + oscrypto.errors.SignatureError - when the signature is determined to be invalid + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + if certificate_or_public_key.algorithm != 'dsa': + raise ValueError(pretty_message( + ''' + The key specified is not a DSA public key, but %s + ''', + certificate_or_public_key.algorithm.upper() + )) + + return _verify(certificate_or_public_key, signature, data, hash_algorithm) + + +def ecdsa_verify(certificate_or_public_key, signature, data, hash_algorithm): + """ + Verifies an ECDSA signature + + :param certificate_or_public_key: + A Certificate or PublicKey instance to verify the signature with + + :param signature: + A byte string of the signature to verify + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or "sha512" + + :raises: + oscrypto.errors.SignatureError - when the signature is determined to be invalid + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + if certificate_or_public_key.algorithm != 'ec': + raise ValueError(pretty_message( + ''' + The key specified is not an EC public key, but %s + ''', + certificate_or_public_key.algorithm.upper() + )) + + return _verify(certificate_or_public_key, signature, data, hash_algorithm) + + +def _verify(certificate_or_public_key, signature, data, hash_algorithm, rsa_pss_padding=False): + """ + Verifies an RSA, DSA or ECDSA signature + + :param certificate_or_public_key: + A Certificate or PublicKey instance to verify the signature with + + :param signature: + A byte string of the signature to verify + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or "sha512" + + :param rsa_pss_padding: + If the certificate_or_public_key is an RSA key, this enables PSS padding + + :raises: + oscrypto.errors.SignatureError - when the signature is determined to be invalid + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + if not isinstance(certificate_or_public_key, (Certificate, PublicKey)): + raise TypeError(pretty_message( + ''' + certificate_or_public_key must be an instance of the Certificate or + PublicKey class, not %s + ''', + type_name(certificate_or_public_key) + )) + + if not isinstance(signature, byte_cls): + raise TypeError(pretty_message( + ''' + signature must be a byte string, not %s + ''', + type_name(signature) + )) + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + cp_alg = certificate_or_public_key.algorithm + cp_is_rsa = cp_alg == 'rsa' or cp_alg == 'rsassa_pss' + + valid_hash_algorithms = set(['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']) + if cp_is_rsa and not rsa_pss_padding: + valid_hash_algorithms |= set(['raw']) + + if hash_algorithm not in valid_hash_algorithms: + valid_hash_algorithms_error = '"md5", "sha1", "sha224", "sha256", "sha384", "sha512"' + if cp_is_rsa and not rsa_pss_padding: + valid_hash_algorithms_error += ', "raw"' + raise ValueError(pretty_message( + ''' + hash_algorithm must be one of %s, not %s + ''', + valid_hash_algorithms_error, + repr(hash_algorithm) + )) + + if not cp_is_rsa and rsa_pss_padding: + raise ValueError(pretty_message( + ''' + PSS padding can only be used with RSA keys - the key provided is a + %s key + ''', + cp_alg.upper() + )) + + if cp_is_rsa and hash_algorithm == 'raw': + if len(data) > certificate_or_public_key.byte_size - 11: + raise ValueError(pretty_message( + ''' + data must be 11 bytes shorter than the key size when + hash_algorithm is "raw" - key size is %s bytes, but data is + %s bytes long + ''', + certificate_or_public_key.byte_size, + len(data) + )) + + rsa = None + + try: + rsa = libcrypto.EVP_PKEY_get1_RSA(certificate_or_public_key.evp_pkey) + if is_null(rsa): + handle_openssl_error(0) + + buffer_size = _evp_pkey_get_size(certificate_or_public_key.evp_pkey) + decrypted_buffer = buffer_from_bytes(buffer_size) + decrypted_length = libcrypto.RSA_public_decrypt( + len(signature), + signature, + decrypted_buffer, + rsa, + LibcryptoConst.RSA_PKCS1_PADDING + ) + handle_openssl_error(decrypted_length) + + decrypted_bytes = bytes_from_buffer(decrypted_buffer, decrypted_length) + + if not constant_compare(data, decrypted_bytes): + raise SignatureError('Signature is invalid') + return + + finally: + if rsa: + libcrypto.RSA_free(rsa) + + evp_md_ctx = None + rsa = None + dsa = None + dsa_sig = None + ec_key = None + ecdsa_sig = None + + try: + if libcrypto_version_info < (1, 1): + evp_md_ctx = libcrypto.EVP_MD_CTX_create() + else: + evp_md_ctx = libcrypto.EVP_MD_CTX_new() + + evp_md = { + 'md5': libcrypto.EVP_md5, + 'sha1': libcrypto.EVP_sha1, + 'sha224': libcrypto.EVP_sha224, + 'sha256': libcrypto.EVP_sha256, + 'sha384': libcrypto.EVP_sha384, + 'sha512': libcrypto.EVP_sha512 + }[hash_algorithm]() + + if libcrypto_version_info < (1,): + if cp_is_rsa and rsa_pss_padding: + digest = getattr(hashlib, hash_algorithm)(data).digest() + + rsa = libcrypto.EVP_PKEY_get1_RSA(certificate_or_public_key.evp_pkey) + if is_null(rsa): + handle_openssl_error(0) + + buffer_size = _evp_pkey_get_size(certificate_or_public_key.evp_pkey) + decoded_buffer = buffer_from_bytes(buffer_size) + decoded_length = libcrypto.RSA_public_decrypt( + len(signature), + signature, + decoded_buffer, + rsa, + LibcryptoConst.RSA_NO_PADDING + ) + handle_openssl_error(decoded_length) + + res = libcrypto.RSA_verify_PKCS1_PSS( + rsa, + digest, + evp_md, + decoded_buffer, + LibcryptoConst.EVP_MD_CTX_FLAG_PSS_MDLEN + ) + + elif cp_is_rsa: + res = libcrypto.EVP_DigestInit_ex(evp_md_ctx, evp_md, null()) + handle_openssl_error(res) + + res = libcrypto.EVP_DigestUpdate(evp_md_ctx, data, len(data)) + handle_openssl_error(res) + + res = libcrypto.EVP_VerifyFinal( + evp_md_ctx, + signature, + len(signature), + certificate_or_public_key.evp_pkey + ) + + elif cp_alg == 'dsa': + digest = getattr(hashlib, hash_algorithm)(data).digest() + + signature_buffer = buffer_from_bytes(signature) + signature_pointer = buffer_pointer(signature_buffer) + dsa_sig = libcrypto.d2i_DSA_SIG(null(), signature_pointer, len(signature)) + if is_null(dsa_sig): + raise SignatureError('Signature is invalid') + + dsa = libcrypto.EVP_PKEY_get1_DSA(certificate_or_public_key.evp_pkey) + if is_null(dsa): + handle_openssl_error(0) + + res = libcrypto.DSA_do_verify(digest, len(digest), dsa_sig, dsa) + + elif cp_alg == 'ec': + digest = getattr(hashlib, hash_algorithm)(data).digest() + + signature_buffer = buffer_from_bytes(signature) + signature_pointer = buffer_pointer(signature_buffer) + ecdsa_sig = libcrypto.d2i_ECDSA_SIG(null(), signature_pointer, len(signature)) + if is_null(ecdsa_sig): + raise SignatureError('Signature is invalid') + + ec_key = libcrypto.EVP_PKEY_get1_EC_KEY(certificate_or_public_key.evp_pkey) + if is_null(ec_key): + handle_openssl_error(0) + + res = libcrypto.ECDSA_do_verify(digest, len(digest), ecdsa_sig, ec_key) + + else: + evp_pkey_ctx_pointer_pointer = new(libcrypto, 'EVP_PKEY_CTX **') + res = libcrypto.EVP_DigestVerifyInit( + evp_md_ctx, + evp_pkey_ctx_pointer_pointer, + evp_md, + null(), + certificate_or_public_key.evp_pkey + ) + handle_openssl_error(res) + evp_pkey_ctx_pointer = unwrap(evp_pkey_ctx_pointer_pointer) + + if rsa_pss_padding: + # Enable PSS padding + res = libcrypto.EVP_PKEY_CTX_ctrl( + evp_pkey_ctx_pointer, + LibcryptoConst.EVP_PKEY_RSA, + -1, # All operations + LibcryptoConst.EVP_PKEY_CTRL_RSA_PADDING, + LibcryptoConst.RSA_PKCS1_PSS_PADDING, + null() + ) + handle_openssl_error(res) + + # Use the hash algorithm output length as the salt length + if libcrypto_version_info < (3, 0): + res = libcrypto.EVP_PKEY_CTX_ctrl( + evp_pkey_ctx_pointer, + LibcryptoConst.EVP_PKEY_RSA, + LibcryptoConst.EVP_PKEY_OP_SIGN | LibcryptoConst.EVP_PKEY_OP_VERIFY, + LibcryptoConst.EVP_PKEY_CTRL_RSA_PSS_SALTLEN, + -1, + null() + ) + handle_openssl_error(res) + + res = libcrypto.EVP_DigestUpdate(evp_md_ctx, data, len(data)) + handle_openssl_error(res) + + res = libcrypto.EVP_DigestVerifyFinal(evp_md_ctx, signature, len(signature)) + + if res < 1: + raise SignatureError('Signature is invalid') + handle_openssl_error(res) + + finally: + if evp_md_ctx: + if libcrypto_version_info < (1, 1): + libcrypto.EVP_MD_CTX_destroy(evp_md_ctx) + else: + libcrypto.EVP_MD_CTX_free(evp_md_ctx) + if rsa: + libcrypto.RSA_free(rsa) + if dsa: + libcrypto.DSA_free(dsa) + if dsa_sig: + libcrypto.DSA_SIG_free(dsa_sig) + if ec_key: + libcrypto.EC_KEY_free(ec_key) + if ecdsa_sig: + libcrypto.ECDSA_SIG_free(ecdsa_sig) + + +def rsa_pkcs1v15_sign(private_key, data, hash_algorithm): + """ + Generates an RSASSA-PKCS-v1.5 signature. + + When the hash_algorithm is "raw", the operation is identical to RSA + private key encryption. That is: the data is not hashed and no ASN.1 + structure with an algorithm identifier of the hash algorithm is placed in + the encrypted byte string. + + :param private_key: + The PrivateKey to generate the signature with + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha224", "sha256", "sha384", + "sha512" or "raw" + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the signature + """ + + if private_key.algorithm != 'rsa': + raise ValueError(pretty_message( + ''' + The key specified is not an RSA private key, but %s + ''', + private_key.algorithm.upper() + )) + + return _sign(private_key, data, hash_algorithm) + + +def rsa_pss_sign(private_key, data, hash_algorithm): + """ + Generates an RSASSA-PSS signature. For the PSS padding the mask gen + algorithm will be mgf1 using the same hash algorithm as the signature. The + salt length with be the length of the hash algorithm, and the trailer field + with be the standard 0xBC byte. + + :param private_key: + The PrivateKey to generate the signature with + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or "sha512" + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the signature + """ + + pkey_alg = private_key.algorithm + + if pkey_alg != 'rsa' and pkey_alg != 'rsassa_pss': + raise ValueError(pretty_message( + ''' + The key specified is not an RSA private key, but %s + ''', + pkey_alg.upper() + )) + + return _sign(private_key, data, hash_algorithm, rsa_pss_padding=True) + + +def dsa_sign(private_key, data, hash_algorithm): + """ + Generates a DSA signature + + :param private_key: + The PrivateKey to generate the signature with + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or "sha512" + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the signature + """ + + if private_key.algorithm != 'dsa': + raise ValueError(pretty_message( + ''' + The key specified is not a DSA private key, but %s + ''', + private_key.algorithm.upper() + )) + + return _sign(private_key, data, hash_algorithm) + + +def ecdsa_sign(private_key, data, hash_algorithm): + """ + Generates an ECDSA signature + + :param private_key: + The PrivateKey to generate the signature with + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or "sha512" + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the signature + """ + + if private_key.algorithm != 'ec': + raise ValueError(pretty_message( + ''' + The key specified is not an EC private key, but %s + ''', + private_key.algorithm.upper() + )) + + return _sign(private_key, data, hash_algorithm) + + +def _sign(private_key, data, hash_algorithm, rsa_pss_padding=False): + """ + Generates an RSA, DSA or ECDSA signature + + :param private_key: + The PrivateKey to generate the signature with + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or "sha512" + + :param rsa_pss_padding: + If the private_key is an RSA key, this enables PSS padding + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the signature + """ + + if not isinstance(private_key, PrivateKey): + raise TypeError(pretty_message( + ''' + private_key must be an instance of PrivateKey, not %s + ''', + type_name(private_key) + )) + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + pkey_alg = private_key.algorithm + pkey_is_rsa = pkey_alg == 'rsa' or pkey_alg == 'rsassa_pss' + + valid_hash_algorithms = set(['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']) + if pkey_alg == 'rsa' and not rsa_pss_padding: + valid_hash_algorithms |= set(['raw']) + + if hash_algorithm not in valid_hash_algorithms: + valid_hash_algorithms_error = '"md5", "sha1", "sha224", "sha256", "sha384", "sha512"' + if pkey_is_rsa and not rsa_pss_padding: + valid_hash_algorithms_error += ', "raw"' + raise ValueError(pretty_message( + ''' + hash_algorithm must be one of %s, not %s + ''', + valid_hash_algorithms_error, + repr(hash_algorithm) + )) + + if not pkey_is_rsa and rsa_pss_padding: + raise ValueError(pretty_message( + ''' + PSS padding can only be used with RSA keys - the key provided is a + %s key + ''', + pkey_alg.upper() + )) + + if pkey_is_rsa and hash_algorithm == 'raw': + if len(data) > private_key.byte_size - 11: + raise ValueError(pretty_message( + ''' + data must be 11 bytes shorter than the key size when + hash_algorithm is "raw" - key size is %s bytes, but data is + %s bytes long + ''', + private_key.byte_size, + len(data) + )) + + rsa = None + + try: + rsa = libcrypto.EVP_PKEY_get1_RSA(private_key.evp_pkey) + if is_null(rsa): + handle_openssl_error(0) + + buffer_size = _evp_pkey_get_size(private_key.evp_pkey) + + signature_buffer = buffer_from_bytes(buffer_size) + signature_length = libcrypto.RSA_private_encrypt( + len(data), + data, + signature_buffer, + rsa, + LibcryptoConst.RSA_PKCS1_PADDING + ) + handle_openssl_error(signature_length) + + return bytes_from_buffer(signature_buffer, signature_length) + + finally: + if rsa: + libcrypto.RSA_free(rsa) + + evp_md_ctx = None + rsa = None + dsa = None + dsa_sig = None + ec_key = None + ecdsa_sig = None + + try: + if libcrypto_version_info < (1, 1): + evp_md_ctx = libcrypto.EVP_MD_CTX_create() + else: + evp_md_ctx = libcrypto.EVP_MD_CTX_new() + + evp_md = { + 'md5': libcrypto.EVP_md5, + 'sha1': libcrypto.EVP_sha1, + 'sha224': libcrypto.EVP_sha224, + 'sha256': libcrypto.EVP_sha256, + 'sha384': libcrypto.EVP_sha384, + 'sha512': libcrypto.EVP_sha512 + }[hash_algorithm]() + + if libcrypto_version_info < (1,): + if pkey_is_rsa and rsa_pss_padding: + digest = getattr(hashlib, hash_algorithm)(data).digest() + + rsa = libcrypto.EVP_PKEY_get1_RSA(private_key.evp_pkey) + if is_null(rsa): + handle_openssl_error(0) + + buffer_size = _evp_pkey_get_size(private_key.evp_pkey) + em_buffer = buffer_from_bytes(buffer_size) + res = libcrypto.RSA_padding_add_PKCS1_PSS( + rsa, + em_buffer, + digest, + evp_md, + LibcryptoConst.EVP_MD_CTX_FLAG_PSS_MDLEN + ) + handle_openssl_error(res) + + signature_buffer = buffer_from_bytes(buffer_size) + signature_length = libcrypto.RSA_private_encrypt( + buffer_size, + em_buffer, + signature_buffer, + rsa, + LibcryptoConst.RSA_NO_PADDING + ) + handle_openssl_error(signature_length) + + elif pkey_is_rsa: + buffer_size = _evp_pkey_get_size(private_key.evp_pkey) + signature_buffer = buffer_from_bytes(buffer_size) + signature_length = new(libcrypto, 'unsigned int *') + + res = libcrypto.EVP_DigestInit_ex(evp_md_ctx, evp_md, null()) + handle_openssl_error(res) + + res = libcrypto.EVP_DigestUpdate(evp_md_ctx, data, len(data)) + handle_openssl_error(res) + + res = libcrypto.EVP_SignFinal( + evp_md_ctx, + signature_buffer, + signature_length, + private_key.evp_pkey + ) + handle_openssl_error(res) + + signature_length = deref(signature_length) + + elif pkey_alg == 'dsa': + digest = getattr(hashlib, hash_algorithm)(data).digest() + + dsa = libcrypto.EVP_PKEY_get1_DSA(private_key.evp_pkey) + if is_null(dsa): + handle_openssl_error(0) + + dsa_sig = libcrypto.DSA_do_sign(digest, len(digest), dsa) + if is_null(dsa_sig): + handle_openssl_error(0) + + buffer_size = libcrypto.i2d_DSA_SIG(dsa_sig, null()) + signature_buffer = buffer_from_bytes(buffer_size) + signature_pointer = buffer_pointer(signature_buffer) + signature_length = libcrypto.i2d_DSA_SIG(dsa_sig, signature_pointer) + handle_openssl_error(signature_length) + + elif pkey_alg == 'ec': + digest = getattr(hashlib, hash_algorithm)(data).digest() + + ec_key = libcrypto.EVP_PKEY_get1_EC_KEY(private_key.evp_pkey) + if is_null(ec_key): + handle_openssl_error(0) + + ecdsa_sig = libcrypto.ECDSA_do_sign(digest, len(digest), ec_key) + if is_null(ecdsa_sig): + handle_openssl_error(0) + + buffer_size = libcrypto.i2d_ECDSA_SIG(ecdsa_sig, null()) + signature_buffer = buffer_from_bytes(buffer_size) + signature_pointer = buffer_pointer(signature_buffer) + signature_length = libcrypto.i2d_ECDSA_SIG(ecdsa_sig, signature_pointer) + handle_openssl_error(signature_length) + + else: + buffer_size = _evp_pkey_get_size(private_key.evp_pkey) + signature_buffer = buffer_from_bytes(buffer_size) + signature_length = new(libcrypto, 'size_t *', buffer_size) + + evp_pkey_ctx_pointer_pointer = new(libcrypto, 'EVP_PKEY_CTX **') + res = libcrypto.EVP_DigestSignInit( + evp_md_ctx, + evp_pkey_ctx_pointer_pointer, + evp_md, + null(), + private_key.evp_pkey + ) + handle_openssl_error(res) + evp_pkey_ctx_pointer = unwrap(evp_pkey_ctx_pointer_pointer) + + if rsa_pss_padding: + # Enable PSS padding + res = libcrypto.EVP_PKEY_CTX_ctrl( + evp_pkey_ctx_pointer, + LibcryptoConst.EVP_PKEY_RSA, + -1, # All operations + LibcryptoConst.EVP_PKEY_CTRL_RSA_PADDING, + LibcryptoConst.RSA_PKCS1_PSS_PADDING, + null() + ) + handle_openssl_error(res) + + # Use the hash algorithm output length as the salt length + if libcrypto_version_info < (3, 0): + res = libcrypto.EVP_PKEY_CTX_ctrl( + evp_pkey_ctx_pointer, + LibcryptoConst.EVP_PKEY_RSA, + LibcryptoConst.EVP_PKEY_OP_SIGN | LibcryptoConst.EVP_PKEY_OP_VERIFY, + LibcryptoConst.EVP_PKEY_CTRL_RSA_PSS_SALTLEN, + -1, + null() + ) + handle_openssl_error(res) + + res = libcrypto.EVP_DigestUpdate(evp_md_ctx, data, len(data)) + handle_openssl_error(res) + + res = libcrypto.EVP_DigestSignFinal(evp_md_ctx, signature_buffer, signature_length) + handle_openssl_error(res) + + signature_length = deref(signature_length) + + return bytes_from_buffer(signature_buffer, signature_length) + + finally: + if evp_md_ctx: + if libcrypto_version_info < (1, 1): + libcrypto.EVP_MD_CTX_destroy(evp_md_ctx) + else: + libcrypto.EVP_MD_CTX_free(evp_md_ctx) + if rsa: + libcrypto.RSA_free(rsa) + if dsa: + libcrypto.DSA_free(dsa) + if dsa_sig: + libcrypto.DSA_SIG_free(dsa_sig) + if ec_key: + libcrypto.EC_KEY_free(ec_key) + if ecdsa_sig: + libcrypto.ECDSA_SIG_free(ecdsa_sig) diff --git a/tasks/lib/package_control/deps/oscrypto/_openssl/symmetric.py b/tasks/lib/package_control/deps/oscrypto/_openssl/symmetric.py new file mode 100644 index 0000000..d390f89 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_openssl/symmetric.py @@ -0,0 +1,845 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import math + +from .._errors import pretty_message +from .._ffi import new, null, is_null, buffer_from_bytes, bytes_from_buffer, deref +from ._libcrypto import libcrypto, libcrypto_legacy_support, LibcryptoConst, handle_openssl_error +from ..util import rand_bytes +from .._types import type_name, byte_cls + + +__all__ = [ + 'aes_cbc_no_padding_decrypt', + 'aes_cbc_no_padding_encrypt', + 'aes_cbc_pkcs7_decrypt', + 'aes_cbc_pkcs7_encrypt', + 'des_cbc_pkcs5_decrypt', + 'des_cbc_pkcs5_encrypt', + 'rc2_cbc_pkcs5_decrypt', + 'rc2_cbc_pkcs5_encrypt', + 'rc4_decrypt', + 'rc4_encrypt', + 'tripledes_cbc_pkcs5_decrypt', + 'tripledes_cbc_pkcs5_encrypt', +] + + +def aes_cbc_no_padding_encrypt(key, data, iv): + """ + Encrypts plaintext using AES in CBC mode with a 128, 192 or 256 bit key and + no padding. This means the ciphertext must be an exact multiple of 16 bytes + long. + + :param key: + The encryption key - a byte string either 16, 24 or 32 bytes long + + :param data: + The plaintext - a byte string + + :param iv: + The initialization vector - either a byte string 16-bytes long or None + to generate an IV + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by OpenSSL + + :return: + A tuple of two byte strings (iv, ciphertext) + """ + + cipher = _calculate_aes_cipher(key) + + if not iv: + iv = rand_bytes(16) + elif len(iv) != 16: + raise ValueError(pretty_message( + ''' + iv must be 16 bytes long - is %s + ''', + len(iv) + )) + + if len(data) % 16 != 0: + raise ValueError(pretty_message( + ''' + data must be a multiple of 16 bytes long - is %s + ''', + len(data) + )) + + return (iv, _encrypt(cipher, key, data, iv, False)) + + +def aes_cbc_no_padding_decrypt(key, data, iv): + """ + Decrypts AES ciphertext in CBC mode using a 128, 192 or 256 bit key and no + padding. + + :param key: + The encryption key - a byte string either 16, 24 or 32 bytes long + + :param data: + The ciphertext - a byte string + + :param iv: + The initialization vector - a byte string 16-bytes long + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by OpenSSL + + :return: + A byte string of the plaintext + """ + + cipher = _calculate_aes_cipher(key) + + if len(iv) != 16: + raise ValueError(pretty_message( + ''' + iv must be 16 bytes long - is %s + ''', + len(iv) + )) + + return _decrypt(cipher, key, data, iv, False) + + +def aes_cbc_pkcs7_encrypt(key, data, iv): + """ + Encrypts plaintext using AES in CBC mode with a 128, 192 or 256 bit key and + PKCS#7 padding. + + :param key: + The encryption key - a byte string either 16, 24 or 32 bytes long + + :param data: + The plaintext - a byte string + + :param iv: + The initialization vector - either a byte string 16-bytes long or None + to generate an IV + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by OpenSSL + + :return: + A tuple of two byte strings (iv, ciphertext) + """ + + cipher = _calculate_aes_cipher(key) + + if not iv: + iv = rand_bytes(16) + elif len(iv) != 16: + raise ValueError(pretty_message( + ''' + iv must be 16 bytes long - is %s + ''', + len(iv) + )) + + return (iv, _encrypt(cipher, key, data, iv, True)) + + +def aes_cbc_pkcs7_decrypt(key, data, iv): + """ + Decrypts AES ciphertext in CBC mode using a 128, 192 or 256 bit key + + :param key: + The encryption key - a byte string either 16, 24 or 32 bytes long + + :param data: + The ciphertext - a byte string + + :param iv: + The initialization vector - a byte string 16-bytes long + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by OpenSSL + + :return: + A byte string of the plaintext + """ + + cipher = _calculate_aes_cipher(key) + + if len(iv) != 16: + raise ValueError(pretty_message( + ''' + iv must be 16 bytes long - is %s + ''', + len(iv) + )) + + return _decrypt(cipher, key, data, iv, True) + + +def _calculate_aes_cipher(key): + """ + Determines if the key is a valid AES 128, 192 or 256 key + + :param key: + A byte string of the key to use + + :raises: + ValueError - when an invalid key is provided + + :return: + A unicode string of the AES variation - "aes128", "aes192" or "aes256" + """ + + if len(key) not in [16, 24, 32]: + raise ValueError(pretty_message( + ''' + key must be either 16, 24 or 32 bytes (128, 192 or 256 bits) + long - is %s + ''', + len(key) + )) + + if len(key) == 16: + cipher = 'aes128' + elif len(key) == 24: + cipher = 'aes192' + elif len(key) == 32: + cipher = 'aes256' + + return cipher + + +def rc4_encrypt(key, data): + """ + Encrypts plaintext using RC4 with a 40-128 bit key + + :param key: + The encryption key - a byte string 5-16 bytes long + + :param data: + The plaintext - a byte string + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by OpenSSL + + :return: + A byte string of the ciphertext + """ + + if not libcrypto_legacy_support: + raise EnvironmentError('OpenSSL has been compiled without RC4 support') + + if len(key) < 5 or len(key) > 16: + raise ValueError(pretty_message( + ''' + key must be 5 to 16 bytes (40 to 128 bits) long - is %s + ''', + len(key) + )) + + return _encrypt('rc4', key, data, None, None) + + +def rc4_decrypt(key, data): + """ + Decrypts RC4 ciphertext using a 40-128 bit key + + :param key: + The encryption key - a byte string 5-16 bytes long + + :param data: + The ciphertext - a byte string + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by OpenSSL + + :return: + A byte string of the plaintext + """ + + if not libcrypto_legacy_support: + raise EnvironmentError('OpenSSL has been compiled without RC4 support') + + if len(key) < 5 or len(key) > 16: + raise ValueError(pretty_message( + ''' + key must be 5 to 16 bytes (40 to 128 bits) long - is %s + ''', + len(key) + )) + + return _decrypt('rc4', key, data, None, None) + + +def rc2_cbc_pkcs5_encrypt(key, data, iv): + """ + Encrypts plaintext using RC2 in CBC mode with a 40-128 bit key and PKCS#5 + padding. + + :param key: + The encryption key - a byte string 8 bytes long + + :param data: + The plaintext - a byte string + + :param iv: + The initialization vector - a byte string 8-bytes long or None + to generate an IV + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by OpenSSL + + :return: + A tuple of two byte strings (iv, ciphertext) + """ + + if not libcrypto_legacy_support: + raise EnvironmentError('OpenSSL has been compiled without RC2 support') + + if len(key) < 5 or len(key) > 16: + raise ValueError(pretty_message( + ''' + key must be 5 to 16 bytes (40 to 128 bits) long - is %s + ''', + len(key) + )) + + if not iv: + iv = rand_bytes(8) + elif len(iv) != 8: + raise ValueError(pretty_message( + ''' + iv must be 8 bytes long - is %s + ''', + len(iv) + )) + + return (iv, _encrypt('rc2', key, data, iv, True)) + + +def rc2_cbc_pkcs5_decrypt(key, data, iv): + """ + Decrypts RC2 ciphertext ib CBC mode using a 40-128 bit key and PKCS#5 + padding. + + :param key: + The encryption key - a byte string 8 bytes long + + :param data: + The ciphertext - a byte string + + :param iv: + The initialization vector - a byte string 8 bytes long + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by OpenSSL + + :return: + A byte string of the plaintext + """ + + if not libcrypto_legacy_support: + raise EnvironmentError('OpenSSL has been compiled without RC2 support') + + if len(key) < 5 or len(key) > 16: + raise ValueError(pretty_message( + ''' + key must be 5 to 16 bytes (40 to 128 bits) long - is %s + ''', + len(key) + )) + + if len(iv) != 8: + raise ValueError(pretty_message( + ''' + iv must be 8 bytes long - is %s + ''', + len(iv) + )) + + return _decrypt('rc2', key, data, iv, True) + + +def tripledes_cbc_pkcs5_encrypt(key, data, iv): + """ + Encrypts plaintext using 3DES in CBC mode using either the 2 or 3 key + variant (16 or 24 byte long key) and PKCS#5 padding. + + :param key: + The encryption key - a byte string 16 or 24 bytes long (2 or 3 key mode) + + :param data: + The plaintext - a byte string + + :param iv: + The initialization vector - a byte string 8-bytes long or None + to generate an IV + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by OpenSSL + + :return: + A tuple of two byte strings (iv, ciphertext) + """ + + if len(key) != 16 and len(key) != 24: + raise ValueError(pretty_message( + ''' + key must be 16 bytes (2 key) or 24 bytes (3 key) long - %s + ''', + len(key) + )) + + if not iv: + iv = rand_bytes(8) + elif len(iv) != 8: + raise ValueError(pretty_message( + ''' + iv must be 8 bytes long - %s + ''', + len(iv) + )) + + cipher = 'tripledes_3key' + # Expand 2-key to actual 24 byte byte string used by cipher + if len(key) == 16: + key = key + key[0:8] + cipher = 'tripledes_2key' + + return (iv, _encrypt(cipher, key, data, iv, True)) + + +def tripledes_cbc_pkcs5_decrypt(key, data, iv): + """ + Decrypts 3DES ciphertext in CBC mode using either the 2 or 3 key variant + (16 or 24 byte long key) and PKCS#5 padding. + + :param key: + The encryption key - a byte string 16 or 24 bytes long (2 or 3 key mode) + + :param data: + The ciphertext - a byte string + + :param iv: + The initialization vector - a byte string 8-bytes long + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by OpenSSL + + :return: + A byte string of the plaintext + """ + + if len(key) != 16 and len(key) != 24: + raise ValueError(pretty_message( + ''' + key must be 16 bytes (2 key) or 24 bytes (3 key) long - is %s + ''', + len(key) + )) + + if len(iv) != 8: + raise ValueError(pretty_message( + ''' + iv must be 8 bytes long - is %s + ''', + len(iv) + )) + + cipher = 'tripledes_3key' + # Expand 2-key to actual 24 byte byte string used by cipher + if len(key) == 16: + key = key + key[0:8] + cipher = 'tripledes_2key' + + return _decrypt(cipher, key, data, iv, True) + + +def des_cbc_pkcs5_encrypt(key, data, iv): + """ + Encrypts plaintext using DES in CBC mode with a 56 bit key and PKCS#5 + padding. + + :param key: + The encryption key - a byte string 8 bytes long (includes error correction bits) + + :param data: + The plaintext - a byte string + + :param iv: + The initialization vector - a byte string 8-bytes long or None + to generate an IV + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by OpenSSL + + :return: + A tuple of two byte strings (iv, ciphertext) + """ + + if not libcrypto_legacy_support: + raise EnvironmentError('OpenSSL has been compiled without DES support') + + if len(key) != 8: + raise ValueError(pretty_message( + ''' + key must be 8 bytes (56 bits + 8 parity bits) long - is %s + ''', + len(key) + )) + + if not iv: + iv = rand_bytes(8) + elif len(iv) != 8: + raise ValueError(pretty_message( + ''' + iv must be 8 bytes long - is %s + ''', + len(iv) + )) + + return (iv, _encrypt('des', key, data, iv, True)) + + +def des_cbc_pkcs5_decrypt(key, data, iv): + """ + Decrypts DES ciphertext in CBC mode using a 56 bit key and PKCS#5 padding. + + :param key: + The encryption key - a byte string 8 bytes long (includes error correction bits) + + :param data: + The ciphertext - a byte string + + :param iv: + The initialization vector - a byte string 8-bytes long + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by OpenSSL + + :return: + A byte string of the plaintext + """ + + if not libcrypto_legacy_support: + raise EnvironmentError('OpenSSL has been compiled without DES support') + + if len(key) != 8: + raise ValueError(pretty_message( + ''' + key must be 8 bytes (56 bits + 8 parity bits) long - is %s + ''', + len(key) + )) + + if len(iv) != 8: + raise ValueError(pretty_message( + ''' + iv must be 8 bytes long - is %s + ''', + len(iv) + )) + + return _decrypt('des', key, data, iv, True) + + +def _encrypt(cipher, key, data, iv, padding): + """ + Encrypts plaintext + + :param cipher: + A unicode string of "aes128", "aes192", "aes256", "des", + "tripledes_2key", "tripledes_3key", "rc2", "rc4" + + :param key: + The encryption key - a byte string 5-32 bytes long + + :param data: + The plaintext - a byte string + + :param iv: + The initialization vector - a byte string - unused for RC4 + + :param padding: + Boolean, if padding should be used - unused for RC4 + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by OpenSSL + + :return: + A byte string of the ciphertext + """ + + if not isinstance(key, byte_cls): + raise TypeError(pretty_message( + ''' + key must be a byte string, not %s + ''', + type_name(key) + )) + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + if cipher != 'rc4' and not isinstance(iv, byte_cls): + raise TypeError(pretty_message( + ''' + iv must be a byte string, not %s + ''', + type_name(iv) + )) + + if cipher != 'rc4' and not padding: + # AES in CBC mode can be allowed with no padding if + # the data is an exact multiple of the block size + is_aes = cipher in set(['aes128', 'aes192', 'aes256']) + if not is_aes or (is_aes and (len(data) % 16) != 0): + raise ValueError('padding must be specified') + + evp_cipher_ctx = None + + try: + evp_cipher_ctx = libcrypto.EVP_CIPHER_CTX_new() + if is_null(evp_cipher_ctx): + handle_openssl_error(0) + + evp_cipher, buffer_size = _setup_evp_encrypt_decrypt(cipher, data) + + if iv is None: + iv = null() + + if cipher in set(['rc2', 'rc4']): + res = libcrypto.EVP_EncryptInit_ex(evp_cipher_ctx, evp_cipher, null(), null(), null()) + handle_openssl_error(res) + res = libcrypto.EVP_CIPHER_CTX_set_key_length(evp_cipher_ctx, len(key)) + handle_openssl_error(res) + if cipher == 'rc2': + res = libcrypto.EVP_CIPHER_CTX_ctrl( + evp_cipher_ctx, + LibcryptoConst.EVP_CTRL_SET_RC2_KEY_BITS, + len(key) * 8, + null() + ) + handle_openssl_error(res) + evp_cipher = null() + + res = libcrypto.EVP_EncryptInit_ex(evp_cipher_ctx, evp_cipher, null(), key, iv) + handle_openssl_error(res) + + if padding is not None: + res = libcrypto.EVP_CIPHER_CTX_set_padding(evp_cipher_ctx, int(padding)) + handle_openssl_error(res) + + buffer = buffer_from_bytes(buffer_size) + output_length = new(libcrypto, 'int *') + + res = libcrypto.EVP_EncryptUpdate(evp_cipher_ctx, buffer, output_length, data, len(data)) + handle_openssl_error(res) + + output = bytes_from_buffer(buffer, deref(output_length)) + + res = libcrypto.EVP_EncryptFinal_ex(evp_cipher_ctx, buffer, output_length) + handle_openssl_error(res) + + output += bytes_from_buffer(buffer, deref(output_length)) + + return output + + finally: + if evp_cipher_ctx: + libcrypto.EVP_CIPHER_CTX_free(evp_cipher_ctx) + + +def _decrypt(cipher, key, data, iv, padding): + """ + Decrypts AES/RC4/RC2/3DES/DES ciphertext + + :param cipher: + A unicode string of "aes128", "aes192", "aes256", "des", + "tripledes_2key", "tripledes_3key", "rc2", "rc4" + + :param key: + The encryption key - a byte string 5-32 bytes long + + :param data: + The ciphertext - a byte string + + :param iv: + The initialization vector - a byte string - unused for RC4 + + :param padding: + Boolean, if padding should be used - unused for RC4 + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by OpenSSL + + :return: + A byte string of the plaintext + """ + + if not isinstance(key, byte_cls): + raise TypeError(pretty_message( + ''' + key must be a byte string, not %s + ''', + type_name(key) + )) + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + if cipher != 'rc4' and not isinstance(iv, byte_cls): + raise TypeError(pretty_message( + ''' + iv must be a byte string, not %s + ''', + type_name(iv) + )) + + if cipher not in set(['rc4', 'aes128', 'aes192', 'aes256']) and not padding: + raise ValueError('padding must be specified') + + evp_cipher_ctx = None + + try: + evp_cipher_ctx = libcrypto.EVP_CIPHER_CTX_new() + if is_null(evp_cipher_ctx): + handle_openssl_error(0) + + evp_cipher, buffer_size = _setup_evp_encrypt_decrypt(cipher, data) + + if iv is None: + iv = null() + + if cipher in set(['rc2', 'rc4']): + res = libcrypto.EVP_DecryptInit_ex(evp_cipher_ctx, evp_cipher, null(), null(), null()) + handle_openssl_error(res) + res = libcrypto.EVP_CIPHER_CTX_set_key_length(evp_cipher_ctx, len(key)) + handle_openssl_error(res) + if cipher == 'rc2': + res = libcrypto.EVP_CIPHER_CTX_ctrl( + evp_cipher_ctx, + LibcryptoConst.EVP_CTRL_SET_RC2_KEY_BITS, + len(key) * 8, + null() + ) + handle_openssl_error(res) + evp_cipher = null() + + res = libcrypto.EVP_DecryptInit_ex(evp_cipher_ctx, evp_cipher, null(), key, iv) + handle_openssl_error(res) + + if padding is not None: + res = libcrypto.EVP_CIPHER_CTX_set_padding(evp_cipher_ctx, int(padding)) + handle_openssl_error(res) + + buffer = buffer_from_bytes(buffer_size) + output_length = new(libcrypto, 'int *') + + res = libcrypto.EVP_DecryptUpdate(evp_cipher_ctx, buffer, output_length, data, len(data)) + handle_openssl_error(res) + + output = bytes_from_buffer(buffer, deref(output_length)) + + res = libcrypto.EVP_DecryptFinal_ex(evp_cipher_ctx, buffer, output_length) + handle_openssl_error(res) + + output += bytes_from_buffer(buffer, deref(output_length)) + + return output + + finally: + if evp_cipher_ctx: + libcrypto.EVP_CIPHER_CTX_free(evp_cipher_ctx) + + +def _setup_evp_encrypt_decrypt(cipher, data): + """ + Creates an EVP_CIPHER pointer object and determines the buffer size + necessary for the parameter specified. + + :param evp_cipher_ctx: + An EVP_CIPHER_CTX pointer + + :param cipher: + A unicode string of "aes128", "aes192", "aes256", "des", + "tripledes_2key", "tripledes_3key", "rc2", "rc4" + + :param key: + The key byte string + + :param data: + The plaintext or ciphertext as a byte string + + :param padding: + If padding is to be used + + :return: + A 2-element tuple with the first element being an EVP_CIPHER pointer + and the second being an integer that is the required buffer size + """ + + evp_cipher = { + 'aes128': libcrypto.EVP_aes_128_cbc, + 'aes192': libcrypto.EVP_aes_192_cbc, + 'aes256': libcrypto.EVP_aes_256_cbc, + 'rc2': libcrypto.EVP_rc2_cbc, + 'rc4': libcrypto.EVP_rc4, + 'des': libcrypto.EVP_des_cbc, + 'tripledes_2key': libcrypto.EVP_des_ede_cbc, + 'tripledes_3key': libcrypto.EVP_des_ede3_cbc, + }[cipher]() + + if cipher == 'rc4': + buffer_size = len(data) + else: + block_size = { + 'aes128': 16, + 'aes192': 16, + 'aes256': 16, + 'rc2': 8, + 'des': 8, + 'tripledes_2key': 8, + 'tripledes_3key': 8, + }[cipher] + buffer_size = block_size * int(math.ceil(len(data) / block_size)) + + return (evp_cipher, buffer_size) diff --git a/tasks/lib/package_control/deps/oscrypto/_openssl/tls.py b/tasks/lib/package_control/deps/oscrypto/_openssl/tls.py new file mode 100644 index 0000000..6f180f4 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_openssl/tls.py @@ -0,0 +1,1284 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys +import re +import socket as socket_ +import select +import numbers + +from ._libssl import libssl, LibsslConst +from ._libcrypto import libcrypto, libcrypto_version_info, handle_openssl_error, peek_openssl_error +from .. import _backend_config +from .._asn1 import Certificate as Asn1Certificate +from .._errors import pretty_message +from .._ffi import null, bytes_from_buffer, buffer_from_bytes, is_null, buffer_pointer +from .._types import type_name, str_cls, byte_cls, int_types +from ..errors import TLSError, TLSDisconnectError, TLSGracefulDisconnectError +from .._tls import ( + detect_client_auth_request, + extract_chain, + get_dh_params_length, + parse_session_info, + raise_client_auth, + raise_dh_params, + raise_disconnection, + raise_expired_not_yet_valid, + raise_handshake, + raise_hostname, + raise_no_issuer, + raise_protocol_error, + raise_protocol_version, + raise_self_signed, + raise_verification, + raise_weak_signature, + parse_tls_records, + parse_handshake_messages, +) +from .asymmetric import load_certificate, Certificate +from ..keys import parse_certificate +from ..trust_list import get_path + +if sys.version_info < (3,): + range = xrange # noqa + +if sys.version_info < (3, 7): + Pattern = re._pattern_type +else: + Pattern = re.Pattern + + +__all__ = [ + 'TLSSession', + 'TLSSocket', +] + + +_trust_list_path = _backend_config().get('trust_list_path') +_line_regex = re.compile(b'(\r\n|\r|\n)') +_PROTOCOL_MAP = { + 'SSLv2': LibsslConst.SSL_OP_NO_SSLv2, + 'SSLv3': LibsslConst.SSL_OP_NO_SSLv3, + 'TLSv1': LibsslConst.SSL_OP_NO_TLSv1, + 'TLSv1.1': LibsslConst.SSL_OP_NO_TLSv1_1, + 'TLSv1.2': LibsslConst.SSL_OP_NO_TLSv1_2, +} + + +def _homogenize_openssl3_error(error_tuple): + """ + Takes a 3-element tuple from peek_openssl_error() and modifies it + to handle the changes in OpenSSL 3.0. That release removed the + concept of an error function, meaning the second item in the tuple + will always be 0. + + :param error_tuple: + A 3-element tuple of integers + + :return: + A 3-element tuple of integers + """ + + if libcrypto_version_info < (3,): + return error_tuple + return (error_tuple[0], 0, error_tuple[2]) + + +class TLSSession(object): + """ + A TLS session object that multiple TLSSocket objects can share for the + sake of session reuse + """ + + _protocols = None + _ciphers = None + _manual_validation = None + _extra_trust_roots = None + _ssl_ctx = None + _ssl_session = None + + def __init__(self, protocol=None, manual_validation=False, extra_trust_roots=None): + """ + :param protocol: + A unicode string or set of unicode strings representing allowable + protocols to negotiate with the server: + + - "TLSv1.2" + - "TLSv1.1" + - "TLSv1" + - "SSLv3" + + Default is: {"TLSv1", "TLSv1.1", "TLSv1.2"} + + :param manual_validation: + If certificate and certificate path validation should be skipped + and left to the developer to implement + + :param extra_trust_roots: + A list containing one or more certificates to be treated as trust + roots, in one of the following formats: + - A byte string of the DER encoded certificate + - A unicode string of the certificate filename + - An asn1crypto.x509.Certificate object + - An oscrypto.asymmetric.Certificate object + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + if not isinstance(manual_validation, bool): + raise TypeError(pretty_message( + ''' + manual_validation must be a boolean, not %s + ''', + type_name(manual_validation) + )) + + self._manual_validation = manual_validation + + if protocol is None: + protocol = set(['TLSv1', 'TLSv1.1', 'TLSv1.2']) + + if isinstance(protocol, str_cls): + protocol = set([protocol]) + elif not isinstance(protocol, set): + raise TypeError(pretty_message( + ''' + protocol must be a unicode string or set of unicode strings, + not %s + ''', + type_name(protocol) + )) + + valid_protocols = set(['SSLv3', 'TLSv1', 'TLSv1.1', 'TLSv1.2']) + unsupported_protocols = protocol - valid_protocols + if unsupported_protocols: + raise ValueError(pretty_message( + ''' + protocol must contain only the unicode strings "SSLv3", "TLSv1", + "TLSv1.1", "TLSv1.2", not %s + ''', + repr(unsupported_protocols) + )) + + self._protocols = protocol + + self._extra_trust_roots = [] + if extra_trust_roots: + for extra_trust_root in extra_trust_roots: + if isinstance(extra_trust_root, Certificate): + extra_trust_root = extra_trust_root.asn1 + elif isinstance(extra_trust_root, byte_cls): + extra_trust_root = parse_certificate(extra_trust_root) + elif isinstance(extra_trust_root, str_cls): + with open(extra_trust_root, 'rb') as f: + extra_trust_root = parse_certificate(f.read()) + elif not isinstance(extra_trust_root, Asn1Certificate): + raise TypeError(pretty_message( + ''' + extra_trust_roots must be a list of byte strings, unicode + strings, asn1crypto.x509.Certificate objects or + oscrypto.asymmetric.Certificate objects, not %s + ''', + type_name(extra_trust_root) + )) + self._extra_trust_roots.append(extra_trust_root) + + ssl_ctx = None + try: + if libcrypto_version_info < (1, 1): + method = libssl.SSLv23_method() + else: + method = libssl.TLS_method() + ssl_ctx = libssl.SSL_CTX_new(method) + if is_null(ssl_ctx): + handle_openssl_error(0) + self._ssl_ctx = ssl_ctx + + libssl.SSL_CTX_set_timeout(ssl_ctx, 600) + + # Allow caching SSL sessions + libssl.SSL_CTX_ctrl( + ssl_ctx, + LibsslConst.SSL_CTRL_SET_SESS_CACHE_MODE, + LibsslConst.SSL_SESS_CACHE_CLIENT, + null() + ) + + if sys.platform in set(['win32', 'darwin']): + trust_list_path = _trust_list_path + if trust_list_path is None: + trust_list_path = get_path() + + if sys.platform == 'win32': + path_encoding = 'mbcs' + else: + path_encoding = 'utf-8' + result = libssl.SSL_CTX_load_verify_locations( + ssl_ctx, + trust_list_path.encode(path_encoding), + null() + ) + + else: + result = libssl.SSL_CTX_set_default_verify_paths(ssl_ctx) + handle_openssl_error(result) + + verify_mode = LibsslConst.SSL_VERIFY_NONE if manual_validation else LibsslConst.SSL_VERIFY_PEER + libssl.SSL_CTX_set_verify(ssl_ctx, verify_mode, null()) + + # Modern cipher suite list from https://wiki.mozilla.org/Security/Server_Side_TLS late August 2015 + result = libssl.SSL_CTX_set_cipher_list( + ssl_ctx, + ( + b'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:' + b'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:' + b'DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:' + b'kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:' + b'ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:' + b'ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:' + b'DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:' + b'DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:' + b'AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:' + b'AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:' + b'!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:' + b'!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA' + ) + ) + handle_openssl_error(result) + + disabled_protocols = set(['SSLv2']) + disabled_protocols |= (valid_protocols - self._protocols) + for disabled_protocol in disabled_protocols: + libssl.SSL_CTX_ctrl( + ssl_ctx, + LibsslConst.SSL_CTRL_OPTIONS, + _PROTOCOL_MAP[disabled_protocol], + null() + ) + + if self._extra_trust_roots: + x509_store = libssl.SSL_CTX_get_cert_store(ssl_ctx) + for cert in self._extra_trust_roots: + oscrypto_cert = load_certificate(cert) + result = libssl.X509_STORE_add_cert( + x509_store, + oscrypto_cert.x509 + ) + handle_openssl_error(result) + + except (Exception): + if ssl_ctx: + libssl.SSL_CTX_free(ssl_ctx) + self._ssl_ctx = None + raise + + def __del__(self): + if self._ssl_ctx: + libssl.SSL_CTX_free(self._ssl_ctx) + self._ssl_ctx = None + + if self._ssl_session: + libssl.SSL_SESSION_free(self._ssl_session) + self._ssl_session = None + + +class TLSSocket(object): + """ + A wrapper around a socket.socket that adds TLS + """ + + _socket = None + + # An oscrypto.tls.TLSSession object + _session = None + + # An OpenSSL SSL struct pointer + _ssl = None + + # OpenSSL memory bios used for reading/writing data to and + # from the socket + _rbio = None + _wbio = None + + # Size of _bio_write_buffer and _read_buffer + _buffer_size = 8192 + + # A buffer used to pull bytes out of the _wbio memory bio to + # be written to the socket + _bio_write_buffer = None + + # A buffer used to push bytes into the _rbio memory bio to + # be decrypted by OpenSSL + _read_buffer = None + + # Raw ciphertext from the socker that hasn't need fed to OpenSSL yet + _raw_bytes = None + + # Plaintext that has been decrypted, but not asked for yet + _decrypted_bytes = None + + _hostname = None + + _certificate = None + _intermediates = None + + _protocol = None + _cipher_suite = None + _compression = None + _session_id = None + _session_ticket = None + + # If we explicitly asked for the connection to be closed + _local_closed = False + + _gracefully_closed = False + + @classmethod + def wrap(cls, socket, hostname, session=None): + """ + Takes an existing socket and adds TLS + + :param socket: + A socket.socket object to wrap with TLS + + :param hostname: + A unicode string of the hostname or IP the socket is connected to + + :param session: + An existing TLSSession object to allow for session reuse, specific + protocol or manual certificate validation + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + if not isinstance(socket, socket_.socket): + raise TypeError(pretty_message( + ''' + socket must be an instance of socket.socket, not %s + ''', + type_name(socket) + )) + + if not isinstance(hostname, str_cls): + raise TypeError(pretty_message( + ''' + hostname must be a unicode string, not %s + ''', + type_name(hostname) + )) + + if session is not None and not isinstance(session, TLSSession): + raise TypeError(pretty_message( + ''' + session must be an instance of oscrypto.tls.TLSSession, not %s + ''', + type_name(session) + )) + + new_socket = cls(None, None, session=session) + new_socket._socket = socket + new_socket._hostname = hostname + new_socket._handshake() + + return new_socket + + def __init__(self, address, port, timeout=10, session=None): + """ + :param address: + A unicode string of the domain name or IP address to connect to + + :param port: + An integer of the port number to connect to + + :param timeout: + An integer timeout to use for the socket + + :param session: + An oscrypto.tls.TLSSession object to allow for session reuse and + controlling the protocols and validation performed + """ + + self._raw_bytes = b'' + self._decrypted_bytes = b'' + + if address is None and port is None: + self._socket = None + + else: + if not isinstance(address, str_cls): + raise TypeError(pretty_message( + ''' + address must be a unicode string, not %s + ''', + type_name(address) + )) + + if not isinstance(port, int_types): + raise TypeError(pretty_message( + ''' + port must be an integer, not %s + ''', + type_name(port) + )) + + if timeout is not None and not isinstance(timeout, numbers.Number): + raise TypeError(pretty_message( + ''' + timeout must be a number, not %s + ''', + type_name(timeout) + )) + + self._socket = socket_.create_connection((address, port), timeout) + self._socket.settimeout(timeout) + + if session is None: + session = TLSSession() + + elif not isinstance(session, TLSSession): + raise TypeError(pretty_message( + ''' + session must be an instance of oscrypto.tls.TLSSession, not %s + ''', + type_name(session) + )) + + self._session = session + + if self._socket: + self._hostname = address + self._handshake() + + def _handshake(self): + """ + Perform an initial TLS handshake + """ + + self._ssl = None + self._rbio = None + self._wbio = None + + try: + self._ssl = libssl.SSL_new(self._session._ssl_ctx) + if is_null(self._ssl): + self._ssl = None + handle_openssl_error(0) + + mem_bio = libssl.BIO_s_mem() + + self._rbio = libssl.BIO_new(mem_bio) + if is_null(self._rbio): + handle_openssl_error(0) + + self._wbio = libssl.BIO_new(mem_bio) + if is_null(self._wbio): + handle_openssl_error(0) + + libssl.SSL_set_bio(self._ssl, self._rbio, self._wbio) + + utf8_domain = self._hostname.encode('utf-8') + libssl.SSL_ctrl( + self._ssl, + LibsslConst.SSL_CTRL_SET_TLSEXT_HOSTNAME, + LibsslConst.TLSEXT_NAMETYPE_host_name, + utf8_domain + ) + + libssl.SSL_set_connect_state(self._ssl) + + if self._session._ssl_session: + libssl.SSL_set_session(self._ssl, self._session._ssl_session) + + self._bio_write_buffer = buffer_from_bytes(self._buffer_size) + self._read_buffer = buffer_from_bytes(self._buffer_size) + + handshake_server_bytes = b'' + handshake_client_bytes = b'' + + while True: + result = libssl.SSL_do_handshake(self._ssl) + handshake_client_bytes += self._raw_write() + + if result == 1: + break + + error = libssl.SSL_get_error(self._ssl, result) + if error == LibsslConst.SSL_ERROR_WANT_READ: + chunk = self._raw_read() + if chunk == b'': + if handshake_server_bytes == b'': + raise_disconnection() + if detect_client_auth_request(handshake_server_bytes): + raise_client_auth() + raise_protocol_error(handshake_server_bytes) + handshake_server_bytes += chunk + + elif error == LibsslConst.SSL_ERROR_WANT_WRITE: + handshake_client_bytes += self._raw_write() + + elif error == LibsslConst.SSL_ERROR_ZERO_RETURN: + self._gracefully_closed = True + self._shutdown(False) + self._raise_closed() + + else: + info = peek_openssl_error() + + dh_key_info_1 = ( + LibsslConst.ERR_LIB_SSL, + LibsslConst.SSL_F_SSL3_CHECK_CERT_AND_ALGORITHM, + LibsslConst.SSL_R_DH_KEY_TOO_SMALL + ) + dh_key_info_1 = _homogenize_openssl3_error(dh_key_info_1) + + dh_key_info_2 = ( + LibsslConst.ERR_LIB_SSL, + LibsslConst.SSL_F_TLS_PROCESS_SKE_DHE, + LibsslConst.SSL_R_DH_KEY_TOO_SMALL + ) + dh_key_info_2 = _homogenize_openssl3_error(dh_key_info_2) + + dh_key_info_3 = ( + LibsslConst.ERR_LIB_SSL, + LibsslConst.SSL_F_SSL3_GET_KEY_EXCHANGE, + LibsslConst.SSL_R_BAD_DH_P_LENGTH + ) + dh_key_info_3 = _homogenize_openssl3_error(dh_key_info_3) + + if info == dh_key_info_1 or info == dh_key_info_2 or info == dh_key_info_3: + raise_dh_params() + + if libcrypto_version_info < (1, 1): + unknown_protocol_info = ( + LibsslConst.ERR_LIB_SSL, + LibsslConst.SSL_F_SSL23_GET_SERVER_HELLO, + LibsslConst.SSL_R_UNKNOWN_PROTOCOL + ) + else: + unknown_protocol_info = ( + LibsslConst.ERR_LIB_SSL, + LibsslConst.SSL_F_SSL3_GET_RECORD, + LibsslConst.SSL_R_WRONG_VERSION_NUMBER + ) + unknown_protocol_info = _homogenize_openssl3_error(unknown_protocol_info) + + if info == unknown_protocol_info: + raise_protocol_error(handshake_server_bytes) + + tls_version_info_error = ( + LibsslConst.ERR_LIB_SSL, + LibsslConst.SSL_F_SSL23_GET_SERVER_HELLO, + LibsslConst.SSL_R_TLSV1_ALERT_PROTOCOL_VERSION + ) + tls_version_info_error = _homogenize_openssl3_error(tls_version_info_error) + if info == tls_version_info_error: + raise_protocol_version() + + handshake_error_info = ( + LibsslConst.ERR_LIB_SSL, + LibsslConst.SSL_F_SSL23_GET_SERVER_HELLO, + LibsslConst.SSL_R_SSLV3_ALERT_HANDSHAKE_FAILURE + ) + # OpenSSL 3.0 no longer has func codes, so this can be confused + # with the following handler which needs to check for client auth + if libcrypto_version_info < (3, ) and info == handshake_error_info: + raise_handshake() + + handshake_failure_info = ( + LibsslConst.ERR_LIB_SSL, + LibsslConst.SSL_F_SSL3_READ_BYTES, + LibsslConst.SSL_R_SSLV3_ALERT_HANDSHAKE_FAILURE + ) + handshake_failure_info = _homogenize_openssl3_error(handshake_failure_info) + if info == handshake_failure_info: + saw_client_auth = False + for record_type, _, record_data in parse_tls_records(handshake_server_bytes): + if record_type != b'\x16': + continue + for message_type, message_data in parse_handshake_messages(record_data): + if message_type == b'\x0d': + saw_client_auth = True + break + if saw_client_auth: + raise_client_auth() + raise_handshake() + + if libcrypto_version_info < (1, 1): + cert_verify_failed_info = ( + LibsslConst.ERR_LIB_SSL, + LibsslConst.SSL_F_SSL3_GET_SERVER_CERTIFICATE, + LibsslConst.SSL_R_CERTIFICATE_VERIFY_FAILED + ) + else: + cert_verify_failed_info = ( + LibsslConst.ERR_LIB_SSL, + LibsslConst.SSL_F_TLS_PROCESS_SERVER_CERTIFICATE, + LibsslConst.SSL_R_CERTIFICATE_VERIFY_FAILED + ) + cert_verify_failed_info = _homogenize_openssl3_error(cert_verify_failed_info) + + # It would appear that some versions of OpenSSL (such as on Fedora 30) + # don't even have the MD5 digest algorithm included any longer? To + # give a more useful error message we handle this specifically. + unknown_hash_algo_info = ( + LibsslConst.ERR_LIB_ASN1, + LibsslConst.ASN1_F_ASN1_ITEM_VERIFY, + LibsslConst.ASN1_R_UNKNOWN_MESSAGE_DIGEST_ALGORITHM + ) + unknown_hash_algo_info = _homogenize_openssl3_error(unknown_hash_algo_info) + + if info == unknown_hash_algo_info: + chain = extract_chain(handshake_server_bytes) + if chain: + cert = chain[0] + oscrypto_cert = load_certificate(cert) + if oscrypto_cert.asn1.hash_algo in set(['md5', 'md2']): + raise_weak_signature(oscrypto_cert) + + if info == cert_verify_failed_info: + verify_result = libssl.SSL_get_verify_result(self._ssl) + chain = extract_chain(handshake_server_bytes) + + self_signed = False + time_invalid = False + no_issuer = False + cert = None + oscrypto_cert = None + + if chain: + cert = chain[0] + oscrypto_cert = load_certificate(cert) + self_signed = oscrypto_cert.self_signed + + issuer_error_codes = set([ + LibsslConst.X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT, + LibsslConst.X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN, + LibsslConst.X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY + ]) + if verify_result in issuer_error_codes: + no_issuer = not self_signed + + time_error_codes = set([ + LibsslConst.X509_V_ERR_CERT_HAS_EXPIRED, + LibsslConst.X509_V_ERR_CERT_NOT_YET_VALID + ]) + time_invalid = verify_result in time_error_codes + + if time_invalid: + raise_expired_not_yet_valid(cert) + if no_issuer: + raise_no_issuer(cert) + if self_signed: + raise_self_signed(cert) + if oscrypto_cert and oscrypto_cert.asn1.hash_algo in set(['md5', 'md2']): + raise_weak_signature(oscrypto_cert) + raise_verification(cert) + + handle_openssl_error(0, TLSError) + + session_info = parse_session_info( + handshake_server_bytes, + handshake_client_bytes + ) + self._protocol = session_info['protocol'] + self._cipher_suite = session_info['cipher_suite'] + self._compression = session_info['compression'] + self._session_id = session_info['session_id'] + self._session_ticket = session_info['session_ticket'] + + if self._cipher_suite.find('_DHE_') != -1: + dh_params_length = get_dh_params_length(handshake_server_bytes) + if dh_params_length < 1024: + self.close() + raise_dh_params() + + # When saving the session for future requests, we use + # SSL_get1_session() variant to increase the reference count. This + # prevents the session from being freed when one connection closes + # before another is opened. However, since we increase the ref + # count, we also have to explicitly free any previous session. + if self._session_id == 'new' or self._session_ticket == 'new': + if self._session._ssl_session: + libssl.SSL_SESSION_free(self._session._ssl_session) + self._session._ssl_session = libssl.SSL_get1_session(self._ssl) + + if not self._session._manual_validation: + if self.certificate.hash_algo in set(['md5', 'md2']): + raise_weak_signature(self.certificate) + + # OpenSSL does not do hostname or IP address checking in the end + # entity certificate, so we must perform that check + if not self.certificate.is_valid_domain_ip(self._hostname): + raise_hostname(self.certificate, self._hostname) + + except (OSError, socket_.error): + if self._ssl: + libssl.SSL_free(self._ssl) + self._ssl = None + self._rbio = None + self._wbio = None + # The BIOs are freed by SSL_free(), so we only need to free + # them if for some reason SSL_free() was not called + else: + if self._rbio: + libssl.BIO_free(self._rbio) + self._rbio = None + if self._wbio: + libssl.BIO_free(self._wbio) + self._wbio = None + self.close() + + raise + + def _raw_read(self): + """ + Reads data from the socket and writes it to the memory bio + used by libssl to decrypt the data. Returns the unencrypted + data for the purpose of debugging handshakes. + + :return: + A byte string of ciphertext from the socket. Used for + debugging the handshake only. + """ + + data = self._raw_bytes + try: + data += self._socket.recv(8192) + except (socket_.error): + pass + output = data + written = libssl.BIO_write(self._rbio, data, len(data)) + self._raw_bytes = data[written:] + return output + + def _raw_write(self): + """ + Takes ciphertext from the memory bio and writes it to the + socket. + + :return: + A byte string of ciphertext going to the socket. Used + for debugging the handshake only. + """ + + data_available = libssl.BIO_ctrl_pending(self._wbio) + if data_available == 0: + return b'' + to_read = min(self._buffer_size, data_available) + read = libssl.BIO_read(self._wbio, self._bio_write_buffer, to_read) + to_write = bytes_from_buffer(self._bio_write_buffer, read) + output = to_write + while len(to_write): + raise_disconnect = False + try: + sent = self._socket.send(to_write) + except (socket_.error) as e: + # Handle ECONNRESET and EPIPE + if e.errno == 104 or e.errno == 32: + raise_disconnect = True + # Handle EPROTOTYPE. Newer versions of macOS will return this + # if we try to call send() while the socket is being torn down + elif sys.platform == 'darwin' and e.errno == 41: + raise_disconnect = True + else: + raise + + if raise_disconnect: + raise_disconnection() + to_write = to_write[sent:] + if len(to_write): + self.select_write() + return output + + def read(self, max_length): + """ + Reads data from the TLS-wrapped socket + + :param max_length: + The number of bytes to read - output may be less than this + + :raises: + socket.socket - when a non-TLS socket error occurs + oscrypto.errors.TLSError - when a TLS-related error occurs + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the data read + """ + + if not isinstance(max_length, int_types): + raise TypeError(pretty_message( + ''' + max_length must be an integer, not %s + ''', + type_name(max_length) + )) + + buffered_length = len(self._decrypted_bytes) + + # If we already have enough buffered data, just use that + if buffered_length >= max_length: + output = self._decrypted_bytes[0:max_length] + self._decrypted_bytes = self._decrypted_bytes[max_length:] + return output + + if self._ssl is None: + self._raise_closed() + + # Don't block if we have buffered data available, since it is ok to + # return less than the max_length + if buffered_length > 0 and not self.select_read(0): + output = self._decrypted_bytes + self._decrypted_bytes = b'' + return output + + # Only read enough to get the requested amount when + # combined with buffered data + to_read = min(self._buffer_size, max_length - buffered_length) + + output = self._decrypted_bytes + + # The SSL_read() loop handles renegotiations, so we need to handle + # requests for both reads and writes + again = True + while again: + again = False + result = libssl.SSL_read(self._ssl, self._read_buffer, to_read) + self._raw_write() + if result <= 0: + + error = libssl.SSL_get_error(self._ssl, result) + if error == LibsslConst.SSL_ERROR_WANT_READ: + if self._raw_read() != b'': + again = True + continue + raise_disconnection() + + elif error == LibsslConst.SSL_ERROR_WANT_WRITE: + self._raw_write() + again = True + continue + + elif error == LibsslConst.SSL_ERROR_ZERO_RETURN: + self._gracefully_closed = True + self._shutdown(False) + break + + else: + handle_openssl_error(0, TLSError) + + output += bytes_from_buffer(self._read_buffer, result) + + if self._gracefully_closed and len(output) == 0: + self._raise_closed() + + self._decrypted_bytes = output[max_length:] + return output[0:max_length] + + def select_read(self, timeout=None): + """ + Blocks until the socket is ready to be read from, or the timeout is hit + + :param timeout: + A float - the period of time to wait for data to be read. None for + no time limit. + + :return: + A boolean - if data is ready to be read. Will only be False if + timeout is not None. + """ + + # If we have buffered data, we consider a read possible + if len(self._decrypted_bytes) > 0: + return True + + read_ready, _, _ = select.select([self._socket], [], [], timeout) + return len(read_ready) > 0 + + def read_until(self, marker): + """ + Reads data from the socket until a marker is found. Data read includes + the marker. + + :param marker: + A byte string or regex object from re.compile(). Used to determine + when to stop reading. Regex objects are more inefficient since + they must scan the entire byte string of read data each time data + is read off the socket. + + :return: + A byte string of the data read, including the marker + """ + + if not isinstance(marker, byte_cls) and not isinstance(marker, Pattern): + raise TypeError(pretty_message( + ''' + marker must be a byte string or compiled regex object, not %s + ''', + type_name(marker) + )) + + output = b'' + + is_regex = isinstance(marker, Pattern) + + while True: + if len(self._decrypted_bytes) > 0: + chunk = self._decrypted_bytes + self._decrypted_bytes = b'' + else: + if self._ssl is None: + self._raise_closed() + to_read = libssl.SSL_pending(self._ssl) or 8192 + chunk = self.read(to_read) + + offset = len(output) + output += chunk + + if is_regex: + match = marker.search(output) + if match is not None: + end = match.end() + break + else: + # If the marker was not found last time, we have to start + # at a position where the marker would have its final char + # in the newly read chunk + start = max(0, offset - len(marker) - 1) + match = output.find(marker, start) + if match != -1: + end = match + len(marker) + break + + self._decrypted_bytes = output[end:] + self._decrypted_bytes + return output[0:end] + + def read_line(self): + r""" + Reads a line from the socket, including the line ending of "\r\n", "\r", + or "\n" + + :return: + A byte string of the next line from the socket + """ + + return self.read_until(_line_regex) + + def read_exactly(self, num_bytes): + """ + Reads exactly the specified number of bytes from the socket + + :param num_bytes: + An integer - the exact number of bytes to read + + :return: + A byte string of the data that was read + """ + + output = b'' + remaining = num_bytes + while remaining > 0: + output += self.read(remaining) + remaining = num_bytes - len(output) + + return output + + def write(self, data): + """ + Writes data to the TLS-wrapped socket + + :param data: + A byte string to write to the socket + + :raises: + socket.socket - when a non-TLS socket error occurs + oscrypto.errors.TLSError - when a TLS-related error occurs + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + data_len = len(data) + while data_len: + if self._ssl is None: + self._raise_closed() + result = libssl.SSL_write(self._ssl, data, data_len) + self._raw_write() + if result <= 0: + + error = libssl.SSL_get_error(self._ssl, result) + if error == LibsslConst.SSL_ERROR_WANT_READ: + if self._raw_read() != b'': + continue + raise_disconnection() + + elif error == LibsslConst.SSL_ERROR_WANT_WRITE: + self._raw_write() + continue + + elif error == LibsslConst.SSL_ERROR_ZERO_RETURN: + self._gracefully_closed = True + self._shutdown(False) + self._raise_closed() + + else: + handle_openssl_error(0, TLSError) + + data = data[result:] + data_len = len(data) + + def select_write(self, timeout=None): + """ + Blocks until the socket is ready to be written to, or the timeout is hit + + :param timeout: + A float - the period of time to wait for the socket to be ready to + written to. None for no time limit. + + :return: + A boolean - if the socket is ready for writing. Will only be False + if timeout is not None. + """ + + _, write_ready, _ = select.select([], [self._socket], [], timeout) + return len(write_ready) > 0 + + def _shutdown(self, manual): + """ + Shuts down the TLS session and then shuts down the underlying socket + + :param manual: + A boolean if the connection was manually shutdown + """ + + if self._ssl is None: + return + + while True: + result = libssl.SSL_shutdown(self._ssl) + + # Don't be noisy if the socket is already closed + try: + self._raw_write() + except (TLSDisconnectError): + pass + + if result >= 0: + break + if result < 0: + error = libssl.SSL_get_error(self._ssl, result) + if error == LibsslConst.SSL_ERROR_WANT_READ: + if self._raw_read() != b'': + continue + else: + break + + elif error == LibsslConst.SSL_ERROR_WANT_WRITE: + self._raw_write() + continue + + else: + handle_openssl_error(0, TLSError) + + if manual: + self._local_closed = True + + libssl.SSL_free(self._ssl) + self._ssl = None + # BIOs are freed by SSL_free() + self._rbio = None + self._wbio = None + + try: + self._socket.shutdown(socket_.SHUT_RDWR) + except (socket_.error): + pass + + def shutdown(self): + """ + Shuts down the TLS session and then shuts down the underlying socket + """ + + self._shutdown(True) + + def close(self): + """ + Shuts down the TLS session and socket and forcibly closes it + """ + + try: + self.shutdown() + + finally: + if self._socket: + try: + self._socket.close() + except (socket_.error): + pass + self._socket = None + + def _read_certificates(self): + """ + Reads end-entity and intermediate certificate information from the + TLS session + """ + + stack_pointer = libssl.SSL_get_peer_cert_chain(self._ssl) + if is_null(stack_pointer): + handle_openssl_error(0, TLSError) + + if libcrypto_version_info < (1, 1): + number_certs = libssl.sk_num(stack_pointer) + else: + number_certs = libssl.OPENSSL_sk_num(stack_pointer) + + self._intermediates = [] + + for index in range(0, number_certs): + if libcrypto_version_info < (1, 1): + x509_ = libssl.sk_value(stack_pointer, index) + else: + x509_ = libssl.OPENSSL_sk_value(stack_pointer, index) + buffer_size = libcrypto.i2d_X509(x509_, null()) + cert_buffer = buffer_from_bytes(buffer_size) + cert_pointer = buffer_pointer(cert_buffer) + cert_length = libcrypto.i2d_X509(x509_, cert_pointer) + handle_openssl_error(cert_length) + cert_data = bytes_from_buffer(cert_buffer, cert_length) + + cert = Asn1Certificate.load(cert_data) + + if index == 0: + self._certificate = cert + else: + self._intermediates.append(cert) + + def _raise_closed(self): + """ + Raises an exception describing if the local or remote end closed the + connection + """ + + if self._local_closed: + raise TLSDisconnectError('The connection was already closed') + elif self._gracefully_closed: + raise TLSGracefulDisconnectError('The remote end closed the connection') + else: + raise TLSDisconnectError('The connection was closed') + + @property + def certificate(self): + """ + An asn1crypto.x509.Certificate object of the end-entity certificate + presented by the server + """ + + if self._ssl is None: + self._raise_closed() + + if self._certificate is None: + self._read_certificates() + + return self._certificate + + @property + def intermediates(self): + """ + A list of asn1crypto.x509.Certificate objects that were presented as + intermediates by the server + """ + + if self._ssl is None: + self._raise_closed() + + if self._certificate is None: + self._read_certificates() + + return self._intermediates + + @property + def cipher_suite(self): + """ + A unicode string of the IANA cipher suite name of the negotiated + cipher suite + """ + + return self._cipher_suite + + @property + def protocol(self): + """ + A unicode string of: "TLSv1.2", "TLSv1.1", "TLSv1", "SSLv3" + """ + + return self._protocol + + @property + def compression(self): + """ + A boolean if compression is enabled + """ + + return self._compression + + @property + def session_id(self): + """ + A unicode string of "new" or "reused" or None for no ticket + """ + + return self._session_id + + @property + def session_ticket(self): + """ + A unicode string of "new" or "reused" or None for no ticket + """ + + return self._session_ticket + + @property + def session(self): + """ + The oscrypto.tls.TLSSession object used for this connection + """ + + return self._session + + @property + def hostname(self): + """ + A unicode string of the TLS server domain name or IP address + """ + + return self._hostname + + @property + def port(self): + """ + An integer of the port number the socket is connected to + """ + + return self.socket.getpeername()[1] + + @property + def socket(self): + """ + The underlying socket.socket connection + """ + + if self._ssl is None: + self._raise_closed() + + return self._socket + + def __del__(self): + self.close() diff --git a/tasks/lib/package_control/deps/oscrypto/_openssl/util.py b/tasks/lib/package_control/deps/oscrypto/_openssl/util.py new file mode 100644 index 0000000..fa9bea8 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_openssl/util.py @@ -0,0 +1,243 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from .._errors import pretty_message +from .._ffi import buffer_from_bytes, bytes_from_buffer +from ._libcrypto import libcrypto, libcrypto_version_info, handle_openssl_error +from .._rand import rand_bytes +from .._types import type_name, byte_cls, int_types + + +__all__ = [ + 'pbkdf2', + 'pkcs12_kdf', + 'rand_bytes', +] + + +# OpenSSL 0.9.8 does not include PBKDF2 +if libcrypto_version_info < (1,): + from .._pkcs5 import pbkdf2 + +else: + def pbkdf2(hash_algorithm, password, salt, iterations, key_length): + """ + PBKDF2 from PKCS#5 + + :param hash_algorithm: + The string name of the hash algorithm to use: "sha1", "sha224", "sha256", "sha384", "sha512" + + :param password: + A byte string of the password to use an input to the KDF + + :param salt: + A cryptographic random byte string + + :param iterations: + The numbers of iterations to use when deriving the key + + :param key_length: + The length of the desired key in bytes + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + + :return: + The derived key as a byte string + """ + + if not isinstance(password, byte_cls): + raise TypeError(pretty_message( + ''' + password must be a byte string, not %s + ''', + type_name(password) + )) + + if not isinstance(salt, byte_cls): + raise TypeError(pretty_message( + ''' + salt must be a byte string, not %s + ''', + type_name(salt) + )) + + if not isinstance(iterations, int_types): + raise TypeError(pretty_message( + ''' + iterations must be an integer, not %s + ''', + type_name(iterations) + )) + + if iterations < 1: + raise ValueError('iterations must be greater than 0') + + if not isinstance(key_length, int_types): + raise TypeError(pretty_message( + ''' + key_length must be an integer, not %s + ''', + type_name(key_length) + )) + + if key_length < 1: + raise ValueError('key_length must be greater than 0') + + if hash_algorithm not in set(['sha1', 'sha224', 'sha256', 'sha384', 'sha512']): + raise ValueError(pretty_message( + ''' + hash_algorithm must be one of "sha1", "sha224", "sha256", "sha384", + "sha512", not %s + ''', + repr(hash_algorithm) + )) + + evp_md = { + 'sha1': libcrypto.EVP_sha1, + 'sha224': libcrypto.EVP_sha224, + 'sha256': libcrypto.EVP_sha256, + 'sha384': libcrypto.EVP_sha384, + 'sha512': libcrypto.EVP_sha512 + }[hash_algorithm]() + + output_buffer = buffer_from_bytes(key_length) + result = libcrypto.PKCS5_PBKDF2_HMAC( + password, + len(password), + salt, + len(salt), + iterations, + evp_md, + key_length, + output_buffer + ) + handle_openssl_error(result) + + return bytes_from_buffer(output_buffer) + + pbkdf2.pure_python = False + + +def pkcs12_kdf(hash_algorithm, password, salt, iterations, key_length, id_): + """ + KDF from RFC7292 appendix B.2 - https://tools.ietf.org/html/rfc7292#page-19 + + :param hash_algorithm: + The string name of the hash algorithm to use: "md5", "sha1", "sha224", "sha256", "sha384", "sha512" + + :param password: + A byte string of the password to use an input to the KDF + + :param salt: + A cryptographic random byte string + + :param iterations: + The numbers of iterations to use when deriving the key + + :param key_length: + The length of the desired key in bytes + + :param id_: + The ID of the usage - 1 for key, 2 for iv, 3 for mac + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + + :return: + The derived key as a byte string + """ + + if not isinstance(password, byte_cls): + raise TypeError(pretty_message( + ''' + password must be a byte string, not %s + ''', + type_name(password) + )) + + if not isinstance(salt, byte_cls): + raise TypeError(pretty_message( + ''' + salt must be a byte string, not %s + ''', + type_name(salt) + )) + + if not isinstance(iterations, int_types): + raise TypeError(pretty_message( + ''' + iterations must be an integer, not %s + ''', + type_name(iterations) + )) + + if iterations < 1: + raise ValueError(pretty_message( + ''' + iterations must be greater than 0 - is %s + ''', + repr(iterations) + )) + + if not isinstance(key_length, int_types): + raise TypeError(pretty_message( + ''' + key_length must be an integer, not %s + ''', + type_name(key_length) + )) + + if key_length < 1: + raise ValueError(pretty_message( + ''' + key_length must be greater than 0 - is %s + ''', + repr(key_length) + )) + + if hash_algorithm not in set(['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']): + raise ValueError(pretty_message( + ''' + hash_algorithm must be one of "md5", "sha1", "sha224", "sha256", + "sha384", "sha512", not %s + ''', + repr(hash_algorithm) + )) + + if id_ not in set([1, 2, 3]): + raise ValueError(pretty_message( + ''' + id_ must be one of 1, 2, 3, not %s + ''', + repr(id_) + )) + + utf16_password = password.decode('utf-8').encode('utf-16be') + b'\x00\x00' + + digest_type = { + 'md5': libcrypto.EVP_md5, + 'sha1': libcrypto.EVP_sha1, + 'sha224': libcrypto.EVP_sha224, + 'sha256': libcrypto.EVP_sha256, + 'sha384': libcrypto.EVP_sha384, + 'sha512': libcrypto.EVP_sha512, + }[hash_algorithm]() + + output_buffer = buffer_from_bytes(key_length) + result = libcrypto.PKCS12_key_gen_uni( + utf16_password, + len(utf16_password), + salt, + len(salt), + id_, + iterations, + key_length, + output_buffer, + digest_type + ) + handle_openssl_error(result) + + return bytes_from_buffer(output_buffer) diff --git a/tasks/lib/package_control/deps/oscrypto/_pkcs1.py b/tasks/lib/package_control/deps/oscrypto/_pkcs1.py new file mode 100644 index 0000000..66f5ed3 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_pkcs1.py @@ -0,0 +1,740 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys +import hashlib +import math +import platform +import struct +import os + +from . import backend +from .util import constant_compare, rand_bytes +from ._asn1 import ( + Certificate, + int_from_bytes, + int_to_bytes, + PrivateKeyInfo, + PublicKeyInfo, +) +from ._errors import pretty_message +from ._int import fill_width +from ._types import type_name, byte_cls, int_types + +if sys.version_info < (3,): + chr_cls = chr + range = xrange # noqa + +else: + def chr_cls(num): + return bytes([num]) + + +_backend = backend() + + +__all__ = [ + 'add_pss_padding', + 'add_pkcs1v15_signature_padding', + 'raw_rsa_private_crypt', + 'raw_rsa_public_crypt', + 'remove_pkcs1v15_encryption_padding', + 'remove_pkcs1v15_signature_padding', + 'verify_pss_padding', +] + + +def _is_osx_107(): + """ + :return: + A bool if the current machine is running OS X 10.7 + """ + + if sys.platform != 'darwin': + return False + version = platform.mac_ver()[0] + return tuple(map(int, version.split('.')))[0:2] == (10, 7) + + +def add_pss_padding(hash_algorithm, salt_length, key_length, message): + """ + Pads a byte string using the EMSA-PSS-Encode operation described in PKCS#1 + v2.2. + + :param hash_algorithm: + The string name of the hash algorithm to use: "sha1", "sha224", + "sha256", "sha384", "sha512" + + :param salt_length: + The length of the salt as an integer - typically the same as the length + of the output from the hash_algorithm + + :param key_length: + The length of the RSA key, in bits + + :param message: + A byte string of the message to pad + + :return: + The encoded (passed) message + """ + + if _backend != 'winlegacy' and sys.platform != 'darwin': + raise SystemError(pretty_message( + ''' + Pure-python RSA PSS signature padding addition code is only for + Windows XP/2003 and OS X + ''' + )) + + if not isinstance(message, byte_cls): + raise TypeError(pretty_message( + ''' + message must be a byte string, not %s + ''', + type_name(message) + )) + + if not isinstance(salt_length, int_types): + raise TypeError(pretty_message( + ''' + salt_length must be an integer, not %s + ''', + type_name(salt_length) + )) + + if salt_length < 0: + raise ValueError(pretty_message( + ''' + salt_length must be 0 or more - is %s + ''', + repr(salt_length) + )) + + if not isinstance(key_length, int_types): + raise TypeError(pretty_message( + ''' + key_length must be an integer, not %s + ''', + type_name(key_length) + )) + + if key_length < 512: + raise ValueError(pretty_message( + ''' + key_length must be 512 or more - is %s + ''', + repr(key_length) + )) + + if hash_algorithm not in set(['sha1', 'sha224', 'sha256', 'sha384', 'sha512']): + raise ValueError(pretty_message( + ''' + hash_algorithm must be one of "sha1", "sha224", "sha256", "sha384", + "sha512", not %s + ''', + repr(hash_algorithm) + )) + + hash_func = getattr(hashlib, hash_algorithm) + + # The maximal bit size of a non-negative integer is one less than the bit + # size of the key since the first bit is used to store sign + em_bits = key_length - 1 + em_len = int(math.ceil(em_bits / 8)) + + message_digest = hash_func(message).digest() + hash_length = len(message_digest) + + if em_len < hash_length + salt_length + 2: + raise ValueError(pretty_message( + ''' + Key is not long enough to use with specified hash_algorithm and + salt_length + ''' + )) + + if salt_length > 0: + salt = os.urandom(salt_length) + else: + salt = b'' + + m_prime = (b'\x00' * 8) + message_digest + salt + + m_prime_digest = hash_func(m_prime).digest() + + padding = b'\x00' * (em_len - salt_length - hash_length - 2) + + db = padding + b'\x01' + salt + + db_mask = _mgf1(hash_algorithm, m_prime_digest, em_len - hash_length - 1) + + masked_db = int_to_bytes(int_from_bytes(db) ^ int_from_bytes(db_mask)) + masked_db = fill_width(masked_db, len(db_mask)) + + zero_bits = (8 * em_len) - em_bits + left_bit_mask = ('0' * zero_bits) + ('1' * (8 - zero_bits)) + left_int_mask = int(left_bit_mask, 2) + + if left_int_mask != 255: + masked_db = chr_cls(left_int_mask & ord(masked_db[0:1])) + masked_db[1:] + + return masked_db + m_prime_digest + b'\xBC' + + +def verify_pss_padding(hash_algorithm, salt_length, key_length, message, signature): + """ + Verifies the PSS padding on an encoded message + + :param hash_algorithm: + The string name of the hash algorithm to use: "sha1", "sha224", + "sha256", "sha384", "sha512" + + :param salt_length: + The length of the salt as an integer - typically the same as the length + of the output from the hash_algorithm + + :param key_length: + The length of the RSA key, in bits + + :param message: + A byte string of the message to pad + + :param signature: + The signature to verify + + :return: + A boolean indicating if the signature is invalid + """ + + if _backend != 'winlegacy' and sys.platform != 'darwin': + raise SystemError(pretty_message( + ''' + Pure-python RSA PSS signature padding verification code is only for + Windows XP/2003 and OS X + ''' + )) + + if not isinstance(message, byte_cls): + raise TypeError(pretty_message( + ''' + message must be a byte string, not %s + ''', + type_name(message) + )) + + if not isinstance(signature, byte_cls): + raise TypeError(pretty_message( + ''' + signature must be a byte string, not %s + ''', + type_name(signature) + )) + + if not isinstance(salt_length, int_types): + raise TypeError(pretty_message( + ''' + salt_length must be an integer, not %s + ''', + type_name(salt_length) + )) + + if salt_length < 0: + raise ValueError(pretty_message( + ''' + salt_length must be 0 or more - is %s + ''', + repr(salt_length) + )) + + if hash_algorithm not in set(['sha1', 'sha224', 'sha256', 'sha384', 'sha512']): + raise ValueError(pretty_message( + ''' + hash_algorithm must be one of "sha1", "sha224", "sha256", "sha384", + "sha512", not %s + ''', + repr(hash_algorithm) + )) + + hash_func = getattr(hashlib, hash_algorithm) + + em_bits = key_length - 1 + em_len = int(math.ceil(em_bits / 8)) + + message_digest = hash_func(message).digest() + hash_length = len(message_digest) + + if em_len < hash_length + salt_length + 2: + return False + + if signature[-1:] != b'\xBC': + return False + + zero_bits = (8 * em_len) - em_bits + + masked_db_length = em_len - hash_length - 1 + masked_db = signature[0:masked_db_length] + + first_byte = ord(masked_db[0:1]) + bits_that_should_be_zero = first_byte >> (8 - zero_bits) + if bits_that_should_be_zero != 0: + return False + + m_prime_digest = signature[masked_db_length:masked_db_length + hash_length] + + db_mask = _mgf1(hash_algorithm, m_prime_digest, em_len - hash_length - 1) + + left_bit_mask = ('0' * zero_bits) + ('1' * (8 - zero_bits)) + left_int_mask = int(left_bit_mask, 2) + + if left_int_mask != 255: + db_mask = chr_cls(left_int_mask & ord(db_mask[0:1])) + db_mask[1:] + + db = int_to_bytes(int_from_bytes(masked_db) ^ int_from_bytes(db_mask)) + if len(db) < len(masked_db): + db = (b'\x00' * (len(masked_db) - len(db))) + db + + zero_length = em_len - hash_length - salt_length - 2 + zero_string = b'\x00' * zero_length + if not constant_compare(db[0:zero_length], zero_string): + return False + + if db[zero_length:zero_length + 1] != b'\x01': + return False + + salt = db[0 - salt_length:] + + m_prime = (b'\x00' * 8) + message_digest + salt + + h_prime = hash_func(m_prime).digest() + + return constant_compare(m_prime_digest, h_prime) + + +def _mgf1(hash_algorithm, seed, mask_length): + """ + The PKCS#1 MGF1 mask generation algorithm + + :param hash_algorithm: + The string name of the hash algorithm to use: "sha1", "sha224", + "sha256", "sha384", "sha512" + + :param seed: + A byte string to use as the seed for the mask + + :param mask_length: + The desired mask length, as an integer + + :return: + A byte string of the mask + """ + + if not isinstance(seed, byte_cls): + raise TypeError(pretty_message( + ''' + seed must be a byte string, not %s + ''', + type_name(seed) + )) + + if not isinstance(mask_length, int_types): + raise TypeError(pretty_message( + ''' + mask_length must be an integer, not %s + ''', + type_name(mask_length) + )) + + if mask_length < 1: + raise ValueError(pretty_message( + ''' + mask_length must be greater than 0 - is %s + ''', + repr(mask_length) + )) + + if hash_algorithm not in set(['sha1', 'sha224', 'sha256', 'sha384', 'sha512']): + raise ValueError(pretty_message( + ''' + hash_algorithm must be one of "sha1", "sha224", "sha256", "sha384", + "sha512", not %s + ''', + repr(hash_algorithm) + )) + + output = b'' + + hash_length = { + 'sha1': 20, + 'sha224': 28, + 'sha256': 32, + 'sha384': 48, + 'sha512': 64 + }[hash_algorithm] + + iterations = int(math.ceil(mask_length / hash_length)) + + pack = struct.Struct(b'>I').pack + hash_func = getattr(hashlib, hash_algorithm) + + for counter in range(0, iterations): + b = pack(counter) + output += hash_func(seed + b).digest() + + return output[0:mask_length] + + +def add_pkcs1v15_signature_padding(key_length, data): + """ + Adds PKCS#1 v1.5 padding to a message to be signed + + :param key_length: + An integer of the number of bytes in the key + + :param data: + A byte string to pad + + :return: + The padded data as a byte string + """ + + if _backend != 'winlegacy': + raise SystemError(pretty_message( + ''' + Pure-python RSA PKCSv1.5 signature padding addition code is only + for Windows XP/2003 + ''' + )) + + return _add_pkcs1v15_padding(key_length, data, 'signing') + + +def remove_pkcs1v15_signature_padding(key_length, data): + """ + Removes PKCS#1 v1.5 padding from a signed message using constant time + operations + + :param key_length: + An integer of the number of bytes in the key + + :param data: + A byte string to unpad + + :return: + The unpadded data as a byte string + """ + + if _backend != 'winlegacy': + raise SystemError(pretty_message( + ''' + Pure-python RSA PKCSv1.5 signature padding removal code is only for + Windows XP/2003 + ''' + )) + + return _remove_pkcs1v15_padding(key_length, data, 'verifying') + + +def remove_pkcs1v15_encryption_padding(key_length, data): + """ + Removes PKCS#1 v1.5 padding from a decrypted message using constant time + operations + + :param key_length: + An integer of the number of bytes in the key + + :param data: + A byte string to unpad + + :return: + The unpadded data as a byte string + """ + + if not _is_osx_107(): + raise SystemError(pretty_message( + ''' + Pure-python RSA PKCSv1.5 encryption padding removal code is only + for OS X 10.7 + ''' + )) + + return _remove_pkcs1v15_padding(key_length, data, 'decrypting') + + +def _add_pkcs1v15_padding(key_length, data, operation): + """ + Adds PKCS#1 v1.5 padding to a message + + :param key_length: + An integer of the number of bytes in the key + + :param data: + A byte string to unpad + + :param operation: + A unicode string of "encrypting" or "signing" + + :return: + The padded data as a byte string + """ + + if operation == 'encrypting': + second_byte = b'\x02' + else: + second_byte = b'\x01' + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + if not isinstance(key_length, int_types): + raise TypeError(pretty_message( + ''' + key_length must be an integer, not %s + ''', + type_name(key_length) + )) + + if key_length < 64: + raise ValueError(pretty_message( + ''' + key_length must be 64 or more - is %s + ''', + repr(key_length) + )) + + if len(data) > key_length - 11: + raise ValueError(pretty_message( + ''' + data must be between 1 and %s bytes long - is %s + ''', + key_length - 11, + len(data) + )) + + required_bytes = key_length - 3 - len(data) + padding = b'' + while required_bytes > 0: + temp_padding = rand_bytes(required_bytes) + # Remove null bytes since they are markers in PKCS#1 v1.5 + temp_padding = b''.join(temp_padding.split(b'\x00')) + padding += temp_padding + required_bytes -= len(temp_padding) + + return b'\x00' + second_byte + padding + b'\x00' + data + + +def _remove_pkcs1v15_padding(key_length, data, operation): + """ + Removes PKCS#1 v1.5 padding from a message using constant time operations + + :param key_length: + An integer of the number of bytes in the key + + :param data: + A byte string to unpad + + :param operation: + A unicode string of "decrypting" or "verifying" + + :return: + The unpadded data as a byte string + """ + + if operation == 'decrypting': + second_byte = 2 + else: + second_byte = 1 + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + if not isinstance(key_length, int_types): + raise TypeError(pretty_message( + ''' + key_length must be an integer, not %s + ''', + type_name(key_length) + )) + + if key_length < 64: + raise ValueError(pretty_message( + ''' + key_length must be 64 or more - is %s + ''', + repr(key_length) + )) + + if len(data) != key_length: + raise ValueError('Error %s' % operation) + + error = 0 + trash = 0 + padding_end = 0 + + # Uses bitwise operations on an error variable and another trash variable + # to perform constant time error checking/token scanning on the data + for i in range(0, len(data)): + byte = data[i:i + 1] + byte_num = ord(byte) + + # First byte should be \x00 + if i == 0: + error |= byte_num + + # Second byte should be \x02 for decryption, \x01 for verification + elif i == 1: + error |= int((byte_num | second_byte) != second_byte) + + # Bytes 3-10 should not be \x00 + elif i < 10: + error |= int((byte_num ^ 0) == 0) + + # Byte 11 or after that is zero is end of padding + else: + non_zero = byte_num | 0 + if padding_end == 0: + if non_zero: + trash |= i + else: + padding_end |= i + else: + if non_zero: + trash |= i + else: + trash |= i + + if error != 0: + raise ValueError('Error %s' % operation) + + return data[padding_end + 1:] + + +def raw_rsa_private_crypt(private_key, data): + """ + Performs a raw RSA algorithm in a byte string using a private key. + This is a low-level primitive and is prone to disastrous results if used + incorrectly. + + :param private_key: + An oscrypto.asymmetric.PrivateKey object + + :param data: + A byte string of the plaintext to be signed or ciphertext to be + decrypted. Must be less than or equal to the length of the private key. + In the case of signing, padding must already be applied. In the case of + decryption, padding must be removed afterward. + + :return: + A byte string of the transformed data + """ + + if _backend != 'winlegacy': + raise SystemError('Pure-python RSA crypt is only for Windows XP/2003') + + if not hasattr(private_key, 'asn1') or not isinstance(private_key.asn1, PrivateKeyInfo): + raise TypeError(pretty_message( + ''' + private_key must be an instance of the + oscrypto.asymmetric.PrivateKey class, not %s + ''', + type_name(private_key) + )) + + algo = private_key.asn1['private_key_algorithm']['algorithm'].native + if algo != 'rsa' and algo != 'rsassa_pss': + raise ValueError(pretty_message( + ''' + private_key must be an RSA key, not %s + ''', + algo.upper() + )) + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + rsa_private_key = private_key.asn1['private_key'].parsed + transformed_int = pow( + int_from_bytes(data), + rsa_private_key['private_exponent'].native, + rsa_private_key['modulus'].native + ) + return int_to_bytes(transformed_int, width=private_key.asn1.byte_size) + + +def raw_rsa_public_crypt(certificate_or_public_key, data): + """ + Performs a raw RSA algorithm in a byte string using a certificate or + public key. This is a low-level primitive and is prone to disastrous results + if used incorrectly. + + :param certificate_or_public_key: + An oscrypto.asymmetric.PublicKey or oscrypto.asymmetric.Certificate + object + + :param data: + A byte string of the signature when verifying, or padded plaintext when + encrypting. Must be less than or equal to the length of the public key. + When verifying, padding will need to be removed afterwards. When + encrypting, padding must be applied before. + + :return: + A byte string of the transformed data + """ + + if _backend != 'winlegacy': + raise SystemError('Pure-python RSA crypt is only for Windows XP/2003') + + has_asn1 = hasattr(certificate_or_public_key, 'asn1') + valid_types = (PublicKeyInfo, Certificate) + if not has_asn1 or not isinstance(certificate_or_public_key.asn1, valid_types): + raise TypeError(pretty_message( + ''' + certificate_or_public_key must be an instance of the + oscrypto.asymmetric.PublicKey or oscrypto.asymmetric.Certificate + classes, not %s + ''', + type_name(certificate_or_public_key) + )) + + algo = certificate_or_public_key.asn1['algorithm']['algorithm'].native + if algo != 'rsa' and algo != 'rsassa_pss': + raise ValueError(pretty_message( + ''' + certificate_or_public_key must be an RSA key, not %s + ''', + algo.upper() + )) + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + rsa_public_key = certificate_or_public_key.asn1['public_key'].parsed + transformed_int = pow( + int_from_bytes(data), + rsa_public_key['public_exponent'].native, + rsa_public_key['modulus'].native + ) + return int_to_bytes( + transformed_int, + width=certificate_or_public_key.asn1.byte_size + ) diff --git a/tasks/lib/package_control/deps/oscrypto/_pkcs12.py b/tasks/lib/package_control/deps/oscrypto/_pkcs12.py new file mode 100644 index 0000000..b788178 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_pkcs12.py @@ -0,0 +1,198 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys +import hashlib +import math + +from ._asn1 import int_from_bytes, int_to_bytes +from ._errors import pretty_message +from ._types import type_name, byte_cls, int_types + + +if sys.version_info < (3,): + chr_cls = chr + +else: + def chr_cls(num): + return bytes([num]) + + +__all__ = [ + 'pkcs12_kdf', +] + + +def pkcs12_kdf(hash_algorithm, password, salt, iterations, key_length, id_): + """ + KDF from RFC7292 appendix b.2 - https://tools.ietf.org/html/rfc7292#page-19 + + :param hash_algorithm: + The string name of the hash algorithm to use: "md5", "sha1", "sha224", + "sha256", "sha384", "sha512" + + :param password: + A byte string of the password to use an input to the KDF + + :param salt: + A cryptographic random byte string + + :param iterations: + The numbers of iterations to use when deriving the key + + :param key_length: + The length of the desired key in bytes + + :param id_: + The ID of the usage - 1 for key, 2 for iv, 3 for mac + + :return: + The derived key as a byte string + """ + + if not isinstance(password, byte_cls): + raise TypeError(pretty_message( + ''' + password must be a byte string, not %s + ''', + type_name(password) + )) + + if not isinstance(salt, byte_cls): + raise TypeError(pretty_message( + ''' + salt must be a byte string, not %s + ''', + type_name(salt) + )) + + if not isinstance(iterations, int_types): + raise TypeError(pretty_message( + ''' + iterations must be an integer, not %s + ''', + type_name(iterations) + )) + + if iterations < 1: + raise ValueError(pretty_message( + ''' + iterations must be greater than 0 - is %s + ''', + repr(iterations) + )) + + if not isinstance(key_length, int_types): + raise TypeError(pretty_message( + ''' + key_length must be an integer, not %s + ''', + type_name(key_length) + )) + + if key_length < 1: + raise ValueError(pretty_message( + ''' + key_length must be greater than 0 - is %s + ''', + repr(key_length) + )) + + if hash_algorithm not in set(['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']): + raise ValueError(pretty_message( + ''' + hash_algorithm must be one of "md5", "sha1", "sha224", "sha256", + "sha384", "sha512", not %s + ''', + repr(hash_algorithm) + )) + + if id_ not in set([1, 2, 3]): + raise ValueError(pretty_message( + ''' + id_ must be one of 1, 2, 3, not %s + ''', + repr(id_) + )) + + utf16_password = password.decode('utf-8').encode('utf-16be') + b'\x00\x00' + + algo = getattr(hashlib, hash_algorithm) + + # u and v values are bytes (not bits as in the RFC) + u = { + 'md5': 16, + 'sha1': 20, + 'sha224': 28, + 'sha256': 32, + 'sha384': 48, + 'sha512': 64 + }[hash_algorithm] + + if hash_algorithm in ['sha384', 'sha512']: + v = 128 + else: + v = 64 + + # Step 1 + d = chr_cls(id_) * v + + # Step 2 + s = b'' + if salt != b'': + s_len = v * int(math.ceil(float(len(salt)) / v)) + while len(s) < s_len: + s += salt + s = s[0:s_len] + + # Step 3 + p = b'' + if utf16_password != b'': + p_len = v * int(math.ceil(float(len(utf16_password)) / v)) + while len(p) < p_len: + p += utf16_password + p = p[0:p_len] + + # Step 4 + i = s + p + + # Step 5 + c = int(math.ceil(float(key_length) / u)) + + a = b'\x00' * (c * u) + + for num in range(1, c + 1): + # Step 6A + a2 = algo(d + i).digest() + for _ in range(2, iterations + 1): + a2 = algo(a2).digest() + + if num < c: + # Step 6B + b = b'' + while len(b) < v: + b += a2 + + b = int_from_bytes(b[0:v]) + 1 + + # Step 6C + for num2 in range(0, len(i) // v): + start = num2 * v + end = (num2 + 1) * v + i_num2 = i[start:end] + + i_num2 = int_to_bytes(int_from_bytes(i_num2) + b) + + # Ensure the new slice is the right size + i_num2_l = len(i_num2) + if i_num2_l > v: + i_num2 = i_num2[i_num2_l - v:] + + i = i[0:start] + i_num2 + i[end:] + + # Step 7 (one piece at a time) + begin = (num - 1) * u + to_copy = min(key_length, u) + a = a[0:begin] + a2[0:to_copy] + a[begin + to_copy:] + + return a[0:key_length] diff --git a/tasks/lib/package_control/deps/oscrypto/_pkcs5.py b/tasks/lib/package_control/deps/oscrypto/_pkcs5.py new file mode 100644 index 0000000..405d419 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_pkcs5.py @@ -0,0 +1,142 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys +import hashlib +import hmac +import struct + +from ._asn1 import int_from_bytes, int_to_bytes +from ._errors import pretty_message +from ._types import type_name, byte_cls, int_types + +if sys.version_info < (3,): + chr_cls = chr + +else: + def chr_cls(num): + return bytes([num]) + + +__all__ = [ + 'pbkdf2', +] + + +def pbkdf2(hash_algorithm, password, salt, iterations, key_length): + """ + Implements PBKDF2 from PKCS#5 v2.2 in pure Python + + :param hash_algorithm: + The string name of the hash algorithm to use: "md5", "sha1", "sha224", + "sha256", "sha384", "sha512" + + :param password: + A byte string of the password to use an input to the KDF + + :param salt: + A cryptographic random byte string + + :param iterations: + The numbers of iterations to use when deriving the key + + :param key_length: + The length of the desired key in bytes + + :return: + The derived key as a byte string + """ + + if not isinstance(password, byte_cls): + raise TypeError(pretty_message( + ''' + password must be a byte string, not %s + ''', + type_name(password) + )) + + if not isinstance(salt, byte_cls): + raise TypeError(pretty_message( + ''' + salt must be a byte string, not %s + ''', + type_name(salt) + )) + + if not isinstance(iterations, int_types): + raise TypeError(pretty_message( + ''' + iterations must be an integer, not %s + ''', + type_name(iterations) + )) + + if iterations < 1: + raise ValueError(pretty_message( + ''' + iterations must be greater than 0 - is %s + ''', + repr(iterations) + )) + + if not isinstance(key_length, int_types): + raise TypeError(pretty_message( + ''' + key_length must be an integer, not %s + ''', + type_name(key_length) + )) + + if key_length < 1: + raise ValueError(pretty_message( + ''' + key_length must be greater than 0 - is %s + ''', + repr(key_length) + )) + + if hash_algorithm not in set(['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']): + raise ValueError(pretty_message( + ''' + hash_algorithm must be one of "md5", "sha1", "sha224", "sha256", + "sha384", "sha512", not %s + ''', + repr(hash_algorithm) + )) + + algo = getattr(hashlib, hash_algorithm) + + hash_length = { + 'md5': 16, + 'sha1': 20, + 'sha224': 28, + 'sha256': 32, + 'sha384': 48, + 'sha512': 64 + }[hash_algorithm] + + original_hmac = hmac.new(password, None, algo) + + block = 1 + output = b'' + + while len(output) < key_length: + prf = original_hmac.copy() + prf.update(salt + struct.pack(b'>I', block)) + last = prf.digest() + + u = int_from_bytes(last) + + for _ in range(iterations-1): + prf = original_hmac.copy() + prf.update(last) + last = prf.digest() + u ^= int_from_bytes(last) + + output += int_to_bytes(u, width=hash_length) + block += 1 + + return output[0:key_length] + + +pbkdf2.pure_python = True diff --git a/tasks/lib/package_control/deps/oscrypto/_rand.py b/tasks/lib/package_control/deps/oscrypto/_rand.py new file mode 100644 index 0000000..805e2cd --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_rand.py @@ -0,0 +1,45 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import os + +from ._errors import pretty_message +from ._types import type_name, int_types + + +__all__ = [ + 'rand_bytes', +] + + +def rand_bytes(length): + """ + Returns a number of random bytes suitable for cryptographic purposes + + :param length: + The desired number of bytes + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by OpenSSL + + :return: + A byte string + """ + + if not isinstance(length, int_types): + raise TypeError(pretty_message( + ''' + length must be an integer, not %s + ''', + type_name(length) + )) + + if length < 1: + raise ValueError('length must be greater than 0') + + if length > 1024: + raise ValueError('length must not be greater than 1024') + + return os.urandom(length) diff --git a/tasks/lib/package_control/deps/oscrypto/_tls.py b/tasks/lib/package_control/deps/oscrypto/_tls.py new file mode 100644 index 0000000..260e9cf --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_tls.py @@ -0,0 +1,604 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import re +from datetime import datetime + +from ._asn1 import Certificate, int_from_bytes, timezone +from ._cipher_suites import CIPHER_SUITE_MAP +from .errors import TLSVerificationError, TLSDisconnectError, TLSError + + +__all__ = [ + 'detect_client_auth_request', + 'extract_chain', + 'get_dh_params_length', + 'parse_alert', + 'parse_handshake_messages', + 'parse_session_info', + 'parse_tls_records', + 'raise_client_auth', + 'raise_dh_params', + 'raise_disconnection', + 'raise_expired_not_yet_valid', + 'raise_handshake', + 'raise_hostname', + 'raise_no_issuer', + 'raise_protocol_error', + 'raise_revoked', + 'raise_self_signed', + 'raise_verification', + 'raise_weak_signature', +] + + +def extract_chain(server_handshake_bytes): + """ + Extracts the X.509 certificates from the server handshake bytes for use + when debugging + + :param server_handshake_bytes: + A byte string of the handshake data received from the server + + :return: + A list of asn1crypto.x509.Certificate objects + """ + + output = [] + + chain_bytes = None + + for record_type, _, record_data in parse_tls_records(server_handshake_bytes): + if record_type != b'\x16': + continue + for message_type, message_data in parse_handshake_messages(record_data): + if message_type == b'\x0b': + chain_bytes = message_data + break + if chain_bytes: + break + + if chain_bytes: + # The first 3 bytes are the cert chain length + pointer = 3 + while pointer < len(chain_bytes): + cert_length = int_from_bytes(chain_bytes[pointer:pointer + 3]) + cert_start = pointer + 3 + cert_end = cert_start + cert_length + pointer = cert_end + cert_bytes = chain_bytes[cert_start:cert_end] + output.append(Certificate.load(cert_bytes)) + + return output + + +def detect_client_auth_request(server_handshake_bytes): + """ + Determines if a CertificateRequest message is sent from the server asking + the client for a certificate + + :param server_handshake_bytes: + A byte string of the handshake data received from the server + + :return: + A boolean - if a client certificate request was found + """ + + for record_type, _, record_data in parse_tls_records(server_handshake_bytes): + if record_type != b'\x16': + continue + for message_type, message_data in parse_handshake_messages(record_data): + if message_type == b'\x0d': + return True + return False + + +def get_dh_params_length(server_handshake_bytes): + """ + Determines the length of the DH params from the ServerKeyExchange + + :param server_handshake_bytes: + A byte string of the handshake data received from the server + + :return: + None or an integer of the bit size of the DH parameters + """ + + output = None + + dh_params_bytes = None + + for record_type, _, record_data in parse_tls_records(server_handshake_bytes): + if record_type != b'\x16': + continue + for message_type, message_data in parse_handshake_messages(record_data): + if message_type == b'\x0c': + dh_params_bytes = message_data + break + if dh_params_bytes: + break + + if dh_params_bytes: + output = int_from_bytes(dh_params_bytes[0:2]) * 8 + + return output + + +def parse_alert(server_handshake_bytes): + """ + Parses the handshake for protocol alerts + + :param server_handshake_bytes: + A byte string of the handshake data received from the server + + :return: + None or an 2-element tuple of integers: + 0: 1 (warning) or 2 (fatal) + 1: The alert description (see https://tools.ietf.org/html/rfc5246#section-7.2) + """ + + for record_type, _, record_data in parse_tls_records(server_handshake_bytes): + if record_type != b'\x15': + continue + if len(record_data) != 2: + return None + return (int_from_bytes(record_data[0:1]), int_from_bytes(record_data[1:2])) + return None + + +def parse_session_info(server_handshake_bytes, client_handshake_bytes): + """ + Parse the TLS handshake from the client to the server to extract information + including the cipher suite selected, if compression is enabled, the + session id and if a new or reused session ticket exists. + + :param server_handshake_bytes: + A byte string of the handshake data received from the server + + :param client_handshake_bytes: + A byte string of the handshake data sent to the server + + :return: + A dict with the following keys: + - "protocol": unicode string + - "cipher_suite": unicode string + - "compression": boolean + - "session_id": "new", "reused" or None + - "session_ticket: "new", "reused" or None + """ + + protocol = None + cipher_suite = None + compression = False + session_id = None + session_ticket = None + + server_session_id = None + client_session_id = None + + for record_type, _, record_data in parse_tls_records(server_handshake_bytes): + if record_type != b'\x16': + continue + for message_type, message_data in parse_handshake_messages(record_data): + # Ensure we are working with a ServerHello message + if message_type != b'\x02': + continue + protocol = { + b'\x03\x00': "SSLv3", + b'\x03\x01': "TLSv1", + b'\x03\x02': "TLSv1.1", + b'\x03\x03': "TLSv1.2", + b'\x03\x04': "TLSv1.3", + }[message_data[0:2]] + + session_id_length = int_from_bytes(message_data[34:35]) + if session_id_length > 0: + server_session_id = message_data[35:35 + session_id_length] + + cipher_suite_start = 35 + session_id_length + cipher_suite_bytes = message_data[cipher_suite_start:cipher_suite_start + 2] + cipher_suite = CIPHER_SUITE_MAP[cipher_suite_bytes] + + compression_start = cipher_suite_start + 2 + compression = message_data[compression_start:compression_start + 1] != b'\x00' + + extensions_length_start = compression_start + 1 + extensions_data = message_data[extensions_length_start:] + for extension_type, extension_data in _parse_hello_extensions(extensions_data): + if extension_type == 35: + session_ticket = "new" + break + break + + for record_type, _, record_data in parse_tls_records(client_handshake_bytes): + if record_type != b'\x16': + continue + for message_type, message_data in parse_handshake_messages(record_data): + # Ensure we are working with a ClientHello message + if message_type != b'\x01': + continue + + session_id_length = int_from_bytes(message_data[34:35]) + if session_id_length > 0: + client_session_id = message_data[35:35 + session_id_length] + + cipher_suite_start = 35 + session_id_length + cipher_suite_length = int_from_bytes(message_data[cipher_suite_start:cipher_suite_start + 2]) + + compression_start = cipher_suite_start + 2 + cipher_suite_length + compression_length = int_from_bytes(message_data[compression_start:compression_start + 1]) + + # On subsequent requests, the session ticket will only be seen + # in the ClientHello message + if server_session_id is None and session_ticket is None: + extensions_length_start = compression_start + 1 + compression_length + extensions_data = message_data[extensions_length_start:] + for extension_type, extension_data in _parse_hello_extensions(extensions_data): + if extension_type == 35: + session_ticket = "reused" + break + break + + if server_session_id is not None: + if client_session_id is None: + session_id = "new" + else: + if client_session_id != server_session_id: + session_id = "new" + else: + session_id = "reused" + + return { + "protocol": protocol, + "cipher_suite": cipher_suite, + "compression": compression, + "session_id": session_id, + "session_ticket": session_ticket, + } + + +def parse_tls_records(data): + """ + Creates a generator returning tuples of information about each record + in a byte string of data from a TLS client or server. Stops as soon as it + find a ChangeCipherSpec message since all data from then on is encrypted. + + :param data: + A byte string of TLS records + + :return: + A generator that yields 3-element tuples: + [0] Byte string of record type + [1] Byte string of protocol version + [2] Byte string of record data + """ + + pointer = 0 + data_len = len(data) + while pointer < data_len: + # Don't try to parse any more once the ChangeCipherSpec is found + if data[pointer:pointer + 1] == b'\x14': + break + length = int_from_bytes(data[pointer + 3:pointer + 5]) + yield ( + data[pointer:pointer + 1], + data[pointer + 1:pointer + 3], + data[pointer + 5:pointer + 5 + length] + ) + pointer += 5 + length + + +def parse_handshake_messages(data): + """ + Creates a generator returning tuples of information about each message in + a byte string of data from a TLS handshake record + + :param data: + A byte string of a TLS handshake record data + + :return: + A generator that yields 2-element tuples: + [0] Byte string of message type + [1] Byte string of message data + """ + + pointer = 0 + data_len = len(data) + while pointer < data_len: + length = int_from_bytes(data[pointer + 1:pointer + 4]) + yield ( + data[pointer:pointer + 1], + data[pointer + 4:pointer + 4 + length] + ) + pointer += 4 + length + + +def _parse_hello_extensions(data): + """ + Creates a generator returning tuples of information about each extension + from a byte string of extension data contained in a ServerHello ores + ClientHello message + + :param data: + A byte string of a extension data from a TLS ServerHello or ClientHello + message + + :return: + A generator that yields 2-element tuples: + [0] Byte string of extension type + [1] Byte string of extension data + """ + + if data == b'': + return + + extentions_length = int_from_bytes(data[0:2]) + extensions_start = 2 + extensions_end = 2 + extentions_length + + pointer = extensions_start + while pointer < extensions_end: + extension_type = int_from_bytes(data[pointer:pointer + 2]) + extension_length = int_from_bytes(data[pointer + 2:pointer + 4]) + yield ( + extension_type, + data[pointer + 4:pointer + 4 + extension_length] + ) + pointer += 4 + extension_length + + +def raise_hostname(certificate, hostname): + """ + Raises a TLSVerificationError due to a hostname mismatch + + :param certificate: + An asn1crypto.x509.Certificate object + + :raises: + TLSVerificationError + """ + + is_ip = re.match('^\\d+\\.\\d+\\.\\d+\\.\\d+$', hostname) or hostname.find(':') != -1 + if is_ip: + hostname_type = 'IP address %s' % hostname + else: + hostname_type = 'domain name %s' % hostname + message = 'Server certificate verification failed - %s does not match' % hostname_type + valid_ips = ', '.join(certificate.valid_ips) + valid_domains = ', '.join(certificate.valid_domains) + if valid_domains: + message += ' valid domains: %s' % valid_domains + if valid_domains and valid_ips: + message += ' or' + if valid_ips: + message += ' valid IP addresses: %s' % valid_ips + raise TLSVerificationError(message, certificate) + + +def raise_verification(certificate): + """ + Raises a generic TLSVerificationError + + :param certificate: + An asn1crypto.x509.Certificate object + + :raises: + TLSVerificationError + """ + + message = 'Server certificate verification failed' + raise TLSVerificationError(message, certificate) + + +def raise_weak_signature(certificate): + """ + Raises a TLSVerificationError when a certificate uses a weak signature + algorithm + + :param certificate: + An asn1crypto.x509.Certificate object + + :raises: + TLSVerificationError + """ + + message = 'Server certificate verification failed - weak certificate signature algorithm' + raise TLSVerificationError(message, certificate) + + +def raise_client_auth(): + """ + Raises a TLSError indicating client authentication is required + + :raises: + TLSError + """ + + message = 'TLS handshake failed - client authentication required' + raise TLSError(message) + + +def raise_revoked(certificate): + """ + Raises a TLSVerificationError due to the certificate being revoked + + :param certificate: + An asn1crypto.x509.Certificate object + + :raises: + TLSVerificationError + """ + + message = 'Server certificate verification failed - certificate has been revoked' + raise TLSVerificationError(message, certificate) + + +def raise_no_issuer(certificate): + """ + Raises a TLSVerificationError due to no issuer certificate found in trust + roots + + :param certificate: + An asn1crypto.x509.Certificate object + + :raises: + TLSVerificationError + """ + + message = 'Server certificate verification failed - certificate issuer not found in trusted root certificate store' + raise TLSVerificationError(message, certificate) + + +def raise_self_signed(certificate): + """ + Raises a TLSVerificationError due to a self-signed certificate + roots + + :param certificate: + An asn1crypto.x509.Certificate object + + :raises: + TLSVerificationError + """ + + message = 'Server certificate verification failed - certificate is self-signed' + raise TLSVerificationError(message, certificate) + + +def raise_lifetime_too_long(certificate): + """ + Raises a TLSVerificationError due to a certificate lifetime exceeding + the CAB forum certificate lifetime limit + + :param certificate: + An asn1crypto.x509.Certificate object + + :raises: + TLSVerificationError + """ + + message = 'Server certificate verification failed - certificate lifetime is too long' + raise TLSVerificationError(message, certificate) + + +def raise_expired_not_yet_valid(certificate): + """ + Raises a TLSVerificationError due to certificate being expired, or not yet + being valid + + :param certificate: + An asn1crypto.x509.Certificate object + + :raises: + TLSVerificationError + """ + + validity = certificate['tbs_certificate']['validity'] + not_after = validity['not_after'].native + not_before = validity['not_before'].native + + now = datetime.now(timezone.utc) + + if not_before > now: + formatted_before = not_before.strftime('%Y-%m-%d %H:%M:%SZ') + message = 'Server certificate verification failed - certificate not valid until %s' % formatted_before + elif not_after < now: + formatted_after = not_after.strftime('%Y-%m-%d %H:%M:%SZ') + message = 'Server certificate verification failed - certificate expired %s' % formatted_after + + raise TLSVerificationError(message, certificate) + + +def raise_disconnection(): + """ + Raises a TLSDisconnectError due to a disconnection + + :raises: + TLSDisconnectError + """ + + raise TLSDisconnectError('The remote end closed the connection') + + +def raise_protocol_error(server_handshake_bytes): + """ + Raises a TLSError due to a protocol error + + :param server_handshake_bytes: + A byte string of the handshake data received from the server + + :raises: + TLSError + """ + + other_protocol = detect_other_protocol(server_handshake_bytes) + + if other_protocol: + raise TLSError('TLS protocol error - server responded using %s' % other_protocol) + + raise TLSError('TLS protocol error - server responded using a different protocol') + + +def raise_handshake(): + """ + Raises a TLSError due to a handshake error + + :raises: + TLSError + """ + + raise TLSError('TLS handshake failed') + + +def raise_protocol_version(): + """ + Raises a TLSError due to a TLS version incompatibility + + :raises: + TLSError + """ + + raise TLSError('TLS handshake failed - protocol version error') + + +def raise_dh_params(): + """ + Raises a TLSError due to weak DH params + + :raises: + TLSError + """ + + raise TLSError('TLS handshake failed - weak DH parameters') + + +def detect_other_protocol(server_handshake_bytes): + """ + Looks at the server handshake bytes to try and detect a different protocol + + :param server_handshake_bytes: + A byte string of the handshake data received from the server + + :return: + None, or a unicode string of "ftp", "http", "imap", "pop3", "smtp" + """ + + if server_handshake_bytes[0:5] == b'HTTP/': + return 'HTTP' + + if server_handshake_bytes[0:4] == b'220 ': + if re.match(b'^[^\r\n]*ftp', server_handshake_bytes, re.I): + return 'FTP' + else: + return 'SMTP' + + if server_handshake_bytes[0:4] == b'220-': + return 'FTP' + + if server_handshake_bytes[0:4] == b'+OK ': + return 'POP3' + + if server_handshake_bytes[0:4] == b'* OK' or server_handshake_bytes[0:9] == b'* PREAUTH': + return 'IMAP' + + return None diff --git a/tasks/lib/package_control/deps/oscrypto/_types.py b/tasks/lib/package_control/deps/oscrypto/_types.py new file mode 100644 index 0000000..d28dd0f --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_types.py @@ -0,0 +1,41 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys +import inspect + + +if sys.version_info < (3,): + str_cls = unicode # noqa + byte_cls = str + int_types = (int, long) # noqa + + def bytes_to_list(byte_string): + return [ord(b) for b in byte_string] + +else: + str_cls = str + byte_cls = bytes + int_types = (int,) + + bytes_to_list = list + + +def type_name(value): + """ + Returns a user-readable name for the type of an object + + :param value: + A value to get the type name of + + :return: + A unicode string of the object's type name + """ + + if inspect.isclass(value): + cls = value + else: + cls = value.__class__ + if cls.__module__ in set(['builtins', '__builtin__']): + return cls.__name__ + return '%s.%s' % (cls.__module__, cls.__name__) diff --git a/tasks/lib/package_control/deps/oscrypto/_win/__init__.py b/tasks/lib/package_control/deps/oscrypto/_win/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tasks/lib/package_control/deps/oscrypto/_win/_advapi32.py b/tasks/lib/package_control/deps/oscrypto/_win/_advapi32.py new file mode 100644 index 0000000..f2250cb --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_win/_advapi32.py @@ -0,0 +1,165 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys + +from .. import ffi +from ._decode import _try_decode +from ..errors import SignatureError +from .._ffi import new, unwrap, null +from .._types import str_cls + +if ffi() == 'cffi': + from ._advapi32_cffi import advapi32, get_error +else: + from ._advapi32_ctypes import advapi32, get_error + + +__all__ = [ + 'advapi32', + 'Advapi32Const', + 'handle_error', +] + + +_gwv = sys.getwindowsversion() +_win_version_info = (_gwv[0], _gwv[1]) + + +def open_context_handle(provider, verify_only=True): + if provider == Advapi32Const.MS_ENH_RSA_AES_PROV: + provider_type = Advapi32Const.PROV_RSA_AES + elif provider == Advapi32Const.MS_ENH_DSS_DH_PROV: + provider_type = Advapi32Const.PROV_DSS_DH + else: + raise ValueError('Invalid provider specified: %s' % provider) + + # The DSS provider needs a container to allow importing and exporting + # private keys, but all of the RSA stuff works fine with CRYPT_VERIFYCONTEXT + if verify_only or provider != Advapi32Const.MS_ENH_DSS_DH_PROV: + container_name = null() + flags = Advapi32Const.CRYPT_VERIFYCONTEXT + else: + container_name = Advapi32Const.CONTAINER_NAME + flags = Advapi32Const.CRYPT_NEWKEYSET + + context_handle_pointer = new(advapi32, 'HCRYPTPROV *') + res = advapi32.CryptAcquireContextW( + context_handle_pointer, + container_name, + provider, + provider_type, + flags + ) + # If using the DSS provider and the container exists, just open it + if not res and get_error()[0] == Advapi32Const.NTE_EXISTS: + res = advapi32.CryptAcquireContextW( + context_handle_pointer, + container_name, + provider, + provider_type, + 0 + ) + handle_error(res) + + return unwrap(context_handle_pointer) + + +def close_context_handle(handle): + res = advapi32.CryptReleaseContext(handle, 0) + handle_error(res) + + +def handle_error(result): + """ + Extracts the last Windows error message into a python unicode string + + :param result: + A function result, 0 or None indicates failure + + :return: + A unicode string error message + """ + + if result: + return + + code, error_string = get_error() + + if code == Advapi32Const.NTE_BAD_SIGNATURE: + raise SignatureError('Signature is invalid') + + if not isinstance(error_string, str_cls): + error_string = _try_decode(error_string) + + raise OSError(error_string) + + +class Advapi32Const(): + # Name we give to a container used to make DSA private key import/export work + CONTAINER_NAME = 'oscrypto temporary DSS keyset' + + PROV_RSA_AES = 24 + PROV_DSS_DH = 13 + + X509_PUBLIC_KEY_INFO = 8 + PKCS_PRIVATE_KEY_INFO = 44 + X509_DSS_SIGNATURE = 40 + CRYPT_NO_SALT = 0x00000010 + + MS_ENH_DSS_DH_PROV = "Microsoft Enhanced DSS and Diffie-Hellman Cryptographic Provider" + # This is the name for Windows Server 2003 and newer and Windows Vista and newer + MS_ENH_RSA_AES_PROV = "Microsoft Enhanced RSA and AES Cryptographic Provider" + + CRYPT_EXPORTABLE = 1 + CRYPT_NEWKEYSET = 0x00000008 + CRYPT_VERIFYCONTEXT = 0xF0000000 + + CALG_MD5 = 0x00008003 + CALG_SHA1 = 0x00008004 + CALG_SHA_256 = 0x0000800c + CALG_SHA_384 = 0x0000800d + CALG_SHA_512 = 0x0000800e + + CALG_RC2 = 0x00006602 + CALG_RC4 = 0x00006801 + CALG_DES = 0x00006601 + CALG_3DES_112 = 0x00006609 + CALG_3DES = 0x00006603 + CALG_AES_128 = 0x0000660e + CALG_AES_192 = 0x0000660f + CALG_AES_256 = 0x00006610 + + CALG_DSS_SIGN = 0x00002200 + CALG_RSA_SIGN = 0x00002400 + CALG_RSA_KEYX = 0x0000a400 + + CRYPT_MODE_CBC = 1 + + PKCS5_PADDING = 1 + + CUR_BLOB_VERSION = 2 + PUBLICKEYBLOB = 6 + PRIVATEKEYBLOB = 7 + PLAINTEXTKEYBLOB = 8 + + KP_IV = 1 + KP_PADDING = 3 + KP_MODE = 4 + KP_EFFECTIVE_KEYLEN = 19 + + CRYPT_OAEP = 0x00000040 + + NTE_BAD_SIGNATURE = -2146893818 # 0x80090006 + NTE_EXISTS = -2146893809 # 0x8009000F + AT_SIGNATURE = 2 + + RSA1 = 0x31415352 + RSA2 = 0x32415352 + DSS1 = 0x31535344 + DSS2 = 0x32535344 + + +if _win_version_info == (5, 1): + # This is the Windows XP name for the provider + Advapi32Const.MS_ENH_RSA_AES_PROV = "Microsoft Enhanced RSA and AES Cryptographic Provider (Prototype)" diff --git a/tasks/lib/package_control/deps/oscrypto/_win/_advapi32_cffi.py b/tasks/lib/package_control/deps/oscrypto/_win/_advapi32_cffi.py new file mode 100644 index 0000000..49932ff --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_win/_advapi32_cffi.py @@ -0,0 +1,145 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from .._ffi import register_ffi +from .._types import str_cls +from ..errors import LibraryNotFoundError + +import cffi + + +__all__ = [ + 'advapi32', + 'get_error', +] + + +ffi = cffi.FFI() +if cffi.__version_info__ >= (0, 9): + ffi.set_unicode(True) +ffi.cdef(""" + typedef HANDLE HCRYPTPROV; + typedef HANDLE HCRYPTKEY; + typedef HANDLE HCRYPTHASH; + typedef unsigned int ALG_ID; + + typedef struct _CRYPTOAPI_BLOB { + DWORD cbData; + BYTE *pbData; + } CRYPT_INTEGER_BLOB, CRYPT_OBJID_BLOB, CRYPT_DER_BLOB, CRYPT_ATTR_BLOB; + + typedef struct _CRYPT_ALGORITHM_IDENTIFIER { + LPSTR pszObjId; + CRYPT_OBJID_BLOB Parameters; + } CRYPT_ALGORITHM_IDENTIFIER; + + typedef struct _CRYPT_BIT_BLOB { + DWORD cbData; + BYTE *pbData; + DWORD cUnusedBits; + } CRYPT_BIT_BLOB; + + typedef struct _CERT_PUBLIC_KEY_INFO { + CRYPT_ALGORITHM_IDENTIFIER Algorithm; + CRYPT_BIT_BLOB PublicKey; + } CERT_PUBLIC_KEY_INFO; + + typedef struct _CRYPT_ATTRIBUTE { + LPSTR pszObjId; + DWORD cValue; + CRYPT_ATTR_BLOB *rgValue; + } CRYPT_ATTRIBUTE; + + typedef struct _CRYPT_ATTRIBUTES { + DWORD cAttr; + CRYPT_ATTRIBUTE *rgAttr; + } CRYPT_ATTRIBUTES; + + typedef struct _CRYPT_PRIVATE_KEY_INFO { + DWORD Version; + CRYPT_ALGORITHM_IDENTIFIER Algorithm; + CRYPT_DER_BLOB PrivateKey; + CRYPT_ATTRIBUTES *pAttributes; + } CRYPT_PRIVATE_KEY_INFO; + + typedef struct _PUBLICKEYSTRUC { + BYTE bType; + BYTE bVersion; + WORD reserved; + ALG_ID aiKeyAlg; + } BLOBHEADER, PUBLICKEYSTRUC; + + typedef struct _DSSPUBKEY { + DWORD magic; + DWORD bitlen; + } DSSPUBKEY; + + typedef struct _DSSBLOBHEADER { + PUBLICKEYSTRUC publickeystruc; + DSSPUBKEY dsspubkey; + } DSSBLOBHEADER; + + typedef struct _RSAPUBKEY { + DWORD magic; + DWORD bitlen; + DWORD pubexp; + } RSAPUBKEY; + + typedef struct _RSABLOBHEADER { + PUBLICKEYSTRUC publickeystruc; + RSAPUBKEY rsapubkey; + } RSABLOBHEADER; + + typedef struct _PLAINTEXTKEYBLOB { + BLOBHEADER hdr; + DWORD dwKeySize; + // rgbKeyData omitted since it is a flexible array member + } PLAINTEXTKEYBLOB; + + typedef struct _DSSSEED { + DWORD counter; + BYTE seed[20]; + } DSSSEED; + + BOOL CryptAcquireContextW(HCRYPTPROV *phProv, LPCWSTR pszContainer, LPCWSTR pszProvider, + DWORD dwProvType, DWORD dwFlags); + BOOL CryptReleaseContext(HCRYPTPROV hProv, DWORD dwFlags); + + BOOL CryptImportKey(HCRYPTPROV hProv, BYTE *pbData, DWORD dwDataLen, + HCRYPTKEY hPubKey, DWORD dwFlags, HCRYPTKEY *phKey); + BOOL CryptGenKey(HCRYPTPROV hProv, ALG_ID Algid, DWORD dwFlags, HCRYPTKEY *phKey); + BOOL CryptGetKeyParam(HCRYPTKEY hKey, DWORD dwParam, BYTE *pbData, DWORD *pdwDataLen, DWORD dwFlags); + BOOL CryptSetKeyParam(HCRYPTKEY hKey, DWORD dwParam, void *pbData, DWORD dwFlags); + BOOL CryptExportKey(HCRYPTKEY hKey, HCRYPTKEY hExpKey, DWORD dwBlobType, + DWORD dwFlags, BYTE *pbData, DWORD *pdwDataLen); + BOOL CryptDestroyKey(HCRYPTKEY hKey); + + BOOL CryptCreateHash(HCRYPTPROV hProv, ALG_ID Algid, HCRYPTKEY hKey, + DWORD dwFlags, HCRYPTHASH *phHash); + BOOL CryptHashData(HCRYPTHASH hHash, BYTE *pbData, DWORD dwDataLen, DWORD dwFlags); + BOOL CryptSetHashParam(HCRYPTHASH hHash, DWORD dwParam, BYTE *pbData, DWORD dwFlags); + BOOL CryptSignHashW(HCRYPTHASH hHash, DWORD dwKeySpec, LPCWSTR sDescription, + DWORD dwFlags, BYTE *pbSignature, DWORD *pdwSigLen); + BOOL CryptVerifySignatureW(HCRYPTHASH hHash, BYTE *pbSignature, DWORD dwSigLen, + HCRYPTKEY hPubKey, LPCWSTR sDescription, DWORD dwFlags); + BOOL CryptDestroyHash(HCRYPTHASH hHash); + + BOOL CryptEncrypt(HCRYPTKEY hKey, HCRYPTHASH hHash, BOOL Final, DWORD dwFlags, + BYTE *pbData, DWORD *pdwDataLen, DWORD dwBufLen); + BOOL CryptDecrypt(HCRYPTKEY hKey, HCRYPTHASH hHash, BOOL Final, DWORD dwFlags, + BYTE *pbData, DWORD *pdwDataLen); +""") + + +try: + advapi32 = ffi.dlopen('advapi32.dll') + register_ffi(advapi32, ffi) + +except (OSError) as e: + if str_cls(e).find('cannot load library') != -1: + raise LibraryNotFoundError('advapi32.dll could not be found') + raise + + +def get_error(): + return ffi.getwinerror() diff --git a/tasks/lib/package_control/deps/oscrypto/_win/_advapi32_ctypes.py b/tasks/lib/package_control/deps/oscrypto/_win/_advapi32_ctypes.py new file mode 100644 index 0000000..ba5ee10 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_win/_advapi32_ctypes.py @@ -0,0 +1,295 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import ctypes +from ctypes import windll, wintypes, POINTER, Structure, c_void_p, c_char_p, c_uint +from ctypes.wintypes import BOOL, DWORD + +from .._ffi import FFIEngineError +from .._types import str_cls +from ..errors import LibraryNotFoundError + + +__all__ = [ + 'advapi32', + 'get_error', +] + + +try: + advapi32 = windll.advapi32 +except (OSError) as e: + if str_cls(e).find('The specified module could not be found') != -1: + raise LibraryNotFoundError('advapi32.dll could not be found') + raise + +HCRYPTPROV = wintypes.HANDLE +HCRYPTKEY = wintypes.HANDLE +HCRYPTHASH = wintypes.HANDLE +PBYTE = c_char_p +ALG_ID = c_uint + +try: + class CRYPTOAPI_BLOB(Structure): # noqa + _fields_ = [ + ("cbData", DWORD), + ("pbData", POINTER(ctypes.c_byte)), + ] + CRYPT_INTEGER_BLOB = CRYPTOAPI_BLOB + CRYPT_OBJID_BLOB = CRYPTOAPI_BLOB + CRYPT_DER_BLOB = CRYPTOAPI_BLOB + CRYPT_ATTR_BLOB = CRYPTOAPI_BLOB + + class CRYPT_ALGORITHM_IDENTIFIER(Structure): + _fields = [ + ('pszObjId', wintypes.LPSTR), + ('Parameters', CRYPT_OBJID_BLOB), + ] + + class CRYPT_BIT_BLOB(Structure): + _fields_ = [ + ('cbData', DWORD), + ('pbData', PBYTE), + ('cUnusedBits', DWORD), + ] + + class CERT_PUBLIC_KEY_INFO(Structure): + _fields_ = [ + ('Algorithm', CRYPT_ALGORITHM_IDENTIFIER), + ('PublicKey', CRYPT_BIT_BLOB), + ] + + class CRYPT_ATTRIBUTE(Structure): + _fields_ = [ + ('pszObjId', wintypes.LPSTR), + ('cValue', DWORD), + ('rgValue', POINTER(CRYPT_ATTR_BLOB)), + ] + + class CRYPT_ATTRIBUTES(Structure): + _fields_ = [ + ('cAttr', DWORD), + ('rgAttr', POINTER(CRYPT_ATTRIBUTE)), + ] + + class CRYPT_PRIVATE_KEY_INFO(Structure): + _fields_ = [ + ('Version', DWORD), + ('Algorithm', CRYPT_ALGORITHM_IDENTIFIER), + ('PrivateKey', CRYPT_DER_BLOB), + ('pAttributes', POINTER(CRYPT_ATTRIBUTES)), + ] + + class PUBLICKEYSTRUC(Structure): + _fields_ = [ + ('bType', wintypes.BYTE), + ('bVersion', wintypes.BYTE), + ('reserved', wintypes.WORD), + ('aiKeyAlg', ALG_ID), + ] + BLOBHEADER = PUBLICKEYSTRUC + + class DSSPUBKEY(Structure): + _fields_ = [ + ('magic', DWORD), + ('bitlen', DWORD), + ] + + class DSSBLOBHEADER(Structure): + _fields_ = [ + ('publickeystruc', PUBLICKEYSTRUC), + ('dsspubkey', DSSPUBKEY), + ] + + class RSAPUBKEY(Structure): + _fields_ = [ + ('magic', DWORD), + ('bitlen', DWORD), + ('pubexp', DWORD), + ] + + class RSABLOBHEADER(Structure): + _fields_ = [ + ('publickeystruc', PUBLICKEYSTRUC), + ('rsapubkey', RSAPUBKEY), + ] + + class PLAINTEXTKEYBLOB(Structure): + _fields_ = [ + ('hdr', BLOBHEADER), + ('dwKeySize', DWORD), + # rgbKeyData omitted since it is a flexible array member + ] + + class DSSSEED(Structure): + _fields_ = [ + ('counter', DWORD), + ('seed', wintypes.BYTE * 20), + ] + + advapi32.CryptAcquireContextW.argtypes = [ + POINTER(HCRYPTPROV), + wintypes.LPCWSTR, + wintypes.LPCWSTR, + DWORD, + DWORD + ] + advapi32.CryptAcquireContextW.restype = wintypes.BOOL + + advapi32.CryptReleaseContext.argtypes = [ + HCRYPTPROV, + DWORD + ] + advapi32.CryptReleaseContext.restype = wintypes.BOOL + + advapi32.CryptImportKey.argtypes = [ + HCRYPTPROV, + PBYTE, + DWORD, + HCRYPTKEY, + DWORD, + POINTER(HCRYPTKEY) + ] + advapi32.CryptImportKey.restype = BOOL + + advapi32.CryptGenKey.argtypes = [ + HCRYPTPROV, + ALG_ID, + DWORD, + POINTER(HCRYPTKEY) + ] + advapi32.CryptGenKey.restype = wintypes.BOOL + + advapi32.CryptGetKeyParam.argtypes = [ + HCRYPTKEY, + DWORD, + PBYTE, + POINTER(DWORD), + DWORD + ] + advapi32.CryptGetKeyParam.restype = wintypes.BOOL + + advapi32.CryptSetKeyParam.argtypes = [ + HCRYPTKEY, + DWORD, + c_void_p, + DWORD + ] + advapi32.CryptSetKeyParam.restype = wintypes.BOOL + + advapi32.CryptExportKey.argtypes = [ + HCRYPTKEY, + HCRYPTKEY, + DWORD, + DWORD, + PBYTE, + POINTER(DWORD) + ] + advapi32.CryptExportKey.restype = BOOL + + advapi32.CryptDestroyKey.argtypes = [ + HCRYPTKEY + ] + advapi32.CryptDestroyKey.restype = wintypes.BOOL + + advapi32.CryptCreateHash.argtypes = [ + HCRYPTPROV, + ALG_ID, + HCRYPTKEY, + DWORD, + POINTER(HCRYPTHASH) + ] + advapi32.CryptCreateHash.restype = BOOL + + advapi32.CryptHashData.argtypes = [ + HCRYPTHASH, + PBYTE, + DWORD, + DWORD + ] + advapi32.CryptHashData.restype = BOOL + + advapi32.CryptSetHashParam.argtypes = [ + HCRYPTHASH, + DWORD, + PBYTE, + DWORD + ] + advapi32.CryptSetHashParam.restype = BOOL + + advapi32.CryptSignHashW.argtypes = [ + HCRYPTHASH, + DWORD, + wintypes.LPCWSTR, + DWORD, + PBYTE, + POINTER(DWORD) + ] + advapi32.CryptSignHashW.restype = BOOL + + advapi32.CryptVerifySignatureW.argtypes = [ + HCRYPTHASH, + PBYTE, + DWORD, + HCRYPTKEY, + wintypes.LPCWSTR, + DWORD + ] + advapi32.CryptVerifySignatureW.restype = BOOL + + advapi32.CryptDestroyHash.argtypes = [ + HCRYPTHASH + ] + advapi32.CryptDestroyHash.restype = wintypes.BOOL + + advapi32.CryptEncrypt.argtypes = [ + HCRYPTKEY, + HCRYPTHASH, + BOOL, + DWORD, + PBYTE, + POINTER(DWORD), + DWORD + ] + advapi32.CryptEncrypt.restype = BOOL + + advapi32.CryptDecrypt.argtypes = [ + HCRYPTKEY, + HCRYPTHASH, + BOOL, + DWORD, + PBYTE, + POINTER(DWORD) + ] + advapi32.CryptDecrypt.restype = BOOL + +except (AttributeError): + raise FFIEngineError('Error initializing ctypes') + + +setattr(advapi32, 'HCRYPTPROV', HCRYPTPROV) +setattr(advapi32, 'HCRYPTKEY', HCRYPTKEY) +setattr(advapi32, 'HCRYPTHASH', HCRYPTHASH) +setattr(advapi32, 'CRYPT_INTEGER_BLOB', CRYPT_INTEGER_BLOB) +setattr(advapi32, 'CRYPT_OBJID_BLOB', CRYPT_OBJID_BLOB) +setattr(advapi32, 'CRYPT_DER_BLOB', CRYPT_DER_BLOB) +setattr(advapi32, 'CRYPT_ATTR_BLOB', CRYPT_ATTR_BLOB) +setattr(advapi32, 'CRYPT_ALGORITHM_IDENTIFIER', CRYPT_ALGORITHM_IDENTIFIER) +setattr(advapi32, 'CRYPT_BIT_BLOB', CRYPT_BIT_BLOB) +setattr(advapi32, 'CERT_PUBLIC_KEY_INFO', CERT_PUBLIC_KEY_INFO) +setattr(advapi32, 'CRYPT_PRIVATE_KEY_INFO', CRYPT_PRIVATE_KEY_INFO) +setattr(advapi32, 'CRYPT_ATTRIBUTE', CRYPT_ATTRIBUTE) +setattr(advapi32, 'CRYPT_ATTRIBUTES', CRYPT_ATTRIBUTES) +setattr(advapi32, 'PUBLICKEYSTRUC', PUBLICKEYSTRUC) +setattr(advapi32, 'DSSPUBKEY', DSSPUBKEY) +setattr(advapi32, 'DSSBLOBHEADER', DSSBLOBHEADER) +setattr(advapi32, 'RSAPUBKEY', RSAPUBKEY) +setattr(advapi32, 'RSABLOBHEADER', RSABLOBHEADER) +setattr(advapi32, 'BLOBHEADER', BLOBHEADER) +setattr(advapi32, 'PLAINTEXTKEYBLOB', PLAINTEXTKEYBLOB) +setattr(advapi32, 'DSSSEED', DSSSEED) + + +def get_error(): + error = ctypes.GetLastError() + return (error, ctypes.FormatError(error)) diff --git a/tasks/lib/package_control/deps/oscrypto/_win/_cng.py b/tasks/lib/package_control/deps/oscrypto/_win/_cng.py new file mode 100644 index 0000000..0eb02d7 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_win/_cng.py @@ -0,0 +1,148 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from .. import ffi +from .._ffi import new, null, unwrap + +if ffi() == 'cffi': + from ._cng_cffi import bcrypt +else: + from ._cng_ctypes import bcrypt + + +__all__ = [ + 'bcrypt', + 'BcryptConst', + 'close_alg_handle', + 'handle_error', + 'open_alg_handle', +] + + +def open_alg_handle(constant, flags=0): + handle_pointer = new(bcrypt, 'BCRYPT_ALG_HANDLE *') + res = bcrypt.BCryptOpenAlgorithmProvider(handle_pointer, constant, null(), flags) + handle_error(res) + + return unwrap(handle_pointer) + + +def close_alg_handle(handle): + res = bcrypt.BCryptCloseAlgorithmProvider(handle, 0) + handle_error(res) + + +def handle_error(error_num): + """ + Extracts the last Windows error message into a python unicode string + + :param error_num: + The number to get the error string for + + :return: + A unicode string error message + """ + + if error_num == 0: + return + + messages = { + BcryptConst.STATUS_NOT_FOUND: 'The object was not found', + BcryptConst.STATUS_INVALID_PARAMETER: 'An invalid parameter was passed to a service or function', + BcryptConst.STATUS_NO_MEMORY: ( + 'Not enough virtual memory or paging file quota is available to complete the specified operation' + ), + BcryptConst.STATUS_INVALID_HANDLE: 'An invalid HANDLE was specified', + BcryptConst.STATUS_INVALID_SIGNATURE: 'The cryptographic signature is invalid', + BcryptConst.STATUS_NOT_SUPPORTED: 'The request is not supported', + BcryptConst.STATUS_BUFFER_TOO_SMALL: 'The buffer is too small to contain the entry', + BcryptConst.STATUS_INVALID_BUFFER_SIZE: 'The size of the buffer is invalid for the specified operation', + } + + output = 'NTSTATUS error 0x%0.2X' % error_num + + if error_num is not None and error_num in messages: + output += ': ' + messages[error_num] + + raise OSError(output) + + +class BcryptConst(): + BCRYPT_RNG_ALGORITHM = 'RNG' + + BCRYPT_KEY_LENGTH = 'KeyLength' + BCRYPT_EFFECTIVE_KEY_LENGTH = 'EffectiveKeyLength' + + BCRYPT_RSAPRIVATE_BLOB = 'RSAPRIVATEBLOB' + BCRYPT_RSAFULLPRIVATE_BLOB = 'RSAFULLPRIVATEBLOB' + BCRYPT_RSAPUBLIC_BLOB = 'RSAPUBLICBLOB' + BCRYPT_DSA_PRIVATE_BLOB = 'DSAPRIVATEBLOB' + BCRYPT_DSA_PUBLIC_BLOB = 'DSAPUBLICBLOB' + BCRYPT_ECCPRIVATE_BLOB = 'ECCPRIVATEBLOB' + BCRYPT_ECCPUBLIC_BLOB = 'ECCPUBLICBLOB' + + BCRYPT_RSAPUBLIC_MAGIC = 0x31415352 + BCRYPT_RSAPRIVATE_MAGIC = 0x32415352 + BCRYPT_RSAFULLPRIVATE_MAGIC = 0x33415352 + + BCRYPT_DSA_PUBLIC_MAGIC = 0x42505344 + BCRYPT_DSA_PRIVATE_MAGIC = 0x56505344 + BCRYPT_DSA_PUBLIC_MAGIC_V2 = 0x32425044 + BCRYPT_DSA_PRIVATE_MAGIC_V2 = 0x32565044 + + DSA_HASH_ALGORITHM_SHA1 = 0 + DSA_HASH_ALGORITHM_SHA256 = 1 + DSA_HASH_ALGORITHM_SHA512 = 2 + + DSA_FIPS186_2 = 0 + DSA_FIPS186_3 = 1 + + BCRYPT_NO_KEY_VALIDATION = 8 + + BCRYPT_ECDSA_PUBLIC_P256_MAGIC = 0x31534345 + BCRYPT_ECDSA_PRIVATE_P256_MAGIC = 0x32534345 + BCRYPT_ECDSA_PUBLIC_P384_MAGIC = 0x33534345 + BCRYPT_ECDSA_PRIVATE_P384_MAGIC = 0x34534345 + BCRYPT_ECDSA_PUBLIC_P521_MAGIC = 0x35534345 + BCRYPT_ECDSA_PRIVATE_P521_MAGIC = 0x36534345 + + STATUS_SUCCESS = 0x00000000 + STATUS_NOT_FOUND = 0xC0000225 + STATUS_INVALID_PARAMETER = 0xC000000D + STATUS_NO_MEMORY = 0xC0000017 + STATUS_INVALID_HANDLE = 0xC0000008 + STATUS_INVALID_SIGNATURE = 0xC000A000 + STATUS_NOT_SUPPORTED = 0xC00000BB + STATUS_BUFFER_TOO_SMALL = 0xC0000023 + STATUS_INVALID_BUFFER_SIZE = 0xC0000206 + + BCRYPT_KEY_DATA_BLOB_MAGIC = 0x4d42444b + BCRYPT_KEY_DATA_BLOB_VERSION1 = 0x00000001 + BCRYPT_KEY_DATA_BLOB = 'KeyDataBlob' + + BCRYPT_PAD_PKCS1 = 0x00000002 + BCRYPT_PAD_OAEP = 0x00000004 + BCRYPT_PAD_PSS = 0x00000008 + + BCRYPT_3DES_ALGORITHM = '3DES' + BCRYPT_3DES_112_ALGORITHM = '3DES_112' + BCRYPT_AES_ALGORITHM = 'AES' + BCRYPT_DES_ALGORITHM = 'DES' + BCRYPT_RC2_ALGORITHM = 'RC2' + BCRYPT_RC4_ALGORITHM = 'RC4' + + BCRYPT_DSA_ALGORITHM = 'DSA' + BCRYPT_ECDSA_P256_ALGORITHM = 'ECDSA_P256' + BCRYPT_ECDSA_P384_ALGORITHM = 'ECDSA_P384' + BCRYPT_ECDSA_P521_ALGORITHM = 'ECDSA_P521' + BCRYPT_RSA_ALGORITHM = 'RSA' + + BCRYPT_MD5_ALGORITHM = 'MD5' + BCRYPT_SHA1_ALGORITHM = 'SHA1' + BCRYPT_SHA256_ALGORITHM = 'SHA256' + BCRYPT_SHA384_ALGORITHM = 'SHA384' + BCRYPT_SHA512_ALGORITHM = 'SHA512' + + BCRYPT_ALG_HANDLE_HMAC_FLAG = 0x00000008 + + BCRYPT_BLOCK_PADDING = 0x00000001 diff --git a/tasks/lib/package_control/deps/oscrypto/_win/_cng_cffi.py b/tasks/lib/package_control/deps/oscrypto/_win/_cng_cffi.py new file mode 100644 index 0000000..3e9d5ce --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_win/_cng_cffi.py @@ -0,0 +1,120 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from .._ffi import register_ffi +from .._types import str_cls +from ..errors import LibraryNotFoundError + +from cffi import FFI + + +__all__ = [ + 'bcrypt', +] + + +ffi = FFI() +ffi.cdef(""" + typedef HANDLE BCRYPT_ALG_HANDLE; + typedef HANDLE BCRYPT_KEY_HANDLE; + typedef ULONG NTSTATUS; + typedef unsigned char *PUCHAR; + typedef unsigned char *PBYTE; + + + typedef struct _BCRYPT_RSAKEY_BLOB { + ULONG Magic; + ULONG BitLength; + ULONG cbPublicExp; + ULONG cbModulus; + ULONG cbPrime1; + ULONG cbPrime2; + } BCRYPT_RSAKEY_BLOB; + + typedef struct _BCRYPT_DSA_KEY_BLOB { + ULONG dwMagic; + ULONG cbKey; + UCHAR Count[4]; + UCHAR Seed[20]; + UCHAR q[20]; + } BCRYPT_DSA_KEY_BLOB; + + typedef struct _BCRYPT_DSA_KEY_BLOB_V2 { + ULONG dwMagic; + ULONG cbKey; + INT hashAlgorithm; + INT standardVersion; + ULONG cbSeedLength; + ULONG cbGroupSize; + UCHAR Count[4]; + } BCRYPT_DSA_KEY_BLOB_V2; + + typedef struct _BCRYPT_ECCKEY_BLOB { + ULONG dwMagic; + ULONG cbKey; + } BCRYPT_ECCKEY_BLOB; + + typedef struct _BCRYPT_PKCS1_PADDING_INFO { + LPCWSTR pszAlgId; + } BCRYPT_PKCS1_PADDING_INFO; + + typedef struct _BCRYPT_PSS_PADDING_INFO { + LPCWSTR pszAlgId; + ULONG cbSalt; + } BCRYPT_PSS_PADDING_INFO; + + typedef struct _BCRYPT_OAEP_PADDING_INFO { + LPCWSTR pszAlgId; + PUCHAR pbLabel; + ULONG cbLabel; + } BCRYPT_OAEP_PADDING_INFO; + + typedef struct _BCRYPT_KEY_DATA_BLOB_HEADER { + ULONG dwMagic; + ULONG dwVersion; + ULONG cbKeyData; + } BCRYPT_KEY_DATA_BLOB_HEADER; + + NTSTATUS BCryptOpenAlgorithmProvider(BCRYPT_ALG_HANDLE *phAlgorithm, LPCWSTR pszAlgId, LPCWSTR pszImplementation, + DWORD dwFlags); + NTSTATUS BCryptCloseAlgorithmProvider(BCRYPT_ALG_HANDLE hAlgorithm, DWORD dwFlags); + NTSTATUS BCryptSetProperty(HANDLE hObject, LPCWSTR pszProperty, ULONG *pbInput, ULONG cbInput, ULONG dwFlags); + + NTSTATUS BCryptImportKeyPair(BCRYPT_ALG_HANDLE hAlgorithm, BCRYPT_KEY_HANDLE hImportKey, LPCWSTR pszBlobType, + BCRYPT_KEY_HANDLE *phKey, PUCHAR pbInput, ULONG cbInput, ULONG dwFlags); + NTSTATUS BCryptImportKey(BCRYPT_ALG_HANDLE hAlgorithm, BCRYPT_KEY_HANDLE hImportKey, LPCWSTR pszBlobType, + BCRYPT_KEY_HANDLE *phKey, PUCHAR pbKeyObject, ULONG cbKeyObject, PUCHAR pbInput, ULONG cbInput, + ULONG dwFlags); + NTSTATUS BCryptDestroyKey(BCRYPT_KEY_HANDLE hKey); + + NTSTATUS BCryptVerifySignature(BCRYPT_KEY_HANDLE hKey, void *pPaddingInfo, PUCHAR pbHash, ULONG cbHash, + PUCHAR pbSignature, ULONG cbSignature, ULONG dwFlags); + NTSTATUS BCryptSignHash(BCRYPT_KEY_HANDLE hKey, void * pPaddingInfo, PBYTE pbInput, DWORD cbInput, PBYTE pbOutput, + DWORD cbOutput, DWORD *pcbResult, ULONG dwFlags); + + NTSTATUS BCryptEncrypt(BCRYPT_KEY_HANDLE hKey, PUCHAR pbInput, ULONG cbInput, void *pPaddingInfo, PUCHAR pbIV, + ULONG cbIV, PUCHAR pbOutput, ULONG cbOutput, ULONG *pcbResult, ULONG dwFlags); + NTSTATUS BCryptDecrypt(BCRYPT_KEY_HANDLE hKey, PUCHAR pbInput, ULONG cbInput, void *pPaddingInfo, PUCHAR pbIV, + ULONG cbIV, PUCHAR pbOutput, ULONG cbOutput, ULONG *pcbResult, ULONG dwFlags); + + NTSTATUS BCryptDeriveKeyPBKDF2(BCRYPT_ALG_HANDLE hPrf, PUCHAR pbPassword, ULONG cbPassword, PUCHAR pbSalt, + ULONG cbSalt, ULONGLONG cIterations, PUCHAR pbDerivedKey, ULONG cbDerivedKey, ULONG dwFlags); + + NTSTATUS BCryptGenRandom(BCRYPT_ALG_HANDLE hAlgorithm, PUCHAR pbBuffer, ULONG cbBuffer, ULONG dwFlags); + + NTSTATUS BCryptGenerateKeyPair(BCRYPT_ALG_HANDLE hAlgorithm, BCRYPT_KEY_HANDLE *phKey, ULONG dwLength, + ULONG dwFlags); + NTSTATUS BCryptFinalizeKeyPair(BCRYPT_KEY_HANDLE hKey, ULONG dwFlags); + NTSTATUS BCryptExportKey(BCRYPT_KEY_HANDLE hKey, BCRYPT_KEY_HANDLE hExportKey, LPCWSTR pszBlobType, + PUCHAR pbOutput, ULONG cbOutput, ULONG *pcbResult, ULONG dwFlags); +""") + + +try: + bcrypt = ffi.dlopen('bcrypt.dll') + register_ffi(bcrypt, ffi) + +except (OSError) as e: + if str_cls(e).find('cannot load library') != -1: + raise LibraryNotFoundError('bcrypt.dll could not be found - Windows XP and Server 2003 are not supported') + raise diff --git a/tasks/lib/package_control/deps/oscrypto/_win/_cng_ctypes.py b/tasks/lib/package_control/deps/oscrypto/_win/_cng_ctypes.py new file mode 100644 index 0000000..fcfae6b --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_win/_cng_ctypes.py @@ -0,0 +1,263 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from ctypes import windll, wintypes, POINTER, Structure, c_void_p, c_ulonglong, c_char_p, c_byte +from ctypes.wintypes import ULONG, DWORD, LPCWSTR + +from .._ffi import FFIEngineError +from .._types import str_cls +from ..errors import LibraryNotFoundError + + +__all__ = [ + 'bcrypt', +] + + +try: + bcrypt = windll.bcrypt +except (OSError) as e: + if str_cls(e).find('The specified module could not be found') != -1: + raise LibraryNotFoundError('bcrypt.dll could not be found - Windows XP and Server 2003 are not supported') + raise + +BCRYPT_ALG_HANDLE = wintypes.HANDLE +BCRYPT_KEY_HANDLE = wintypes.HANDLE +NTSTATUS = wintypes.ULONG +PUCHAR = c_char_p +PBYTE = c_char_p + +try: + bcrypt.BCryptOpenAlgorithmProvider.argtypes = [ + POINTER(BCRYPT_ALG_HANDLE), + LPCWSTR, + LPCWSTR, + DWORD + ] + bcrypt.BCryptOpenAlgorithmProvider.restype = NTSTATUS + + bcrypt.BCryptCloseAlgorithmProvider.argtypes = [ + BCRYPT_ALG_HANDLE, + ULONG + ] + bcrypt.BCryptCloseAlgorithmProvider.restype = NTSTATUS + + bcrypt.BCryptImportKeyPair.argtypes = [ + BCRYPT_ALG_HANDLE, + BCRYPT_KEY_HANDLE, + LPCWSTR, + POINTER(BCRYPT_KEY_HANDLE), + PUCHAR, + ULONG, + ULONG + ] + bcrypt.BCryptImportKeyPair.restype = NTSTATUS + + bcrypt.BCryptImportKey.argtypes = [ + BCRYPT_ALG_HANDLE, + BCRYPT_KEY_HANDLE, + LPCWSTR, + POINTER(BCRYPT_KEY_HANDLE), + PUCHAR, + ULONG, + PUCHAR, + ULONG, + ULONG + ] + bcrypt.BCryptImportKey.restype = NTSTATUS + + bcrypt.BCryptDestroyKey.argtypes = [ + BCRYPT_KEY_HANDLE + ] + bcrypt.BCryptDestroyKey.restype = NTSTATUS + + bcrypt.BCryptVerifySignature.argtypes = [ + BCRYPT_KEY_HANDLE, + c_void_p, + PUCHAR, + ULONG, + PUCHAR, + ULONG, + ULONG + ] + bcrypt.BCryptVerifySignature.restype = NTSTATUS + + bcrypt.BCryptSignHash.argtypes = [ + BCRYPT_KEY_HANDLE, + c_void_p, + PBYTE, + DWORD, + PBYTE, + DWORD, + POINTER(DWORD), + ULONG + ] + bcrypt.BCryptSignHash.restype = NTSTATUS + + bcrypt.BCryptSetProperty.argtypes = [ + BCRYPT_KEY_HANDLE, + LPCWSTR, + c_void_p, + ULONG, + ULONG + ] + bcrypt.BCryptSetProperty.restype = NTSTATUS + + bcrypt.BCryptEncrypt.argtypes = [ + BCRYPT_KEY_HANDLE, + PUCHAR, + ULONG, + c_void_p, + PUCHAR, + ULONG, + PUCHAR, + ULONG, + POINTER(ULONG), + ULONG + ] + bcrypt.BCryptEncrypt.restype = NTSTATUS + + bcrypt.BCryptDecrypt.argtypes = [ + BCRYPT_KEY_HANDLE, + PUCHAR, + ULONG, + c_void_p, + PUCHAR, + ULONG, + PUCHAR, + ULONG, + POINTER(ULONG), + ULONG + ] + bcrypt.BCryptDecrypt.restype = NTSTATUS + + bcrypt.BCryptDeriveKeyPBKDF2.argtypes = [ + BCRYPT_ALG_HANDLE, + PUCHAR, + ULONG, + PUCHAR, + ULONG, + c_ulonglong, + PUCHAR, + ULONG, + ULONG + ] + bcrypt.BCryptDeriveKeyPBKDF2.restype = NTSTATUS + + bcrypt.BCryptGenRandom.argtypes = [ + BCRYPT_ALG_HANDLE, + PUCHAR, + ULONG, + ULONG + ] + bcrypt.BCryptGenRandom.restype = NTSTATUS + + bcrypt.BCryptGenerateKeyPair.argtypes = [ + BCRYPT_ALG_HANDLE, + POINTER(BCRYPT_KEY_HANDLE), + ULONG, + ULONG + ] + bcrypt.BCryptGenerateKeyPair.restype = NTSTATUS + + bcrypt.BCryptFinalizeKeyPair.argtypes = [ + BCRYPT_KEY_HANDLE, + ULONG + ] + bcrypt.BCryptFinalizeKeyPair.restype = NTSTATUS + + bcrypt.BCryptExportKey.argtypes = [ + BCRYPT_KEY_HANDLE, + BCRYPT_KEY_HANDLE, + LPCWSTR, + PUCHAR, + ULONG, + POINTER(ULONG), + ULONG + ] + bcrypt.BCryptExportKey.restype = NTSTATUS + +except (AttributeError): + raise FFIEngineError('Error initializing ctypes') + + +class BCRYPT_RSAKEY_BLOB(Structure): # noqa + _fields_ = [ + ('Magic', ULONG), + ('BitLength', ULONG), + ('cbPublicExp', ULONG), + ('cbModulus', ULONG), + ('cbPrime1', ULONG), + ('cbPrime2', ULONG), + ] + + +class BCRYPT_DSA_KEY_BLOB(Structure): # noqa + _fields_ = [ + ('dwMagic', ULONG), + ('cbKey', ULONG), + ('Count', c_byte * 4), + ('Seed', c_byte * 20), + ('q', c_byte * 20), + ] + + +class BCRYPT_DSA_KEY_BLOB_V2(Structure): # noqa + _fields_ = [ + ('dwMagic', ULONG), + ('cbKey', ULONG), + ('hashAlgorithm', wintypes.INT), + ('standardVersion', wintypes.INT), + ('cbSeedLength', ULONG), + ('cbGroupSize', ULONG), + ('Count', c_byte * 4), + ] + + +class BCRYPT_ECCKEY_BLOB(Structure): # noqa + _fields_ = [ + ('dwMagic', ULONG), + ('cbKey', ULONG), + ] + + +class BCRYPT_PKCS1_PADDING_INFO(Structure): # noqa + _fields_ = [ + ('pszAlgId', LPCWSTR), + ] + + +class BCRYPT_PSS_PADDING_INFO(Structure): # noqa + _fields_ = [ + ('pszAlgId', LPCWSTR), + ('cbSalt', ULONG), + ] + + +class BCRYPT_OAEP_PADDING_INFO(Structure): # noqa + _fields_ = [ + ('pszAlgId', LPCWSTR), + ('pbLabel', PUCHAR), + ('cbLabel', ULONG), + ] + + +class BCRYPT_KEY_DATA_BLOB_HEADER(Structure): # noqa + _fields_ = [ + ('dwMagic', ULONG), + ('dwVersion', ULONG), + ('cbKeyData', ULONG), + ] + + +setattr(bcrypt, 'BCRYPT_ALG_HANDLE', BCRYPT_ALG_HANDLE) +setattr(bcrypt, 'BCRYPT_KEY_HANDLE', BCRYPT_KEY_HANDLE) + +setattr(bcrypt, 'BCRYPT_RSAKEY_BLOB', BCRYPT_RSAKEY_BLOB) +setattr(bcrypt, 'BCRYPT_DSA_KEY_BLOB', BCRYPT_DSA_KEY_BLOB) +setattr(bcrypt, 'BCRYPT_DSA_KEY_BLOB_V2', BCRYPT_DSA_KEY_BLOB_V2) +setattr(bcrypt, 'BCRYPT_ECCKEY_BLOB', BCRYPT_ECCKEY_BLOB) +setattr(bcrypt, 'BCRYPT_PKCS1_PADDING_INFO', BCRYPT_PKCS1_PADDING_INFO) +setattr(bcrypt, 'BCRYPT_PSS_PADDING_INFO', BCRYPT_PSS_PADDING_INFO) +setattr(bcrypt, 'BCRYPT_OAEP_PADDING_INFO', BCRYPT_OAEP_PADDING_INFO) +setattr(bcrypt, 'BCRYPT_KEY_DATA_BLOB_HEADER', BCRYPT_KEY_DATA_BLOB_HEADER) diff --git a/tasks/lib/package_control/deps/oscrypto/_win/_crypt32.py b/tasks/lib/package_control/deps/oscrypto/_win/_crypt32.py new file mode 100644 index 0000000..bc6e855 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_win/_crypt32.py @@ -0,0 +1,73 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from .. import ffi +from ._decode import _try_decode +from .._ffi import buffer_from_bytes +from .._types import str_cls + +if ffi() == 'cffi': + from ._crypt32_cffi import crypt32, get_error +else: + from ._crypt32_ctypes import crypt32, get_error + + +__all__ = [ + 'crypt32', + 'Crypt32Const', + 'handle_error', +] + + +def handle_error(result): + """ + Extracts the last Windows error message into a python unicode string + + :param result: + A function result, 0 or None indicates failure + + :return: + A unicode string error message + """ + + if result: + return + + _, error_string = get_error() + + if not isinstance(error_string, str_cls): + error_string = _try_decode(error_string) + + raise OSError(error_string) + + +class Crypt32Const(): + X509_ASN_ENCODING = 1 + + ERROR_INSUFFICIENT_BUFFER = 122 + CERT_FIND_PROP_ONLY_ENHKEY_USAGE_FLAG = 0x4 + CRYPT_E_NOT_FOUND = -2146885628 + + CERT_STORE_PROV_MEMORY = b'Memory' + CERT_STORE_CREATE_NEW_FLAG = 0x00002000 + CERT_STORE_ADD_USE_EXISTING = 2 + USAGE_MATCH_TYPE_OR = 1 + CERT_CHAIN_POLICY_SSL = 4 + AUTHTYPE_SERVER = 2 + CERT_CHAIN_POLICY_ALLOW_UNKNOWN_CA_FLAG = 0x00000010 + CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS = 0x00000F00 + CERT_CHAIN_CACHE_END_CERT = 1 + CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY = 0x80000000 + + TRUST_E_CERT_SIGNATURE = 0x80096004 + + CERT_E_EXPIRED = 0x800B0101 + CERT_E_ROLE = 0x800B0103 + CERT_E_PURPOSE = 0x800B0106 + CERT_E_UNTRUSTEDROOT = 0x800B0109 + CERT_E_CN_NO_MATCH = 0x800B010F + CRYPT_E_REVOKED = 0x80092010 + + PKIX_KP_SERVER_AUTH = buffer_from_bytes(b"1.3.6.1.5.5.7.3.1\x00") + SERVER_GATED_CRYPTO = buffer_from_bytes(b"1.3.6.1.4.1.311.10.3.3\x00") + SGC_NETSCAPE = buffer_from_bytes(b"2.16.840.1.113730.4.1\x00") diff --git a/tasks/lib/package_control/deps/oscrypto/_win/_crypt32_cffi.py b/tasks/lib/package_control/deps/oscrypto/_win/_crypt32_cffi.py new file mode 100644 index 0000000..3952682 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_win/_crypt32_cffi.py @@ -0,0 +1,188 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys + +from .._ffi import register_ffi +from .._types import str_cls +from ..errors import LibraryNotFoundError + +import cffi + + +__all__ = [ + 'crypt32', + 'get_error', +] + + +ffi = cffi.FFI() +if cffi.__version_info__ >= (0, 9): + ffi.set_unicode(True) +if sys.maxsize > 2 ** 32: + ffi.cdef("typedef uint64_t ULONG_PTR;") +else: + ffi.cdef("typedef unsigned long ULONG_PTR;") +ffi.cdef(""" + typedef HANDLE HCERTSTORE; + typedef unsigned char *PBYTE; + + + typedef struct _CRYPTOAPI_BLOB { + DWORD cbData; + PBYTE pbData; + } CRYPTOAPI_BLOB; + typedef CRYPTOAPI_BLOB CRYPT_INTEGER_BLOB; + typedef CRYPTOAPI_BLOB CERT_NAME_BLOB; + typedef CRYPTOAPI_BLOB CRYPT_BIT_BLOB; + typedef CRYPTOAPI_BLOB CRYPT_OBJID_BLOB; + + typedef struct _CRYPT_ALGORITHM_IDENTIFIER { + LPSTR pszObjId; + CRYPT_OBJID_BLOB Parameters; + } CRYPT_ALGORITHM_IDENTIFIER; + + typedef struct _FILETIME { + DWORD dwLowDateTime; + DWORD dwHighDateTime; + } FILETIME; + + typedef struct _CERT_PUBLIC_KEY_INFO { + CRYPT_ALGORITHM_IDENTIFIER Algorithm; + CRYPT_BIT_BLOB PublicKey; + } CERT_PUBLIC_KEY_INFO; + + typedef struct _CERT_EXTENSION { + LPSTR pszObjId; + BOOL fCritical; + CRYPT_OBJID_BLOB Value; + } CERT_EXTENSION, *PCERT_EXTENSION; + + typedef struct _CERT_INFO { + DWORD dwVersion; + CRYPT_INTEGER_BLOB SerialNumber; + CRYPT_ALGORITHM_IDENTIFIER SignatureAlgorithm; + CERT_NAME_BLOB Issuer; + FILETIME NotBefore; + FILETIME NotAfter; + CERT_NAME_BLOB Subject; + CERT_PUBLIC_KEY_INFO SubjectPublicKeyInfo; + CRYPT_BIT_BLOB IssuerUniqueId; + CRYPT_BIT_BLOB SubjectUniqueId; + DWORD cExtension; + PCERT_EXTENSION *rgExtension; + } CERT_INFO, *PCERT_INFO; + + typedef struct _CERT_CONTEXT { + DWORD dwCertEncodingType; + PBYTE pbCertEncoded; + DWORD cbCertEncoded; + PCERT_INFO pCertInfo; + HCERTSTORE hCertStore; + } CERT_CONTEXT, *PCERT_CONTEXT; + + typedef struct _CERT_TRUST_STATUS { + DWORD dwErrorStatus; + DWORD dwInfoStatus; + } CERT_TRUST_STATUS, *PCERT_TRUST_STATUS; + + typedef struct _CERT_ENHKEY_USAGE { + DWORD cUsageIdentifier; + LPSTR *rgpszUsageIdentifier; + } CERT_ENHKEY_USAGE, *PCERT_ENHKEY_USAGE; + + typedef struct _CERT_CHAIN_ELEMENT { + DWORD cbSize; + PCERT_CONTEXT pCertContext; + CERT_TRUST_STATUS TrustStatus; + void *pRevocationInfo; + PCERT_ENHKEY_USAGE pIssuanceUsage; + PCERT_ENHKEY_USAGE pApplicationUsage; + LPCWSTR pwszExtendedErrorInfo; + } CERT_CHAIN_ELEMENT, *PCERT_CHAIN_ELEMENT; + + typedef struct _CERT_SIMPLE_CHAIN { + DWORD cbSize; + CERT_TRUST_STATUS TrustStatus; + DWORD cElement; + PCERT_CHAIN_ELEMENT *rgpElement; + void *pTrustListInfo; + BOOL fHasRevocationFreshnessTime; + DWORD dwRevocationFreshnessTime; + } CERT_SIMPLE_CHAIN, *PCERT_SIMPLE_CHAIN; + + typedef struct _CERT_CHAIN_CONTEXT { + DWORD cbSize; + CERT_TRUST_STATUS TrustStatus; + DWORD cChain; + PCERT_SIMPLE_CHAIN *rgpChain; + DWORD cLowerQualityChainContext; + void *rgpLowerQualityChainContext; + BOOL fHasRevocationFreshnessTime; + DWORD dwRevocationFreshnessTime; + } CERT_CHAIN_CONTEXT, *PCERT_CHAIN_CONTEXT; + + typedef struct _CERT_USAGE_MATCH { + DWORD dwType; + CERT_ENHKEY_USAGE Usage; + } CERT_USAGE_MATCH; + + typedef struct _CERT_CHAIN_PARA { + DWORD cbSize; + CERT_USAGE_MATCH RequestedUsage; + } CERT_CHAIN_PARA; + + typedef struct _CERT_CHAIN_POLICY_PARA { + DWORD cbSize; + DWORD dwFlags; + void *pvExtraPolicyPara; + } CERT_CHAIN_POLICY_PARA; + + typedef struct _HTTPSPolicyCallbackData { + DWORD cbSize; + DWORD dwAuthType; + DWORD fdwChecks; + WCHAR *pwszServerName; + } SSL_EXTRA_CERT_CHAIN_POLICY_PARA; + + typedef struct _CERT_CHAIN_POLICY_STATUS { + DWORD cbSize; + DWORD dwError; + LONG lChainIndex; + LONG lElementIndex; + void *pvExtraPolicyStatus; + } CERT_CHAIN_POLICY_STATUS; + + typedef HANDLE HCERTCHAINENGINE; + typedef HANDLE HCRYPTPROV; + + HCERTSTORE CertOpenStore(LPCSTR lpszStoreProvider, DWORD dwMsgAndCertEncodingType, HCRYPTPROV hCryptProv, + DWORD dwFlags, void *pvPara); + BOOL CertAddEncodedCertificateToStore(HCERTSTORE hCertStore, DWORD dwCertEncodingType, BYTE *pbCertEncoded, + DWORD cbCertEncoded, DWORD dwAddDisposition, PCERT_CONTEXT *ppCertContext); + BOOL CertGetCertificateChain(HCERTCHAINENGINE hChainEngine, CERT_CONTEXT *pCertContext, FILETIME *pTime, + HCERTSTORE hAdditionalStore, CERT_CHAIN_PARA *pChainPara, DWORD dwFlags, void *pvReserved, + PCERT_CHAIN_CONTEXT *ppChainContext); + BOOL CertVerifyCertificateChainPolicy(ULONG_PTR pszPolicyOID, PCERT_CHAIN_CONTEXT pChainContext, + CERT_CHAIN_POLICY_PARA *pPolicyPara, CERT_CHAIN_POLICY_STATUS *pPolicyStatus); + void CertFreeCertificateChain(PCERT_CHAIN_CONTEXT pChainContext); + + HCERTSTORE CertOpenSystemStoreW(HANDLE hprov, LPCWSTR szSubsystemProtocol); + PCERT_CONTEXT CertEnumCertificatesInStore(HCERTSTORE hCertStore, CERT_CONTEXT *pPrevCertContext); + BOOL CertCloseStore(HCERTSTORE hCertStore, DWORD dwFlags); + BOOL CertGetEnhancedKeyUsage(CERT_CONTEXT *pCertContext, DWORD dwFlags, CERT_ENHKEY_USAGE *pUsage, DWORD *pcbUsage); +""") + + +try: + crypt32 = ffi.dlopen('crypt32.dll') + register_ffi(crypt32, ffi) + +except (OSError) as e: + if str_cls(e).find('cannot load library') != -1: + raise LibraryNotFoundError('crypt32.dll could not be found') + raise + + +def get_error(): + return ffi.getwinerror() diff --git a/tasks/lib/package_control/deps/oscrypto/_win/_crypt32_ctypes.py b/tasks/lib/package_control/deps/oscrypto/_win/_crypt32_ctypes.py new file mode 100644 index 0000000..b981856 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_win/_crypt32_ctypes.py @@ -0,0 +1,276 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys + +import ctypes +from ctypes import windll, wintypes, POINTER, Structure, c_void_p, c_char_p +from ctypes.wintypes import DWORD + +from .._ffi import FFIEngineError +from .._types import str_cls +from ..errors import LibraryNotFoundError +from ._kernel32 import kernel32 + + +__all__ = [ + 'crypt32', + 'get_error', +] + + +try: + crypt32 = windll.crypt32 +except (OSError) as e: + if str_cls(e).find('The specified module could not be found') != -1: + raise LibraryNotFoundError('crypt32.dll could not be found') + raise + +HCERTSTORE = wintypes.HANDLE +HCERTCHAINENGINE = wintypes.HANDLE +HCRYPTPROV = wintypes.HANDLE +HCRYPTKEY = wintypes.HANDLE +PBYTE = c_char_p +if sys.maxsize > 2 ** 32: + ULONG_PTR = ctypes.c_uint64 +else: + ULONG_PTR = ctypes.c_ulong + +try: + class CRYPTOAPI_BLOB(Structure): # noqa + _fields_ = [ + ("cbData", DWORD), + ("pbData", c_void_p), + ] + CRYPT_INTEGER_BLOB = CRYPTOAPI_BLOB + CERT_NAME_BLOB = CRYPTOAPI_BLOB + CRYPT_BIT_BLOB = CRYPTOAPI_BLOB + CRYPT_OBJID_BLOB = CRYPTOAPI_BLOB + + class CRYPT_ALGORITHM_IDENTIFIER(Structure): # noqa + _fields_ = [ + ("pszObjId", wintypes.LPSTR), + ("Parameters", CRYPT_OBJID_BLOB), + ] + + class CERT_PUBLIC_KEY_INFO(Structure): # noqa + _fields_ = [ + ("Algorithm", CRYPT_ALGORITHM_IDENTIFIER), + ("PublicKey", CRYPT_BIT_BLOB), + ] + + class CERT_EXTENSION(Structure): # noqa + _fields_ = [ + ("pszObjId", wintypes.LPSTR), + ("fCritical", wintypes.BOOL), + ("Value", CRYPT_OBJID_BLOB), + ] + PCERT_EXTENSION = POINTER(CERT_EXTENSION) + + class CERT_INFO(Structure): # noqa + _fields_ = [ + ("dwVersion", DWORD), + ("SerialNumber", CRYPT_INTEGER_BLOB), + ("SignatureAlgorithm", CRYPT_ALGORITHM_IDENTIFIER), + ("Issuer", CERT_NAME_BLOB), + ("NotBefore", kernel32.FILETIME), + ("NotAfter", kernel32.FILETIME), + ("Subject", CERT_NAME_BLOB), + ("SubjectPublicKeyInfo", CERT_PUBLIC_KEY_INFO), + ("IssuerUniqueId", CRYPT_BIT_BLOB), + ("SubjectUniqueId", CRYPT_BIT_BLOB), + ("cExtension", DWORD), + ("rgExtension", POINTER(PCERT_EXTENSION)), + ] + PCERT_INFO = POINTER(CERT_INFO) + + class CERT_CONTEXT(Structure): # noqa + _fields_ = [ + ("dwCertEncodingType", DWORD), + ("pbCertEncoded", c_void_p), + ("cbCertEncoded", DWORD), + ("pCertInfo", PCERT_INFO), + ("hCertStore", HCERTSTORE) + ] + + PCERT_CONTEXT = POINTER(CERT_CONTEXT) + + class CERT_ENHKEY_USAGE(Structure): # noqa + _fields_ = [ + ('cUsageIdentifier', DWORD), + ('rgpszUsageIdentifier', POINTER(POINTER(wintypes.BYTE))), + ] + + PCERT_ENHKEY_USAGE = POINTER(CERT_ENHKEY_USAGE) + + class CERT_TRUST_STATUS(Structure): # noqa + _fields_ = [ + ('dwErrorStatus', DWORD), + ('dwInfoStatus', DWORD), + ] + + class CERT_CHAIN_ELEMENT(Structure): # noqa + _fields_ = [ + ('cbSize', DWORD), + ('pCertContext', PCERT_CONTEXT), + ('TrustStatus', CERT_TRUST_STATUS), + ('pRevocationInfo', c_void_p), + ('pIssuanceUsage', PCERT_ENHKEY_USAGE), + ('pApplicationUsage', PCERT_ENHKEY_USAGE), + ('pwszExtendedErrorInfo', wintypes.LPCWSTR), + ] + + PCERT_CHAIN_ELEMENT = POINTER(CERT_CHAIN_ELEMENT) + + class CERT_SIMPLE_CHAIN(Structure): # noqa + _fields_ = [ + ('cbSize', DWORD), + ('TrustStatus', CERT_TRUST_STATUS), + ('cElement', DWORD), + ('rgpElement', POINTER(PCERT_CHAIN_ELEMENT)), + ('pTrustListInfo', c_void_p), + ('fHasRevocationFreshnessTime', wintypes.BOOL), + ('dwRevocationFreshnessTime', DWORD), + ] + + PCERT_SIMPLE_CHAIN = POINTER(CERT_SIMPLE_CHAIN) + + class CERT_CHAIN_CONTEXT(Structure): # noqa + _fields_ = [ + ('cbSize', DWORD), + ('TrustStatus', CERT_TRUST_STATUS), + ('cChain', DWORD), + ('rgpChain', POINTER(PCERT_SIMPLE_CHAIN)), + ('cLowerQualityChainContext', DWORD), + ('rgpLowerQualityChainContext', c_void_p), + ('fHasRevocationFreshnessTime', wintypes.BOOL), + ('dwRevocationFreshnessTime', DWORD), + ] + + PCERT_CHAIN_CONTEXT = POINTER(CERT_CHAIN_CONTEXT) + + class CERT_USAGE_MATCH(Structure): # noqa + _fields_ = [ + ('dwType', DWORD), + ('Usage', CERT_ENHKEY_USAGE), + ] + + class CERT_CHAIN_PARA(Structure): # noqa + _fields_ = [ + ('cbSize', DWORD), + ('RequestedUsage', CERT_USAGE_MATCH), + ] + + class CERT_CHAIN_POLICY_PARA(Structure): # noqa + _fields_ = [ + ('cbSize', DWORD), + ('dwFlags', DWORD), + ('pvExtraPolicyPara', c_void_p), + ] + + class SSL_EXTRA_CERT_CHAIN_POLICY_PARA(Structure): # noqa + _fields_ = [ + ('cbSize', DWORD), + ('dwAuthType', DWORD), + ('fdwChecks', DWORD), + ('pwszServerName', wintypes.LPCWSTR), + ] + + class CERT_CHAIN_POLICY_STATUS(Structure): # noqa + _fields_ = [ + ('cbSize', DWORD), + ('dwError', DWORD), + ('lChainIndex', wintypes.LONG), + ('lElementIndex', wintypes.LONG), + ('pvExtraPolicyStatus', c_void_p), + ] + + crypt32.CertOpenStore.argtypes = [ + wintypes.LPCSTR, + DWORD, + HCRYPTPROV, + DWORD, + c_void_p + ] + crypt32.CertOpenStore.restype = HCERTSTORE + + crypt32.CertAddEncodedCertificateToStore.argtypes = [ + HCERTSTORE, + DWORD, + PBYTE, + DWORD, + DWORD, + POINTER(PCERT_CONTEXT) + ] + crypt32.CertAddEncodedCertificateToStore.restype = wintypes.BOOL + + crypt32.CertGetCertificateChain.argtypes = [ + HCERTCHAINENGINE, + PCERT_CONTEXT, + POINTER(kernel32.FILETIME), + HCERTSTORE, + POINTER(CERT_CHAIN_PARA), + DWORD, + c_void_p, + POINTER(PCERT_CHAIN_CONTEXT) + ] + crypt32.CertGetCertificateChain.restype = wintypes.BOOL + + crypt32.CertVerifyCertificateChainPolicy.argtypes = [ + ULONG_PTR, + PCERT_CHAIN_CONTEXT, + POINTER(CERT_CHAIN_POLICY_PARA), + POINTER(CERT_CHAIN_POLICY_STATUS) + ] + crypt32.CertVerifyCertificateChainPolicy.restype = wintypes.BOOL + + crypt32.CertFreeCertificateChain.argtypes = [ + PCERT_CHAIN_CONTEXT + ] + crypt32.CertFreeCertificateChain.restype = None + + crypt32.CertOpenSystemStoreW.argtypes = [ + wintypes.HANDLE, + wintypes.LPCWSTR + ] + crypt32.CertOpenSystemStoreW.restype = HCERTSTORE + + crypt32.CertEnumCertificatesInStore.argtypes = [ + HCERTSTORE, + PCERT_CONTEXT + ] + crypt32.CertEnumCertificatesInStore.restype = PCERT_CONTEXT + + crypt32.CertCloseStore.argtypes = [ + HCERTSTORE, + DWORD + ] + crypt32.CertCloseStore.restype = wintypes.BOOL + + crypt32.CertGetEnhancedKeyUsage.argtypes = [ + PCERT_CONTEXT, + DWORD, + c_void_p, + POINTER(DWORD) + ] + crypt32.CertGetEnhancedKeyUsage.restype = wintypes.BOOL + +except (AttributeError): + raise FFIEngineError('Error initializing ctypes') + + +setattr(crypt32, 'FILETIME', kernel32.FILETIME) +setattr(crypt32, 'CERT_ENHKEY_USAGE', CERT_ENHKEY_USAGE) +setattr(crypt32, 'CERT_CONTEXT', CERT_CONTEXT) +setattr(crypt32, 'PCERT_CONTEXT', PCERT_CONTEXT) +setattr(crypt32, 'CERT_USAGE_MATCH', CERT_USAGE_MATCH) +setattr(crypt32, 'CERT_CHAIN_PARA', CERT_CHAIN_PARA) +setattr(crypt32, 'CERT_CHAIN_POLICY_PARA', CERT_CHAIN_POLICY_PARA) +setattr(crypt32, 'SSL_EXTRA_CERT_CHAIN_POLICY_PARA', SSL_EXTRA_CERT_CHAIN_POLICY_PARA) +setattr(crypt32, 'CERT_CHAIN_POLICY_STATUS', CERT_CHAIN_POLICY_STATUS) +setattr(crypt32, 'PCERT_CHAIN_CONTEXT', PCERT_CHAIN_CONTEXT) + + +def get_error(): + error = ctypes.GetLastError() + return (error, ctypes.FormatError(error)) diff --git a/tasks/lib/package_control/deps/oscrypto/_win/_decode.py b/tasks/lib/package_control/deps/oscrypto/_win/_decode.py new file mode 100644 index 0000000..0bf8901 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_win/_decode.py @@ -0,0 +1,36 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import locale + +from .._types import str_cls + + +_encoding = locale.getpreferredencoding() +_fallback_encodings = ['utf-8', 'cp1252'] + + +def _try_decode(byte_string): + """ + Tries decoding a byte string from the OS into a unicode string + + :param byte_string: + A byte string + + :return: + A unicode string + """ + + try: + return str_cls(byte_string, _encoding) + + # If the "correct" encoding did not work, try some defaults, and then just + # obliterate characters that we can't seen to decode properly + except (UnicodeDecodeError): + for encoding in _fallback_encodings: + try: + return str_cls(byte_string, encoding, errors='strict') + except (UnicodeDecodeError): + pass + + return str_cls(byte_string, errors='replace') diff --git a/tasks/lib/package_control/deps/oscrypto/_win/_kernel32.py b/tasks/lib/package_control/deps/oscrypto/_win/_kernel32.py new file mode 100644 index 0000000..94b560f --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_win/_kernel32.py @@ -0,0 +1,39 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from .. import ffi +from ._decode import _try_decode +from .._types import str_cls + +if ffi() == 'cffi': + from ._kernel32_cffi import kernel32, get_error +else: + from ._kernel32_ctypes import kernel32, get_error + + +__all__ = [ + 'handle_error', + 'kernel32', +] + + +def handle_error(result): + """ + Extracts the last Windows error message into a python unicode string + + :param result: + A function result, 0 or None indicates failure + + :return: + A unicode string error message + """ + + if result: + return + + _, error_string = get_error() + + if not isinstance(error_string, str_cls): + error_string = _try_decode(error_string) + + raise OSError(error_string) diff --git a/tasks/lib/package_control/deps/oscrypto/_win/_kernel32_cffi.py b/tasks/lib/package_control/deps/oscrypto/_win/_kernel32_cffi.py new file mode 100644 index 0000000..1ddbae5 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_win/_kernel32_cffi.py @@ -0,0 +1,44 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from .._ffi import register_ffi +from .._types import str_cls +from ..errors import LibraryNotFoundError + +import cffi + + +__all__ = [ + 'get_error', + 'kernel32', +] + + +ffi = cffi.FFI() +if cffi.__version_info__ >= (0, 9): + ffi.set_unicode(True) +ffi.cdef(""" + typedef long long LARGE_INTEGER; + BOOL QueryPerformanceCounter(LARGE_INTEGER *lpPerformanceCount); + + typedef struct _FILETIME { + DWORD dwLowDateTime; + DWORD dwHighDateTime; + } FILETIME; + + void GetSystemTimeAsFileTime(FILETIME *lpSystemTimeAsFileTime); +""") + + +try: + kernel32 = ffi.dlopen('kernel32.dll') + register_ffi(kernel32, ffi) + +except (OSError) as e: + if str_cls(e).find('cannot load library') != -1: + raise LibraryNotFoundError('kernel32.dll could not be found') + raise + + +def get_error(): + return ffi.getwinerror() diff --git a/tasks/lib/package_control/deps/oscrypto/_win/_kernel32_ctypes.py b/tasks/lib/package_control/deps/oscrypto/_win/_kernel32_ctypes.py new file mode 100644 index 0000000..67c965c --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_win/_kernel32_ctypes.py @@ -0,0 +1,50 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import ctypes +from ctypes import windll, wintypes, POINTER, c_longlong, Structure + +from .._ffi import FFIEngineError +from .._types import str_cls +from ..errors import LibraryNotFoundError + + +__all__ = [ + 'get_error', + 'kernel32', +] + + +try: + kernel32 = windll.kernel32 +except (OSError) as e: + if str_cls(e).find('The specified module could not be found') != -1: + raise LibraryNotFoundError('kernel32.dll could not be found') + raise + +LARGE_INTEGER = c_longlong + +try: + kernel32.QueryPerformanceCounter.argtypes = [POINTER(LARGE_INTEGER)] + kernel32.QueryPerformanceCounter.restype = wintypes.BOOL + + class FILETIME(Structure): + _fields_ = [ + ("dwLowDateTime", wintypes.DWORD), + ("dwHighDateTime", wintypes.DWORD), + ] + + kernel32.GetSystemTimeAsFileTime.argtypes = [POINTER(FILETIME)] + kernel32.GetSystemTimeAsFileTime.restype = None + +except (AttributeError): + raise FFIEngineError('Error initializing ctypes') + + +setattr(kernel32, 'LARGE_INTEGER', LARGE_INTEGER) +setattr(kernel32, 'FILETIME', FILETIME) + + +def get_error(): + error = ctypes.GetLastError() + return (error, ctypes.FormatError(error)) diff --git a/tasks/lib/package_control/deps/oscrypto/_win/_secur32.py b/tasks/lib/package_control/deps/oscrypto/_win/_secur32.py new file mode 100644 index 0000000..c956078 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_win/_secur32.py @@ -0,0 +1,145 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from .. import ffi +from ._decode import _try_decode +from ..errors import TLSError +from .._types import str_cls + +if ffi() == 'cffi': + from ._secur32_cffi import secur32, get_error +else: + from ._secur32_ctypes import secur32, get_error + + +__all__ = [ + 'handle_error', + 'secur32', + 'Secur32Const', +] + + +def handle_error(result, exception_class=None): + """ + Extracts the last Windows error message into a python unicode string + + :param result: + A function result, 0 or None indicates failure + + :param exception_class: + The exception class to use for the exception if an error occurred + + :return: + A unicode string error message + """ + + if result == 0: + return + + if result == Secur32Const.SEC_E_OUT_OF_SEQUENCE: + raise TLSError('A packet was received out of order') + + if result == Secur32Const.SEC_E_MESSAGE_ALTERED: + raise TLSError('A packet was received altered') + + if result == Secur32Const.SEC_E_CONTEXT_EXPIRED: + raise TLSError('The TLS session expired') + + _, error_string = get_error() + + if not isinstance(error_string, str_cls): + error_string = _try_decode(error_string) + + if exception_class is None: + exception_class = OSError + + raise exception_class(('SECURITY_STATUS error 0x%0.2X: ' % result) + error_string) + + +class Secur32Const(): + SCHANNEL_CRED_VERSION = 4 + + SECPKG_CRED_OUTBOUND = 0x00000002 + UNISP_NAME = "Microsoft Unified Security Protocol Provider" + + SCH_CRED_MANUAL_CRED_VALIDATION = 0x00000008 + SCH_CRED_AUTO_CRED_VALIDATION = 0x00000020 + SCH_USE_STRONG_CRYPTO = 0x00400000 + SCH_CRED_NO_DEFAULT_CREDS = 0x00000010 + + SECBUFFER_VERSION = 0 + + SEC_E_OK = 0x00000000 + SEC_I_CONTINUE_NEEDED = 0x00090312 + SEC_I_CONTEXT_EXPIRED = 0x00090317 + SEC_I_RENEGOTIATE = 0x00090321 + SEC_E_INCOMPLETE_MESSAGE = 0x80090318 + SEC_E_INVALID_TOKEN = 0x80090308 + SEC_E_OUT_OF_SEQUENCE = 0x8009031 + SEC_E_MESSAGE_ALTERED = 0x8009030F + SEC_E_CONTEXT_EXPIRED = 0x80090317 + SEC_E_INVALID_PARAMETER = 0x8009035D + + SEC_E_WRONG_PRINCIPAL = 0x80090322 # Domain name mismatch + SEC_E_UNTRUSTED_ROOT = 0x80090325 + SEC_E_CERT_EXPIRED = 0x80090328 + SEC_E_ILLEGAL_MESSAGE = 0x80090326 # Handshake error + SEC_E_INTERNAL_ERROR = 0x80090304 # Occurs when DH params are too small + SEC_E_BUFFER_TOO_SMALL = 0x80090321 + SEC_I_INCOMPLETE_CREDENTIALS = 0x00090320 + + ISC_REQ_REPLAY_DETECT = 4 + ISC_REQ_SEQUENCE_DETECT = 8 + ISC_REQ_CONFIDENTIALITY = 16 + ISC_REQ_ALLOCATE_MEMORY = 256 + ISC_REQ_INTEGRITY = 65536 + ISC_REQ_STREAM = 0x00008000 + ISC_REQ_USE_SUPPLIED_CREDS = 0x00000080 + + ISC_RET_REPLAY_DETECT = 4 + ISC_RET_SEQUENCE_DETECT = 8 + ISC_RET_CONFIDENTIALITY = 16 + ISC_RET_ALLOCATED_MEMORY = 256 + ISC_RET_INTEGRITY = 65536 + ISC_RET_STREAM = 0x00008000 + + SECBUFFER_ALERT = 17 + SECBUFFER_STREAM_HEADER = 7 + SECBUFFER_STREAM_TRAILER = 6 + SECBUFFER_EXTRA = 5 + SECBUFFER_TOKEN = 2 + SECBUFFER_DATA = 1 + SECBUFFER_EMPTY = 0 + + SECPKG_ATTR_STREAM_SIZES = 0x04 + SECPKG_ATTR_CONNECTION_INFO = 0x5A + SECPKG_ATTR_REMOTE_CERT_CONTEXT = 0x53 + + SP_PROT_TLS1_2_CLIENT = 0x800 + SP_PROT_TLS1_1_CLIENT = 0x200 + SP_PROT_TLS1_CLIENT = 0x80 + SP_PROT_SSL3_CLIENT = 0x20 + SP_PROT_SSL2_CLIENT = 0x8 + + CALG_AES_256 = 0x00006610 + CALG_AES_128 = 0x0000660E + CALG_3DES = 0x00006603 + CALG_RC4 = 0x00006801 + CALG_RC2 = 0x00006602 + CALG_DES = 0x00006601 + + CALG_MD5 = 0x00008003 + CALG_SHA1 = 0x00008004 + CALG_SHA256 = 0x0000800C + CALG_SHA384 = 0x0000800D + CALG_SHA512 = 0x0000800E + + CALG_DH_SF = 0x0000AA01 + CALG_DH_EPHEM = 0x0000AA02 + CALG_ECDH = 0x0000AA05 + CALG_ECDHE = 0x0000AE06 + CALG_RSA_KEYX = 0x0000A400 + + CALG_RSA_SIGN = 0x00002400 + CALG_ECDSA = 0x00002203 + CALG_DSS_SIGN = 0x00002200 diff --git a/tasks/lib/package_control/deps/oscrypto/_win/_secur32_cffi.py b/tasks/lib/package_control/deps/oscrypto/_win/_secur32_cffi.py new file mode 100644 index 0000000..2d80e2b --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_win/_secur32_cffi.py @@ -0,0 +1,129 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys + +from .._ffi import register_ffi +from .._types import str_cls +from ..errors import LibraryNotFoundError + +import cffi + + +__all__ = [ + 'get_error', + 'secur32', +] + + +ffi = cffi.FFI() +if cffi.__version_info__ >= (0, 9): + ffi.set_unicode(True) +if sys.maxsize > 2 ** 32: + ffi.cdef("typedef uint64_t ULONG_PTR;") +else: + ffi.cdef("typedef unsigned long ULONG_PTR;") +ffi.cdef(""" + typedef HANDLE HCERTSTORE; + typedef unsigned int ALG_ID; + typedef WCHAR SEC_WCHAR; + typedef unsigned long SECURITY_STATUS; + typedef void *LUID; + typedef void *SEC_GET_KEY_FN; + + typedef struct _SecHandle { + ULONG_PTR dwLower; + ULONG_PTR dwUpper; + } SecHandle; + typedef SecHandle CredHandle; + typedef SecHandle CtxtHandle; + + typedef struct _SCHANNEL_CRED { + DWORD dwVersion; + DWORD cCreds; + void *paCred; + HCERTSTORE hRootStore; + DWORD cMappers; + void **aphMappers; + DWORD cSupportedAlgs; + ALG_ID *palgSupportedAlgs; + DWORD grbitEnabledProtocols; + DWORD dwMinimumCipherStrength; + DWORD dwMaximumCipherStrength; + DWORD dwSessionLifespan; + DWORD dwFlags; + DWORD dwCredFormat; + } SCHANNEL_CRED; + + typedef struct _TimeStamp { + DWORD dwLowDateTime; + DWORD dwHighDateTime; + } TimeStamp; + + typedef struct _SecBuffer { + ULONG cbBuffer; + ULONG BufferType; + BYTE *pvBuffer; + } SecBuffer; + + typedef struct _SecBufferDesc { + ULONG ulVersion; + ULONG cBuffers; + SecBuffer *pBuffers; + } SecBufferDesc; + + typedef struct _SecPkgContext_StreamSizes { + ULONG cbHeader; + ULONG cbTrailer; + ULONG cbMaximumMessage; + ULONG cBuffers; + ULONG cbBlockSize; + } SecPkgContext_StreamSizes; + + typedef struct _CERT_CONTEXT { + DWORD dwCertEncodingType; + BYTE *pbCertEncoded; + DWORD cbCertEncoded; + void *pCertInfo; + HCERTSTORE hCertStore; + } CERT_CONTEXT; + + typedef struct _SecPkgContext_ConnectionInfo { + DWORD dwProtocol; + ALG_ID aiCipher; + DWORD dwCipherStrength; + ALG_ID aiHash; + DWORD dwHashStrength; + ALG_ID aiExch; + DWORD dwExchStrength; + } SecPkgContext_ConnectionInfo; + + SECURITY_STATUS AcquireCredentialsHandleW(SEC_WCHAR *pszPrincipal, SEC_WCHAR *pszPackage, ULONG fCredentialUse, + LUID *pvLogonID, void *pAuthData, SEC_GET_KEY_FN pGetKeyFn, void *pvGetKeyArgument, + CredHandle *phCredential, TimeStamp *ptsExpiry); + SECURITY_STATUS FreeCredentialsHandle(CredHandle *phCredential); + SECURITY_STATUS InitializeSecurityContextW(CredHandle *phCredential, CtxtHandle *phContext, + SEC_WCHAR *pszTargetName, ULONG fContextReq, ULONG Reserved1, ULONG TargetDataRep, + SecBufferDesc *pInput, ULONG Reserved2, CtxtHandle *phNewContext, SecBufferDesc *pOutput, + ULONG *pfContextAttr, TimeStamp *ptsExpiry); + SECURITY_STATUS FreeContextBuffer(void *pvContextBuffer); + SECURITY_STATUS ApplyControlToken(CtxtHandle *phContext, SecBufferDesc *pInput); + SECURITY_STATUS DeleteSecurityContext(CtxtHandle *phContext); + SECURITY_STATUS QueryContextAttributesW(CtxtHandle *phContext, ULONG ulAttribute, void *pBuffer); + SECURITY_STATUS EncryptMessage(CtxtHandle *phContext, ULONG fQOP, SecBufferDesc *pMessage, ULONG MessageSeqNo); + SECURITY_STATUS DecryptMessage(CtxtHandle *phContext, SecBufferDesc *pMessage, ULONG MessageSeqNo, ULONG *pfQOP); +""") + + +try: + secur32 = ffi.dlopen('secur32.dll') + register_ffi(secur32, ffi) + +except (OSError) as e: + if str_cls(e).find('cannot load library') != -1: + raise LibraryNotFoundError('secur32.dll could not be found') + raise + + +def get_error(): + return ffi.getwinerror() diff --git a/tasks/lib/package_control/deps/oscrypto/_win/_secur32_ctypes.py b/tasks/lib/package_control/deps/oscrypto/_win/_secur32_ctypes.py new file mode 100644 index 0000000..fb3415d --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_win/_secur32_ctypes.py @@ -0,0 +1,198 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys + +import ctypes +from ctypes import windll, wintypes, POINTER, c_void_p, c_uint, Structure +from ctypes.wintypes import DWORD, ULONG + +from .._ffi import FFIEngineError +from .._types import str_cls +from ..errors import LibraryNotFoundError + + +__all__ = [ + 'get_error', + 'secur32', +] + + +try: + secur32 = windll.secur32 +except (OSError) as e: + if str_cls(e).find('The specified module could not be found') != -1: + raise LibraryNotFoundError('secur32.dll could not be found') + raise + +HCERTSTORE = wintypes.HANDLE +ALG_ID = c_uint +if sys.maxsize > 2 ** 32: + ULONG_PTR = ctypes.c_uint64 +else: + ULONG_PTR = ctypes.c_ulong +SEC_GET_KEY_FN = c_void_p +LUID = c_void_p +SECURITY_STATUS = ctypes.c_ulong +SEC_WCHAR = wintypes.WCHAR + +try: + class SecHandle(Structure): + _fields_ = [ + ('dwLower', ULONG_PTR), + ('dwUpper', ULONG_PTR), + ] + + CredHandle = SecHandle + CtxtHandle = SecHandle + + class SCHANNEL_CRED(Structure): # noqa + _fields_ = [ + ('dwVersion', DWORD), + ('cCreds', DWORD), + ('paCred', c_void_p), + ('hRootStore', HCERTSTORE), + ('cMappers', DWORD), + ('aphMappers', POINTER(c_void_p)), + ('cSupportedAlgs', DWORD), + ('palgSupportedAlgs', POINTER(ALG_ID)), + ('grbitEnabledProtocols', DWORD), + ('dwMinimumCipherStrength', DWORD), + ('dwMaximumCipherStrength', DWORD), + ('dwSessionLifespan', DWORD), + ('dwFlags', DWORD), + ('dwCredFormat', DWORD), + ] + + class TimeStamp(Structure): + _fields_ = [ + ('dwLowDateTime', DWORD), + ('dwHighDateTime', DWORD), + ] + + class SecBuffer(Structure): + _fields_ = [ + ('cbBuffer', ULONG), + ('BufferType', ULONG), + ('pvBuffer', POINTER(ctypes.c_byte)), + ] + + PSecBuffer = POINTER(SecBuffer) + + class SecBufferDesc(Structure): + _fields_ = [ + ('ulVersion', ULONG), + ('cBuffers', ULONG), + ('pBuffers', PSecBuffer), + ] + + class SecPkgContext_StreamSizes(Structure): # noqa + _fields_ = [ + ('cbHeader', ULONG), + ('cbTrailer', ULONG), + ('cbMaximumMessage', ULONG), + ('cBuffers', ULONG), + ('cbBlockSize', ULONG), + ] + + class SecPkgContext_ConnectionInfo(Structure): # noqa + _fields_ = [ + ('dwProtocol', DWORD), + ('aiCipher', ALG_ID), + ('dwCipherStrength', DWORD), + ('aiHash', ALG_ID), + ('dwHashStrength', DWORD), + ('aiExch', ALG_ID), + ('dwExchStrength', DWORD), + ] + + secur32.AcquireCredentialsHandleW.argtypes = [ + POINTER(SEC_WCHAR), + POINTER(SEC_WCHAR), + ULONG, + POINTER(LUID), + c_void_p, + SEC_GET_KEY_FN, + c_void_p, + POINTER(CredHandle), + POINTER(TimeStamp) + ] + secur32.AcquireCredentialsHandleW.restype = SECURITY_STATUS + + secur32.FreeCredentialsHandle.argtypes = [ + POINTER(CredHandle) + ] + secur32.FreeCredentialsHandle.restype = SECURITY_STATUS + + secur32.InitializeSecurityContextW.argtypes = [ + POINTER(CredHandle), + POINTER(CtxtHandle), + POINTER(SEC_WCHAR), + ULONG, + ULONG, + ULONG, + POINTER(SecBufferDesc), + ULONG, + POINTER(CtxtHandle), + POINTER(SecBufferDesc), + POINTER(ULONG), + POINTER(TimeStamp) + ] + secur32.InitializeSecurityContextW.restype = SECURITY_STATUS + + secur32.FreeContextBuffer.argtypes = [ + c_void_p + ] + secur32.FreeContextBuffer.restype = SECURITY_STATUS + + secur32.ApplyControlToken.argtypes = [ + POINTER(CtxtHandle), + POINTER(SecBufferDesc) + ] + secur32.ApplyControlToken.restype = SECURITY_STATUS + + secur32.DeleteSecurityContext.argtypes = [ + POINTER(CtxtHandle) + ] + secur32.DeleteSecurityContext.restype = SECURITY_STATUS + + secur32.QueryContextAttributesW.argtypes = [ + POINTER(CtxtHandle), + ULONG, + c_void_p + ] + secur32.QueryContextAttributesW.restype = SECURITY_STATUS + + secur32.EncryptMessage.argtypes = [ + POINTER(CtxtHandle), + ULONG, + POINTER(SecBufferDesc), + ULONG + ] + secur32.EncryptMessage.restype = SECURITY_STATUS + + secur32.DecryptMessage.argtypes = [ + POINTER(CtxtHandle), + POINTER(SecBufferDesc), + ULONG, + POINTER(ULONG) + ] + secur32.DecryptMessage.restype = SECURITY_STATUS + +except (AttributeError): + raise FFIEngineError('Error initializing ctypes') + + +setattr(secur32, 'ALG_ID', ALG_ID) +setattr(secur32, 'CredHandle', CredHandle) +setattr(secur32, 'CtxtHandle', CtxtHandle) +setattr(secur32, 'SecBuffer', SecBuffer) +setattr(secur32, 'SecBufferDesc', SecBufferDesc) +setattr(secur32, 'SecPkgContext_StreamSizes', SecPkgContext_StreamSizes) +setattr(secur32, 'SecPkgContext_ConnectionInfo', SecPkgContext_ConnectionInfo) +setattr(secur32, 'SCHANNEL_CRED', SCHANNEL_CRED) + + +def get_error(): + error = ctypes.GetLastError() + return (error, ctypes.FormatError(error)) diff --git a/tasks/lib/package_control/deps/oscrypto/_win/asymmetric.py b/tasks/lib/package_control/deps/oscrypto/_win/asymmetric.py new file mode 100644 index 0000000..dc985b0 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_win/asymmetric.py @@ -0,0 +1,3544 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import os +import sys +import hashlib +import random + +from .._asn1 import ( + Certificate as Asn1Certificate, + DHParameters, + DSAParams, + DSASignature, + ECDomainParameters, + ECPrivateKey, + Integer, + int_from_bytes, + int_to_bytes, + PrivateKeyAlgorithm, + PrivateKeyInfo, + PublicKeyAlgorithm, + PublicKeyInfo, + RSAPrivateKey, + RSAPublicKey, +) +from .._asymmetric import ( + _CertificateBase, + _fingerprint, + _parse_pkcs12, + _PrivateKeyBase, + _PublicKeyBase, + _unwrap_private_key_info, + parse_certificate, + parse_private, + parse_public, +) +from .._errors import pretty_message +from .._ffi import ( + buffer_from_bytes, + buffer_from_unicode, + byte_array, + bytes_from_buffer, + cast, + deref, + native, + new, + null, + pointer_set, + sizeof, + struct, + struct_bytes, + struct_from_buffer, + unwrap, + write_to_buffer, +) +from .. import backend +from .._int import fill_width +from ..errors import AsymmetricKeyError, IncompleteAsymmetricKeyError, SignatureError +from .._types import type_name, str_cls, byte_cls, int_types +from .._pkcs1 import ( + add_pkcs1v15_signature_padding, + add_pss_padding, + raw_rsa_private_crypt, + raw_rsa_public_crypt, + remove_pkcs1v15_signature_padding, + verify_pss_padding, +) +from ..util import constant_compare + +_gwv = sys.getwindowsversion() +_win_version_info = (_gwv[0], _gwv[1]) +_backend = backend() + +if _backend == 'winlegacy': + from ._advapi32 import advapi32, Advapi32Const, handle_error, open_context_handle, close_context_handle + from .._ecdsa import ( + ec_generate_pair as _pure_python_ec_generate_pair, + ec_compute_public_key_point as _pure_python_ec_compute_public_key_point, + ec_public_key_info, + ecdsa_sign as _pure_python_ecdsa_sign, + ecdsa_verify as _pure_python_ecdsa_verify, + ) +else: + from ._cng import bcrypt, BcryptConst, handle_error, open_alg_handle, close_alg_handle + + +__all__ = [ + 'Certificate', + 'dsa_sign', + 'dsa_verify', + 'ecdsa_sign', + 'ecdsa_verify', + 'generate_pair', + 'load_certificate', + 'load_pkcs12', + 'load_private_key', + 'load_public_key', + 'parse_pkcs12', + 'PrivateKey', + 'PublicKey', + 'rsa_oaep_decrypt', + 'rsa_oaep_encrypt', + 'rsa_pkcs1v15_decrypt', + 'rsa_pkcs1v15_encrypt', + 'rsa_pkcs1v15_sign', + 'rsa_pkcs1v15_verify', + 'rsa_pss_sign', + 'rsa_pss_verify', +] + + +# A list of primes from OpenSSL's bn_prime.h to use when testing primality of a +# large integer +_SMALL_PRIMES = [ + 2, 3, 5, 7, 11, 13, 17, 19, + 23, 29, 31, 37, 41, 43, 47, 53, + 59, 61, 67, 71, 73, 79, 83, 89, + 97, 101, 103, 107, 109, 113, 127, 131, + 137, 139, 149, 151, 157, 163, 167, 173, + 179, 181, 191, 193, 197, 199, 211, 223, + 227, 229, 233, 239, 241, 251, 257, 263, + 269, 271, 277, 281, 283, 293, 307, 311, + 313, 317, 331, 337, 347, 349, 353, 359, + 367, 373, 379, 383, 389, 397, 401, 409, + 419, 421, 431, 433, 439, 443, 449, 457, + 461, 463, 467, 479, 487, 491, 499, 503, + 509, 521, 523, 541, 547, 557, 563, 569, + 571, 577, 587, 593, 599, 601, 607, 613, + 617, 619, 631, 641, 643, 647, 653, 659, + 661, 673, 677, 683, 691, 701, 709, 719, + 727, 733, 739, 743, 751, 757, 761, 769, + 773, 787, 797, 809, 811, 821, 823, 827, + 829, 839, 853, 857, 859, 863, 877, 881, + 883, 887, 907, 911, 919, 929, 937, 941, + 947, 953, 967, 971, 977, 983, 991, 997, + 1009, 1013, 1019, 1021, 1031, 1033, 1039, 1049, + 1051, 1061, 1063, 1069, 1087, 1091, 1093, 1097, + 1103, 1109, 1117, 1123, 1129, 1151, 1153, 1163, + 1171, 1181, 1187, 1193, 1201, 1213, 1217, 1223, + 1229, 1231, 1237, 1249, 1259, 1277, 1279, 1283, + 1289, 1291, 1297, 1301, 1303, 1307, 1319, 1321, + 1327, 1361, 1367, 1373, 1381, 1399, 1409, 1423, + 1427, 1429, 1433, 1439, 1447, 1451, 1453, 1459, + 1471, 1481, 1483, 1487, 1489, 1493, 1499, 1511, + 1523, 1531, 1543, 1549, 1553, 1559, 1567, 1571, + 1579, 1583, 1597, 1601, 1607, 1609, 1613, 1619, + 1621, 1627, 1637, 1657, 1663, 1667, 1669, 1693, + 1697, 1699, 1709, 1721, 1723, 1733, 1741, 1747, + 1753, 1759, 1777, 1783, 1787, 1789, 1801, 1811, + 1823, 1831, 1847, 1861, 1867, 1871, 1873, 1877, + 1879, 1889, 1901, 1907, 1913, 1931, 1933, 1949, + 1951, 1973, 1979, 1987, 1993, 1997, 1999, 2003, + 2011, 2017, 2027, 2029, 2039, 2053, 2063, 2069, + 2081, 2083, 2087, 2089, 2099, 2111, 2113, 2129, + 2131, 2137, 2141, 2143, 2153, 2161, 2179, 2203, + 2207, 2213, 2221, 2237, 2239, 2243, 2251, 2267, + 2269, 2273, 2281, 2287, 2293, 2297, 2309, 2311, + 2333, 2339, 2341, 2347, 2351, 2357, 2371, 2377, + 2381, 2383, 2389, 2393, 2399, 2411, 2417, 2423, + 2437, 2441, 2447, 2459, 2467, 2473, 2477, 2503, + 2521, 2531, 2539, 2543, 2549, 2551, 2557, 2579, + 2591, 2593, 2609, 2617, 2621, 2633, 2647, 2657, + 2659, 2663, 2671, 2677, 2683, 2687, 2689, 2693, + 2699, 2707, 2711, 2713, 2719, 2729, 2731, 2741, + 2749, 2753, 2767, 2777, 2789, 2791, 2797, 2801, + 2803, 2819, 2833, 2837, 2843, 2851, 2857, 2861, + 2879, 2887, 2897, 2903, 2909, 2917, 2927, 2939, + 2953, 2957, 2963, 2969, 2971, 2999, 3001, 3011, + 3019, 3023, 3037, 3041, 3049, 3061, 3067, 3079, + 3083, 3089, 3109, 3119, 3121, 3137, 3163, 3167, + 3169, 3181, 3187, 3191, 3203, 3209, 3217, 3221, + 3229, 3251, 3253, 3257, 3259, 3271, 3299, 3301, + 3307, 3313, 3319, 3323, 3329, 3331, 3343, 3347, + 3359, 3361, 3371, 3373, 3389, 3391, 3407, 3413, + 3433, 3449, 3457, 3461, 3463, 3467, 3469, 3491, + 3499, 3511, 3517, 3527, 3529, 3533, 3539, 3541, + 3547, 3557, 3559, 3571, 3581, 3583, 3593, 3607, + 3613, 3617, 3623, 3631, 3637, 3643, 3659, 3671, + 3673, 3677, 3691, 3697, 3701, 3709, 3719, 3727, + 3733, 3739, 3761, 3767, 3769, 3779, 3793, 3797, + 3803, 3821, 3823, 3833, 3847, 3851, 3853, 3863, + 3877, 3881, 3889, 3907, 3911, 3917, 3919, 3923, + 3929, 3931, 3943, 3947, 3967, 3989, 4001, 4003, + 4007, 4013, 4019, 4021, 4027, 4049, 4051, 4057, + 4073, 4079, 4091, 4093, 4099, 4111, 4127, 4129, + 4133, 4139, 4153, 4157, 4159, 4177, 4201, 4211, + 4217, 4219, 4229, 4231, 4241, 4243, 4253, 4259, + 4261, 4271, 4273, 4283, 4289, 4297, 4327, 4337, + 4339, 4349, 4357, 4363, 4373, 4391, 4397, 4409, + 4421, 4423, 4441, 4447, 4451, 4457, 4463, 4481, + 4483, 4493, 4507, 4513, 4517, 4519, 4523, 4547, + 4549, 4561, 4567, 4583, 4591, 4597, 4603, 4621, + 4637, 4639, 4643, 4649, 4651, 4657, 4663, 4673, + 4679, 4691, 4703, 4721, 4723, 4729, 4733, 4751, + 4759, 4783, 4787, 4789, 4793, 4799, 4801, 4813, + 4817, 4831, 4861, 4871, 4877, 4889, 4903, 4909, + 4919, 4931, 4933, 4937, 4943, 4951, 4957, 4967, + 4969, 4973, 4987, 4993, 4999, 5003, 5009, 5011, + 5021, 5023, 5039, 5051, 5059, 5077, 5081, 5087, + 5099, 5101, 5107, 5113, 5119, 5147, 5153, 5167, + 5171, 5179, 5189, 5197, 5209, 5227, 5231, 5233, + 5237, 5261, 5273, 5279, 5281, 5297, 5303, 5309, + 5323, 5333, 5347, 5351, 5381, 5387, 5393, 5399, + 5407, 5413, 5417, 5419, 5431, 5437, 5441, 5443, + 5449, 5471, 5477, 5479, 5483, 5501, 5503, 5507, + 5519, 5521, 5527, 5531, 5557, 5563, 5569, 5573, + 5581, 5591, 5623, 5639, 5641, 5647, 5651, 5653, + 5657, 5659, 5669, 5683, 5689, 5693, 5701, 5711, + 5717, 5737, 5741, 5743, 5749, 5779, 5783, 5791, + 5801, 5807, 5813, 5821, 5827, 5839, 5843, 5849, + 5851, 5857, 5861, 5867, 5869, 5879, 5881, 5897, + 5903, 5923, 5927, 5939, 5953, 5981, 5987, 6007, + 6011, 6029, 6037, 6043, 6047, 6053, 6067, 6073, + 6079, 6089, 6091, 6101, 6113, 6121, 6131, 6133, + 6143, 6151, 6163, 6173, 6197, 6199, 6203, 6211, + 6217, 6221, 6229, 6247, 6257, 6263, 6269, 6271, + 6277, 6287, 6299, 6301, 6311, 6317, 6323, 6329, + 6337, 6343, 6353, 6359, 6361, 6367, 6373, 6379, + 6389, 6397, 6421, 6427, 6449, 6451, 6469, 6473, + 6481, 6491, 6521, 6529, 6547, 6551, 6553, 6563, + 6569, 6571, 6577, 6581, 6599, 6607, 6619, 6637, + 6653, 6659, 6661, 6673, 6679, 6689, 6691, 6701, + 6703, 6709, 6719, 6733, 6737, 6761, 6763, 6779, + 6781, 6791, 6793, 6803, 6823, 6827, 6829, 6833, + 6841, 6857, 6863, 6869, 6871, 6883, 6899, 6907, + 6911, 6917, 6947, 6949, 6959, 6961, 6967, 6971, + 6977, 6983, 6991, 6997, 7001, 7013, 7019, 7027, + 7039, 7043, 7057, 7069, 7079, 7103, 7109, 7121, + 7127, 7129, 7151, 7159, 7177, 7187, 7193, 7207, + 7211, 7213, 7219, 7229, 7237, 7243, 7247, 7253, + 7283, 7297, 7307, 7309, 7321, 7331, 7333, 7349, + 7351, 7369, 7393, 7411, 7417, 7433, 7451, 7457, + 7459, 7477, 7481, 7487, 7489, 7499, 7507, 7517, + 7523, 7529, 7537, 7541, 7547, 7549, 7559, 7561, + 7573, 7577, 7583, 7589, 7591, 7603, 7607, 7621, + 7639, 7643, 7649, 7669, 7673, 7681, 7687, 7691, + 7699, 7703, 7717, 7723, 7727, 7741, 7753, 7757, + 7759, 7789, 7793, 7817, 7823, 7829, 7841, 7853, + 7867, 7873, 7877, 7879, 7883, 7901, 7907, 7919, + 7927, 7933, 7937, 7949, 7951, 7963, 7993, 8009, + 8011, 8017, 8039, 8053, 8059, 8069, 8081, 8087, + 8089, 8093, 8101, 8111, 8117, 8123, 8147, 8161, + 8167, 8171, 8179, 8191, 8209, 8219, 8221, 8231, + 8233, 8237, 8243, 8263, 8269, 8273, 8287, 8291, + 8293, 8297, 8311, 8317, 8329, 8353, 8363, 8369, + 8377, 8387, 8389, 8419, 8423, 8429, 8431, 8443, + 8447, 8461, 8467, 8501, 8513, 8521, 8527, 8537, + 8539, 8543, 8563, 8573, 8581, 8597, 8599, 8609, + 8623, 8627, 8629, 8641, 8647, 8663, 8669, 8677, + 8681, 8689, 8693, 8699, 8707, 8713, 8719, 8731, + 8737, 8741, 8747, 8753, 8761, 8779, 8783, 8803, + 8807, 8819, 8821, 8831, 8837, 8839, 8849, 8861, + 8863, 8867, 8887, 8893, 8923, 8929, 8933, 8941, + 8951, 8963, 8969, 8971, 8999, 9001, 9007, 9011, + 9013, 9029, 9041, 9043, 9049, 9059, 9067, 9091, + 9103, 9109, 9127, 9133, 9137, 9151, 9157, 9161, + 9173, 9181, 9187, 9199, 9203, 9209, 9221, 9227, + 9239, 9241, 9257, 9277, 9281, 9283, 9293, 9311, + 9319, 9323, 9337, 9341, 9343, 9349, 9371, 9377, + 9391, 9397, 9403, 9413, 9419, 9421, 9431, 9433, + 9437, 9439, 9461, 9463, 9467, 9473, 9479, 9491, + 9497, 9511, 9521, 9533, 9539, 9547, 9551, 9587, + 9601, 9613, 9619, 9623, 9629, 9631, 9643, 9649, + 9661, 9677, 9679, 9689, 9697, 9719, 9721, 9733, + 9739, 9743, 9749, 9767, 9769, 9781, 9787, 9791, + 9803, 9811, 9817, 9829, 9833, 9839, 9851, 9857, + 9859, 9871, 9883, 9887, 9901, 9907, 9923, 9929, + 9931, 9941, 9949, 9967, 9973, 10007, 10009, 10037, + 10039, 10061, 10067, 10069, 10079, 10091, 10093, 10099, + 10103, 10111, 10133, 10139, 10141, 10151, 10159, 10163, + 10169, 10177, 10181, 10193, 10211, 10223, 10243, 10247, + 10253, 10259, 10267, 10271, 10273, 10289, 10301, 10303, + 10313, 10321, 10331, 10333, 10337, 10343, 10357, 10369, + 10391, 10399, 10427, 10429, 10433, 10453, 10457, 10459, + 10463, 10477, 10487, 10499, 10501, 10513, 10529, 10531, + 10559, 10567, 10589, 10597, 10601, 10607, 10613, 10627, + 10631, 10639, 10651, 10657, 10663, 10667, 10687, 10691, + 10709, 10711, 10723, 10729, 10733, 10739, 10753, 10771, + 10781, 10789, 10799, 10831, 10837, 10847, 10853, 10859, + 10861, 10867, 10883, 10889, 10891, 10903, 10909, 10937, + 10939, 10949, 10957, 10973, 10979, 10987, 10993, 11003, + 11027, 11047, 11057, 11059, 11069, 11071, 11083, 11087, + 11093, 11113, 11117, 11119, 11131, 11149, 11159, 11161, + 11171, 11173, 11177, 11197, 11213, 11239, 11243, 11251, + 11257, 11261, 11273, 11279, 11287, 11299, 11311, 11317, + 11321, 11329, 11351, 11353, 11369, 11383, 11393, 11399, + 11411, 11423, 11437, 11443, 11447, 11467, 11471, 11483, + 11489, 11491, 11497, 11503, 11519, 11527, 11549, 11551, + 11579, 11587, 11593, 11597, 11617, 11621, 11633, 11657, + 11677, 11681, 11689, 11699, 11701, 11717, 11719, 11731, + 11743, 11777, 11779, 11783, 11789, 11801, 11807, 11813, + 11821, 11827, 11831, 11833, 11839, 11863, 11867, 11887, + 11897, 11903, 11909, 11923, 11927, 11933, 11939, 11941, + 11953, 11959, 11969, 11971, 11981, 11987, 12007, 12011, + 12037, 12041, 12043, 12049, 12071, 12073, 12097, 12101, + 12107, 12109, 12113, 12119, 12143, 12149, 12157, 12161, + 12163, 12197, 12203, 12211, 12227, 12239, 12241, 12251, + 12253, 12263, 12269, 12277, 12281, 12289, 12301, 12323, + 12329, 12343, 12347, 12373, 12377, 12379, 12391, 12401, + 12409, 12413, 12421, 12433, 12437, 12451, 12457, 12473, + 12479, 12487, 12491, 12497, 12503, 12511, 12517, 12527, + 12539, 12541, 12547, 12553, 12569, 12577, 12583, 12589, + 12601, 12611, 12613, 12619, 12637, 12641, 12647, 12653, + 12659, 12671, 12689, 12697, 12703, 12713, 12721, 12739, + 12743, 12757, 12763, 12781, 12791, 12799, 12809, 12821, + 12823, 12829, 12841, 12853, 12889, 12893, 12899, 12907, + 12911, 12917, 12919, 12923, 12941, 12953, 12959, 12967, + 12973, 12979, 12983, 13001, 13003, 13007, 13009, 13033, + 13037, 13043, 13049, 13063, 13093, 13099, 13103, 13109, + 13121, 13127, 13147, 13151, 13159, 13163, 13171, 13177, + 13183, 13187, 13217, 13219, 13229, 13241, 13249, 13259, + 13267, 13291, 13297, 13309, 13313, 13327, 13331, 13337, + 13339, 13367, 13381, 13397, 13399, 13411, 13417, 13421, + 13441, 13451, 13457, 13463, 13469, 13477, 13487, 13499, + 13513, 13523, 13537, 13553, 13567, 13577, 13591, 13597, + 13613, 13619, 13627, 13633, 13649, 13669, 13679, 13681, + 13687, 13691, 13693, 13697, 13709, 13711, 13721, 13723, + 13729, 13751, 13757, 13759, 13763, 13781, 13789, 13799, + 13807, 13829, 13831, 13841, 13859, 13873, 13877, 13879, + 13883, 13901, 13903, 13907, 13913, 13921, 13931, 13933, + 13963, 13967, 13997, 13999, 14009, 14011, 14029, 14033, + 14051, 14057, 14071, 14081, 14083, 14087, 14107, 14143, + 14149, 14153, 14159, 14173, 14177, 14197, 14207, 14221, + 14243, 14249, 14251, 14281, 14293, 14303, 14321, 14323, + 14327, 14341, 14347, 14369, 14387, 14389, 14401, 14407, + 14411, 14419, 14423, 14431, 14437, 14447, 14449, 14461, + 14479, 14489, 14503, 14519, 14533, 14537, 14543, 14549, + 14551, 14557, 14561, 14563, 14591, 14593, 14621, 14627, + 14629, 14633, 14639, 14653, 14657, 14669, 14683, 14699, + 14713, 14717, 14723, 14731, 14737, 14741, 14747, 14753, + 14759, 14767, 14771, 14779, 14783, 14797, 14813, 14821, + 14827, 14831, 14843, 14851, 14867, 14869, 14879, 14887, + 14891, 14897, 14923, 14929, 14939, 14947, 14951, 14957, + 14969, 14983, 15013, 15017, 15031, 15053, 15061, 15073, + 15077, 15083, 15091, 15101, 15107, 15121, 15131, 15137, + 15139, 15149, 15161, 15173, 15187, 15193, 15199, 15217, + 15227, 15233, 15241, 15259, 15263, 15269, 15271, 15277, + 15287, 15289, 15299, 15307, 15313, 15319, 15329, 15331, + 15349, 15359, 15361, 15373, 15377, 15383, 15391, 15401, + 15413, 15427, 15439, 15443, 15451, 15461, 15467, 15473, + 15493, 15497, 15511, 15527, 15541, 15551, 15559, 15569, + 15581, 15583, 15601, 15607, 15619, 15629, 15641, 15643, + 15647, 15649, 15661, 15667, 15671, 15679, 15683, 15727, + 15731, 15733, 15737, 15739, 15749, 15761, 15767, 15773, + 15787, 15791, 15797, 15803, 15809, 15817, 15823, 15859, + 15877, 15881, 15887, 15889, 15901, 15907, 15913, 15919, + 15923, 15937, 15959, 15971, 15973, 15991, 16001, 16007, + 16033, 16057, 16061, 16063, 16067, 16069, 16073, 16087, + 16091, 16097, 16103, 16111, 16127, 16139, 16141, 16183, + 16187, 16189, 16193, 16217, 16223, 16229, 16231, 16249, + 16253, 16267, 16273, 16301, 16319, 16333, 16339, 16349, + 16361, 16363, 16369, 16381, 16411, 16417, 16421, 16427, + 16433, 16447, 16451, 16453, 16477, 16481, 16487, 16493, + 16519, 16529, 16547, 16553, 16561, 16567, 16573, 16603, + 16607, 16619, 16631, 16633, 16649, 16651, 16657, 16661, + 16673, 16691, 16693, 16699, 16703, 16729, 16741, 16747, + 16759, 16763, 16787, 16811, 16823, 16829, 16831, 16843, + 16871, 16879, 16883, 16889, 16901, 16903, 16921, 16927, + 16931, 16937, 16943, 16963, 16979, 16981, 16987, 16993, + 17011, 17021, 17027, 17029, 17033, 17041, 17047, 17053, + 17077, 17093, 17099, 17107, 17117, 17123, 17137, 17159, + 17167, 17183, 17189, 17191, 17203, 17207, 17209, 17231, + 17239, 17257, 17291, 17293, 17299, 17317, 17321, 17327, + 17333, 17341, 17351, 17359, 17377, 17383, 17387, 17389, + 17393, 17401, 17417, 17419, 17431, 17443, 17449, 17467, + 17471, 17477, 17483, 17489, 17491, 17497, 17509, 17519, + 17539, 17551, 17569, 17573, 17579, 17581, 17597, 17599, + 17609, 17623, 17627, 17657, 17659, 17669, 17681, 17683, + 17707, 17713, 17729, 17737, 17747, 17749, 17761, 17783, + 17789, 17791, 17807, 17827, 17837, 17839, 17851, 17863, +] + + +class _WinKey(): + + # A CNG BCRYPT_KEY_HANDLE on Vista and newer, an HCRYPTKEY on XP and 2003 + key_handle = None + # On XP and 2003, we have to carry around more info + context_handle = None + ex_key_handle = None + + # A reference to the library used in the destructor to make sure it hasn't + # been garbage collected by the time this object is garbage collected + _lib = None + + def __init__(self, key_handle, asn1): + """ + :param key_handle: + A CNG BCRYPT_KEY_HANDLE value (Vista and newer) or an HCRYPTKEY + (XP and 2003) from loading/importing the key + + :param asn1: + An asn1crypto object for the concrete type + """ + + self.key_handle = key_handle + self.asn1 = asn1 + + if _backend == 'winlegacy': + self._lib = advapi32 + else: + self._lib = bcrypt + + def __del__(self): + if self.key_handle: + if _backend == 'winlegacy': + res = self._lib.CryptDestroyKey(self.key_handle) + else: + res = self._lib.BCryptDestroyKey(self.key_handle) + handle_error(res) + self.key_handle = None + if self.context_handle and _backend == 'winlegacy': + close_context_handle(self.context_handle) + self.context_handle = None + self._lib = None + + +class PrivateKey(_WinKey, _PrivateKeyBase): + """ + Container for the OS crypto library representation of a private key + """ + + _public_key = None + + def __init__(self, key_handle, asn1): + """ + :param key_handle: + A CNG BCRYPT_KEY_HANDLE value (Vista and newer) or an HCRYPTKEY + (XP and 2003) from loading/importing the key + + :param asn1: + An asn1crypto.keys.PrivateKeyInfo object + """ + + _WinKey.__init__(self, key_handle, asn1) + + @property + def public_key(self): + """ + :return: + A PublicKey object corresponding to this private key. + """ + + if _backend == 'winlegacy': + if self.algorithm == 'ec': + pub_point = _pure_python_ec_compute_public_key_point(self.asn1) + self._public_key = PublicKey(None, ec_public_key_info(pub_point, self.curve)) + elif self.algorithm == 'dsa': + # The DSA provider won't allow exporting the private key with + # CryptoImportKey flags set to 0 and won't allow flags to be set + # to CRYPT_EXPORTABLE, so we manually recreated the public key + # ASN.1 + params = self.asn1['private_key_algorithm']['parameters'] + pub_asn1 = PublicKeyInfo({ + 'algorithm': PublicKeyAlgorithm({ + 'algorithm': 'dsa', + 'parameters': params + }), + 'public_key': Integer(pow( + params['g'].native, + self.asn1['private_key'].parsed.native, + params['p'].native + )) + }) + self._public_key = load_public_key(pub_asn1) + else: + # This suffers from similar problems as above, although not + # as insurmountable. This is just a simpler/faster solution + # since the private key has all of the data we need anyway + parsed = self.asn1['private_key'].parsed + pub_asn1 = PublicKeyInfo({ + 'algorithm': PublicKeyAlgorithm({ + 'algorithm': 'rsa' + }), + 'public_key': RSAPublicKey({ + 'modulus': parsed['modulus'], + 'public_exponent': parsed['public_exponent'] + }) + }) + self._public_key = load_public_key(pub_asn1) + else: + pub_asn1, _ = _bcrypt_key_handle_to_asn1(self.algorithm, self.bit_size, self.key_handle) + self._public_key = load_public_key(pub_asn1) + return self._public_key + + @property + def fingerprint(self): + """ + Creates a fingerprint that can be compared with a public key to see if + the two form a pair. + + This fingerprint is not compatible with fingerprints generated by any + other software. + + :return: + A byte string that is a sha256 hash of selected components (based + on the key type) + """ + + if self._fingerprint is None: + self._fingerprint = _fingerprint(self.asn1, load_private_key) + return self._fingerprint + + +class PublicKey(_WinKey, _PublicKeyBase): + """ + Container for the OS crypto library representation of a public key + """ + + def __init__(self, key_handle, asn1): + """ + :param key_handle: + A CNG BCRYPT_KEY_HANDLE value (Vista and newer) or an HCRYPTKEY + (XP and 2003) from loading/importing the key + + :param asn1: + An asn1crypto.keys.PublicKeyInfo object + """ + + _WinKey.__init__(self, key_handle, asn1) + + +class Certificate(_WinKey, _CertificateBase): + """ + Container for the OS crypto library representation of a certificate + """ + + _public_key = None + _self_signed = None + + def __init__(self, key_handle, asn1): + """ + :param key_handle: + A CNG BCRYPT_KEY_HANDLE value (Vista and newer) or an HCRYPTKEY + (XP and 2003) from loading/importing the certificate + + :param asn1: + An asn1crypto.x509.Certificate object + """ + + _WinKey.__init__(self, key_handle, asn1) + + @property + def public_key(self): + """ + :return: + The PublicKey object for the public key this certificate contains + """ + + if self._public_key is None: + self._public_key = load_public_key(self.asn1['tbs_certificate']['subject_public_key_info']) + return self._public_key + + @property + def self_signed(self): + """ + :return: + A boolean - if the certificate is self-signed + """ + + if self._self_signed is None: + self._self_signed = False + if self.asn1.self_signed in set(['yes', 'maybe']): + + signature_algo = self.asn1['signature_algorithm'].signature_algo + hash_algo = self.asn1['signature_algorithm'].hash_algo + + if signature_algo == 'rsassa_pkcs1v15': + verify_func = rsa_pkcs1v15_verify + elif signature_algo == 'rsassa_pss': + verify_func = rsa_pss_verify + elif signature_algo == 'dsa': + verify_func = dsa_verify + elif signature_algo == 'ecdsa': + verify_func = ecdsa_verify + else: + raise OSError(pretty_message( + ''' + Unable to verify the signature of the certificate since + it uses the unsupported algorithm %s + ''', + signature_algo + )) + + try: + verify_func( + self, + self.asn1['signature_value'].native, + self.asn1['tbs_certificate'].dump(), + hash_algo + ) + self._self_signed = True + except (SignatureError): + pass + + return self._self_signed + + +def generate_pair(algorithm, bit_size=None, curve=None): + """ + Generates a public/private key pair + + :param algorithm: + The key algorithm - "rsa", "dsa" or "ec" + + :param bit_size: + An integer - used for "rsa" and "dsa". For "rsa" the value maye be 1024, + 2048, 3072 or 4096. For "dsa" the value may be 1024, plus 2048 or 3072 + if on Windows 8 or newer. + + :param curve: + A unicode string - used for "ec" keys. Valid values include "secp256r1", + "secp384r1" and "secp521r1". + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A 2-element tuple of (PublicKey, PrivateKey). The contents of each key + may be saved by calling .asn1.dump(). + """ + + if algorithm not in set(['rsa', 'dsa', 'ec']): + raise ValueError(pretty_message( + ''' + algorithm must be one of "rsa", "dsa", "ec", not %s + ''', + repr(algorithm) + )) + + if algorithm == 'rsa': + if bit_size not in set([1024, 2048, 3072, 4096]): + raise ValueError(pretty_message( + ''' + bit_size must be one of 1024, 2048, 3072, 4096, not %s + ''', + repr(bit_size) + )) + + elif algorithm == 'dsa': + # Windows Vista and 7 only support SHA1-based DSA keys + if _win_version_info < (6, 2) or _backend == 'winlegacy': + if bit_size != 1024: + raise ValueError(pretty_message( + ''' + bit_size must be 1024, not %s + ''', + repr(bit_size) + )) + else: + if bit_size not in set([1024, 2048, 3072]): + raise ValueError(pretty_message( + ''' + bit_size must be one of 1024, 2048, 3072, not %s + ''', + repr(bit_size) + )) + + elif algorithm == 'ec': + if curve not in set(['secp256r1', 'secp384r1', 'secp521r1']): + raise ValueError(pretty_message( + ''' + curve must be one of "secp256r1", "secp384r1", "secp521r1", not %s + ''', + repr(curve) + )) + + if _backend == 'winlegacy': + if algorithm == 'ec': + pub_info, priv_info = _pure_python_ec_generate_pair(curve) + return (PublicKey(None, pub_info), PrivateKey(None, priv_info)) + return _advapi32_generate_pair(algorithm, bit_size) + else: + return _bcrypt_generate_pair(algorithm, bit_size, curve) + + +def _advapi32_key_handle_to_asn1(algorithm, bit_size, key_handle): + """ + Accepts an key handle and exports it to ASN.1 + + :param algorithm: + The key algorithm - "rsa" or "dsa" + + :param bit_size: + An integer - only used when algorithm is "rsa" + + :param key_handle: + The handle to export + + :return: + A 2-element tuple of asn1crypto.keys.PrivateKeyInfo and + asn1crypto.keys.PublicKeyInfo + """ + + if algorithm == 'rsa': + struct_type = 'RSABLOBHEADER' + else: + struct_type = 'DSSBLOBHEADER' + + out_len = new(advapi32, 'DWORD *') + res = advapi32.CryptExportKey( + key_handle, + null(), + Advapi32Const.PRIVATEKEYBLOB, + 0, + null(), + out_len + ) + handle_error(res) + + buffer_length = deref(out_len) + buffer_ = buffer_from_bytes(buffer_length) + res = advapi32.CryptExportKey( + key_handle, + null(), + Advapi32Const.PRIVATEKEYBLOB, + 0, + buffer_, + out_len + ) + handle_error(res) + + blob_struct_pointer = struct_from_buffer(advapi32, struct_type, buffer_) + blob_struct = unwrap(blob_struct_pointer) + struct_size = sizeof(advapi32, blob_struct) + + private_blob = bytes_from_buffer(buffer_, buffer_length)[struct_size:] + + if algorithm == 'rsa': + public_info, private_info = _advapi32_interpret_rsa_key_blob(bit_size, blob_struct, private_blob) + + else: + # The public key for a DSA key is not available in from the private + # key blob, so we have to separately export the public key + public_out_len = new(advapi32, 'DWORD *') + res = advapi32.CryptExportKey( + key_handle, + null(), + Advapi32Const.PUBLICKEYBLOB, + 0, + null(), + public_out_len + ) + handle_error(res) + + public_buffer_length = deref(public_out_len) + public_buffer = buffer_from_bytes(public_buffer_length) + res = advapi32.CryptExportKey( + key_handle, + null(), + Advapi32Const.PUBLICKEYBLOB, + 0, + public_buffer, + public_out_len + ) + handle_error(res) + + public_blob = bytes_from_buffer(public_buffer, public_buffer_length)[struct_size:] + + public_info, private_info = _advapi32_interpret_dsa_key_blob(bit_size, public_blob, private_blob) + + return (public_info, private_info) + + +def _advapi32_generate_pair(algorithm, bit_size=None): + """ + Generates a public/private key pair using CryptoAPI + + :param algorithm: + The key algorithm - "rsa" or "dsa" + + :param bit_size: + An integer - used for "rsa" and "dsa". For "rsa" the value maye be 1024, + 2048, 3072 or 4096. For "dsa" the value may be 1024. + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A 2-element tuple of (PublicKey, PrivateKey). The contents of each key + may be saved by calling .asn1.dump(). + """ + + if algorithm == 'rsa': + provider = Advapi32Const.MS_ENH_RSA_AES_PROV + algorithm_id = Advapi32Const.CALG_RSA_SIGN + else: + provider = Advapi32Const.MS_ENH_DSS_DH_PROV + algorithm_id = Advapi32Const.CALG_DSS_SIGN + + context_handle = None + key_handle = None + + try: + context_handle = open_context_handle(provider, verify_only=False) + + key_handle_pointer = new(advapi32, 'HCRYPTKEY *') + flags = (bit_size << 16) | Advapi32Const.CRYPT_EXPORTABLE + res = advapi32.CryptGenKey(context_handle, algorithm_id, flags, key_handle_pointer) + handle_error(res) + + key_handle = unwrap(key_handle_pointer) + + public_info, private_info = _advapi32_key_handle_to_asn1(algorithm, bit_size, key_handle) + + return (load_public_key(public_info), load_private_key(private_info)) + + finally: + if context_handle: + close_context_handle(context_handle) + if key_handle: + advapi32.CryptDestroyKey(key_handle) + + +def _bcrypt_key_handle_to_asn1(algorithm, bit_size, key_handle): + """ + Accepts an key handle and exports it to ASN.1 + + :param algorithm: + The key algorithm - "rsa", "dsa" or "ec" + + :param bit_size: + An integer - only used when algorithm is "dsa" + + :param key_handle: + The handle to export + + :return: + A 2-element tuple of asn1crypto.keys.PrivateKeyInfo and + asn1crypto.keys.PublicKeyInfo + """ + + if algorithm == 'rsa': + struct_type = 'BCRYPT_RSAKEY_BLOB' + private_blob_type = BcryptConst.BCRYPT_RSAFULLPRIVATE_BLOB + public_blob_type = BcryptConst.BCRYPT_RSAPUBLIC_BLOB + + elif algorithm == 'dsa': + if bit_size > 1024: + struct_type = 'BCRYPT_DSA_KEY_BLOB_V2' + else: + struct_type = 'BCRYPT_DSA_KEY_BLOB' + private_blob_type = BcryptConst.BCRYPT_DSA_PRIVATE_BLOB + public_blob_type = BcryptConst.BCRYPT_DSA_PUBLIC_BLOB + + else: + struct_type = 'BCRYPT_ECCKEY_BLOB' + private_blob_type = BcryptConst.BCRYPT_ECCPRIVATE_BLOB + public_blob_type = BcryptConst.BCRYPT_ECCPUBLIC_BLOB + + private_out_len = new(bcrypt, 'ULONG *') + res = bcrypt.BCryptExportKey(key_handle, null(), private_blob_type, null(), 0, private_out_len, 0) + handle_error(res) + + private_buffer_length = deref(private_out_len) + private_buffer = buffer_from_bytes(private_buffer_length) + res = bcrypt.BCryptExportKey( + key_handle, + null(), + private_blob_type, + private_buffer, + private_buffer_length, + private_out_len, + 0 + ) + handle_error(res) + private_blob_struct_pointer = struct_from_buffer(bcrypt, struct_type, private_buffer) + private_blob_struct = unwrap(private_blob_struct_pointer) + struct_size = sizeof(bcrypt, private_blob_struct) + private_blob = bytes_from_buffer(private_buffer, private_buffer_length)[struct_size:] + + if algorithm == 'rsa': + private_key = _bcrypt_interpret_rsa_key_blob('private', private_blob_struct, private_blob) + elif algorithm == 'dsa': + if bit_size > 1024: + private_key = _bcrypt_interpret_dsa_key_blob('private', 2, private_blob_struct, private_blob) + else: + private_key = _bcrypt_interpret_dsa_key_blob('private', 1, private_blob_struct, private_blob) + else: + private_key = _bcrypt_interpret_ec_key_blob('private', private_blob_struct, private_blob) + + public_out_len = new(bcrypt, 'ULONG *') + res = bcrypt.BCryptExportKey(key_handle, null(), public_blob_type, null(), 0, public_out_len, 0) + handle_error(res) + + public_buffer_length = deref(public_out_len) + public_buffer = buffer_from_bytes(public_buffer_length) + res = bcrypt.BCryptExportKey( + key_handle, + null(), + public_blob_type, + public_buffer, + public_buffer_length, + public_out_len, + 0 + ) + handle_error(res) + public_blob_struct_pointer = struct_from_buffer(bcrypt, struct_type, public_buffer) + public_blob_struct = unwrap(public_blob_struct_pointer) + struct_size = sizeof(bcrypt, public_blob_struct) + public_blob = bytes_from_buffer(public_buffer, public_buffer_length)[struct_size:] + + if algorithm == 'rsa': + public_key = _bcrypt_interpret_rsa_key_blob('public', public_blob_struct, public_blob) + elif algorithm == 'dsa': + if bit_size > 1024: + public_key = _bcrypt_interpret_dsa_key_blob('public', 2, public_blob_struct, public_blob) + else: + public_key = _bcrypt_interpret_dsa_key_blob('public', 1, public_blob_struct, public_blob) + else: + public_key = _bcrypt_interpret_ec_key_blob('public', public_blob_struct, public_blob) + + return (public_key, private_key) + + +def _bcrypt_generate_pair(algorithm, bit_size=None, curve=None): + """ + Generates a public/private key pair using CNG + + :param algorithm: + The key algorithm - "rsa", "dsa" or "ec" + + :param bit_size: + An integer - used for "rsa" and "dsa". For "rsa" the value maye be 1024, + 2048, 3072 or 4096. For "dsa" the value may be 1024, plus 2048 or 3072 + if on Windows 8 or newer. + + :param curve: + A unicode string - used for "ec" keys. Valid values include "secp256r1", + "secp384r1" and "secp521r1". + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A 2-element tuple of (PublicKey, PrivateKey). The contents of each key + may be saved by calling .asn1.dump(). + """ + + if algorithm == 'rsa': + alg_constant = BcryptConst.BCRYPT_RSA_ALGORITHM + + elif algorithm == 'dsa': + alg_constant = BcryptConst.BCRYPT_DSA_ALGORITHM + + else: + alg_constant = { + 'secp256r1': BcryptConst.BCRYPT_ECDSA_P256_ALGORITHM, + 'secp384r1': BcryptConst.BCRYPT_ECDSA_P384_ALGORITHM, + 'secp521r1': BcryptConst.BCRYPT_ECDSA_P521_ALGORITHM, + }[curve] + bit_size = { + 'secp256r1': 256, + 'secp384r1': 384, + 'secp521r1': 521, + }[curve] + + key_handle = None + try: + alg_handle = open_alg_handle(alg_constant) + key_handle_pointer = new(bcrypt, 'BCRYPT_KEY_HANDLE *') + res = bcrypt.BCryptGenerateKeyPair(alg_handle, key_handle_pointer, bit_size, 0) + handle_error(res) + key_handle = unwrap(key_handle_pointer) + + res = bcrypt.BCryptFinalizeKeyPair(key_handle, 0) + handle_error(res) + + public_key, private_key = _bcrypt_key_handle_to_asn1(algorithm, bit_size, key_handle) + + finally: + if key_handle: + bcrypt.BCryptDestroyKey(key_handle) + + return (load_public_key(public_key), load_private_key(private_key)) + + +def generate_dh_parameters(bit_size): + """ + Generates DH parameters for use with Diffie-Hellman key exchange. Returns + a structure in the format of DHParameter defined in PKCS#3, which is also + used by the OpenSSL dhparam tool. + + THIS CAN BE VERY TIME CONSUMING! + + :param bit_size: + The integer bit size of the parameters to generate. Must be between 512 + and 4096, and divisible by 64. Recommended secure value as of early 2016 + is 2048, with an absolute minimum of 1024. + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + An asn1crypto.algos.DHParameters object. Use + oscrypto.asymmetric.dump_dh_parameters() to save to disk for usage with + web servers. + """ + + if not isinstance(bit_size, int_types): + raise TypeError(pretty_message( + ''' + bit_size must be an integer, not %s + ''', + type_name(bit_size) + )) + + if bit_size < 512: + raise ValueError('bit_size must be greater than or equal to 512') + + if bit_size > 4096: + raise ValueError('bit_size must be less than or equal to 4096') + + if bit_size % 64 != 0: + raise ValueError('bit_size must be a multiple of 64') + + alg_handle = None + + # The following algorithm has elements taken from OpenSSL. In short, it + # generates random numbers and then ensures that they are valid for the + # hardcoded generator of 2, and then ensures the number is a "safe" prime + # by ensuring p//2 is prime also. + + # OpenSSL allows use of generator 2 or 5, but we hardcode 2 since it is + # the default, and what is used by Security.framework on OS X also. + g = 2 + + try: + byte_size = bit_size // 8 + if _backend == 'win': + alg_handle = open_alg_handle(BcryptConst.BCRYPT_RNG_ALGORITHM) + buffer = buffer_from_bytes(byte_size) + + while True: + if _backend == 'winlegacy': + rb = os.urandom(byte_size) + else: + res = bcrypt.BCryptGenRandom(alg_handle, buffer, byte_size, 0) + handle_error(res) + rb = bytes_from_buffer(buffer) + + p = int_from_bytes(rb) + + # If a number is even, it can't be prime + if p % 2 == 0: + continue + + # Perform the generator checks outlined in OpenSSL's + # dh_builtin_genparams() located in dh_gen.c + if g == 2: + if p % 24 != 11: + continue + elif g == 5: + rem = p % 10 + if rem != 3 and rem != 7: + continue + + divisible = False + for prime in _SMALL_PRIMES: + if p % prime == 0: + divisible = True + break + + # If the number is not divisible by any of the small primes, then + # move on to the full Miller-Rabin test. + if not divisible and _is_prime(bit_size, p): + q = p // 2 + if _is_prime(bit_size, q): + return DHParameters({'p': p, 'g': g}) + + finally: + if alg_handle: + close_alg_handle(alg_handle) + + +def _is_prime(bit_size, n): + """ + An implementation of Miller–Rabin for checking if a number is prime. + + :param bit_size: + An integer of the number of bits in the prime number + + :param n: + An integer, the prime number + + :return: + A boolean + """ + + r = 0 + s = n - 1 + while s % 2 == 0: + r += 1 + s //= 2 + + if bit_size >= 1300: + k = 2 + elif bit_size >= 850: + k = 3 + elif bit_size >= 650: + k = 4 + elif bit_size >= 550: + k = 5 + elif bit_size >= 450: + k = 6 + + for _ in range(k): + a = random.randrange(2, n - 1) + x = pow(a, s, n) + if x == 1 or x == n - 1: + continue + for _ in range(r - 1): + x = pow(x, 2, n) + if x == n - 1: + break + else: + return False + + return True + + +def _advapi32_interpret_rsa_key_blob(bit_size, blob_struct, blob): + """ + Takes a CryptoAPI RSA private key blob and converts it into the ASN.1 + structures for the public and private keys + + :param bit_size: + The integer bit size of the key + + :param blob_struct: + An instance of the advapi32.RSAPUBKEY struct + + :param blob: + A byte string of the binary data after the header + + :return: + A 2-element tuple of (asn1crypto.keys.PublicKeyInfo, + asn1crypto.keys.PrivateKeyInfo) + """ + + len1 = bit_size // 8 + len2 = bit_size // 16 + + prime1_offset = len1 + prime2_offset = prime1_offset + len2 + exponent1_offset = prime2_offset + len2 + exponent2_offset = exponent1_offset + len2 + coefficient_offset = exponent2_offset + len2 + private_exponent_offset = coefficient_offset + len2 + + public_exponent = blob_struct.rsapubkey.pubexp + modulus = int_from_bytes(blob[0:prime1_offset][::-1]) + prime1 = int_from_bytes(blob[prime1_offset:prime2_offset][::-1]) + prime2 = int_from_bytes(blob[prime2_offset:exponent1_offset][::-1]) + exponent1 = int_from_bytes(blob[exponent1_offset:exponent2_offset][::-1]) + exponent2 = int_from_bytes(blob[exponent2_offset:coefficient_offset][::-1]) + coefficient = int_from_bytes(blob[coefficient_offset:private_exponent_offset][::-1]) + private_exponent = int_from_bytes(blob[private_exponent_offset:private_exponent_offset + len1][::-1]) + + public_key_info = PublicKeyInfo({ + 'algorithm': PublicKeyAlgorithm({ + 'algorithm': 'rsa', + }), + 'public_key': RSAPublicKey({ + 'modulus': modulus, + 'public_exponent': public_exponent, + }), + }) + + rsa_private_key = RSAPrivateKey({ + 'version': 'two-prime', + 'modulus': modulus, + 'public_exponent': public_exponent, + 'private_exponent': private_exponent, + 'prime1': prime1, + 'prime2': prime2, + 'exponent1': exponent1, + 'exponent2': exponent2, + 'coefficient': coefficient, + }) + + private_key_info = PrivateKeyInfo({ + 'version': 0, + 'private_key_algorithm': PrivateKeyAlgorithm({ + 'algorithm': 'rsa', + }), + 'private_key': rsa_private_key, + }) + + return (public_key_info, private_key_info) + + +def _advapi32_interpret_dsa_key_blob(bit_size, public_blob, private_blob): + """ + Takes a CryptoAPI DSS private key blob and converts it into the ASN.1 + structures for the public and private keys + + :param bit_size: + The integer bit size of the key + + :param public_blob: + A byte string of the binary data after the public key header + + :param private_blob: + A byte string of the binary data after the private key header + + :return: + A 2-element tuple of (asn1crypto.keys.PublicKeyInfo, + asn1crypto.keys.PrivateKeyInfo) + """ + + len1 = 20 + len2 = bit_size // 8 + + q_offset = len2 + g_offset = q_offset + len1 + x_offset = g_offset + len2 + y_offset = x_offset + + p = int_from_bytes(private_blob[0:q_offset][::-1]) + q = int_from_bytes(private_blob[q_offset:g_offset][::-1]) + g = int_from_bytes(private_blob[g_offset:x_offset][::-1]) + x = int_from_bytes(private_blob[x_offset:x_offset + len1][::-1]) + y = int_from_bytes(public_blob[y_offset:y_offset + len2][::-1]) + + public_key_info = PublicKeyInfo({ + 'algorithm': PublicKeyAlgorithm({ + 'algorithm': 'dsa', + 'parameters': DSAParams({ + 'p': p, + 'q': q, + 'g': g, + }) + }), + 'public_key': Integer(y), + }) + + private_key_info = PrivateKeyInfo({ + 'version': 0, + 'private_key_algorithm': PrivateKeyAlgorithm({ + 'algorithm': 'dsa', + 'parameters': DSAParams({ + 'p': p, + 'q': q, + 'g': g, + }) + }), + 'private_key': Integer(x), + }) + + return (public_key_info, private_key_info) + + +def _bcrypt_interpret_rsa_key_blob(key_type, blob_struct, blob): + """ + Take a CNG BCRYPT_RSAFULLPRIVATE_BLOB and converts it into an ASN.1 + structure + + :param key_type: + A unicode string of "private" or "public" + + :param blob_struct: + An instance of BCRYPT_RSAKEY_BLOB + + :param blob: + A byte string of the binary data contained after the struct + + :return: + An asn1crypto.keys.PrivateKeyInfo or asn1crypto.keys.PublicKeyInfo + object, based on the key_type param + """ + + public_exponent_byte_length = native(int, blob_struct.cbPublicExp) + modulus_byte_length = native(int, blob_struct.cbModulus) + + modulus_offset = public_exponent_byte_length + + public_exponent = int_from_bytes(blob[0:modulus_offset]) + modulus = int_from_bytes(blob[modulus_offset:modulus_offset + modulus_byte_length]) + + if key_type == 'public': + return PublicKeyInfo({ + 'algorithm': PublicKeyAlgorithm({ + 'algorithm': 'rsa', + }), + 'public_key': RSAPublicKey({ + 'modulus': modulus, + 'public_exponent': public_exponent, + }), + }) + + elif key_type == 'private': + prime1_byte_length = native(int, blob_struct.cbPrime1) + prime2_byte_length = native(int, blob_struct.cbPrime2) + + prime1_offset = modulus_offset + modulus_byte_length + prime2_offset = prime1_offset + prime1_byte_length + exponent1_offset = prime2_offset + prime2_byte_length + exponent2_offset = exponent1_offset + prime2_byte_length + coefficient_offset = exponent2_offset + prime2_byte_length + private_exponent_offset = coefficient_offset + prime1_byte_length + + prime1 = int_from_bytes(blob[prime1_offset:prime2_offset]) + prime2 = int_from_bytes(blob[prime2_offset:exponent1_offset]) + exponent1 = int_from_bytes(blob[exponent1_offset:exponent2_offset]) + exponent2 = int_from_bytes(blob[exponent2_offset:coefficient_offset]) + coefficient = int_from_bytes(blob[coefficient_offset:private_exponent_offset]) + private_exponent = int_from_bytes(blob[private_exponent_offset:private_exponent_offset + modulus_byte_length]) + + rsa_private_key = RSAPrivateKey({ + 'version': 'two-prime', + 'modulus': modulus, + 'public_exponent': public_exponent, + 'private_exponent': private_exponent, + 'prime1': prime1, + 'prime2': prime2, + 'exponent1': exponent1, + 'exponent2': exponent2, + 'coefficient': coefficient, + }) + + return PrivateKeyInfo({ + 'version': 0, + 'private_key_algorithm': PrivateKeyAlgorithm({ + 'algorithm': 'rsa', + }), + 'private_key': rsa_private_key, + }) + + else: + raise ValueError(pretty_message( + ''' + key_type must be one of "public", "private", not %s + ''', + repr(key_type) + )) + + +def _bcrypt_interpret_dsa_key_blob(key_type, version, blob_struct, blob): + """ + Take a CNG BCRYPT_DSA_KEY_BLOB or BCRYPT_DSA_KEY_BLOB_V2 and converts it + into an ASN.1 structure + + :param key_type: + A unicode string of "private" or "public" + + :param version: + An integer - 1 or 2, indicating the blob is BCRYPT_DSA_KEY_BLOB or + BCRYPT_DSA_KEY_BLOB_V2 + + :param blob_struct: + An instance of BCRYPT_DSA_KEY_BLOB or BCRYPT_DSA_KEY_BLOB_V2 + + :param blob: + A byte string of the binary data contained after the struct + + :return: + An asn1crypto.keys.PrivateKeyInfo or asn1crypto.keys.PublicKeyInfo + object, based on the key_type param + """ + + key_byte_length = native(int, blob_struct.cbKey) + + if version == 1: + q = int_from_bytes(native(byte_cls, blob_struct.q)) + + g_offset = key_byte_length + public_offset = g_offset + key_byte_length + private_offset = public_offset + key_byte_length + + p = int_from_bytes(blob[0:g_offset]) + g = int_from_bytes(blob[g_offset:public_offset]) + + elif version == 2: + seed_byte_length = native(int, blob_struct.cbSeedLength) + group_byte_length = native(int, blob_struct.cbGroupSize) + + q_offset = seed_byte_length + p_offset = q_offset + group_byte_length + g_offset = p_offset + key_byte_length + public_offset = g_offset + key_byte_length + private_offset = public_offset + key_byte_length + + # The seed is skipped since it is not part of the ASN.1 structure + q = int_from_bytes(blob[q_offset:p_offset]) + p = int_from_bytes(blob[p_offset:g_offset]) + g = int_from_bytes(blob[g_offset:public_offset]) + + else: + raise ValueError('version must be 1 or 2, not %s' % repr(version)) + + if key_type == 'public': + public = int_from_bytes(blob[public_offset:private_offset]) + return PublicKeyInfo({ + 'algorithm': PublicKeyAlgorithm({ + 'algorithm': 'dsa', + 'parameters': DSAParams({ + 'p': p, + 'q': q, + 'g': g, + }) + }), + 'public_key': Integer(public), + }) + + elif key_type == 'private': + private = int_from_bytes(blob[private_offset:private_offset + key_byte_length]) + return PrivateKeyInfo({ + 'version': 0, + 'private_key_algorithm': PrivateKeyAlgorithm({ + 'algorithm': 'dsa', + 'parameters': DSAParams({ + 'p': p, + 'q': q, + 'g': g, + }) + }), + 'private_key': Integer(private), + }) + + else: + raise ValueError(pretty_message( + ''' + key_type must be one of "public", "private", not %s + ''', + repr(key_type) + )) + + +def _bcrypt_interpret_ec_key_blob(key_type, blob_struct, blob): + """ + Take a CNG BCRYPT_ECCKEY_BLOB and converts it into an ASN.1 structure + + :param key_type: + A unicode string of "private" or "public" + + :param blob_struct: + An instance of BCRYPT_ECCKEY_BLOB + + :param blob: + A byte string of the binary data contained after the struct + + :return: + An asn1crypto.keys.PrivateKeyInfo or asn1crypto.keys.PublicKeyInfo + object, based on the key_type param + """ + + magic = native(int, blob_struct.dwMagic) + key_byte_length = native(int, blob_struct.cbKey) + + curve = { + BcryptConst.BCRYPT_ECDSA_PRIVATE_P256_MAGIC: 'secp256r1', + BcryptConst.BCRYPT_ECDSA_PRIVATE_P384_MAGIC: 'secp384r1', + BcryptConst.BCRYPT_ECDSA_PRIVATE_P521_MAGIC: 'secp521r1', + BcryptConst.BCRYPT_ECDSA_PUBLIC_P256_MAGIC: 'secp256r1', + BcryptConst.BCRYPT_ECDSA_PUBLIC_P384_MAGIC: 'secp384r1', + BcryptConst.BCRYPT_ECDSA_PUBLIC_P521_MAGIC: 'secp521r1', + }[magic] + + public = b'\x04' + blob[0:key_byte_length * 2] + + if key_type == 'public': + return PublicKeyInfo({ + 'algorithm': PublicKeyAlgorithm({ + 'algorithm': 'ec', + 'parameters': ECDomainParameters( + name='named', + value=curve + ) + }), + 'public_key': public, + }) + + elif key_type == 'private': + private = int_from_bytes(blob[key_byte_length * 2:key_byte_length * 3]) + return PrivateKeyInfo({ + 'version': 0, + 'private_key_algorithm': PrivateKeyAlgorithm({ + 'algorithm': 'ec', + 'parameters': ECDomainParameters( + name='named', + value=curve + ) + }), + 'private_key': ECPrivateKey({ + 'version': 'ecPrivkeyVer1', + 'private_key': private, + 'public_key': public, + }), + }) + + else: + raise ValueError(pretty_message( + ''' + key_type must be one of "public", "private", not %s + ''', + repr(key_type) + )) + + +def load_certificate(source): + """ + Loads an x509 certificate into a Certificate object + + :param source: + A byte string of file contents or a unicode string filename + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A Certificate object + """ + + if isinstance(source, Asn1Certificate): + certificate = source + + elif isinstance(source, byte_cls): + certificate = parse_certificate(source) + + elif isinstance(source, str_cls): + with open(source, 'rb') as f: + certificate = parse_certificate(f.read()) + + else: + raise TypeError(pretty_message( + ''' + source must be a byte string, unicode string or + asn1crypto.x509.Certificate object, not %s + ''', + type_name(source) + )) + + return _load_key(certificate, Certificate) + + +def _load_key(key_object, container): + """ + Loads a certificate, public key or private key into a Certificate, + PublicKey or PrivateKey object + + :param key_object: + An asn1crypto.x509.Certificate, asn1crypto.keys.PublicKeyInfo or + asn1crypto.keys.PrivateKeyInfo object + + :param container: + The class of the object to hold the key_handle + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + oscrypto.errors.AsymmetricKeyError - when the key is incompatible with the OS crypto library + OSError - when an error is returned by the OS crypto library + + :return: + A PrivateKey, PublicKey or Certificate object, based on container + """ + + key_info = key_object + if isinstance(key_object, Asn1Certificate): + key_info = key_object['tbs_certificate']['subject_public_key_info'] + + algo = key_info.algorithm + curve_name = None + + if algo == 'ec': + curve_type, curve_name = key_info.curve + if curve_type != 'named': + raise AsymmetricKeyError(pretty_message( + ''' + Windows only supports EC keys using named curves + ''' + )) + if curve_name not in set(['secp256r1', 'secp384r1', 'secp521r1']): + raise AsymmetricKeyError(pretty_message( + ''' + Windows only supports EC keys using the named curves + secp256r1, secp384r1 and secp521r1 + ''' + )) + + elif algo == 'dsa': + if key_info.hash_algo is None: + raise IncompleteAsymmetricKeyError(pretty_message( + ''' + The DSA key does not contain the necessary p, q and g + parameters and can not be used + ''' + )) + elif key_info.bit_size > 1024 and (_win_version_info < (6, 2) or _backend == 'winlegacy'): + raise AsymmetricKeyError(pretty_message( + ''' + Windows XP, 2003, Vista, 7 and Server 2008 only support DSA + keys based on SHA1 (1024 bits or less) - this key is based + on %s and is %s bits + ''', + key_info.hash_algo.upper(), + key_info.bit_size + )) + elif key_info.bit_size == 2048 and key_info.hash_algo == 'sha1': + raise AsymmetricKeyError(pretty_message( + ''' + Windows only supports 2048 bit DSA keys based on SHA2 - this + key is 2048 bits and based on SHA1, a non-standard + combination that is usually generated by old versions + of OpenSSL + ''' + )) + + if _backend == 'winlegacy': + if algo == 'ec': + return container(None, key_object) + return _advapi32_load_key(key_object, key_info, container) + return _bcrypt_load_key(key_object, key_info, container, curve_name) + + +def _advapi32_load_key(key_object, key_info, container): + """ + Loads a certificate, public key or private key into a Certificate, + PublicKey or PrivateKey object via CryptoAPI + + :param key_object: + An asn1crypto.x509.Certificate, asn1crypto.keys.PublicKeyInfo or + asn1crypto.keys.PrivateKeyInfo object + + :param key_info: + An asn1crypto.keys.PublicKeyInfo or asn1crypto.keys.PrivateKeyInfo + object + + :param container: + The class of the object to hold the key_handle + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + oscrypto.errors.AsymmetricKeyError - when the key is incompatible with the OS crypto library + OSError - when an error is returned by the OS crypto library + + :return: + A PrivateKey, PublicKey or Certificate object, based on container + """ + + key_type = 'public' if isinstance(key_info, PublicKeyInfo) else 'private' + algo = key_info.algorithm + if algo == 'rsassa_pss': + algo = 'rsa' + + if algo == 'rsa' or algo == 'rsassa_pss': + provider = Advapi32Const.MS_ENH_RSA_AES_PROV + else: + provider = Advapi32Const.MS_ENH_DSS_DH_PROV + + context_handle = None + key_handle = None + + try: + context_handle = open_context_handle(provider, verify_only=key_type == 'public') + + blob = _advapi32_create_blob(key_info, key_type, algo) + buffer_ = buffer_from_bytes(blob) + + key_handle_pointer = new(advapi32, 'HCRYPTKEY *') + res = advapi32.CryptImportKey( + context_handle, + buffer_, + len(blob), + null(), + 0, + key_handle_pointer + ) + handle_error(res) + + key_handle = unwrap(key_handle_pointer) + output = container(key_handle, key_object) + output.context_handle = context_handle + + if algo == 'rsa': + ex_blob = _advapi32_create_blob(key_info, key_type, algo, signing=False) + ex_buffer = buffer_from_bytes(ex_blob) + + ex_key_handle_pointer = new(advapi32, 'HCRYPTKEY *') + res = advapi32.CryptImportKey( + context_handle, + ex_buffer, + len(ex_blob), + null(), + 0, + ex_key_handle_pointer + ) + handle_error(res) + + output.ex_key_handle = unwrap(ex_key_handle_pointer) + + return output + + except (Exception): + if key_handle: + advapi32.CryptDestroyKey(key_handle) + if context_handle: + close_context_handle(context_handle) + raise + + +def _advapi32_create_blob(key_info, key_type, algo, signing=True): + """ + Generates a blob for importing a key to CryptoAPI + + :param key_info: + An asn1crypto.keys.PublicKeyInfo or asn1crypto.keys.PrivateKeyInfo + object + + :param key_type: + A unicode string of "public" or "private" + + :param algo: + A unicode string of "rsa" or "dsa" + + :param signing: + If the key handle is for signing - may only be False for rsa keys + + :return: + A byte string of a blob to pass to advapi32.CryptImportKey() + """ + + if key_type == 'public': + blob_type = Advapi32Const.PUBLICKEYBLOB + else: + blob_type = Advapi32Const.PRIVATEKEYBLOB + + if algo == 'rsa': + struct_type = 'RSABLOBHEADER' + if signing: + algorithm_id = Advapi32Const.CALG_RSA_SIGN + else: + algorithm_id = Advapi32Const.CALG_RSA_KEYX + else: + struct_type = 'DSSBLOBHEADER' + algorithm_id = Advapi32Const.CALG_DSS_SIGN + + blob_header_pointer = struct(advapi32, 'BLOBHEADER') + blob_header = unwrap(blob_header_pointer) + blob_header.bType = blob_type + blob_header.bVersion = Advapi32Const.CUR_BLOB_VERSION + blob_header.reserved = 0 + blob_header.aiKeyAlg = algorithm_id + + blob_struct_pointer = struct(advapi32, struct_type) + blob_struct = unwrap(blob_struct_pointer) + blob_struct.publickeystruc = blob_header + + bit_size = key_info.bit_size + len1 = bit_size // 8 + len2 = bit_size // 16 + + if algo == 'rsa': + pubkey_pointer = struct(advapi32, 'RSAPUBKEY') + pubkey = unwrap(pubkey_pointer) + pubkey.bitlen = bit_size + if key_type == 'public': + parsed_key_info = key_info['public_key'].parsed + pubkey.magic = Advapi32Const.RSA1 + pubkey.pubexp = parsed_key_info['public_exponent'].native + blob_data = int_to_bytes(parsed_key_info['modulus'].native, signed=False, width=len1)[::-1] + else: + parsed_key_info = key_info['private_key'].parsed + pubkey.magic = Advapi32Const.RSA2 + pubkey.pubexp = parsed_key_info['public_exponent'].native + blob_data = int_to_bytes(parsed_key_info['modulus'].native, signed=False, width=len1)[::-1] + blob_data += int_to_bytes(parsed_key_info['prime1'].native, signed=False, width=len2)[::-1] + blob_data += int_to_bytes(parsed_key_info['prime2'].native, signed=False, width=len2)[::-1] + blob_data += int_to_bytes(parsed_key_info['exponent1'].native, signed=False, width=len2)[::-1] + blob_data += int_to_bytes(parsed_key_info['exponent2'].native, signed=False, width=len2)[::-1] + blob_data += int_to_bytes(parsed_key_info['coefficient'].native, signed=False, width=len2)[::-1] + blob_data += int_to_bytes(parsed_key_info['private_exponent'].native, signed=False, width=len1)[::-1] + blob_struct.rsapubkey = pubkey + + else: + pubkey_pointer = struct(advapi32, 'DSSPUBKEY') + pubkey = unwrap(pubkey_pointer) + pubkey.bitlen = bit_size + + if key_type == 'public': + pubkey.magic = Advapi32Const.DSS1 + params = key_info['algorithm']['parameters'].native + key_data = int_to_bytes(key_info['public_key'].parsed.native, signed=False, width=len1)[::-1] + else: + pubkey.magic = Advapi32Const.DSS2 + params = key_info['private_key_algorithm']['parameters'].native + key_data = int_to_bytes(key_info['private_key'].parsed.native, signed=False, width=20)[::-1] + blob_struct.dsspubkey = pubkey + + blob_data = int_to_bytes(params['p'], signed=False, width=len1)[::-1] + blob_data += int_to_bytes(params['q'], signed=False, width=20)[::-1] + blob_data += int_to_bytes(params['g'], signed=False, width=len1)[::-1] + blob_data += key_data + + dssseed_pointer = struct(advapi32, 'DSSSEED') + dssseed = unwrap(dssseed_pointer) + # This indicates no counter or seed info is available + dssseed.counter = 0xffffffff + + blob_data += struct_bytes(dssseed_pointer) + + return struct_bytes(blob_struct_pointer) + blob_data + + +def _bcrypt_load_key(key_object, key_info, container, curve_name): + """ + Loads a certificate, public key or private key into a Certificate, + PublicKey or PrivateKey object via CNG + + :param key_object: + An asn1crypto.x509.Certificate, asn1crypto.keys.PublicKeyInfo or + asn1crypto.keys.PrivateKeyInfo object + + :param key_info: + An asn1crypto.keys.PublicKeyInfo or asn1crypto.keys.PrivateKeyInfo + object + + :param container: + The class of the object to hold the key_handle + + :param curve_name: + None or a unicode string of the curve name for an EC key + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + oscrypto.errors.AsymmetricKeyError - when the key is incompatible with the OS crypto library + OSError - when an error is returned by the OS crypto library + + :return: + A PrivateKey, PublicKey or Certificate object, based on container + """ + + alg_handle = None + key_handle = None + + key_type = 'public' if isinstance(key_info, PublicKeyInfo) else 'private' + algo = key_info.algorithm + if algo == 'rsassa_pss': + algo = 'rsa' + + try: + alg_selector = key_info.curve[1] if algo == 'ec' else algo + alg_constant = { + 'rsa': BcryptConst.BCRYPT_RSA_ALGORITHM, + 'dsa': BcryptConst.BCRYPT_DSA_ALGORITHM, + 'secp256r1': BcryptConst.BCRYPT_ECDSA_P256_ALGORITHM, + 'secp384r1': BcryptConst.BCRYPT_ECDSA_P384_ALGORITHM, + 'secp521r1': BcryptConst.BCRYPT_ECDSA_P521_ALGORITHM, + }[alg_selector] + alg_handle = open_alg_handle(alg_constant) + + if algo == 'rsa': + if key_type == 'public': + blob_type = BcryptConst.BCRYPT_RSAPUBLIC_BLOB + magic = BcryptConst.BCRYPT_RSAPUBLIC_MAGIC + parsed_key = key_info['public_key'].parsed + prime1_size = 0 + prime2_size = 0 + else: + blob_type = BcryptConst.BCRYPT_RSAFULLPRIVATE_BLOB + magic = BcryptConst.BCRYPT_RSAFULLPRIVATE_MAGIC + parsed_key = key_info['private_key'].parsed + prime1 = int_to_bytes(parsed_key['prime1'].native) + prime2 = int_to_bytes(parsed_key['prime2'].native) + exponent1 = int_to_bytes(parsed_key['exponent1'].native) + exponent2 = int_to_bytes(parsed_key['exponent2'].native) + coefficient = int_to_bytes(parsed_key['coefficient'].native) + private_exponent = int_to_bytes(parsed_key['private_exponent'].native) + prime1_size = len(prime1) + prime2_size = len(prime2) + + public_exponent = int_to_bytes(parsed_key['public_exponent'].native) + modulus = int_to_bytes(parsed_key['modulus'].native) + + blob_struct_pointer = struct(bcrypt, 'BCRYPT_RSAKEY_BLOB') + blob_struct = unwrap(blob_struct_pointer) + blob_struct.Magic = magic + blob_struct.BitLength = key_info.bit_size + blob_struct.cbPublicExp = len(public_exponent) + blob_struct.cbModulus = len(modulus) + blob_struct.cbPrime1 = prime1_size + blob_struct.cbPrime2 = prime2_size + + blob = struct_bytes(blob_struct_pointer) + public_exponent + modulus + if key_type == 'private': + blob += prime1 + prime2 + blob += fill_width(exponent1, prime1_size) + blob += fill_width(exponent2, prime2_size) + blob += fill_width(coefficient, prime1_size) + blob += fill_width(private_exponent, len(modulus)) + + elif algo == 'dsa': + if key_type == 'public': + blob_type = BcryptConst.BCRYPT_DSA_PUBLIC_BLOB + public_key = key_info['public_key'].parsed.native + params = key_info['algorithm']['parameters'] + else: + blob_type = BcryptConst.BCRYPT_DSA_PRIVATE_BLOB + public_key = _unwrap_private_key_info(key_info)['public_key'].native + private_bytes = int_to_bytes(key_info['private_key'].parsed.native) + params = key_info['private_key_algorithm']['parameters'] + + public_bytes = int_to_bytes(public_key) + p = int_to_bytes(params['p'].native) + g = int_to_bytes(params['g'].native) + q = int_to_bytes(params['q'].native) + + if key_info.bit_size > 1024: + q_len = len(q) + else: + q_len = 20 + + key_width = max(len(public_bytes), len(g), len(p)) + + public_bytes = fill_width(public_bytes, key_width) + p = fill_width(p, key_width) + g = fill_width(g, key_width) + q = fill_width(q, q_len) + # We don't know the count or seed, so we set them to the max value + # since setting them to 0 results in a parameter error + count = b'\xff' * 4 + seed = b'\xff' * q_len + + if key_info.bit_size > 1024: + if key_type == 'public': + magic = BcryptConst.BCRYPT_DSA_PUBLIC_MAGIC_V2 + else: + magic = BcryptConst.BCRYPT_DSA_PRIVATE_MAGIC_V2 + + blob_struct_pointer = struct(bcrypt, 'BCRYPT_DSA_KEY_BLOB_V2') + blob_struct = unwrap(blob_struct_pointer) + blob_struct.dwMagic = magic + blob_struct.cbKey = key_width + # We don't know if SHA256 was used here, but the output is long + # enough for the generation of q for the supported 2048/224, + # 2048/256 and 3072/256 FIPS approved pairs + blob_struct.hashAlgorithm = BcryptConst.DSA_HASH_ALGORITHM_SHA256 + blob_struct.standardVersion = BcryptConst.DSA_FIPS186_3 + blob_struct.cbSeedLength = q_len + blob_struct.cbGroupSize = q_len + blob_struct.Count = byte_array(count) + + blob = struct_bytes(blob_struct_pointer) + blob += seed + q + p + g + public_bytes + if key_type == 'private': + blob += fill_width(private_bytes, q_len) + + else: + if key_type == 'public': + magic = BcryptConst.BCRYPT_DSA_PUBLIC_MAGIC + else: + magic = BcryptConst.BCRYPT_DSA_PRIVATE_MAGIC + + blob_struct_pointer = struct(bcrypt, 'BCRYPT_DSA_KEY_BLOB') + blob_struct = unwrap(blob_struct_pointer) + blob_struct.dwMagic = magic + blob_struct.cbKey = key_width + blob_struct.Count = byte_array(count) + blob_struct.Seed = byte_array(seed) + blob_struct.q = byte_array(q) + + blob = struct_bytes(blob_struct_pointer) + p + g + public_bytes + if key_type == 'private': + blob += fill_width(private_bytes, q_len) + + elif algo == 'ec': + if key_type == 'public': + blob_type = BcryptConst.BCRYPT_ECCPUBLIC_BLOB + x, y = key_info['public_key'].to_coords() + else: + blob_type = BcryptConst.BCRYPT_ECCPRIVATE_BLOB + public_key = key_info['private_key'].parsed['public_key'] + # We aren't guaranteed to get the public key coords with the + # key info structure, but BCrypt doesn't seem to have an issue + # importing the private key with 0 values, which can only be + # presumed that it is generating the x and y points from the + # private key value and base point + if public_key: + x, y = public_key.to_coords() + else: + x = 0 + y = 0 + private_bytes = int_to_bytes(key_info['private_key'].parsed['private_key'].native) + + blob_struct_pointer = struct(bcrypt, 'BCRYPT_ECCKEY_BLOB') + blob_struct = unwrap(blob_struct_pointer) + + magic = { + ('public', 'secp256r1'): BcryptConst.BCRYPT_ECDSA_PUBLIC_P256_MAGIC, + ('public', 'secp384r1'): BcryptConst.BCRYPT_ECDSA_PUBLIC_P384_MAGIC, + ('public', 'secp521r1'): BcryptConst.BCRYPT_ECDSA_PUBLIC_P521_MAGIC, + ('private', 'secp256r1'): BcryptConst.BCRYPT_ECDSA_PRIVATE_P256_MAGIC, + ('private', 'secp384r1'): BcryptConst.BCRYPT_ECDSA_PRIVATE_P384_MAGIC, + ('private', 'secp521r1'): BcryptConst.BCRYPT_ECDSA_PRIVATE_P521_MAGIC, + }[(key_type, curve_name)] + + key_width = { + 'secp256r1': 32, + 'secp384r1': 48, + 'secp521r1': 66 + }[curve_name] + + x_bytes = int_to_bytes(x) + y_bytes = int_to_bytes(y) + + x_bytes = fill_width(x_bytes, key_width) + y_bytes = fill_width(y_bytes, key_width) + + blob_struct.dwMagic = magic + blob_struct.cbKey = key_width + + blob = struct_bytes(blob_struct_pointer) + x_bytes + y_bytes + if key_type == 'private': + blob += fill_width(private_bytes, key_width) + + key_handle_pointer = new(bcrypt, 'BCRYPT_KEY_HANDLE *') + res = bcrypt.BCryptImportKeyPair( + alg_handle, + null(), + blob_type, + key_handle_pointer, + blob, + len(blob), + BcryptConst.BCRYPT_NO_KEY_VALIDATION + ) + handle_error(res) + + key_handle = unwrap(key_handle_pointer) + return container(key_handle, key_object) + + finally: + if alg_handle: + close_alg_handle(alg_handle) + + +def load_private_key(source, password=None): + """ + Loads a private key into a PrivateKey object + + :param source: + A byte string of file contents, a unicode string filename or an + asn1crypto.keys.PrivateKeyInfo object + + :param password: + A byte or unicode string to decrypt the private key file. Unicode + strings will be encoded using UTF-8. Not used is the source is a + PrivateKeyInfo object. + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + oscrypto.errors.AsymmetricKeyError - when the private key is incompatible with the OS crypto library + OSError - when an error is returned by the OS crypto library + + :return: + A PrivateKey object + """ + + if isinstance(source, PrivateKeyInfo): + private_object = source + + else: + if password is not None: + if isinstance(password, str_cls): + password = password.encode('utf-8') + if not isinstance(password, byte_cls): + raise TypeError(pretty_message( + ''' + password must be a byte string, not %s + ''', + type_name(password) + )) + + if isinstance(source, str_cls): + with open(source, 'rb') as f: + source = f.read() + + elif not isinstance(source, byte_cls): + raise TypeError(pretty_message( + ''' + source must be a byte string, unicode string or + asn1crypto.keys.PrivateKeyInfo object, not %s + ''', + type_name(source) + )) + + private_object = parse_private(source, password) + + return _load_key(private_object, PrivateKey) + + +def load_public_key(source): + """ + Loads a public key into a PublicKey object + + :param source: + A byte string of file contents, a unicode string filename or an + asn1crypto.keys.PublicKeyInfo object + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + oscrypto.errors.AsymmetricKeyError - when the public key is incompatible with the OS crypto library + OSError - when an error is returned by the OS crypto library + + :return: + A PublicKey object + """ + + if isinstance(source, PublicKeyInfo): + public_key = source + + elif isinstance(source, byte_cls): + public_key = parse_public(source) + + elif isinstance(source, str_cls): + with open(source, 'rb') as f: + public_key = parse_public(f.read()) + + else: + raise TypeError(pretty_message( + ''' + source must be a byte string, unicode string or + asn1crypto.keys.PublicKeyInfo object, not %s + ''', + type_name(public_key) + )) + + return _load_key(public_key, PublicKey) + + +def parse_pkcs12(data, password=None): + """ + Parses a PKCS#12 ANS.1 DER-encoded structure and extracts certs and keys + + :param data: + A byte string of a DER-encoded PKCS#12 file + + :param password: + A byte string of the password to any encrypted data + + :raises: + ValueError - when any of the parameters are of the wrong type or value + OSError - when an error is returned by one of the OS decryption functions + + :return: + A three-element tuple of: + 1. An asn1crypto.keys.PrivateKeyInfo object + 2. An asn1crypto.x509.Certificate object + 3. A list of zero or more asn1crypto.x509.Certificate objects that are + "extra" certificates, possibly intermediates from the cert chain + """ + + return _parse_pkcs12(data, password, load_private_key) + + +def load_pkcs12(source, password=None): + """ + Loads a .p12 or .pfx file into a PrivateKey object and one or more + Certificates objects + + :param source: + A byte string of file contents or a unicode string filename + + :param password: + A byte or unicode string to decrypt the PKCS12 file. Unicode strings + will be encoded using UTF-8. + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + oscrypto.errors.AsymmetricKeyError - when a contained key is incompatible with the OS crypto library + OSError - when an error is returned by the OS crypto library + + :return: + A three-element tuple containing (PrivateKey, Certificate, [Certificate, ...]) + """ + + if password is not None: + if isinstance(password, str_cls): + password = password.encode('utf-8') + if not isinstance(password, byte_cls): + raise TypeError(pretty_message( + ''' + password must be a byte string, not %s + ''', + type_name(password) + )) + + if isinstance(source, str_cls): + with open(source, 'rb') as f: + source = f.read() + + elif not isinstance(source, byte_cls): + raise TypeError(pretty_message( + ''' + source must be a byte string or a unicode string, not %s + ''', + type_name(source) + )) + + key_info, cert_info, extra_certs_info = parse_pkcs12(source, password) + + key = None + cert = None + + if key_info: + key = _load_key(key_info, PrivateKey) + + if cert_info: + cert = _load_key(cert_info.public_key, Certificate) + + extra_certs = [_load_key(info.public_key, Certificate) for info in extra_certs_info] + + return (key, cert, extra_certs) + + +def rsa_pkcs1v15_verify(certificate_or_public_key, signature, data, hash_algorithm): + """ + Verifies an RSASSA-PKCS-v1.5 signature. + + When the hash_algorithm is "raw", the operation is identical to RSA + public key decryption. That is: the data is not hashed and no ASN.1 + structure with an algorithm identifier of the hash algorithm is placed in + the encrypted byte string. + + :param certificate_or_public_key: + A Certificate or PublicKey instance to verify the signature with + + :param signature: + A byte string of the signature to verify + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha256", "sha384", "sha512" or "raw" + + :raises: + oscrypto.errors.SignatureError - when the signature is determined to be invalid + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + if certificate_or_public_key.algorithm != 'rsa': + raise ValueError('The key specified is not an RSA public key') + + return _verify(certificate_or_public_key, signature, data, hash_algorithm) + + +def rsa_pss_verify(certificate_or_public_key, signature, data, hash_algorithm): + """ + Verifies an RSASSA-PSS signature. For the PSS padding the mask gen algorithm + will be mgf1 using the same hash algorithm as the signature. The salt length + with be the length of the hash algorithm, and the trailer field with be the + standard 0xBC byte. + + :param certificate_or_public_key: + A Certificate or PublicKey instance to verify the signature with + + :param signature: + A byte string of the signature to verify + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha256", "sha384" or "sha512" + + :raises: + oscrypto.errors.SignatureError - when the signature is determined to be invalid + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + cp_alg = certificate_or_public_key.algorithm + + if cp_alg != 'rsa' and cp_alg != 'rsassa_pss': + raise ValueError('The key specified is not an RSA public key') + + return _verify(certificate_or_public_key, signature, data, hash_algorithm, rsa_pss_padding=True) + + +def dsa_verify(certificate_or_public_key, signature, data, hash_algorithm): + """ + Verifies a DSA signature + + :param certificate_or_public_key: + A Certificate or PublicKey instance to verify the signature with + + :param signature: + A byte string of the signature to verify + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha256", "sha384" or "sha512" + + :raises: + oscrypto.errors.SignatureError - when the signature is determined to be invalid + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + if certificate_or_public_key.algorithm != 'dsa': + raise ValueError('The key specified is not a DSA public key') + + return _verify(certificate_or_public_key, signature, data, hash_algorithm) + + +def ecdsa_verify(certificate_or_public_key, signature, data, hash_algorithm): + """ + Verifies an ECDSA signature + + :param certificate_or_public_key: + A Certificate or PublicKey instance to verify the signature with + + :param signature: + A byte string of the signature to verify + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha256", "sha384" or "sha512" + + :raises: + oscrypto.errors.SignatureError - when the signature is determined to be invalid + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + if certificate_or_public_key.algorithm != 'ec': + raise ValueError('The key specified is not an EC public key') + + return _verify(certificate_or_public_key, signature, data, hash_algorithm) + + +def _verify(certificate_or_public_key, signature, data, hash_algorithm, rsa_pss_padding=False): + """ + Verifies an RSA, DSA or ECDSA signature + + :param certificate_or_public_key: + A Certificate or PublicKey instance to verify the signature with + + :param signature: + A byte string of the signature to verify + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha256", "sha384", "sha512" or "raw" + + :param rsa_pss_padding: + If PSS padding should be used for RSA keys + + :raises: + oscrypto.errors.SignatureError - when the signature is determined to be invalid + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + if not isinstance(certificate_or_public_key, (Certificate, PublicKey)): + raise TypeError(pretty_message( + ''' + certificate_or_public_key must be an instance of the Certificate or + PublicKey class, not %s + ''', + type_name(certificate_or_public_key) + )) + + if not isinstance(signature, byte_cls): + raise TypeError(pretty_message( + ''' + signature must be a byte string, not %s + ''', + type_name(signature) + )) + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + cp_alg = certificate_or_public_key.algorithm + cp_is_rsa = cp_alg == 'rsa' or cp_alg == 'rsassa_pss' + + valid_hash_algorithms = set(['md5', 'sha1', 'sha256', 'sha384', 'sha512']) + if cp_is_rsa and not rsa_pss_padding: + valid_hash_algorithms |= set(['raw']) + + if hash_algorithm not in valid_hash_algorithms: + valid_hash_algorithms_error = '"md5", "sha1", "sha256", "sha384", "sha512"' + if cp_is_rsa and not rsa_pss_padding: + valid_hash_algorithms_error += ', "raw"' + raise ValueError(pretty_message( + ''' + hash_algorithm must be one of %s, not %s + ''', + valid_hash_algorithms_error, + repr(hash_algorithm) + )) + + if not cp_is_rsa and rsa_pss_padding is not False: + raise ValueError(pretty_message( + ''' + PSS padding may only be used with RSA keys - signing via a %s key + was requested + ''', + cp_alg.upper() + )) + + if hash_algorithm == 'raw': + if len(data) > certificate_or_public_key.byte_size - 11: + raise ValueError(pretty_message( + ''' + data must be 11 bytes shorter than the key size when + hash_algorithm is "raw" - key size is %s bytes, but + data is %s bytes long + ''', + certificate_or_public_key.byte_size, + len(data) + )) + + if _backend == 'winlegacy': + if certificate_or_public_key.algorithm == 'ec': + return _pure_python_ecdsa_verify(certificate_or_public_key, signature, data, hash_algorithm) + return _advapi32_verify(certificate_or_public_key, signature, data, hash_algorithm, rsa_pss_padding) + return _bcrypt_verify(certificate_or_public_key, signature, data, hash_algorithm, rsa_pss_padding) + + +def _advapi32_verify(certificate_or_public_key, signature, data, hash_algorithm, rsa_pss_padding=False): + """ + Verifies an RSA, DSA or ECDSA signature via CryptoAPI + + :param certificate_or_public_key: + A Certificate or PublicKey instance to verify the signature with + + :param signature: + A byte string of the signature to verify + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha256", "sha384", "sha512" or "raw" + + :param rsa_pss_padding: + If PSS padding should be used for RSA keys + + :raises: + oscrypto.errors.SignatureError - when the signature is determined to be invalid + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + algo = certificate_or_public_key.algorithm + algo_is_rsa = algo == 'rsa' or algo == 'rsassa_pss' + + if algo_is_rsa and rsa_pss_padding: + hash_length = { + 'sha1': 20, + 'sha224': 28, + 'sha256': 32, + 'sha384': 48, + 'sha512': 64 + }.get(hash_algorithm, 0) + decrypted_signature = raw_rsa_public_crypt(certificate_or_public_key, signature) + key_size = certificate_or_public_key.bit_size + if not verify_pss_padding(hash_algorithm, hash_length, key_size, data, decrypted_signature): + raise SignatureError('Signature is invalid') + return + + if algo_is_rsa and hash_algorithm == 'raw': + padded_plaintext = raw_rsa_public_crypt(certificate_or_public_key, signature) + try: + plaintext = remove_pkcs1v15_signature_padding(certificate_or_public_key.byte_size, padded_plaintext) + if not constant_compare(plaintext, data): + raise ValueError() + except (ValueError): + raise SignatureError('Signature is invalid') + return + + hash_handle = None + + try: + alg_id = { + 'md5': Advapi32Const.CALG_MD5, + 'sha1': Advapi32Const.CALG_SHA1, + 'sha256': Advapi32Const.CALG_SHA_256, + 'sha384': Advapi32Const.CALG_SHA_384, + 'sha512': Advapi32Const.CALG_SHA_512, + }[hash_algorithm] + + hash_handle_pointer = new(advapi32, 'HCRYPTHASH *') + res = advapi32.CryptCreateHash( + certificate_or_public_key.context_handle, + alg_id, + null(), + 0, + hash_handle_pointer + ) + handle_error(res) + + hash_handle = unwrap(hash_handle_pointer) + + res = advapi32.CryptHashData(hash_handle, data, len(data), 0) + handle_error(res) + + if algo == 'dsa': + # Windows doesn't use the ASN.1 Sequence for DSA signatures, + # so we have to convert it here for the verification to work + try: + signature = DSASignature.load(signature).to_p1363() + # Switch the two integers so that the reversal later will + # result in the correct order + half_len = len(signature) // 2 + signature = signature[half_len:] + signature[:half_len] + except (ValueError, OverflowError, TypeError): + raise SignatureError('Signature is invalid') + + # The CryptoAPI expects signatures to be in little endian byte order, + # which is the opposite of other systems, so we must reverse it + reversed_signature = signature[::-1] + + res = advapi32.CryptVerifySignatureW( + hash_handle, + reversed_signature, + len(signature), + certificate_or_public_key.key_handle, + null(), + 0 + ) + handle_error(res) + + finally: + if hash_handle: + advapi32.CryptDestroyHash(hash_handle) + + +def _bcrypt_verify(certificate_or_public_key, signature, data, hash_algorithm, rsa_pss_padding=False): + """ + Verifies an RSA, DSA or ECDSA signature via CNG + + :param certificate_or_public_key: + A Certificate or PublicKey instance to verify the signature with + + :param signature: + A byte string of the signature to verify + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha256", "sha384", "sha512" or "raw" + + :param rsa_pss_padding: + If PSS padding should be used for RSA keys + + :raises: + oscrypto.errors.SignatureError - when the signature is determined to be invalid + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + if hash_algorithm == 'raw': + digest = data + else: + hash_constant = { + 'md5': BcryptConst.BCRYPT_MD5_ALGORITHM, + 'sha1': BcryptConst.BCRYPT_SHA1_ALGORITHM, + 'sha256': BcryptConst.BCRYPT_SHA256_ALGORITHM, + 'sha384': BcryptConst.BCRYPT_SHA384_ALGORITHM, + 'sha512': BcryptConst.BCRYPT_SHA512_ALGORITHM + }[hash_algorithm] + digest = getattr(hashlib, hash_algorithm)(data).digest() + + padding_info = null() + flags = 0 + + cp_alg = certificate_or_public_key.algorithm + cp_is_rsa = cp_alg == 'rsa' or cp_alg == 'rsassa_pss' + + if cp_is_rsa: + if rsa_pss_padding: + flags = BcryptConst.BCRYPT_PAD_PSS + padding_info_struct_pointer = struct(bcrypt, 'BCRYPT_PSS_PADDING_INFO') + padding_info_struct = unwrap(padding_info_struct_pointer) + # This has to be assigned to a variable to prevent cffi from gc'ing it + hash_buffer = buffer_from_unicode(hash_constant) + padding_info_struct.pszAlgId = cast(bcrypt, 'wchar_t *', hash_buffer) + padding_info_struct.cbSalt = len(digest) + else: + flags = BcryptConst.BCRYPT_PAD_PKCS1 + padding_info_struct_pointer = struct(bcrypt, 'BCRYPT_PKCS1_PADDING_INFO') + padding_info_struct = unwrap(padding_info_struct_pointer) + # This has to be assigned to a variable to prevent cffi from gc'ing it + if hash_algorithm == 'raw': + padding_info_struct.pszAlgId = null() + else: + hash_buffer = buffer_from_unicode(hash_constant) + padding_info_struct.pszAlgId = cast(bcrypt, 'wchar_t *', hash_buffer) + padding_info = cast(bcrypt, 'void *', padding_info_struct_pointer) + else: + # Windows doesn't use the ASN.1 Sequence for DSA/ECDSA signatures, + # so we have to convert it here for the verification to work + try: + signature = DSASignature.load(signature).to_p1363() + except (ValueError, OverflowError, TypeError): + raise SignatureError('Signature is invalid') + + res = bcrypt.BCryptVerifySignature( + certificate_or_public_key.key_handle, + padding_info, + digest, + len(digest), + signature, + len(signature), + flags + ) + failure = res == BcryptConst.STATUS_INVALID_SIGNATURE + failure = failure or res == BcryptConst.STATUS_INVALID_PARAMETER + if failure: + raise SignatureError('Signature is invalid') + + handle_error(res) + + +def rsa_pkcs1v15_sign(private_key, data, hash_algorithm): + """ + Generates an RSASSA-PKCS-v1.5 signature. + + When the hash_algorithm is "raw", the operation is identical to RSA + private key encryption. That is: the data is not hashed and no ASN.1 + structure with an algorithm identifier of the hash algorithm is placed in + the encrypted byte string. + + :param private_key: + The PrivateKey to generate the signature with + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha256", "sha384", "sha512" or "raw" + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the signature + """ + + if private_key.algorithm != 'rsa': + raise ValueError('The key specified is not an RSA private key') + + return _sign(private_key, data, hash_algorithm) + + +def rsa_pss_sign(private_key, data, hash_algorithm): + """ + Generates an RSASSA-PSS signature. For the PSS padding the mask gen + algorithm will be mgf1 using the same hash algorithm as the signature. The + salt length with be the length of the hash algorithm, and the trailer field + with be the standard 0xBC byte. + + :param private_key: + The PrivateKey to generate the signature with + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha256", "sha384" or "sha512" + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the signature + """ + + pkey_alg = private_key.algorithm + + if pkey_alg != 'rsa' and pkey_alg != 'rsassa_pss': + raise ValueError('The key specified is not an RSA private key') + + return _sign(private_key, data, hash_algorithm, rsa_pss_padding=True) + + +def dsa_sign(private_key, data, hash_algorithm): + """ + Generates a DSA signature + + :param private_key: + The PrivateKey to generate the signature with + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha256", "sha384" or "sha512" + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the signature + """ + + if private_key.algorithm != 'dsa': + raise ValueError('The key specified is not a DSA private key') + + return _sign(private_key, data, hash_algorithm) + + +def ecdsa_sign(private_key, data, hash_algorithm): + """ + Generates an ECDSA signature + + :param private_key: + The PrivateKey to generate the signature with + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha256", "sha384" or "sha512" + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the signature + """ + + if private_key.algorithm != 'ec': + raise ValueError('The key specified is not an EC private key') + + return _sign(private_key, data, hash_algorithm) + + +def _sign(private_key, data, hash_algorithm, rsa_pss_padding=False): + """ + Generates an RSA, DSA or ECDSA signature + + :param private_key: + The PrivateKey to generate the signature with + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha256", "sha384", "sha512" or "raw" + + :param rsa_pss_padding: + If PSS padding should be used for RSA keys + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the signature + """ + + if not isinstance(private_key, PrivateKey): + raise TypeError(pretty_message( + ''' + private_key must be an instance of PrivateKey, not %s + ''', + type_name(private_key) + )) + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + pkey_alg = private_key.algorithm + pkey_is_rsa = pkey_alg == 'rsa' or pkey_alg == 'rsassa_pss' + + valid_hash_algorithms = set(['md5', 'sha1', 'sha256', 'sha384', 'sha512']) + if private_key.algorithm == 'rsa' and not rsa_pss_padding: + valid_hash_algorithms |= set(['raw']) + + if hash_algorithm not in valid_hash_algorithms: + valid_hash_algorithms_error = '"md5", "sha1", "sha256", "sha384", "sha512"' + if pkey_is_rsa and not rsa_pss_padding: + valid_hash_algorithms_error += ', "raw"' + raise ValueError(pretty_message( + ''' + hash_algorithm must be one of %s, not %s + ''', + valid_hash_algorithms_error, + repr(hash_algorithm) + )) + + if not pkey_is_rsa and rsa_pss_padding is not False: + raise ValueError(pretty_message( + ''' + PSS padding may only be used with RSA keys - signing via a %s key + was requested + ''', + pkey_alg.upper() + )) + + if hash_algorithm == 'raw': + if len(data) > private_key.byte_size - 11: + raise ValueError(pretty_message( + ''' + data must be 11 bytes shorter than the key size when + hash_algorithm is "raw" - key size is %s bytes, but data + is %s bytes long + ''', + private_key.byte_size, + len(data) + )) + + if _backend == 'winlegacy': + if private_key.algorithm == 'ec': + return _pure_python_ecdsa_sign(private_key, data, hash_algorithm) + return _advapi32_sign(private_key, data, hash_algorithm, rsa_pss_padding) + return _bcrypt_sign(private_key, data, hash_algorithm, rsa_pss_padding) + + +def _advapi32_sign(private_key, data, hash_algorithm, rsa_pss_padding=False): + """ + Generates an RSA, DSA or ECDSA signature via CryptoAPI + + :param private_key: + The PrivateKey to generate the signature with + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha256", "sha384", "sha512" or "raw" + + :param rsa_pss_padding: + If PSS padding should be used for RSA keys + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the signature + """ + + algo = private_key.algorithm + algo_is_rsa = algo == 'rsa' or algo == 'rsassa_pss' + + if algo_is_rsa and hash_algorithm == 'raw': + padded_data = add_pkcs1v15_signature_padding(private_key.byte_size, data) + return raw_rsa_private_crypt(private_key, padded_data) + + if algo_is_rsa and rsa_pss_padding: + hash_length = { + 'sha1': 20, + 'sha224': 28, + 'sha256': 32, + 'sha384': 48, + 'sha512': 64 + }.get(hash_algorithm, 0) + padded_data = add_pss_padding(hash_algorithm, hash_length, private_key.bit_size, data) + return raw_rsa_private_crypt(private_key, padded_data) + + if private_key.algorithm == 'dsa' and hash_algorithm == 'md5': + raise ValueError(pretty_message( + ''' + Windows does not support md5 signatures with DSA keys + ''' + )) + + hash_handle = None + + try: + alg_id = { + 'md5': Advapi32Const.CALG_MD5, + 'sha1': Advapi32Const.CALG_SHA1, + 'sha256': Advapi32Const.CALG_SHA_256, + 'sha384': Advapi32Const.CALG_SHA_384, + 'sha512': Advapi32Const.CALG_SHA_512, + }[hash_algorithm] + + hash_handle_pointer = new(advapi32, 'HCRYPTHASH *') + res = advapi32.CryptCreateHash( + private_key.context_handle, + alg_id, + null(), + 0, + hash_handle_pointer + ) + handle_error(res) + + hash_handle = unwrap(hash_handle_pointer) + + res = advapi32.CryptHashData(hash_handle, data, len(data), 0) + handle_error(res) + + out_len = new(advapi32, 'DWORD *') + res = advapi32.CryptSignHashW( + hash_handle, + Advapi32Const.AT_SIGNATURE, + null(), + 0, + null(), + out_len + ) + handle_error(res) + + buffer_length = deref(out_len) + buffer_ = buffer_from_bytes(buffer_length) + + res = advapi32.CryptSignHashW( + hash_handle, + Advapi32Const.AT_SIGNATURE, + null(), + 0, + buffer_, + out_len + ) + handle_error(res) + + output = bytes_from_buffer(buffer_, deref(out_len)) + + # CryptoAPI outputs the signature in little endian byte order, so we + # must swap it for compatibility with other systems + output = output[::-1] + + if algo == 'dsa': + # Switch the two integers because the reversal just before switched + # then + half_len = len(output) // 2 + output = output[half_len:] + output[:half_len] + # Windows doesn't use the ASN.1 Sequence for DSA signatures, + # so we have to convert it here for the verification to work + output = DSASignature.from_p1363(output).dump() + + return output + + finally: + if hash_handle: + advapi32.CryptDestroyHash(hash_handle) + + +def _bcrypt_sign(private_key, data, hash_algorithm, rsa_pss_padding=False): + """ + Generates an RSA, DSA or ECDSA signature via CNG + + :param private_key: + The PrivateKey to generate the signature with + + :param data: + A byte string of the data the signature is for + + :param hash_algorithm: + A unicode string of "md5", "sha1", "sha256", "sha384", "sha512" or "raw" + + :param rsa_pss_padding: + If PSS padding should be used for RSA keys + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the signature + """ + + if hash_algorithm == 'raw': + digest = data + else: + hash_constant = { + 'md5': BcryptConst.BCRYPT_MD5_ALGORITHM, + 'sha1': BcryptConst.BCRYPT_SHA1_ALGORITHM, + 'sha256': BcryptConst.BCRYPT_SHA256_ALGORITHM, + 'sha384': BcryptConst.BCRYPT_SHA384_ALGORITHM, + 'sha512': BcryptConst.BCRYPT_SHA512_ALGORITHM + }[hash_algorithm] + + digest = getattr(hashlib, hash_algorithm)(data).digest() + + padding_info = null() + flags = 0 + + pkey_alg = private_key.algorithm + pkey_is_rsa = pkey_alg == 'rsa' or pkey_alg == 'rsassa_pss' + + if pkey_is_rsa: + if rsa_pss_padding: + hash_length = { + 'md5': 16, + 'sha1': 20, + 'sha256': 32, + 'sha384': 48, + 'sha512': 64 + }[hash_algorithm] + + flags = BcryptConst.BCRYPT_PAD_PSS + padding_info_struct_pointer = struct(bcrypt, 'BCRYPT_PSS_PADDING_INFO') + padding_info_struct = unwrap(padding_info_struct_pointer) + # This has to be assigned to a variable to prevent cffi from gc'ing it + hash_buffer = buffer_from_unicode(hash_constant) + padding_info_struct.pszAlgId = cast(bcrypt, 'wchar_t *', hash_buffer) + padding_info_struct.cbSalt = hash_length + else: + flags = BcryptConst.BCRYPT_PAD_PKCS1 + padding_info_struct_pointer = struct(bcrypt, 'BCRYPT_PKCS1_PADDING_INFO') + padding_info_struct = unwrap(padding_info_struct_pointer) + # This has to be assigned to a variable to prevent cffi from gc'ing it + if hash_algorithm == 'raw': + padding_info_struct.pszAlgId = null() + else: + hash_buffer = buffer_from_unicode(hash_constant) + padding_info_struct.pszAlgId = cast(bcrypt, 'wchar_t *', hash_buffer) + padding_info = cast(bcrypt, 'void *', padding_info_struct_pointer) + + if pkey_alg == 'dsa' and private_key.bit_size > 1024 and hash_algorithm in set(['md5', 'sha1']): + raise ValueError(pretty_message( + ''' + Windows does not support sha1 signatures with DSA keys based on + sha224, sha256 or sha512 + ''' + )) + + out_len = new(bcrypt, 'DWORD *') + res = bcrypt.BCryptSignHash( + private_key.key_handle, + padding_info, + digest, + len(digest), + null(), + 0, + out_len, + flags + ) + handle_error(res) + + buffer_len = deref(out_len) + buffer = buffer_from_bytes(buffer_len) + + if pkey_is_rsa: + padding_info = cast(bcrypt, 'void *', padding_info_struct_pointer) + + res = bcrypt.BCryptSignHash( + private_key.key_handle, + padding_info, + digest, + len(digest), + buffer, + buffer_len, + out_len, + flags + ) + handle_error(res) + signature = bytes_from_buffer(buffer, deref(out_len)) + + if not pkey_is_rsa: + # Windows doesn't use the ASN.1 Sequence for DSA/ECDSA signatures, + # so we have to convert it here for the verification to work + signature = DSASignature.from_p1363(signature).dump() + + return signature + + +def _encrypt(certificate_or_public_key, data, rsa_oaep_padding=False): + """ + Encrypts a value using an RSA public key + + :param certificate_or_public_key: + A Certificate or PublicKey instance to encrypt with + + :param data: + A byte string of the data to encrypt + + :param rsa_oaep_padding: + If OAEP padding should be used instead of PKCS#1 v1.5 + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the ciphertext + """ + + if not isinstance(certificate_or_public_key, (Certificate, PublicKey)): + raise TypeError(pretty_message( + ''' + certificate_or_public_key must be an instance of the Certificate or + PublicKey class, not %s + ''', + type_name(certificate_or_public_key) + )) + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + if not isinstance(rsa_oaep_padding, bool): + raise TypeError(pretty_message( + ''' + rsa_oaep_padding must be a bool, not %s + ''', + type_name(rsa_oaep_padding) + )) + + if _backend == 'winlegacy': + return _advapi32_encrypt(certificate_or_public_key, data, rsa_oaep_padding) + return _bcrypt_encrypt(certificate_or_public_key, data, rsa_oaep_padding) + + +def _advapi32_encrypt(certificate_or_public_key, data, rsa_oaep_padding=False): + """ + Encrypts a value using an RSA public key via CryptoAPI + + :param certificate_or_public_key: + A Certificate or PublicKey instance to encrypt with + + :param data: + A byte string of the data to encrypt + + :param rsa_oaep_padding: + If OAEP padding should be used instead of PKCS#1 v1.5 + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the ciphertext + """ + + flags = 0 + if rsa_oaep_padding: + flags = Advapi32Const.CRYPT_OAEP + + out_len = new(advapi32, 'DWORD *', len(data)) + res = advapi32.CryptEncrypt( + certificate_or_public_key.ex_key_handle, + null(), + True, + flags, + null(), + out_len, + 0 + ) + handle_error(res) + + buffer_len = deref(out_len) + buffer = buffer_from_bytes(buffer_len) + write_to_buffer(buffer, data) + + pointer_set(out_len, len(data)) + res = advapi32.CryptEncrypt( + certificate_or_public_key.ex_key_handle, + null(), + True, + flags, + buffer, + out_len, + buffer_len + ) + handle_error(res) + + return bytes_from_buffer(buffer, deref(out_len))[::-1] + + +def _bcrypt_encrypt(certificate_or_public_key, data, rsa_oaep_padding=False): + """ + Encrypts a value using an RSA public key via CNG + + :param certificate_or_public_key: + A Certificate or PublicKey instance to encrypt with + + :param data: + A byte string of the data to encrypt + + :param rsa_oaep_padding: + If OAEP padding should be used instead of PKCS#1 v1.5 + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the ciphertext + """ + + flags = BcryptConst.BCRYPT_PAD_PKCS1 + if rsa_oaep_padding is True: + flags = BcryptConst.BCRYPT_PAD_OAEP + + padding_info_struct_pointer = struct(bcrypt, 'BCRYPT_OAEP_PADDING_INFO') + padding_info_struct = unwrap(padding_info_struct_pointer) + # This has to be assigned to a variable to prevent cffi from gc'ing it + hash_buffer = buffer_from_unicode(BcryptConst.BCRYPT_SHA1_ALGORITHM) + padding_info_struct.pszAlgId = cast(bcrypt, 'wchar_t *', hash_buffer) + padding_info_struct.pbLabel = null() + padding_info_struct.cbLabel = 0 + padding_info = cast(bcrypt, 'void *', padding_info_struct_pointer) + else: + padding_info = null() + + out_len = new(bcrypt, 'ULONG *') + res = bcrypt.BCryptEncrypt( + certificate_or_public_key.key_handle, + data, + len(data), + padding_info, + null(), + 0, + null(), + 0, + out_len, + flags + ) + handle_error(res) + + buffer_len = deref(out_len) + buffer = buffer_from_bytes(buffer_len) + + res = bcrypt.BCryptEncrypt( + certificate_or_public_key.key_handle, + data, + len(data), + padding_info, + null(), + 0, + buffer, + buffer_len, + out_len, + flags + ) + handle_error(res) + + return bytes_from_buffer(buffer, deref(out_len)) + + +def _decrypt(private_key, ciphertext, rsa_oaep_padding=False): + """ + Encrypts a value using an RSA private key + + :param private_key: + A PrivateKey instance to decrypt with + + :param ciphertext: + A byte string of the data to decrypt + + :param rsa_oaep_padding: + If OAEP padding should be used instead of PKCS#1 v1.5 + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the plaintext + """ + + if not isinstance(private_key, PrivateKey): + raise TypeError(pretty_message( + ''' + private_key must be an instance of the PrivateKey class, not %s + ''', + type_name(private_key) + )) + + if not isinstance(ciphertext, byte_cls): + raise TypeError(pretty_message( + ''' + ciphertext must be a byte string, not %s + ''', + type_name(ciphertext) + )) + + if not isinstance(rsa_oaep_padding, bool): + raise TypeError(pretty_message( + ''' + rsa_oaep_padding must be a bool, not %s + ''', + type_name(rsa_oaep_padding) + )) + + if _backend == 'winlegacy': + return _advapi32_decrypt(private_key, ciphertext, rsa_oaep_padding) + return _bcrypt_decrypt(private_key, ciphertext, rsa_oaep_padding) + + +def _advapi32_decrypt(private_key, ciphertext, rsa_oaep_padding=False): + """ + Encrypts a value using an RSA private key via CryptoAPI + + :param private_key: + A PrivateKey instance to decrypt with + + :param ciphertext: + A byte string of the data to decrypt + + :param rsa_oaep_padding: + If OAEP padding should be used instead of PKCS#1 v1.5 + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the plaintext + """ + + flags = 0 + if rsa_oaep_padding: + flags = Advapi32Const.CRYPT_OAEP + + ciphertext = ciphertext[::-1] + + buffer = buffer_from_bytes(ciphertext) + out_len = new(advapi32, 'DWORD *', len(ciphertext)) + res = advapi32.CryptDecrypt( + private_key.ex_key_handle, + null(), + True, + flags, + buffer, + out_len + ) + handle_error(res) + + return bytes_from_buffer(buffer, deref(out_len)) + + +def _bcrypt_decrypt(private_key, ciphertext, rsa_oaep_padding=False): + """ + Encrypts a value using an RSA private key via CNG + + :param private_key: + A PrivateKey instance to decrypt with + + :param ciphertext: + A byte string of the data to decrypt + + :param rsa_oaep_padding: + If OAEP padding should be used instead of PKCS#1 v1.5 + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the plaintext + """ + + flags = BcryptConst.BCRYPT_PAD_PKCS1 + if rsa_oaep_padding is True: + flags = BcryptConst.BCRYPT_PAD_OAEP + + padding_info_struct_pointer = struct(bcrypt, 'BCRYPT_OAEP_PADDING_INFO') + padding_info_struct = unwrap(padding_info_struct_pointer) + # This has to be assigned to a variable to prevent cffi from gc'ing it + hash_buffer = buffer_from_unicode(BcryptConst.BCRYPT_SHA1_ALGORITHM) + padding_info_struct.pszAlgId = cast(bcrypt, 'wchar_t *', hash_buffer) + padding_info_struct.pbLabel = null() + padding_info_struct.cbLabel = 0 + padding_info = cast(bcrypt, 'void *', padding_info_struct_pointer) + else: + padding_info = null() + + out_len = new(bcrypt, 'ULONG *') + res = bcrypt.BCryptDecrypt( + private_key.key_handle, + ciphertext, + len(ciphertext), + padding_info, + null(), + 0, + null(), + 0, + out_len, + flags + ) + handle_error(res) + + buffer_len = deref(out_len) + buffer = buffer_from_bytes(buffer_len) + + res = bcrypt.BCryptDecrypt( + private_key.key_handle, + ciphertext, + len(ciphertext), + padding_info, + null(), + 0, + buffer, + buffer_len, + out_len, + flags + ) + handle_error(res) + + return bytes_from_buffer(buffer, deref(out_len)) + + +def rsa_pkcs1v15_encrypt(certificate_or_public_key, data): + """ + Encrypts a byte string using an RSA public key or certificate. Uses PKCS#1 + v1.5 padding. + + :param certificate_or_public_key: + A PublicKey or Certificate object + + :param data: + A byte string, with a maximum length 11 bytes less than the key length + (in bytes) + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the encrypted data + """ + + return _encrypt(certificate_or_public_key, data) + + +def rsa_pkcs1v15_decrypt(private_key, ciphertext): + """ + Decrypts a byte string using an RSA private key. Uses PKCS#1 v1.5 padding. + + :param private_key: + A PrivateKey object + + :param ciphertext: + A byte string of the encrypted data + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the original plaintext + """ + + return _decrypt(private_key, ciphertext) + + +def rsa_oaep_encrypt(certificate_or_public_key, data): + """ + Encrypts a byte string using an RSA public key or certificate. Uses PKCS#1 + OAEP padding with SHA1. + + :param certificate_or_public_key: + A PublicKey or Certificate object + + :param data: + A byte string, with a maximum length 41 bytes (or more) less than the + key length (in bytes) + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the encrypted data + """ + + return _encrypt(certificate_or_public_key, data, rsa_oaep_padding=True) + + +def rsa_oaep_decrypt(private_key, ciphertext): + """ + Decrypts a byte string using an RSA private key. Uses PKCS#1 OAEP padding + with SHA1. + + :param private_key: + A PrivateKey object + + :param ciphertext: + A byte string of the encrypted data + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the original plaintext + """ + + return _decrypt(private_key, ciphertext, rsa_oaep_padding=True) diff --git a/tasks/lib/package_control/deps/oscrypto/_win/symmetric.py b/tasks/lib/package_control/deps/oscrypto/_win/symmetric.py new file mode 100644 index 0000000..ff23109 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_win/symmetric.py @@ -0,0 +1,1166 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from .._errors import pretty_message +from .._ffi import ( + buffer_from_bytes, + bytes_from_buffer, + deref, + new, + null, + pointer_set, + struct, + struct_bytes, + unwrap, + write_to_buffer, +) +from .util import rand_bytes +from .. import backend +from .._types import type_name, byte_cls + +_backend = backend() + +if _backend == 'winlegacy': + from ._advapi32 import advapi32, Advapi32Const, handle_error, open_context_handle, close_context_handle +else: + from ._cng import bcrypt, BcryptConst, handle_error, open_alg_handle, close_alg_handle + + +__all__ = [ + 'aes_cbc_no_padding_decrypt', + 'aes_cbc_no_padding_encrypt', + 'aes_cbc_pkcs7_decrypt', + 'aes_cbc_pkcs7_encrypt', + 'des_cbc_pkcs5_decrypt', + 'des_cbc_pkcs5_encrypt', + 'rc2_cbc_pkcs5_decrypt', + 'rc2_cbc_pkcs5_encrypt', + 'rc4_decrypt', + 'rc4_encrypt', + 'tripledes_cbc_pkcs5_decrypt', + 'tripledes_cbc_pkcs5_encrypt', +] + + +def aes_cbc_no_padding_encrypt(key, data, iv): + """ + Encrypts plaintext using AES in CBC mode with a 128, 192 or 256 bit key and + no padding. This means the ciphertext must be an exact multiple of 16 bytes + long. + + :param key: + The encryption key - a byte string either 16, 24 or 32 bytes long + + :param data: + The plaintext - a byte string + + :param iv: + The initialization vector - either a byte string 16-bytes long or None + to generate an IV + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A tuple of two byte strings (iv, ciphertext) + """ + + if len(key) not in [16, 24, 32]: + raise ValueError(pretty_message( + ''' + key must be either 16, 24 or 32 bytes (128, 192 or 256 bits) + long - is %s + ''', + len(key) + )) + + if not iv: + iv = rand_bytes(16) + elif len(iv) != 16: + raise ValueError(pretty_message( + ''' + iv must be 16 bytes long - is %s + ''', + len(iv) + )) + + if len(data) % 16 != 0: + raise ValueError(pretty_message( + ''' + data must be a multiple of 16 bytes long - is %s + ''', + len(data) + )) + + return (iv, _encrypt('aes', key, data, iv, False)) + + +def aes_cbc_no_padding_decrypt(key, data, iv): + """ + Decrypts AES ciphertext in CBC mode using a 128, 192 or 256 bit key and no + padding. + + :param key: + The encryption key - a byte string either 16, 24 or 32 bytes long + + :param data: + The ciphertext - a byte string + + :param iv: + The initialization vector - a byte string 16-bytes long + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the plaintext + """ + + if len(key) not in [16, 24, 32]: + raise ValueError(pretty_message( + ''' + key must be either 16, 24 or 32 bytes (128, 192 or 256 bits) + long - is %s + ''', + len(key) + )) + + if len(iv) != 16: + raise ValueError(pretty_message( + ''' + iv must be 16 bytes long - is %s + ''', + len(iv) + )) + + return _decrypt('aes', key, data, iv, False) + + +def aes_cbc_pkcs7_encrypt(key, data, iv): + """ + Encrypts plaintext using AES in CBC mode with a 128, 192 or 256 bit key and + PKCS#7 padding. + + :param key: + The encryption key - a byte string either 16, 24 or 32 bytes long + + :param data: + The plaintext - a byte string + + :param iv: + The initialization vector - either a byte string 16-bytes long or None + to generate an IV + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A tuple of two byte strings (iv, ciphertext) + """ + + if len(key) not in [16, 24, 32]: + raise ValueError(pretty_message( + ''' + key must be either 16, 24 or 32 bytes (128, 192 or 256 bits) + long - is %s + ''', + len(key) + )) + + if not iv: + iv = rand_bytes(16) + elif len(iv) != 16: + raise ValueError(pretty_message( + ''' + iv must be 16 bytes long - is %s + ''', + len(iv) + )) + + return (iv, _encrypt('aes', key, data, iv, True)) + + +def aes_cbc_pkcs7_decrypt(key, data, iv): + """ + Decrypts AES ciphertext in CBC mode using a 128, 192 or 256 bit key + + :param key: + The encryption key - a byte string either 16, 24 or 32 bytes long + + :param data: + The ciphertext - a byte string + + :param iv: + The initialization vector - a byte string 16-bytes long + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the plaintext + """ + + if len(key) not in [16, 24, 32]: + raise ValueError(pretty_message( + ''' + key must be either 16, 24 or 32 bytes (128, 192 or 256 bits) + long - is %s + ''', + len(key) + )) + + if len(iv) != 16: + raise ValueError(pretty_message( + ''' + iv must be 16 bytes long - is %s + ''', + len(iv) + )) + + return _decrypt('aes', key, data, iv, True) + + +def rc4_encrypt(key, data): + """ + Encrypts plaintext using RC4 with a 40-128 bit key + + :param key: + The encryption key - a byte string 5-16 bytes long + + :param data: + The plaintext - a byte string + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the ciphertext + """ + + if len(key) < 5 or len(key) > 16: + raise ValueError(pretty_message( + ''' + key must be 5 to 16 bytes (40 to 128 bits) long - is %s + ''', + len(key) + )) + + return _encrypt('rc4', key, data, None, None) + + +def rc4_decrypt(key, data): + """ + Decrypts RC4 ciphertext using a 40-128 bit key + + :param key: + The encryption key - a byte string 5-16 bytes long + + :param data: + The ciphertext - a byte string + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the plaintext + """ + + if len(key) < 5 or len(key) > 16: + raise ValueError(pretty_message( + ''' + key must be 5 to 16 bytes (40 to 128 bits) long - is %s + ''', + len(key) + )) + + return _decrypt('rc4', key, data, None, None) + + +def rc2_cbc_pkcs5_encrypt(key, data, iv): + """ + Encrypts plaintext using RC2 with a 64 bit key + + :param key: + The encryption key - a byte string 8 bytes long + + :param data: + The plaintext - a byte string + + :param iv: + The 8-byte initialization vector to use - a byte string - set as None + to generate an appropriate one + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A tuple of two byte strings (iv, ciphertext) + """ + + if len(key) < 5 or len(key) > 16: + raise ValueError(pretty_message( + ''' + key must be 5 to 16 bytes (40 to 128 bits) long - is %s + ''', + len(key) + )) + + if not iv: + iv = rand_bytes(8) + elif len(iv) != 8: + raise ValueError(pretty_message( + ''' + iv must be 8 bytes long - is %s + ''', + len(iv) + )) + + return (iv, _encrypt('rc2', key, data, iv, True)) + + +def rc2_cbc_pkcs5_decrypt(key, data, iv): + """ + Decrypts RC2 ciphertext using a 64 bit key + + :param key: + The encryption key - a byte string 8 bytes long + + :param data: + The ciphertext - a byte string + + :param iv: + The initialization vector used for encryption - a byte string + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the plaintext + """ + + if len(key) < 5 or len(key) > 16: + raise ValueError(pretty_message( + ''' + key must be 5 to 16 bytes (40 to 128 bits) long - is %s + ''', + len(key) + )) + + if len(iv) != 8: + raise ValueError(pretty_message( + ''' + iv must be 8 bytes long - is %s + ''', + len(iv) + )) + + return _decrypt('rc2', key, data, iv, True) + + +def tripledes_cbc_pkcs5_encrypt(key, data, iv): + """ + Encrypts plaintext using 3DES in either 2 or 3 key mode + + :param key: + The encryption key - a byte string 16 or 24 bytes long (2 or 3 key mode) + + :param data: + The plaintext - a byte string + + :param iv: + The 8-byte initialization vector to use - a byte string - set as None + to generate an appropriate one + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A tuple of two byte strings (iv, ciphertext) + """ + + if len(key) != 16 and len(key) != 24: + raise ValueError(pretty_message( + ''' + key must be 16 bytes (2 key) or 24 bytes (3 key) long - is %s + ''', + len(key) + )) + + if not iv: + iv = rand_bytes(8) + elif len(iv) != 8: + raise ValueError(pretty_message( + ''' + iv must be 8 bytes long - is %s + ''', + len(iv) + )) + + cipher = 'tripledes_3key' + if len(key) == 16: + cipher = 'tripledes_2key' + + return (iv, _encrypt(cipher, key, data, iv, True)) + + +def tripledes_cbc_pkcs5_decrypt(key, data, iv): + """ + Decrypts 3DES ciphertext in either 2 or 3 key mode + + :param key: + The encryption key - a byte string 16 or 24 bytes long (2 or 3 key mode) + + :param data: + The ciphertext - a byte string + + :param iv: + The initialization vector used for encryption - a byte string + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the plaintext + """ + + if len(key) != 16 and len(key) != 24: + raise ValueError(pretty_message( + ''' + key must be 16 bytes (2 key) or 24 bytes (3 key) long - is %s + ''', + len(key) + )) + + if len(iv) != 8: + raise ValueError(pretty_message( + ''' + iv must be 8 bytes long - is %s + ''', + len(iv) + )) + + cipher = 'tripledes_3key' + if len(key) == 16: + cipher = 'tripledes_2key' + + return _decrypt(cipher, key, data, iv, True) + + +def des_cbc_pkcs5_encrypt(key, data, iv): + """ + Encrypts plaintext using DES with a 56 bit key + + :param key: + The encryption key - a byte string 8 bytes long (includes error + correction bits) + + :param data: + The plaintext - a byte string + + :param iv: + The 8-byte initialization vector to use - a byte string - set as None + to generate an appropriate one + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A tuple of two byte strings (iv, ciphertext) + """ + + if len(key) != 8: + raise ValueError(pretty_message( + ''' + key must be 8 bytes (56 bits + 8 parity bits) long - is %s + ''', + len(key) + )) + + if not iv: + iv = rand_bytes(8) + elif len(iv) != 8: + raise ValueError(pretty_message( + ''' + iv must be 8 bytes long - is %s + ''', + len(iv) + )) + + return (iv, _encrypt('des', key, data, iv, True)) + + +def des_cbc_pkcs5_decrypt(key, data, iv): + """ + Decrypts DES ciphertext using a 56 bit key + + :param key: + The encryption key - a byte string 8 bytes long (includes error + correction bits) + + :param data: + The ciphertext - a byte string + + :param iv: + The initialization vector used for encryption - a byte string + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the plaintext + """ + + if len(key) != 8: + raise ValueError(pretty_message( + ''' + key must be 8 bytes (56 bits + 8 parity bits) long - is %s + ''', + len(key) + )) + + if len(iv) != 8: + raise ValueError(pretty_message( + ''' + iv must be 8 bytes long - is %s + ''', + len(iv) + )) + + return _decrypt('des', key, data, iv, True) + + +def _advapi32_create_handles(cipher, key, iv): + """ + Creates an HCRYPTPROV and HCRYPTKEY for symmetric encryption/decryption. The + HCRYPTPROV must be released by close_context_handle() and the + HCRYPTKEY must be released by advapi32.CryptDestroyKey() when done. + + :param cipher: + A unicode string of "aes", "des", "tripledes_2key", "tripledes_3key", + "rc2", "rc4" + + :param key: + A byte string of the symmetric key + + :param iv: + The initialization vector - a byte string - unused for RC4 + + :return: + A tuple of (HCRYPTPROV, HCRYPTKEY) + """ + + context_handle = None + + if cipher == 'aes': + algorithm_id = { + 16: Advapi32Const.CALG_AES_128, + 24: Advapi32Const.CALG_AES_192, + 32: Advapi32Const.CALG_AES_256, + }[len(key)] + else: + algorithm_id = { + 'des': Advapi32Const.CALG_DES, + 'tripledes_2key': Advapi32Const.CALG_3DES_112, + 'tripledes_3key': Advapi32Const.CALG_3DES, + 'rc2': Advapi32Const.CALG_RC2, + 'rc4': Advapi32Const.CALG_RC4, + }[cipher] + + provider = Advapi32Const.MS_ENH_RSA_AES_PROV + context_handle = open_context_handle(provider, verify_only=False) + + blob_header_pointer = struct(advapi32, 'BLOBHEADER') + blob_header = unwrap(blob_header_pointer) + blob_header.bType = Advapi32Const.PLAINTEXTKEYBLOB + blob_header.bVersion = Advapi32Const.CUR_BLOB_VERSION + blob_header.reserved = 0 + blob_header.aiKeyAlg = algorithm_id + + blob_struct_pointer = struct(advapi32, 'PLAINTEXTKEYBLOB') + blob_struct = unwrap(blob_struct_pointer) + blob_struct.hdr = blob_header + blob_struct.dwKeySize = len(key) + + blob = struct_bytes(blob_struct_pointer) + key + + flags = 0 + if cipher in set(['rc2', 'rc4']) and len(key) == 5: + flags = Advapi32Const.CRYPT_NO_SALT + + key_handle_pointer = new(advapi32, 'HCRYPTKEY *') + res = advapi32.CryptImportKey( + context_handle, + blob, + len(blob), + null(), + flags, + key_handle_pointer + ) + handle_error(res) + + key_handle = unwrap(key_handle_pointer) + + if cipher == 'rc2': + buf = new(advapi32, 'DWORD *', len(key) * 8) + res = advapi32.CryptSetKeyParam( + key_handle, + Advapi32Const.KP_EFFECTIVE_KEYLEN, + buf, + 0 + ) + handle_error(res) + + if cipher != 'rc4': + res = advapi32.CryptSetKeyParam( + key_handle, + Advapi32Const.KP_IV, + iv, + 0 + ) + handle_error(res) + + buf = new(advapi32, 'DWORD *', Advapi32Const.CRYPT_MODE_CBC) + res = advapi32.CryptSetKeyParam( + key_handle, + Advapi32Const.KP_MODE, + buf, + 0 + ) + handle_error(res) + + buf = new(advapi32, 'DWORD *', Advapi32Const.PKCS5_PADDING) + res = advapi32.CryptSetKeyParam( + key_handle, + Advapi32Const.KP_PADDING, + buf, + 0 + ) + handle_error(res) + + return (context_handle, key_handle) + + +def _bcrypt_create_key_handle(cipher, key): + """ + Creates a BCRYPT_KEY_HANDLE for symmetric encryption/decryption. The + handle must be released by bcrypt.BCryptDestroyKey() when done. + + :param cipher: + A unicode string of "aes", "des", "tripledes_2key", "tripledes_3key", + "rc2", "rc4" + + :param key: + A byte string of the symmetric key + + :return: + A BCRYPT_KEY_HANDLE + """ + + alg_handle = None + + alg_constant = { + 'aes': BcryptConst.BCRYPT_AES_ALGORITHM, + 'des': BcryptConst.BCRYPT_DES_ALGORITHM, + 'tripledes_2key': BcryptConst.BCRYPT_3DES_112_ALGORITHM, + 'tripledes_3key': BcryptConst.BCRYPT_3DES_ALGORITHM, + 'rc2': BcryptConst.BCRYPT_RC2_ALGORITHM, + 'rc4': BcryptConst.BCRYPT_RC4_ALGORITHM, + }[cipher] + + try: + alg_handle = open_alg_handle(alg_constant) + blob_type = BcryptConst.BCRYPT_KEY_DATA_BLOB + + blob_struct_pointer = struct(bcrypt, 'BCRYPT_KEY_DATA_BLOB_HEADER') + blob_struct = unwrap(blob_struct_pointer) + blob_struct.dwMagic = BcryptConst.BCRYPT_KEY_DATA_BLOB_MAGIC + blob_struct.dwVersion = BcryptConst.BCRYPT_KEY_DATA_BLOB_VERSION1 + blob_struct.cbKeyData = len(key) + + blob = struct_bytes(blob_struct_pointer) + key + + if cipher == 'rc2': + buf = new(bcrypt, 'DWORD *', len(key) * 8) + res = bcrypt.BCryptSetProperty( + alg_handle, + BcryptConst.BCRYPT_EFFECTIVE_KEY_LENGTH, + buf, + 4, + 0 + ) + handle_error(res) + + key_handle_pointer = new(bcrypt, 'BCRYPT_KEY_HANDLE *') + res = bcrypt.BCryptImportKey( + alg_handle, + null(), + blob_type, + key_handle_pointer, + null(), + 0, + blob, + len(blob), + 0 + ) + handle_error(res) + + return unwrap(key_handle_pointer) + + finally: + if alg_handle: + close_alg_handle(alg_handle) + + +def _encrypt(cipher, key, data, iv, padding): + """ + Encrypts plaintext + + :param cipher: + A unicode string of "aes", "des", "tripledes_2key", "tripledes_3key", + "rc2", "rc4" + + :param key: + The encryption key - a byte string 5-16 bytes long + + :param data: + The plaintext - a byte string + + :param iv: + The initialization vector - a byte string - unused for RC4 + + :param padding: + Boolean, if padding should be used - unused for RC4 + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the ciphertext + """ + + if not isinstance(key, byte_cls): + raise TypeError(pretty_message( + ''' + key must be a byte string, not %s + ''', + type_name(key) + )) + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + if cipher != 'rc4' and not isinstance(iv, byte_cls): + raise TypeError(pretty_message( + ''' + iv must be a byte string, not %s + ''', + type_name(iv) + )) + + if cipher != 'rc4' and not padding: + # AES in CBC mode can be allowed with no padding if + # the data is an exact multiple of the block size + if not (cipher == 'aes' and len(data) % 16 == 0): + raise ValueError('padding must be specified') + + if _backend == 'winlegacy': + return _advapi32_encrypt(cipher, key, data, iv, padding) + return _bcrypt_encrypt(cipher, key, data, iv, padding) + + +def _advapi32_encrypt(cipher, key, data, iv, padding): + """ + Encrypts plaintext via CryptoAPI + + :param cipher: + A unicode string of "aes", "des", "tripledes_2key", "tripledes_3key", + "rc2", "rc4" + + :param key: + The encryption key - a byte string 5-16 bytes long + + :param data: + The plaintext - a byte string + + :param iv: + The initialization vector - a byte string - unused for RC4 + + :param padding: + Boolean, if padding should be used - unused for RC4 + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the ciphertext + """ + + context_handle = None + key_handle = None + + try: + context_handle, key_handle = _advapi32_create_handles(cipher, key, iv) + + out_len = new(advapi32, 'DWORD *', len(data)) + res = advapi32.CryptEncrypt( + key_handle, + null(), + True, + 0, + null(), + out_len, + 0 + ) + handle_error(res) + + buffer_len = deref(out_len) + buffer = buffer_from_bytes(buffer_len) + write_to_buffer(buffer, data) + + pointer_set(out_len, len(data)) + res = advapi32.CryptEncrypt( + key_handle, + null(), + True, + 0, + buffer, + out_len, + buffer_len + ) + handle_error(res) + + output = bytes_from_buffer(buffer, deref(out_len)) + + # Remove padding when not required. CryptoAPI doesn't support this, so + # we just manually remove it. + if cipher == 'aes' and not padding and len(output) == len(data) + 16: + output = output[:-16] + + return output + + finally: + if key_handle: + advapi32.CryptDestroyKey(key_handle) + if context_handle: + close_context_handle(context_handle) + + +def _bcrypt_encrypt(cipher, key, data, iv, padding): + """ + Encrypts plaintext via CNG + + :param cipher: + A unicode string of "aes", "des", "tripledes_2key", "tripledes_3key", + "rc2", "rc4" + + :param key: + The encryption key - a byte string 5-16 bytes long + + :param data: + The plaintext - a byte string + + :param iv: + The initialization vector - a byte string - unused for RC4 + + :param padding: + Boolean, if padding should be used - unused for RC4 + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the ciphertext + """ + + key_handle = None + + try: + key_handle = _bcrypt_create_key_handle(cipher, key) + + if iv is None: + iv_len = 0 + else: + iv_len = len(iv) + + flags = 0 + if padding is True: + flags = BcryptConst.BCRYPT_BLOCK_PADDING + + out_len = new(bcrypt, 'ULONG *') + res = bcrypt.BCryptEncrypt( + key_handle, + data, + len(data), + null(), + null(), + 0, + null(), + 0, + out_len, + flags + ) + handle_error(res) + + buffer_len = deref(out_len) + buffer = buffer_from_bytes(buffer_len) + iv_buffer = buffer_from_bytes(iv) if iv else null() + + res = bcrypt.BCryptEncrypt( + key_handle, + data, + len(data), + null(), + iv_buffer, + iv_len, + buffer, + buffer_len, + out_len, + flags + ) + handle_error(res) + + return bytes_from_buffer(buffer, deref(out_len)) + + finally: + if key_handle: + bcrypt.BCryptDestroyKey(key_handle) + + +def _decrypt(cipher, key, data, iv, padding): + """ + Decrypts AES/RC4/RC2/3DES/DES ciphertext + + :param cipher: + A unicode string of "aes", "des", "tripledes_2key", "tripledes_3key", + "rc2", "rc4" + + :param key: + The encryption key - a byte string 5-16 bytes long + + :param data: + The ciphertext - a byte string + + :param iv: + The initialization vector - a byte string - unused for RC4 + + :param padding: + Boolean, if padding should be used - unused for RC4 + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the plaintext + """ + + if not isinstance(key, byte_cls): + raise TypeError(pretty_message( + ''' + key must be a byte string, not %s + ''', + type_name(key) + )) + + if not isinstance(data, byte_cls): + raise TypeError(pretty_message( + ''' + data must be a byte string, not %s + ''', + type_name(data) + )) + + if cipher != 'rc4' and not isinstance(iv, byte_cls): + raise TypeError(pretty_message( + ''' + iv must be a byte string, not %s + ''', + type_name(iv) + )) + + if cipher not in set(['rc4', 'aes']) and not padding: + raise ValueError('padding must be specified') + + if _backend == 'winlegacy': + return _advapi32_decrypt(cipher, key, data, iv, padding) + return _bcrypt_decrypt(cipher, key, data, iv, padding) + + +def _advapi32_decrypt(cipher, key, data, iv, padding): + """ + Decrypts AES/RC4/RC2/3DES/DES ciphertext via CryptoAPI + + :param cipher: + A unicode string of "aes", "des", "tripledes_2key", "tripledes_3key", + "rc2", "rc4" + + :param key: + The encryption key - a byte string 5-16 bytes long + + :param data: + The ciphertext - a byte string + + :param iv: + The initialization vector - a byte string - unused for RC4 + + :param padding: + Boolean, if padding should be used - unused for RC4 + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the plaintext + """ + + context_handle = None + key_handle = None + + try: + context_handle, key_handle = _advapi32_create_handles(cipher, key, iv) + + if cipher == 'aes' and not padding and len(data) % 16 != 0: + raise ValueError('Invalid data - ciphertext length must be a multiple of 16') + + buffer = buffer_from_bytes(data) + out_len = new(advapi32, 'DWORD *', len(data)) + res = advapi32.CryptDecrypt( + key_handle, + null(), + # To skip padding, we have to tell the API that this is not + # the final block + False if cipher == 'aes' and not padding else True, + 0, + buffer, + out_len + ) + handle_error(res) + + return bytes_from_buffer(buffer, deref(out_len)) + + finally: + if key_handle: + advapi32.CryptDestroyKey(key_handle) + if context_handle: + close_context_handle(context_handle) + + +def _bcrypt_decrypt(cipher, key, data, iv, padding): + """ + Decrypts AES/RC4/RC2/3DES/DES ciphertext via CNG + + :param cipher: + A unicode string of "aes", "des", "tripledes_2key", "tripledes_3key", + "rc2", "rc4" + + :param key: + The encryption key - a byte string 5-16 bytes long + + :param data: + The ciphertext - a byte string + + :param iv: + The initialization vector - a byte string - unused for RC4 + + :param padding: + Boolean, if padding should be used - unused for RC4 + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the plaintext + """ + + key_handle = None + + try: + key_handle = _bcrypt_create_key_handle(cipher, key) + + if iv is None: + iv_len = 0 + else: + iv_len = len(iv) + + flags = 0 + if padding is True: + flags = BcryptConst.BCRYPT_BLOCK_PADDING + + out_len = new(bcrypt, 'ULONG *') + res = bcrypt.BCryptDecrypt( + key_handle, + data, + len(data), + null(), + null(), + 0, + null(), + 0, + out_len, + flags + ) + handle_error(res) + + buffer_len = deref(out_len) + buffer = buffer_from_bytes(buffer_len) + iv_buffer = buffer_from_bytes(iv) if iv else null() + + res = bcrypt.BCryptDecrypt( + key_handle, + data, + len(data), + null(), + iv_buffer, + iv_len, + buffer, + buffer_len, + out_len, + flags + ) + handle_error(res) + + return bytes_from_buffer(buffer, deref(out_len)) + + finally: + if key_handle: + bcrypt.BCryptDestroyKey(key_handle) diff --git a/tasks/lib/package_control/deps/oscrypto/_win/tls.py b/tasks/lib/package_control/deps/oscrypto/_win/tls.py new file mode 100644 index 0000000..23e2027 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_win/tls.py @@ -0,0 +1,1593 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys +import re +import socket as socket_ +import select +import numbers + +from .._asn1 import Certificate as Asn1Certificate +from .._errors import pretty_message +from .._ffi import ( + buffer_from_bytes, + buffer_from_unicode, + bytes_from_buffer, + cast, + deref, + is_null, + native, + new, + null, + ref, + sizeof, + struct, + unwrap, + write_to_buffer, +) +from ._secur32 import secur32, Secur32Const, handle_error +from ._crypt32 import crypt32, Crypt32Const, handle_error as handle_crypt32_error +from ._kernel32 import kernel32 +from .._types import type_name, str_cls, byte_cls, int_types +from ..errors import TLSError, TLSVerificationError, TLSDisconnectError, TLSGracefulDisconnectError +from .._tls import ( + detect_client_auth_request, + detect_other_protocol, + extract_chain, + get_dh_params_length, + parse_alert, + parse_session_info, + raise_client_auth, + raise_dh_params, + raise_disconnection, + raise_expired_not_yet_valid, + raise_handshake, + raise_hostname, + raise_no_issuer, + raise_protocol_error, + raise_protocol_version, + raise_revoked, + raise_self_signed, + raise_verification, + raise_weak_signature, +) +from .asymmetric import load_certificate, Certificate +from ..keys import parse_certificate + +if sys.version_info < (3,): + range = xrange # noqa + socket_error_cls = socket_.error +else: + socket_error_cls = WindowsError + +if sys.version_info < (3, 7): + Pattern = re._pattern_type +else: + Pattern = re.Pattern + + +__all__ = [ + 'TLSSession', + 'TLSSocket', +] + + +_line_regex = re.compile(b'(\r\n|\r|\n)') + +_gwv = sys.getwindowsversion() +_win_version_info = (_gwv[0], _gwv[1]) + + +class _TLSDowngradeError(TLSVerificationError): + + pass + + +class _TLSRetryError(TLSError): + + """ + TLSv1.2 on Windows 7 and 8 seems to have isuses with some DHE_RSA + ServerKeyExchange messages due to variable length integer encoding. This + exception is used to trigger a reconnection to attempt the handshake again. + """ + + pass + + +class TLSSession(object): + """ + A TLS session object that multiple TLSSocket objects can share for the + sake of session reuse + """ + + _protocols = None + _ciphers = None + _manual_validation = None + _extra_trust_roots = None + _credentials_handle = None + + def __init__(self, protocol=None, manual_validation=False, extra_trust_roots=None): + """ + :param protocol: + A unicode string or set of unicode strings representing allowable + protocols to negotiate with the server: + + - "TLSv1.2" + - "TLSv1.1" + - "TLSv1" + - "SSLv3" + + Default is: {"TLSv1", "TLSv1.1", "TLSv1.2"} + + :param manual_validation: + If certificate and certificate path validation should be skipped + and left to the developer to implement + + :param extra_trust_roots: + A list containing one or more certificates to be treated as trust + roots, in one of the following formats: + - A byte string of the DER encoded certificate + - A unicode string of the certificate filename + - An asn1crypto.x509.Certificate object + - An oscrypto.asymmetric.Certificate object + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + if not isinstance(manual_validation, bool): + raise TypeError(pretty_message( + ''' + manual_validation must be a boolean, not %s + ''', + type_name(manual_validation) + )) + + self._manual_validation = manual_validation + + if protocol is None: + protocol = set(['TLSv1', 'TLSv1.1', 'TLSv1.2']) + + if isinstance(protocol, str_cls): + protocol = set([protocol]) + elif not isinstance(protocol, set): + raise TypeError(pretty_message( + ''' + protocol must be a unicode string or set of unicode strings, + not %s + ''', + type_name(protocol) + )) + + unsupported_protocols = protocol - set(['SSLv3', 'TLSv1', 'TLSv1.1', 'TLSv1.2']) + if unsupported_protocols: + raise ValueError(pretty_message( + ''' + protocol must contain only the unicode strings "SSLv3", "TLSv1", + "TLSv1.1", "TLSv1.2", not %s + ''', + repr(unsupported_protocols) + )) + + self._protocols = protocol + + self._extra_trust_roots = [] + if extra_trust_roots: + for extra_trust_root in extra_trust_roots: + if isinstance(extra_trust_root, Certificate): + extra_trust_root = extra_trust_root.asn1 + elif isinstance(extra_trust_root, byte_cls): + extra_trust_root = parse_certificate(extra_trust_root) + elif isinstance(extra_trust_root, str_cls): + with open(extra_trust_root, 'rb') as f: + extra_trust_root = parse_certificate(f.read()) + elif not isinstance(extra_trust_root, Asn1Certificate): + raise TypeError(pretty_message( + ''' + extra_trust_roots must be a list of byte strings, unicode + strings, asn1crypto.x509.Certificate objects or + oscrypto.asymmetric.Certificate objects, not %s + ''', + type_name(extra_trust_root) + )) + self._extra_trust_roots.append(extra_trust_root) + + self._obtain_credentials() + + def _obtain_credentials(self): + """ + Obtains a credentials handle from secur32.dll for use with SChannel + """ + + protocol_values = { + 'SSLv3': Secur32Const.SP_PROT_SSL3_CLIENT, + 'TLSv1': Secur32Const.SP_PROT_TLS1_CLIENT, + 'TLSv1.1': Secur32Const.SP_PROT_TLS1_1_CLIENT, + 'TLSv1.2': Secur32Const.SP_PROT_TLS1_2_CLIENT, + } + protocol_bit_mask = 0 + for key, value in protocol_values.items(): + if key in self._protocols: + protocol_bit_mask |= value + + algs = [ + Secur32Const.CALG_AES_128, + Secur32Const.CALG_AES_256, + Secur32Const.CALG_3DES, + Secur32Const.CALG_SHA1, + Secur32Const.CALG_ECDHE, + Secur32Const.CALG_DH_EPHEM, + Secur32Const.CALG_RSA_KEYX, + Secur32Const.CALG_RSA_SIGN, + Secur32Const.CALG_ECDSA, + Secur32Const.CALG_DSS_SIGN, + ] + if 'TLSv1.2' in self._protocols: + algs.extend([ + Secur32Const.CALG_SHA512, + Secur32Const.CALG_SHA384, + Secur32Const.CALG_SHA256, + ]) + + alg_array = new(secur32, 'ALG_ID[%s]' % len(algs)) + for index, alg in enumerate(algs): + alg_array[index] = alg + + flags = Secur32Const.SCH_USE_STRONG_CRYPTO | Secur32Const.SCH_CRED_NO_DEFAULT_CREDS + if not self._manual_validation and not self._extra_trust_roots: + flags |= Secur32Const.SCH_CRED_AUTO_CRED_VALIDATION + else: + flags |= Secur32Const.SCH_CRED_MANUAL_CRED_VALIDATION + + schannel_cred_pointer = struct(secur32, 'SCHANNEL_CRED') + schannel_cred = unwrap(schannel_cred_pointer) + + schannel_cred.dwVersion = Secur32Const.SCHANNEL_CRED_VERSION + schannel_cred.cCreds = 0 + schannel_cred.paCred = null() + schannel_cred.hRootStore = null() + schannel_cred.cMappers = 0 + schannel_cred.aphMappers = null() + schannel_cred.cSupportedAlgs = len(alg_array) + schannel_cred.palgSupportedAlgs = alg_array + schannel_cred.grbitEnabledProtocols = protocol_bit_mask + schannel_cred.dwMinimumCipherStrength = 0 + schannel_cred.dwMaximumCipherStrength = 0 + # Default session lifetime is 10 hours + schannel_cred.dwSessionLifespan = 0 + schannel_cred.dwFlags = flags + schannel_cred.dwCredFormat = 0 + + cred_handle_pointer = new(secur32, 'CredHandle *') + + result = secur32.AcquireCredentialsHandleW( + null(), + Secur32Const.UNISP_NAME, + Secur32Const.SECPKG_CRED_OUTBOUND, + null(), + schannel_cred_pointer, + null(), + null(), + cred_handle_pointer, + null() + ) + handle_error(result) + + self._credentials_handle = cred_handle_pointer + + def __del__(self): + if self._credentials_handle: + result = secur32.FreeCredentialsHandle(self._credentials_handle) + handle_error(result) + self._credentials_handle = None + + +class TLSSocket(object): + """ + A wrapper around a socket.socket that adds TLS + """ + + _socket = None + _session = None + + _context_handle_pointer = None + _context_flags = None + _hostname = None + + _header_size = None + _message_size = None + _trailer_size = None + + _received_bytes = None + _decrypted_bytes = None + + _encrypt_desc = None + _encrypt_buffers = None + _encrypt_data_buffer = None + + _decrypt_desc = None + _decrypt_buffers = None + _decrypt_data_buffer = None + + _certificate = None + _intermediates = None + + _protocol = None + _cipher_suite = None + _compression = None + _session_id = None + _session_ticket = None + + _remote_closed = False + + @classmethod + def wrap(cls, socket, hostname, session=None): + """ + Takes an existing socket and adds TLS + + :param socket: + A socket.socket object to wrap with TLS + + :param hostname: + A unicode string of the hostname or IP the socket is connected to + + :param session: + An existing TLSSession object to allow for session reuse, specific + protocol or manual certificate validation + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + if not isinstance(socket, socket_.socket): + raise TypeError(pretty_message( + ''' + socket must be an instance of socket.socket, not %s + ''', + type_name(socket) + )) + + if not isinstance(hostname, str_cls): + raise TypeError(pretty_message( + ''' + hostname must be a unicode string, not %s + ''', + type_name(hostname) + )) + + if session is not None and not isinstance(session, TLSSession): + raise TypeError(pretty_message( + ''' + session must be an instance of oscrypto.tls.TLSSession, not %s + ''', + type_name(session) + )) + + new_socket = cls(None, None, session=session) + new_socket._socket = socket + new_socket._hostname = hostname + + # Since we don't create the socket connection here, we can't try to + # reconnect with a lower version of the TLS protocol, so we just + # move the data to public exception type TLSVerificationError() + try: + new_socket._handshake() + except (_TLSDowngradeError) as e: + new_e = TLSVerificationError(e.message, e.certificate) + raise new_e + except (_TLSRetryError) as e: + new_e = TLSError(e.message) + raise new_e + + return new_socket + + def __init__(self, address, port, timeout=10, session=None): + """ + :param address: + A unicode string of the domain name or IP address to connect to + + :param port: + An integer of the port number to connect to + + :param timeout: + An integer timeout to use for the socket + + :param session: + An oscrypto.tls.TLSSession object to allow for session reuse and + controlling the protocols and validation performed + """ + + self._received_bytes = b'' + self._decrypted_bytes = b'' + + if address is None and port is None: + self._socket = None + + else: + if not isinstance(address, str_cls): + raise TypeError(pretty_message( + ''' + address must be a unicode string, not %s + ''', + type_name(address) + )) + + if not isinstance(port, int_types): + raise TypeError(pretty_message( + ''' + port must be an integer, not %s + ''', + type_name(port) + )) + + if timeout is not None and not isinstance(timeout, numbers.Number): + raise TypeError(pretty_message( + ''' + timeout must be a number, not %s + ''', + type_name(timeout) + )) + + self._socket = socket_.create_connection((address, port), timeout) + self._socket.settimeout(timeout) + + if session is None: + session = TLSSession() + + elif not isinstance(session, TLSSession): + raise TypeError(pretty_message( + ''' + session must be an instance of oscrypto.tls.TLSSession, not %s + ''', + type_name(session) + )) + + self._session = session + + if self._socket: + self._hostname = address + + try: + self._handshake() + except (_TLSDowngradeError): + self.close() + new_session = TLSSession( + session._protocols - set(['TLSv1.2']), + session._manual_validation, + session._extra_trust_roots + ) + session.__del__() + self._received_bytes = b'' + self._session = new_session + self._socket = socket_.create_connection((address, port), timeout) + self._socket.settimeout(timeout) + self._handshake() + except (_TLSRetryError): + self._received_bytes = b'' + self._socket = socket_.create_connection((address, port), timeout) + self._socket.settimeout(timeout) + self._handshake() + + def _create_buffers(self, number): + """ + Creates a SecBufferDesc struct and contained SecBuffer structs + + :param number: + The number of contains SecBuffer objects to create + + :return: + A tuple of (SecBufferDesc pointer, SecBuffer array) + """ + + buffers = new(secur32, 'SecBuffer[%d]' % number) + + for index in range(0, number): + buffers[index].cbBuffer = 0 + buffers[index].BufferType = Secur32Const.SECBUFFER_EMPTY + buffers[index].pvBuffer = null() + + sec_buffer_desc_pointer = struct(secur32, 'SecBufferDesc') + sec_buffer_desc = unwrap(sec_buffer_desc_pointer) + + sec_buffer_desc.ulVersion = Secur32Const.SECBUFFER_VERSION + sec_buffer_desc.cBuffers = number + sec_buffer_desc.pBuffers = buffers + + return (sec_buffer_desc_pointer, buffers) + + def _extra_trust_root_validation(self): + """ + Manually invoked windows certificate chain builder and verification + step when there are extra trust roots to include in the search process + """ + + store = None + cert_chain_context_pointer = None + + try: + # We set up an in-memory store to pass as an extra store to grab + # certificates from when performing the verification + store = crypt32.CertOpenStore( + Crypt32Const.CERT_STORE_PROV_MEMORY, + Crypt32Const.X509_ASN_ENCODING, + null(), + 0, + null() + ) + if is_null(store): + handle_crypt32_error(0) + + cert_hashes = set() + for cert in self._session._extra_trust_roots: + cert_data = cert.dump() + result = crypt32.CertAddEncodedCertificateToStore( + store, + Crypt32Const.X509_ASN_ENCODING, + cert_data, + len(cert_data), + Crypt32Const.CERT_STORE_ADD_USE_EXISTING, + null() + ) + if not result: + handle_crypt32_error(0) + cert_hashes.add(cert.sha256) + + cert_context_pointer_pointer = new(crypt32, 'PCERT_CONTEXT *') + result = secur32.QueryContextAttributesW( + self._context_handle_pointer, + Secur32Const.SECPKG_ATTR_REMOTE_CERT_CONTEXT, + cert_context_pointer_pointer + ) + handle_error(result) + + cert_context_pointer = unwrap(cert_context_pointer_pointer) + cert_context_pointer = cast(crypt32, 'PCERT_CONTEXT', cert_context_pointer) + + # We have to do a funky shuffle here because FILETIME from kernel32 + # is different than FILETIME from crypt32 when using cffi. If we + # overwrite the "now_pointer" variable, cffi releases the backing + # memory and we end up getting a validation error about certificate + # expiration time. + orig_now_pointer = new(kernel32, 'FILETIME *') + kernel32.GetSystemTimeAsFileTime(orig_now_pointer) + now_pointer = cast(crypt32, 'FILETIME *', orig_now_pointer) + + usage_identifiers = new(crypt32, 'char *[3]') + usage_identifiers[0] = cast(crypt32, 'char *', Crypt32Const.PKIX_KP_SERVER_AUTH) + usage_identifiers[1] = cast(crypt32, 'char *', Crypt32Const.SERVER_GATED_CRYPTO) + usage_identifiers[2] = cast(crypt32, 'char *', Crypt32Const.SGC_NETSCAPE) + + cert_enhkey_usage_pointer = struct(crypt32, 'CERT_ENHKEY_USAGE') + cert_enhkey_usage = unwrap(cert_enhkey_usage_pointer) + cert_enhkey_usage.cUsageIdentifier = 3 + cert_enhkey_usage.rgpszUsageIdentifier = cast(crypt32, 'char **', usage_identifiers) + + cert_usage_match_pointer = struct(crypt32, 'CERT_USAGE_MATCH') + cert_usage_match = unwrap(cert_usage_match_pointer) + cert_usage_match.dwType = Crypt32Const.USAGE_MATCH_TYPE_OR + cert_usage_match.Usage = cert_enhkey_usage + + cert_chain_para_pointer = struct(crypt32, 'CERT_CHAIN_PARA') + cert_chain_para = unwrap(cert_chain_para_pointer) + cert_chain_para.RequestedUsage = cert_usage_match + cert_chain_para_size = sizeof(crypt32, cert_chain_para) + cert_chain_para.cbSize = cert_chain_para_size + + cert_chain_context_pointer_pointer = new(crypt32, 'PCERT_CHAIN_CONTEXT *') + result = crypt32.CertGetCertificateChain( + null(), + cert_context_pointer, + now_pointer, + store, + cert_chain_para_pointer, + Crypt32Const.CERT_CHAIN_CACHE_END_CERT | Crypt32Const.CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY, + null(), + cert_chain_context_pointer_pointer + ) + handle_crypt32_error(result) + + cert_chain_policy_para_flags = Crypt32Const.CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS + + cert_chain_context_pointer = unwrap(cert_chain_context_pointer_pointer) + + # Unwrap the chain and if the final element in the chain is one of + # extra trust roots, set flags so that we trust the certificate even + # though it is not in the Trusted Roots store + cert_chain_context = unwrap(cert_chain_context_pointer) + num_chains = native(int, cert_chain_context.cChain) + if num_chains == 1: + first_simple_chain_pointer = unwrap(cert_chain_context.rgpChain) + first_simple_chain = unwrap(first_simple_chain_pointer) + num_elements = native(int, first_simple_chain.cElement) + last_element_pointer = first_simple_chain.rgpElement[num_elements - 1] + last_element = unwrap(last_element_pointer) + last_element_cert = unwrap(last_element.pCertContext) + last_element_cert_data = bytes_from_buffer( + last_element_cert.pbCertEncoded, + native(int, last_element_cert.cbCertEncoded) + ) + last_cert = Asn1Certificate.load(last_element_cert_data) + if last_cert.sha256 in cert_hashes: + cert_chain_policy_para_flags |= Crypt32Const.CERT_CHAIN_POLICY_ALLOW_UNKNOWN_CA_FLAG + + ssl_extra_cert_chain_policy_para_pointer = struct(crypt32, 'SSL_EXTRA_CERT_CHAIN_POLICY_PARA') + ssl_extra_cert_chain_policy_para = unwrap(ssl_extra_cert_chain_policy_para_pointer) + ssl_extra_cert_chain_policy_para.cbSize = sizeof(crypt32, ssl_extra_cert_chain_policy_para) + ssl_extra_cert_chain_policy_para.dwAuthType = Crypt32Const.AUTHTYPE_SERVER + ssl_extra_cert_chain_policy_para.fdwChecks = 0 + ssl_extra_cert_chain_policy_para.pwszServerName = cast( + crypt32, + 'wchar_t *', + buffer_from_unicode(self._hostname) + ) + + cert_chain_policy_para_pointer = struct(crypt32, 'CERT_CHAIN_POLICY_PARA') + cert_chain_policy_para = unwrap(cert_chain_policy_para_pointer) + cert_chain_policy_para.cbSize = sizeof(crypt32, cert_chain_policy_para) + cert_chain_policy_para.dwFlags = cert_chain_policy_para_flags + cert_chain_policy_para.pvExtraPolicyPara = cast(crypt32, 'void *', ssl_extra_cert_chain_policy_para_pointer) + + cert_chain_policy_status_pointer = struct(crypt32, 'CERT_CHAIN_POLICY_STATUS') + cert_chain_policy_status = unwrap(cert_chain_policy_status_pointer) + cert_chain_policy_status.cbSize = sizeof(crypt32, cert_chain_policy_status) + + result = crypt32.CertVerifyCertificateChainPolicy( + Crypt32Const.CERT_CHAIN_POLICY_SSL, + cert_chain_context_pointer, + cert_chain_policy_para_pointer, + cert_chain_policy_status_pointer + ) + handle_crypt32_error(result) + + cert_context = unwrap(cert_context_pointer) + cert_data = bytes_from_buffer(cert_context.pbCertEncoded, native(int, cert_context.cbCertEncoded)) + cert = Asn1Certificate.load(cert_data) + + error = cert_chain_policy_status.dwError + if error: + if error == Crypt32Const.CERT_E_EXPIRED: + raise_expired_not_yet_valid(cert) + if error == Crypt32Const.CERT_E_UNTRUSTEDROOT: + oscrypto_cert = load_certificate(cert) + if oscrypto_cert.self_signed: + raise_self_signed(cert) + else: + raise_no_issuer(cert) + if error == Crypt32Const.CERT_E_CN_NO_MATCH: + raise_hostname(cert, self._hostname) + + if error == Crypt32Const.TRUST_E_CERT_SIGNATURE: + raise_weak_signature(cert) + + if error == Crypt32Const.CRYPT_E_REVOKED: + raise_revoked(cert) + + raise_verification(cert) + + if cert.hash_algo in set(['md5', 'md2']): + raise_weak_signature(cert) + + finally: + if store: + crypt32.CertCloseStore(store, 0) + if cert_chain_context_pointer: + crypt32.CertFreeCertificateChain(cert_chain_context_pointer) + + def _handshake(self, renegotiate=False): + """ + Perform an initial TLS handshake, or a renegotiation + + :param renegotiate: + If the handshake is for a renegotiation + """ + + in_buffers = None + out_buffers = None + new_context_handle_pointer = None + + try: + if renegotiate: + temp_context_handle_pointer = self._context_handle_pointer + else: + new_context_handle_pointer = new(secur32, 'CtxtHandle *') + temp_context_handle_pointer = new_context_handle_pointer + + requested_flags = { + Secur32Const.ISC_REQ_REPLAY_DETECT: 'replay detection', + Secur32Const.ISC_REQ_SEQUENCE_DETECT: 'sequence detection', + Secur32Const.ISC_REQ_CONFIDENTIALITY: 'confidentiality', + Secur32Const.ISC_REQ_ALLOCATE_MEMORY: 'memory allocation', + Secur32Const.ISC_REQ_INTEGRITY: 'integrity', + Secur32Const.ISC_REQ_STREAM: 'stream orientation', + Secur32Const.ISC_REQ_USE_SUPPLIED_CREDS: 'disable automatic client auth', + } + + self._context_flags = 0 + for flag in requested_flags: + self._context_flags |= flag + + in_sec_buffer_desc_pointer, in_buffers = self._create_buffers(2) + in_buffers[0].BufferType = Secur32Const.SECBUFFER_TOKEN + + out_sec_buffer_desc_pointer, out_buffers = self._create_buffers(2) + out_buffers[0].BufferType = Secur32Const.SECBUFFER_TOKEN + out_buffers[1].BufferType = Secur32Const.SECBUFFER_ALERT + + output_context_flags_pointer = new(secur32, 'ULONG *') + + if renegotiate: + first_handle = temp_context_handle_pointer + second_handle = null() + else: + first_handle = null() + second_handle = temp_context_handle_pointer + + result = secur32.InitializeSecurityContextW( + self._session._credentials_handle, + first_handle, + self._hostname, + self._context_flags, + 0, + 0, + null(), + 0, + second_handle, + out_sec_buffer_desc_pointer, + output_context_flags_pointer, + null() + ) + if result not in set([Secur32Const.SEC_E_OK, Secur32Const.SEC_I_CONTINUE_NEEDED]): + handle_error(result, TLSError) + + if not renegotiate: + temp_context_handle_pointer = second_handle + else: + temp_context_handle_pointer = first_handle + + handshake_server_bytes = b'' + handshake_client_bytes = b'' + + if out_buffers[0].cbBuffer > 0: + token = bytes_from_buffer(out_buffers[0].pvBuffer, out_buffers[0].cbBuffer) + handshake_client_bytes += token + self._socket.send(token) + out_buffers[0].cbBuffer = 0 + secur32.FreeContextBuffer(out_buffers[0].pvBuffer) + out_buffers[0].pvBuffer = null() + + in_data_buffer = buffer_from_bytes(32768) + in_buffers[0].pvBuffer = cast(secur32, 'BYTE *', in_data_buffer) + + bytes_read = b'' + while result != Secur32Const.SEC_E_OK: + try: + fail_late = False + bytes_read = self._socket.recv(8192) + if bytes_read == b'': + raise_disconnection() + except (socket_error_cls): + fail_late = True + handshake_server_bytes += bytes_read + self._received_bytes += bytes_read + + in_buffers[0].cbBuffer = len(self._received_bytes) + write_to_buffer(in_data_buffer, self._received_bytes) + + result = secur32.InitializeSecurityContextW( + self._session._credentials_handle, + temp_context_handle_pointer, + self._hostname, + self._context_flags, + 0, + 0, + in_sec_buffer_desc_pointer, + 0, + null(), + out_sec_buffer_desc_pointer, + output_context_flags_pointer, + null() + ) + + if result == Secur32Const.SEC_E_INCOMPLETE_MESSAGE: + in_buffers[0].BufferType = Secur32Const.SECBUFFER_TOKEN + # Windows 10 seems to fill the second input buffer with + # a BufferType of SECBUFFER_MISSING (4), which if not + # cleared causes the handshake to fail. + if in_buffers[1].BufferType != Secur32Const.SECBUFFER_EMPTY: + in_buffers[1].BufferType = Secur32Const.SECBUFFER_EMPTY + in_buffers[1].cbBuffer = 0 + if not is_null(in_buffers[1].pvBuffer): + secur32.FreeContextBuffer(in_buffers[1].pvBuffer) + in_buffers[1].pvBuffer = null() + + if fail_late: + raise_disconnection() + + continue + + if result == Secur32Const.SEC_E_ILLEGAL_MESSAGE: + if detect_client_auth_request(handshake_server_bytes): + raise_client_auth() + alert_info = parse_alert(handshake_server_bytes) + if alert_info and alert_info == (2, 70): + raise_protocol_version() + raise_handshake() + + if result == Secur32Const.SEC_E_WRONG_PRINCIPAL: + chain = extract_chain(handshake_server_bytes) + raise_hostname(chain[0], self._hostname) + + if result == Secur32Const.SEC_E_CERT_EXPIRED: + chain = extract_chain(handshake_server_bytes) + raise_expired_not_yet_valid(chain[0]) + + if result == Secur32Const.SEC_E_UNTRUSTED_ROOT: + chain = extract_chain(handshake_server_bytes) + cert = chain[0] + oscrypto_cert = load_certificate(cert) + if not oscrypto_cert.self_signed: + raise_no_issuer(cert) + raise_self_signed(cert) + + if result == Secur32Const.SEC_E_INTERNAL_ERROR: + if get_dh_params_length(handshake_server_bytes) < 1024: + raise_dh_params() + + if result == Secur32Const.SEC_I_INCOMPLETE_CREDENTIALS: + raise_client_auth() + + if result == Crypt32Const.TRUST_E_CERT_SIGNATURE: + raise_weak_signature(cert) + + if result == Secur32Const.SEC_E_INVALID_TOKEN: + # If an alert it present, there may have been a handshake + # error due to the server using a certificate path with a + # trust root using MD2 or MD5 combined with TLS 1.2. To + # work around this, if the user allows anything other than + # TLS 1.2, we just remove it from the acceptable protocols + # and try again. + if out_buffers[1].cbBuffer > 0: + alert_bytes = bytes_from_buffer(out_buffers[1].pvBuffer, out_buffers[1].cbBuffer) + handshake_client_bytes += alert_bytes + alert_number = alert_bytes[6:7] + if alert_number == b'\x28' or alert_number == b'\x2b': + if 'TLSv1.2' in self._session._protocols and len(self._session._protocols) > 1: + chain = extract_chain(handshake_server_bytes) + raise _TLSDowngradeError( + 'Server certificate verification failed - weak certificate signature algorithm', + chain[0] + ) + if detect_client_auth_request(handshake_server_bytes): + raise_client_auth() + if detect_other_protocol(handshake_server_bytes): + raise_protocol_error(handshake_server_bytes) + raise_handshake() + + # These are semi-common errors with TLSv1.2 on Windows 7 an 8 + # that appears to be due to poor handling of the + # ServerKeyExchange for DHE_RSA cipher suites. The solution + # is to retry the handshake. + if result == Secur32Const.SEC_E_BUFFER_TOO_SMALL or result == Secur32Const.SEC_E_MESSAGE_ALTERED: + if 'TLSv1.2' in self._session._protocols: + raise _TLSRetryError('TLS handshake failed') + + if fail_late: + raise_disconnection() + + if result == Secur32Const.SEC_E_INVALID_PARAMETER: + if get_dh_params_length(handshake_server_bytes) < 1024: + raise_dh_params() + + if result not in set([Secur32Const.SEC_E_OK, Secur32Const.SEC_I_CONTINUE_NEEDED]): + handle_error(result, TLSError) + + if out_buffers[0].cbBuffer > 0: + token = bytes_from_buffer(out_buffers[0].pvBuffer, out_buffers[0].cbBuffer) + handshake_client_bytes += token + self._socket.send(token) + out_buffers[0].cbBuffer = 0 + secur32.FreeContextBuffer(out_buffers[0].pvBuffer) + out_buffers[0].pvBuffer = null() + + if in_buffers[1].BufferType == Secur32Const.SECBUFFER_EXTRA: + extra_amount = in_buffers[1].cbBuffer + self._received_bytes = self._received_bytes[-extra_amount:] + in_buffers[1].BufferType = Secur32Const.SECBUFFER_EMPTY + in_buffers[1].cbBuffer = 0 + secur32.FreeContextBuffer(in_buffers[1].pvBuffer) + in_buffers[1].pvBuffer = null() + + # The handshake is complete, so discard any extra bytes + if result == Secur32Const.SEC_E_OK: + handshake_server_bytes = handshake_server_bytes[-extra_amount:] + + else: + self._received_bytes = b'' + + connection_info_pointer = struct(secur32, 'SecPkgContext_ConnectionInfo') + result = secur32.QueryContextAttributesW( + temp_context_handle_pointer, + Secur32Const.SECPKG_ATTR_CONNECTION_INFO, + connection_info_pointer + ) + handle_error(result, TLSError) + + connection_info = unwrap(connection_info_pointer) + + self._protocol = { + Secur32Const.SP_PROT_SSL2_CLIENT: 'SSLv2', + Secur32Const.SP_PROT_SSL3_CLIENT: 'SSLv3', + Secur32Const.SP_PROT_TLS1_CLIENT: 'TLSv1', + Secur32Const.SP_PROT_TLS1_1_CLIENT: 'TLSv1.1', + Secur32Const.SP_PROT_TLS1_2_CLIENT: 'TLSv1.2', + }.get(native(int, connection_info.dwProtocol), str_cls(connection_info.dwProtocol)) + + if self._protocol in set(['SSLv3', 'TLSv1', 'TLSv1.1', 'TLSv1.2']): + session_info = parse_session_info(handshake_server_bytes, handshake_client_bytes) + self._cipher_suite = session_info['cipher_suite'] + self._compression = session_info['compression'] + self._session_id = session_info['session_id'] + self._session_ticket = session_info['session_ticket'] + + output_context_flags = deref(output_context_flags_pointer) + + for flag in requested_flags: + if (flag | output_context_flags) == 0: + raise OSError(pretty_message( + ''' + Unable to obtain a credential context with the property %s + ''', + requested_flags[flag] + )) + + if not renegotiate: + self._context_handle_pointer = temp_context_handle_pointer + new_context_handle_pointer = None + + stream_sizes_pointer = struct(secur32, 'SecPkgContext_StreamSizes') + result = secur32.QueryContextAttributesW( + self._context_handle_pointer, + Secur32Const.SECPKG_ATTR_STREAM_SIZES, + stream_sizes_pointer + ) + handle_error(result) + + stream_sizes = unwrap(stream_sizes_pointer) + self._header_size = native(int, stream_sizes.cbHeader) + self._message_size = native(int, stream_sizes.cbMaximumMessage) + self._trailer_size = native(int, stream_sizes.cbTrailer) + self._buffer_size = self._header_size + self._message_size + self._trailer_size + + if self._session._extra_trust_roots: + self._extra_trust_root_validation() + + except (OSError, socket_.error): + self.close() + + raise + + finally: + if out_buffers: + if not is_null(out_buffers[0].pvBuffer): + secur32.FreeContextBuffer(out_buffers[0].pvBuffer) + if not is_null(out_buffers[1].pvBuffer): + secur32.FreeContextBuffer(out_buffers[1].pvBuffer) + if new_context_handle_pointer: + secur32.DeleteSecurityContext(new_context_handle_pointer) + + def read(self, max_length): + """ + Reads data from the TLS-wrapped socket + + :param max_length: + The number of bytes to read + + :raises: + socket.socket - when a non-TLS socket error occurs + oscrypto.errors.TLSError - when a TLS-related error occurs + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string of the data read + """ + + if not isinstance(max_length, int_types): + raise TypeError(pretty_message( + ''' + max_length must be an integer, not %s + ''', + type_name(max_length) + )) + + if self._context_handle_pointer is None: + + # Allow the user to read any remaining decrypted data + if self._decrypted_bytes != b'': + output = self._decrypted_bytes[0:max_length] + self._decrypted_bytes = self._decrypted_bytes[max_length:] + return output + + self._raise_closed() + + # The first time read is called, set up a single contiguous buffer that + # it used by DecryptMessage() to populate the three output buffers. + # Since we are creating the buffer, we do not need to free it other + # than allowing Python to GC it once this object is GCed. + if not self._decrypt_data_buffer: + self._decrypt_data_buffer = buffer_from_bytes(self._buffer_size) + self._decrypt_desc, self._decrypt_buffers = self._create_buffers(4) + self._decrypt_buffers[0].BufferType = Secur32Const.SECBUFFER_DATA + self._decrypt_buffers[0].pvBuffer = cast(secur32, 'BYTE *', self._decrypt_data_buffer) + + to_recv = max(max_length, self._buffer_size) + + # These variables are set to reduce dict access and function calls + # in the read loop. Also makes the code easier to read. + null_value = null() + buf0 = self._decrypt_buffers[0] + buf1 = self._decrypt_buffers[1] + buf2 = self._decrypt_buffers[2] + buf3 = self._decrypt_buffers[3] + + def _reset_buffers(): + buf0.BufferType = Secur32Const.SECBUFFER_DATA + buf0.pvBuffer = cast(secur32, 'BYTE *', self._decrypt_data_buffer) + buf0.cbBuffer = 0 + + buf1.BufferType = Secur32Const.SECBUFFER_EMPTY + buf1.pvBuffer = null_value + buf1.cbBuffer = 0 + + buf2.BufferType = Secur32Const.SECBUFFER_EMPTY + buf2.pvBuffer = null_value + buf2.cbBuffer = 0 + + buf3.BufferType = Secur32Const.SECBUFFER_EMPTY + buf3.pvBuffer = null_value + buf3.cbBuffer = 0 + + output = self._decrypted_bytes + output_len = len(output) + + self._decrypted_bytes = b'' + + # Don't block if we have buffered data available + if output_len > 0 and not self.select_read(0): + self._decrypted_bytes = b'' + return output + + # This read loop will only be run if there wasn't enough + # buffered data to fulfill the requested max_length + do_read = len(self._received_bytes) == 0 + + while output_len < max_length: + if do_read: + self._received_bytes += self._socket.recv(to_recv) + if len(self._received_bytes) == 0: + raise_disconnection() + + data_len = min(len(self._received_bytes), self._buffer_size) + if data_len == 0: + break + self._decrypt_buffers[0].cbBuffer = data_len + write_to_buffer(self._decrypt_data_buffer, self._received_bytes[0:data_len]) + + result = secur32.DecryptMessage( + self._context_handle_pointer, + self._decrypt_desc, + 0, + null() + ) + + do_read = False + + if result == Secur32Const.SEC_E_INCOMPLETE_MESSAGE: + _reset_buffers() + do_read = True + continue + + elif result == Secur32Const.SEC_I_CONTEXT_EXPIRED: + self._remote_closed = True + self.shutdown() + break + + elif result == Secur32Const.SEC_I_RENEGOTIATE: + self._handshake(renegotiate=True) + return self.read(max_length) + + elif result != Secur32Const.SEC_E_OK: + handle_error(result, TLSError) + + valid_buffer_types = set([ + Secur32Const.SECBUFFER_EMPTY, + Secur32Const.SECBUFFER_STREAM_HEADER, + Secur32Const.SECBUFFER_STREAM_TRAILER + ]) + extra_amount = None + for buf in (buf0, buf1, buf2, buf3): + buffer_type = buf.BufferType + if buffer_type == Secur32Const.SECBUFFER_DATA: + output += bytes_from_buffer(buf.pvBuffer, buf.cbBuffer) + output_len = len(output) + elif buffer_type == Secur32Const.SECBUFFER_EXTRA: + extra_amount = native(int, buf.cbBuffer) + elif buffer_type not in valid_buffer_types: + raise OSError(pretty_message( + ''' + Unexpected decrypt output buffer of type %s + ''', + buffer_type + )) + + if extra_amount: + self._received_bytes = self._received_bytes[data_len - extra_amount:] + else: + self._received_bytes = self._received_bytes[data_len:] + + # Here we reset the structs for the next call to DecryptMessage() + _reset_buffers() + + # If we have read something, but there is nothing left to read, we + # break so that we don't block for longer than necessary + if self.select_read(0): + do_read = True + + if not do_read and len(self._received_bytes) == 0: + break + + # If the output is more than we requested (because data is decrypted in + # blocks), we save the extra in a buffer + if len(output) > max_length: + self._decrypted_bytes = output[max_length:] + output = output[0:max_length] + + return output + + def select_read(self, timeout=None): + """ + Blocks until the socket is ready to be read from, or the timeout is hit + + :param timeout: + A float - the period of time to wait for data to be read. None for + no time limit. + + :return: + A boolean - if data is ready to be read. Will only be False if + timeout is not None. + """ + + # If we have buffered data, we consider a read possible + if len(self._decrypted_bytes) > 0: + return True + + read_ready, _, _ = select.select([self._socket], [], [], timeout) + return len(read_ready) > 0 + + def read_until(self, marker): + """ + Reads data from the socket until a marker is found. Data read may + include data beyond the marker. + + :param marker: + A byte string or regex object from re.compile(). Used to determine + when to stop reading. Regex objects are more inefficient since + they must scan the entire byte string of read data each time data + is read off the socket. + + :return: + A byte string of the data read + """ + + if not isinstance(marker, byte_cls) and not isinstance(marker, Pattern): + raise TypeError(pretty_message( + ''' + marker must be a byte string or compiled regex object, not %s + ''', + type_name(marker) + )) + + output = b'' + + is_regex = isinstance(marker, Pattern) + + while True: + if len(self._decrypted_bytes) > 0: + chunk = self._decrypted_bytes + self._decrypted_bytes = b'' + else: + chunk = self.read(8192) + + offset = len(output) + output += chunk + + if is_regex: + match = marker.search(output) + if match is not None: + end = match.end() + break + else: + # If the marker was not found last time, we have to start + # at a position where the marker would have its final char + # in the newly read chunk + start = max(0, offset - len(marker) - 1) + match = output.find(marker, start) + if match != -1: + end = match + len(marker) + break + + self._decrypted_bytes = output[end:] + self._decrypted_bytes + return output[0:end] + + def read_line(self): + r""" + Reads a line from the socket, including the line ending of "\r\n", "\r", + or "\n" + + :return: + A byte string of the next line from the socket + """ + + return self.read_until(_line_regex) + + def read_exactly(self, num_bytes): + """ + Reads exactly the specified number of bytes from the socket + + :param num_bytes: + An integer - the exact number of bytes to read + + :return: + A byte string of the data that was read + """ + + output = b'' + remaining = num_bytes + while remaining > 0: + output += self.read(remaining) + remaining = num_bytes - len(output) + + return output + + def write(self, data): + """ + Writes data to the TLS-wrapped socket + + :param data: + A byte string to write to the socket + + :raises: + socket.socket - when a non-TLS socket error occurs + oscrypto.errors.TLSError - when a TLS-related error occurs + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + """ + + if self._context_handle_pointer is None: + self._raise_closed() + + if not self._encrypt_data_buffer: + self._encrypt_data_buffer = buffer_from_bytes(self._header_size + self._message_size + self._trailer_size) + self._encrypt_desc, self._encrypt_buffers = self._create_buffers(4) + + self._encrypt_buffers[0].BufferType = Secur32Const.SECBUFFER_STREAM_HEADER + self._encrypt_buffers[0].cbBuffer = self._header_size + self._encrypt_buffers[0].pvBuffer = cast(secur32, 'BYTE *', self._encrypt_data_buffer) + + self._encrypt_buffers[1].BufferType = Secur32Const.SECBUFFER_DATA + self._encrypt_buffers[1].pvBuffer = ref(self._encrypt_data_buffer, self._header_size) + + self._encrypt_buffers[2].BufferType = Secur32Const.SECBUFFER_STREAM_TRAILER + self._encrypt_buffers[2].cbBuffer = self._trailer_size + self._encrypt_buffers[2].pvBuffer = ref(self._encrypt_data_buffer, self._header_size + self._message_size) + + while len(data) > 0: + to_write = min(len(data), self._message_size) + write_to_buffer(self._encrypt_data_buffer, data[0:to_write], self._header_size) + + self._encrypt_buffers[1].cbBuffer = to_write + self._encrypt_buffers[2].pvBuffer = ref(self._encrypt_data_buffer, self._header_size + to_write) + + result = secur32.EncryptMessage( + self._context_handle_pointer, + 0, + self._encrypt_desc, + 0 + ) + + if result != Secur32Const.SEC_E_OK: + handle_error(result, TLSError) + + to_send = native(int, self._encrypt_buffers[0].cbBuffer) + to_send += native(int, self._encrypt_buffers[1].cbBuffer) + to_send += native(int, self._encrypt_buffers[2].cbBuffer) + try: + self._socket.send(bytes_from_buffer(self._encrypt_data_buffer, to_send)) + except (socket_.error) as e: + if e.errno == 10053: + raise_disconnection() + raise + + data = data[to_send:] + + def select_write(self, timeout=None): + """ + Blocks until the socket is ready to be written to, or the timeout is hit + + :param timeout: + A float - the period of time to wait for the socket to be ready to + written to. None for no time limit. + + :return: + A boolean - if the socket is ready for writing. Will only be False + if timeout is not None. + """ + + _, write_ready, _ = select.select([], [self._socket], [], timeout) + return len(write_ready) > 0 + + def shutdown(self): + """ + Shuts down the TLS session and then shuts down the underlying socket + + :raises: + OSError - when an error is returned by the OS crypto library + """ + + if self._context_handle_pointer is None: + return + + out_buffers = None + try: + # ApplyControlToken fails with SEC_E_UNSUPPORTED_FUNCTION + # when called on Windows 7 + if _win_version_info >= (6, 2): + buffers = new(secur32, 'SecBuffer[1]') + + # This is a SCHANNEL_SHUTDOWN token (DWORD of 1) + buffers[0].cbBuffer = 4 + buffers[0].BufferType = Secur32Const.SECBUFFER_TOKEN + buffers[0].pvBuffer = cast(secur32, 'BYTE *', buffer_from_bytes(b'\x01\x00\x00\x00')) + + sec_buffer_desc_pointer = struct(secur32, 'SecBufferDesc') + sec_buffer_desc = unwrap(sec_buffer_desc_pointer) + + sec_buffer_desc.ulVersion = Secur32Const.SECBUFFER_VERSION + sec_buffer_desc.cBuffers = 1 + sec_buffer_desc.pBuffers = buffers + + result = secur32.ApplyControlToken(self._context_handle_pointer, sec_buffer_desc_pointer) + handle_error(result, TLSError) + + out_sec_buffer_desc_pointer, out_buffers = self._create_buffers(2) + out_buffers[0].BufferType = Secur32Const.SECBUFFER_TOKEN + out_buffers[1].BufferType = Secur32Const.SECBUFFER_ALERT + + output_context_flags_pointer = new(secur32, 'ULONG *') + + result = secur32.InitializeSecurityContextW( + self._session._credentials_handle, + self._context_handle_pointer, + self._hostname, + self._context_flags, + 0, + 0, + null(), + 0, + null(), + out_sec_buffer_desc_pointer, + output_context_flags_pointer, + null() + ) + acceptable_results = set([ + Secur32Const.SEC_E_OK, + Secur32Const.SEC_E_CONTEXT_EXPIRED, + Secur32Const.SEC_I_CONTINUE_NEEDED + ]) + if result not in acceptable_results: + handle_error(result, TLSError) + + token = bytes_from_buffer(out_buffers[0].pvBuffer, out_buffers[0].cbBuffer) + try: + # If there is an error sending the shutdown, ignore it since the + # connection is likely gone at this point + self._socket.send(token) + except (socket_.error): + pass + + finally: + if out_buffers: + if not is_null(out_buffers[0].pvBuffer): + secur32.FreeContextBuffer(out_buffers[0].pvBuffer) + if not is_null(out_buffers[1].pvBuffer): + secur32.FreeContextBuffer(out_buffers[1].pvBuffer) + + secur32.DeleteSecurityContext(self._context_handle_pointer) + self._context_handle_pointer = None + + try: + self._socket.shutdown(socket_.SHUT_RDWR) + except (socket_.error): + pass + + def close(self): + """ + Shuts down the TLS session and socket and forcibly closes it + """ + + try: + self.shutdown() + + finally: + if self._socket: + try: + self._socket.close() + except (socket_.error): + pass + self._socket = None + + def _read_certificates(self): + """ + Reads end-entity and intermediate certificate information from the + TLS session + """ + + cert_context_pointer_pointer = new(crypt32, 'CERT_CONTEXT **') + result = secur32.QueryContextAttributesW( + self._context_handle_pointer, + Secur32Const.SECPKG_ATTR_REMOTE_CERT_CONTEXT, + cert_context_pointer_pointer + ) + handle_error(result, TLSError) + + cert_context_pointer = unwrap(cert_context_pointer_pointer) + cert_context_pointer = cast(crypt32, 'CERT_CONTEXT *', cert_context_pointer) + cert_context = unwrap(cert_context_pointer) + + cert_data = bytes_from_buffer(cert_context.pbCertEncoded, native(int, cert_context.cbCertEncoded)) + self._certificate = Asn1Certificate.load(cert_data) + + self._intermediates = [] + + store_handle = None + try: + store_handle = cert_context.hCertStore + context_pointer = crypt32.CertEnumCertificatesInStore(store_handle, null()) + while not is_null(context_pointer): + context = unwrap(context_pointer) + data = bytes_from_buffer(context.pbCertEncoded, native(int, context.cbCertEncoded)) + # The cert store seems to include the end-entity certificate as + # the last entry, but we already have that from the struct. + if data != cert_data: + self._intermediates.append(Asn1Certificate.load(data)) + context_pointer = crypt32.CertEnumCertificatesInStore(store_handle, context_pointer) + + finally: + if store_handle: + crypt32.CertCloseStore(store_handle, 0) + + def _raise_closed(self): + """ + Raises an exception describing if the local or remote end closed the + connection + """ + + if self._remote_closed: + raise TLSGracefulDisconnectError('The remote end closed the connection') + else: + raise TLSDisconnectError('The connection was already closed') + + @property + def certificate(self): + """ + An asn1crypto.x509.Certificate object of the end-entity certificate + presented by the server + """ + + if self._context_handle_pointer is None: + self._raise_closed() + + if self._certificate is None: + self._read_certificates() + + return self._certificate + + @property + def intermediates(self): + """ + A list of asn1crypto.x509.Certificate objects that were presented as + intermediates by the server + """ + + if self._context_handle_pointer is None: + self._raise_closed() + + if self._certificate is None: + self._read_certificates() + + return self._intermediates + + @property + def cipher_suite(self): + """ + A unicode string of the IANA cipher suite name of the negotiated + cipher suite + """ + + return self._cipher_suite + + @property + def protocol(self): + """ + A unicode string of: "TLSv1.2", "TLSv1.1", "TLSv1", "SSLv3" + """ + + return self._protocol + + @property + def compression(self): + """ + A boolean if compression is enabled + """ + + return self._compression + + @property + def session_id(self): + """ + A unicode string of "new" or "reused" or None for no ticket + """ + + return self._session_id + + @property + def session_ticket(self): + """ + A unicode string of "new" or "reused" or None for no ticket + """ + + return self._session_ticket + + @property + def session(self): + """ + The oscrypto.tls.TLSSession object used for this connection + """ + + return self._session + + @property + def hostname(self): + """ + A unicode string of the TLS server domain name or IP address + """ + + return self._hostname + + @property + def port(self): + """ + An integer of the port number the socket is connected to + """ + + return self.socket.getpeername()[1] + + @property + def socket(self): + """ + The underlying socket.socket connection + """ + + if self._context_handle_pointer is None: + self._raise_closed() + + return self._socket + + def __del__(self): + self.close() diff --git a/tasks/lib/package_control/deps/oscrypto/_win/trust_list.py b/tasks/lib/package_control/deps/oscrypto/_win/trust_list.py new file mode 100644 index 0000000..6bd99e3 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_win/trust_list.py @@ -0,0 +1,226 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import datetime +import hashlib +import struct + +from .._asn1 import Certificate +from .._ffi import ( + array_from_pointer, + buffer_from_bytes, + bytes_from_buffer, + cast, + deref, + is_null, + new, + null, + struct_from_buffer, + unwrap, +) +from ._crypt32 import crypt32, Crypt32Const, get_error, handle_error +from .._types import str_cls + + +__all__ = [ + 'extract_from_system', + 'system_path', +] + + +def system_path(): + return None + + +def extract_from_system(cert_callback=None, callback_only_on_failure=False): + """ + Extracts trusted CA certificates from the Windows certificate store + + :param cert_callback: + A callback that is called once for each certificate in the trust store. + It should accept two parameters: an asn1crypto.x509.Certificate object, + and a reason. The reason will be None if the certificate is being + exported, otherwise it will be a unicode string of the reason it won't. + + :param callback_only_on_failure: + A boolean - if the callback should only be called when a certificate is + not exported. + + :raises: + OSError - when an error is returned by the OS crypto library + + :return: + A list of 3-element tuples: + - 0: a byte string of a DER-encoded certificate + - 1: a set of unicode strings that are OIDs of purposes to trust the + certificate for + - 2: a set of unicode strings that are OIDs of purposes to reject the + certificate for + """ + + certificates = {} + processed = {} + + now = datetime.datetime.utcnow() + + for store in ["ROOT", "CA"]: + store_handle = crypt32.CertOpenSystemStoreW(null(), store) + handle_error(store_handle) + + context_pointer = null() + while True: + context_pointer = crypt32.CertEnumCertificatesInStore(store_handle, context_pointer) + if is_null(context_pointer): + break + context = unwrap(context_pointer) + + trust_all = False + data = None + digest = None + + if context.dwCertEncodingType != Crypt32Const.X509_ASN_ENCODING: + continue + + data = bytes_from_buffer(context.pbCertEncoded, int(context.cbCertEncoded)) + digest = hashlib.sha1(data).digest() + if digest in processed: + continue + + processed[digest] = True + cert_info = unwrap(context.pCertInfo) + + not_before_seconds = _convert_filetime_to_timestamp(cert_info.NotBefore) + try: + not_before = datetime.datetime.fromtimestamp(not_before_seconds) + if not_before > now: + if cert_callback: + cert_callback(Certificate.load(data), 'not yet valid') + continue + except (ValueError, OSError): + # If there is an error converting the not before timestamp, + # it is almost certainly because it is from too long ago, + # which means the cert is definitely valid by now. + pass + + not_after_seconds = _convert_filetime_to_timestamp(cert_info.NotAfter) + try: + not_after = datetime.datetime.fromtimestamp(not_after_seconds) + if not_after < now: + if cert_callback: + cert_callback(Certificate.load(data), 'no longer valid') + continue + except (ValueError, OSError) as e: + # The only reason we would get an exception here is if the + # expiration time is so far in the future that it can't be + # used as a timestamp, or it is before 0. If it is very far + # in the future, the cert is still valid, so we only raise + # an exception if the timestamp is less than zero. + if not_after_seconds < 0: + message = e.args[0] + ' - ' + str_cls(not_after_seconds) + e.args = (message,) + e.args[1:] + raise e + + trust_oids = set() + reject_oids = set() + + # Here we grab the extended key usage properties that Windows + # layers on top of the extended key usage extension that is + # part of the certificate itself. For highest security, users + # should only use certificates for the intersection of the two + # lists of purposes. However, many seen to treat the OS trust + # list as an override. + to_read = new(crypt32, 'DWORD *', 0) + res = crypt32.CertGetEnhancedKeyUsage( + context_pointer, + Crypt32Const.CERT_FIND_PROP_ONLY_ENHKEY_USAGE_FLAG, + null(), + to_read + ) + + # Per the Microsoft documentation, if CRYPT_E_NOT_FOUND is returned + # from get_error(), it means the certificate is valid for all purposes + error_code, _ = get_error() + if not res and error_code != Crypt32Const.CRYPT_E_NOT_FOUND: + handle_error(res) + + if error_code == Crypt32Const.CRYPT_E_NOT_FOUND: + trust_all = True + else: + usage_buffer = buffer_from_bytes(deref(to_read)) + res = crypt32.CertGetEnhancedKeyUsage( + context_pointer, + Crypt32Const.CERT_FIND_PROP_ONLY_ENHKEY_USAGE_FLAG, + cast(crypt32, 'CERT_ENHKEY_USAGE *', usage_buffer), + to_read + ) + handle_error(res) + + key_usage_pointer = struct_from_buffer(crypt32, 'CERT_ENHKEY_USAGE', usage_buffer) + key_usage = unwrap(key_usage_pointer) + + # Having no enhanced usage properties means a cert is distrusted + if key_usage.cUsageIdentifier == 0: + if cert_callback: + cert_callback(Certificate.load(data), 'explicitly distrusted') + continue + + oids = array_from_pointer( + crypt32, + 'LPCSTR', + key_usage.rgpszUsageIdentifier, + key_usage.cUsageIdentifier + ) + for oid in oids: + trust_oids.add(oid.decode('ascii')) + + cert = None + + # If the certificate is not under blanket trust, we have to + # determine what purposes it is rejected for by diffing the + # set of OIDs from the certificate with the OIDs that are + # trusted. + if not trust_all: + cert = Certificate.load(data) + if cert.extended_key_usage_value: + for cert_oid in cert.extended_key_usage_value: + oid = cert_oid.dotted + if oid not in trust_oids: + reject_oids.add(oid) + + if cert_callback and not callback_only_on_failure: + if cert is None: + cert = Certificate.load(data) + cert_callback(cert, None) + + certificates[digest] = (data, trust_oids, reject_oids) + + result = crypt32.CertCloseStore(store_handle, 0) + handle_error(result) + store_handle = None + + return certificates.values() + + +def _convert_filetime_to_timestamp(filetime): + """ + Windows returns times as 64-bit unsigned longs that are the number + of hundreds of nanoseconds since Jan 1 1601. This converts it to + a datetime object. + + :param filetime: + A FILETIME struct object + + :return: + An integer unix timestamp + """ + + hundreds_nano_seconds = struct.unpack( + b'>Q', + struct.pack( + b'>LL', + filetime.dwHighDateTime, + filetime.dwLowDateTime + ) + )[0] + seconds_since_1601 = hundreds_nano_seconds / 10000000 + return seconds_since_1601 - 11644473600 # Seconds from Jan 1 1601 to Jan 1 1970 diff --git a/tasks/lib/package_control/deps/oscrypto/_win/util.py b/tasks/lib/package_control/deps/oscrypto/_win/util.py new file mode 100644 index 0000000..610ca5b --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/_win/util.py @@ -0,0 +1,180 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from .. import backend +from .._errors import pretty_message +from .._ffi import buffer_from_bytes, bytes_from_buffer +from .._pkcs12 import pkcs12_kdf +from .._types import type_name, byte_cls, int_types + + +__all__ = [ + 'pbkdf2', + 'pkcs12_kdf', + 'rand_bytes', +] + + +_backend = backend() + + +if _backend == 'win': + from ._cng import bcrypt, BcryptConst, handle_error, open_alg_handle, close_alg_handle + + def pbkdf2(hash_algorithm, password, salt, iterations, key_length): + """ + PBKDF2 from PKCS#5 + + :param hash_algorithm: + The string name of the hash algorithm to use: "sha1", "sha256", "sha384", "sha512" + + :param password: + A byte string of the password to use an input to the KDF + + :param salt: + A cryptographic random byte string + + :param iterations: + The numbers of iterations to use when deriving the key + + :param key_length: + The length of the desired key in bytes + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + The derived key as a byte string + """ + + if not isinstance(password, byte_cls): + raise TypeError(pretty_message( + ''' + password must be a byte string, not %s + ''', + type_name(password) + )) + + if not isinstance(salt, byte_cls): + raise TypeError(pretty_message( + ''' + salt must be a byte string, not %s + ''', + type_name(salt) + )) + + if not isinstance(iterations, int_types): + raise TypeError(pretty_message( + ''' + iterations must be an integer, not %s + ''', + type_name(iterations) + )) + + if iterations < 1: + raise ValueError('iterations must be greater than 0') + + if not isinstance(key_length, int_types): + raise TypeError(pretty_message( + ''' + key_length must be an integer, not %s + ''', + type_name(key_length) + )) + + if key_length < 1: + raise ValueError('key_length must be greater than 0') + + if hash_algorithm not in set(['sha1', 'sha256', 'sha384', 'sha512']): + raise ValueError(pretty_message( + ''' + hash_algorithm must be one of "sha1", "sha256", "sha384", "sha512", + not %s + ''', + repr(hash_algorithm) + )) + + alg_constant = { + 'sha1': BcryptConst.BCRYPT_SHA1_ALGORITHM, + 'sha256': BcryptConst.BCRYPT_SHA256_ALGORITHM, + 'sha384': BcryptConst.BCRYPT_SHA384_ALGORITHM, + 'sha512': BcryptConst.BCRYPT_SHA512_ALGORITHM + }[hash_algorithm] + + alg_handle = None + + try: + alg_handle = open_alg_handle(alg_constant, BcryptConst.BCRYPT_ALG_HANDLE_HMAC_FLAG) + + output_buffer = buffer_from_bytes(key_length) + res = bcrypt.BCryptDeriveKeyPBKDF2( + alg_handle, + password, + len(password), + salt, + len(salt), + iterations, + output_buffer, + key_length, + 0 + ) + handle_error(res) + + return bytes_from_buffer(output_buffer) + finally: + if alg_handle: + close_alg_handle(alg_handle) + + pbkdf2.pure_python = False + + def rand_bytes(length): + """ + Returns a number of random bytes suitable for cryptographic purposes + + :param length: + The desired number of bytes + + :raises: + ValueError - when any of the parameters contain an invalid value + TypeError - when any of the parameters are of the wrong type + OSError - when an error is returned by the OS crypto library + + :return: + A byte string + """ + + if not isinstance(length, int_types): + raise TypeError(pretty_message( + ''' + length must be an integer, not %s + ''', + type_name(length) + )) + + if length < 1: + raise ValueError('length must be greater than 0') + + if length > 1024: + raise ValueError('length must not be greater than 1024') + + alg_handle = None + + try: + alg_handle = open_alg_handle(BcryptConst.BCRYPT_RNG_ALGORITHM) + buffer = buffer_from_bytes(length) + + res = bcrypt.BCryptGenRandom(alg_handle, buffer, length, 0) + handle_error(res) + + return bytes_from_buffer(buffer) + + finally: + if alg_handle: + close_alg_handle(alg_handle) + +# winlegacy backend +else: + from .._pkcs5 import pbkdf2 + from .._rand import rand_bytes diff --git a/tasks/lib/package_control/deps/oscrypto/asymmetric.py b/tasks/lib/package_control/deps/oscrypto/asymmetric.py new file mode 100644 index 0000000..353d2fd --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/asymmetric.py @@ -0,0 +1,458 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import hashlib +import binascii + +from . import backend +from ._asn1 import ( + armor, + Certificate as Asn1Certificate, + DHParameters, + EncryptedPrivateKeyInfo, + Null, + OrderedDict, + Pbkdf2Salt, + PrivateKeyInfo, + PublicKeyInfo, +) +from ._asymmetric import _unwrap_private_key_info +from ._errors import pretty_message +from ._types import type_name, str_cls +from .kdf import pbkdf2, pbkdf2_iteration_calculator +from .symmetric import aes_cbc_pkcs7_encrypt +from .util import rand_bytes + + +_backend = backend() + + +if _backend == 'mac': + from ._mac.asymmetric import ( + Certificate, + dsa_sign, + dsa_verify, + ecdsa_sign, + ecdsa_verify, + generate_pair, + generate_dh_parameters, + load_certificate, + load_pkcs12, + load_private_key, + load_public_key, + PrivateKey, + PublicKey, + rsa_pkcs1v15_sign, + rsa_pkcs1v15_verify, + rsa_pss_sign, + rsa_pss_verify, + rsa_pkcs1v15_encrypt, + rsa_pkcs1v15_decrypt, + rsa_oaep_encrypt, + rsa_oaep_decrypt, + ) + +elif _backend == 'win' or _backend == 'winlegacy': + from ._win.asymmetric import ( + Certificate, + dsa_sign, + dsa_verify, + ecdsa_sign, + ecdsa_verify, + generate_pair, + generate_dh_parameters, + load_certificate, + load_pkcs12, + load_private_key, + load_public_key, + PrivateKey, + PublicKey, + rsa_pkcs1v15_sign, + rsa_pkcs1v15_verify, + rsa_pss_sign, + rsa_pss_verify, + rsa_pkcs1v15_encrypt, + rsa_pkcs1v15_decrypt, + rsa_oaep_encrypt, + rsa_oaep_decrypt, + ) + +else: + from ._openssl.asymmetric import ( + Certificate, + dsa_sign, + dsa_verify, + ecdsa_sign, + ecdsa_verify, + generate_pair, + generate_dh_parameters, + load_certificate, + load_pkcs12, + load_private_key, + load_public_key, + PrivateKey, + PublicKey, + rsa_pkcs1v15_sign, + rsa_pkcs1v15_verify, + rsa_pss_sign, + rsa_pss_verify, + rsa_pkcs1v15_encrypt, + rsa_pkcs1v15_decrypt, + rsa_oaep_encrypt, + rsa_oaep_decrypt, + ) + + +__all__ = [ + 'Certificate', + 'dsa_sign', + 'dsa_verify', + 'dump_certificate', + 'dump_dh_parameters', + 'dump_openssl_private_key', + 'dump_private_key', + 'dump_public_key', + 'ecdsa_sign', + 'ecdsa_verify', + 'generate_pair', + 'generate_dh_parameters', + 'load_certificate', + 'load_pkcs12', + 'load_private_key', + 'load_public_key', + 'PrivateKey', + 'PublicKey', + 'rsa_oaep_decrypt', + 'rsa_oaep_encrypt', + 'rsa_pkcs1v15_decrypt', + 'rsa_pkcs1v15_encrypt', + 'rsa_pkcs1v15_sign', + 'rsa_pkcs1v15_verify', + 'rsa_pss_sign', + 'rsa_pss_verify', +] + + +def dump_dh_parameters(dh_parameters, encoding='pem'): + """ + Serializes an asn1crypto.algos.DHParameters object into a byte string + + :param dh_parameters: + An asn1crypto.algos.DHParameters object + + :param encoding: + A unicode string of "pem" or "der" + + :return: + A byte string of the encoded DH parameters + """ + + if encoding not in set(['pem', 'der']): + raise ValueError(pretty_message( + ''' + encoding must be one of "pem", "der", not %s + ''', + repr(encoding) + )) + + if not isinstance(dh_parameters, DHParameters): + raise TypeError(pretty_message( + ''' + dh_parameters must be an instance of asn1crypto.algos.DHParameters, + not %s + ''', + type_name(dh_parameters) + )) + + output = dh_parameters.dump() + if encoding == 'pem': + output = armor('DH PARAMETERS', output) + return output + + +def dump_public_key(public_key, encoding='pem'): + """ + Serializes a public key object into a byte string + + :param public_key: + An oscrypto.asymmetric.PublicKey or asn1crypto.keys.PublicKeyInfo object + + :param encoding: + A unicode string of "pem" or "der" + + :return: + A byte string of the encoded public key + """ + + if encoding not in set(['pem', 'der']): + raise ValueError(pretty_message( + ''' + encoding must be one of "pem", "der", not %s + ''', + repr(encoding) + )) + + is_oscrypto = isinstance(public_key, PublicKey) + if not isinstance(public_key, PublicKeyInfo) and not is_oscrypto: + raise TypeError(pretty_message( + ''' + public_key must be an instance of oscrypto.asymmetric.PublicKey or + asn1crypto.keys.PublicKeyInfo, not %s + ''', + type_name(public_key) + )) + + if is_oscrypto: + public_key = public_key.asn1 + + output = public_key.dump() + if encoding == 'pem': + output = armor('PUBLIC KEY', output) + return output + + +def dump_certificate(certificate, encoding='pem'): + """ + Serializes a certificate object into a byte string + + :param certificate: + An oscrypto.asymmetric.Certificate or asn1crypto.x509.Certificate object + + :param encoding: + A unicode string of "pem" or "der" + + :return: + A byte string of the encoded certificate + """ + + if encoding not in set(['pem', 'der']): + raise ValueError(pretty_message( + ''' + encoding must be one of "pem", "der", not %s + ''', + repr(encoding) + )) + + is_oscrypto = isinstance(certificate, Certificate) + if not isinstance(certificate, Asn1Certificate) and not is_oscrypto: + raise TypeError(pretty_message( + ''' + certificate must be an instance of oscrypto.asymmetric.Certificate + or asn1crypto.x509.Certificate, not %s + ''', + type_name(certificate) + )) + + if is_oscrypto: + certificate = certificate.asn1 + + output = certificate.dump() + if encoding == 'pem': + output = armor('CERTIFICATE', output) + return output + + +def dump_private_key(private_key, passphrase, encoding='pem', target_ms=200): + """ + Serializes a private key object into a byte string of the PKCS#8 format + + :param private_key: + An oscrypto.asymmetric.PrivateKey or asn1crypto.keys.PrivateKeyInfo + object + + :param passphrase: + A unicode string of the passphrase to encrypt the private key with. + A passphrase of None will result in no encryption. A blank string will + result in a ValueError to help ensure that the lack of passphrase is + intentional. + + :param encoding: + A unicode string of "pem" or "der" + + :param target_ms: + Use PBKDF2 with the number of iterations that takes about this many + milliseconds on the current machine. + + :raises: + ValueError - when a blank string is provided for the passphrase + + :return: + A byte string of the encoded and encrypted public key + """ + + if encoding not in set(['pem', 'der']): + raise ValueError(pretty_message( + ''' + encoding must be one of "pem", "der", not %s + ''', + repr(encoding) + )) + + if passphrase is not None: + if not isinstance(passphrase, str_cls): + raise TypeError(pretty_message( + ''' + passphrase must be a unicode string, not %s + ''', + type_name(passphrase) + )) + if passphrase == '': + raise ValueError(pretty_message( + ''' + passphrase may not be a blank string - pass None to disable + encryption + ''' + )) + + is_oscrypto = isinstance(private_key, PrivateKey) + if not isinstance(private_key, PrivateKeyInfo) and not is_oscrypto: + raise TypeError(pretty_message( + ''' + private_key must be an instance of oscrypto.asymmetric.PrivateKey + or asn1crypto.keys.PrivateKeyInfo, not %s + ''', + type_name(private_key) + )) + + if is_oscrypto: + private_key = private_key.asn1 + + output = private_key.dump() + + if passphrase is not None: + cipher = 'aes256_cbc' + key_length = 32 + kdf_hmac = 'sha256' + kdf_salt = rand_bytes(key_length) + iterations = pbkdf2_iteration_calculator(kdf_hmac, key_length, target_ms=target_ms, quiet=True) + # Need a bare minimum of 10,000 iterations for PBKDF2 as of 2015 + if iterations < 10000: + iterations = 10000 + + passphrase_bytes = passphrase.encode('utf-8') + key = pbkdf2(kdf_hmac, passphrase_bytes, kdf_salt, iterations, key_length) + iv, ciphertext = aes_cbc_pkcs7_encrypt(key, output, None) + + output = EncryptedPrivateKeyInfo({ + 'encryption_algorithm': { + 'algorithm': 'pbes2', + 'parameters': { + 'key_derivation_func': { + 'algorithm': 'pbkdf2', + 'parameters': { + 'salt': Pbkdf2Salt( + name='specified', + value=kdf_salt + ), + 'iteration_count': iterations, + 'prf': { + 'algorithm': kdf_hmac, + 'parameters': Null() + } + } + }, + 'encryption_scheme': { + 'algorithm': cipher, + 'parameters': iv + } + } + }, + 'encrypted_data': ciphertext + }).dump() + + if encoding == 'pem': + if passphrase is None: + object_type = 'PRIVATE KEY' + else: + object_type = 'ENCRYPTED PRIVATE KEY' + output = armor(object_type, output) + + return output + + +def dump_openssl_private_key(private_key, passphrase): + """ + Serializes a private key object into a byte string of the PEM formats used + by OpenSSL. The format chosen will depend on the type of private key - RSA, + DSA or EC. + + Do not use this method unless you really must interact with a system that + does not support PKCS#8 private keys. The encryption provided by PKCS#8 is + far superior to the OpenSSL formats. This is due to the fact that the + OpenSSL formats don't stretch the passphrase, making it very easy to + brute-force. + + :param private_key: + An oscrypto.asymmetric.PrivateKey or asn1crypto.keys.PrivateKeyInfo + object + + :param passphrase: + A unicode string of the passphrase to encrypt the private key with. + A passphrase of None will result in no encryption. A blank string will + result in a ValueError to help ensure that the lack of passphrase is + intentional. + + :raises: + ValueError - when a blank string is provided for the passphrase + + :return: + A byte string of the encoded and encrypted public key + """ + + if passphrase is not None: + if not isinstance(passphrase, str_cls): + raise TypeError(pretty_message( + ''' + passphrase must be a unicode string, not %s + ''', + type_name(passphrase) + )) + if passphrase == '': + raise ValueError(pretty_message( + ''' + passphrase may not be a blank string - pass None to disable + encryption + ''' + )) + + is_oscrypto = isinstance(private_key, PrivateKey) + if not isinstance(private_key, PrivateKeyInfo) and not is_oscrypto: + raise TypeError(pretty_message( + ''' + private_key must be an instance of oscrypto.asymmetric.PrivateKey or + asn1crypto.keys.PrivateKeyInfo, not %s + ''', + type_name(private_key) + )) + + if is_oscrypto: + private_key = private_key.asn1 + + output = _unwrap_private_key_info(private_key).dump() + + headers = None + if passphrase is not None: + iv = rand_bytes(16) + + headers = OrderedDict() + headers['Proc-Type'] = '4,ENCRYPTED' + headers['DEK-Info'] = 'AES-128-CBC,%s' % binascii.hexlify(iv).decode('ascii') + + key_length = 16 + passphrase_bytes = passphrase.encode('utf-8') + + key = hashlib.md5(passphrase_bytes + iv[0:8]).digest() + while key_length > len(key): + key += hashlib.md5(key + passphrase_bytes + iv[0:8]).digest() + key = key[0:key_length] + + iv, output = aes_cbc_pkcs7_encrypt(key, output, iv) + + if private_key.algorithm == 'ec': + object_type = 'EC PRIVATE KEY' + elif private_key.algorithm == 'rsa': + object_type = 'RSA PRIVATE KEY' + elif private_key.algorithm == 'dsa': + object_type = 'DSA PRIVATE KEY' + + return armor(object_type, output, headers=headers) diff --git a/tasks/lib/package_control/deps/oscrypto/errors.py b/tasks/lib/package_control/deps/oscrypto/errors.py new file mode 100644 index 0000000..581b76f --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/errors.py @@ -0,0 +1,111 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys +import socket + + +__all__ = [ + 'AsymmetricKeyError', + 'CACertsError', + 'LibraryNotFoundError', + 'SignatureError', + 'TLSError', + 'TLSConnectionError', + 'TLSDisconnectError', + 'TLSGracefulDisconnectError', + 'TLSVerificationError', +] + + +class LibraryNotFoundError(Exception): + + """ + An exception when trying to find a shared library + """ + + pass + + +class SignatureError(Exception): + + """ + An exception when validating a signature + """ + + pass + + +class AsymmetricKeyError(Exception): + + """ + An exception when a key is invalid or unsupported + """ + + pass + + +class IncompleteAsymmetricKeyError(AsymmetricKeyError): + + """ + An exception when a key is missing necessary information + """ + + pass + + +class CACertsError(Exception): + + """ + An exception when exporting CA certs from the OS trust store + """ + + pass + + +class TLSError(socket.error): + + """ + An exception related to TLS functionality + """ + + message = None + + def __init__(self, message): + self.args = (message,) + self.message = message + + def __str__(self): + output = self.__unicode__() + if sys.version_info < (3,): + output = output.encode('utf-8') + return output + + def __unicode__(self): + return self.message + + +class TLSConnectionError(TLSError): + pass + + +class TLSDisconnectError(TLSConnectionError): + pass + + +class TLSGracefulDisconnectError(TLSDisconnectError): + pass + + +class TLSVerificationError(TLSError): + + """ + A server certificate verification error happened during a TLS handshake + """ + + certificate = None + + def __init__(self, message, certificate): + TLSError.__init__(self, message) + self.certificate = certificate + self.args = (message, certificate) diff --git a/tasks/lib/package_control/deps/oscrypto/kdf.py b/tasks/lib/package_control/deps/oscrypto/kdf.py new file mode 100644 index 0000000..5db07cd --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/kdf.py @@ -0,0 +1,261 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys +import hashlib +from datetime import datetime + +from . import backend +from .util import rand_bytes +from ._types import type_name, byte_cls, int_types +from ._errors import pretty_message +from ._ffi import new, deref + + +_backend = backend() + + +if _backend == 'mac': + from ._mac.util import pbkdf2, pkcs12_kdf +elif _backend == 'win' or _backend == 'winlegacy': + from ._win.util import pbkdf2, pkcs12_kdf + from ._win._kernel32 import kernel32, handle_error +else: + from ._openssl.util import pbkdf2, pkcs12_kdf + + +__all__ = [ + 'pbkdf1', + 'pbkdf2', + 'pbkdf2_iteration_calculator', + 'pkcs12_kdf', +] + + +if sys.platform == 'win32': + def _get_start(): + number = new(kernel32, 'LARGE_INTEGER *') + res = kernel32.QueryPerformanceCounter(number) + handle_error(res) + return deref(number) + + def _get_elapsed(start): + length = _get_start() - start + return int(length / 1000.0) + +else: + def _get_start(): + return datetime.now() + + def _get_elapsed(start): + length = datetime.now() - start + seconds = length.seconds + (length.days * 24 * 3600) + milliseconds = (length.microseconds / 10 ** 3) + return int(milliseconds + (seconds * 10 ** 3)) + + +def pbkdf2_iteration_calculator(hash_algorithm, key_length, target_ms=100, quiet=False): + """ + Runs pbkdf2() twice to determine the approximate number of iterations to + use to hit a desired time per run. Use this on a production machine to + dynamically adjust the number of iterations as high as you can. + + :param hash_algorithm: + The string name of the hash algorithm to use: "md5", "sha1", "sha224", + "sha256", "sha384", "sha512" + + :param key_length: + The length of the desired key in bytes + + :param target_ms: + The number of milliseconds the derivation should take + + :param quiet: + If no output should be printed as attempts are made + + :return: + An integer number of iterations of PBKDF2 using the specified hash + that will take at least target_ms + """ + + if hash_algorithm not in set(['sha1', 'sha224', 'sha256', 'sha384', 'sha512']): + raise ValueError(pretty_message( + ''' + hash_algorithm must be one of "sha1", "sha224", "sha256", "sha384", + "sha512", not %s + ''', + repr(hash_algorithm) + )) + + if not isinstance(key_length, int_types): + raise TypeError(pretty_message( + ''' + key_length must be an integer, not %s + ''', + type_name(key_length) + )) + + if key_length < 1: + raise ValueError(pretty_message( + ''' + key_length must be greater than 0 - is %s + ''', + repr(key_length) + )) + + if not isinstance(target_ms, int_types): + raise TypeError(pretty_message( + ''' + target_ms must be an integer, not %s + ''', + type_name(target_ms) + )) + + if target_ms < 1: + raise ValueError(pretty_message( + ''' + target_ms must be greater than 0 - is %s + ''', + repr(target_ms) + )) + + if pbkdf2.pure_python: + raise OSError(pretty_message( + ''' + Only a very slow, pure-python version of PBKDF2 is available, + making this function useless + ''' + )) + + iterations = 10000 + password = 'this is a test'.encode('utf-8') + salt = rand_bytes(key_length) + + def _measure(): + start = _get_start() + pbkdf2(hash_algorithm, password, salt, iterations, key_length) + observed_ms = _get_elapsed(start) + if not quiet: + print('%s iterations in %sms' % (iterations, observed_ms)) + return 1.0 / target_ms * observed_ms + + # Measure the initial guess, then estimate how many iterations it would + # take to reach 1/2 of the target ms and try it to get a good final number + fraction = _measure() + iterations = int(iterations / fraction / 2.0) + + fraction = _measure() + iterations = iterations / fraction + + # < 20,000 round to 1000 + # 20,000-100,000 round to 5,000 + # > 100,000 round to 10,000 + round_factor = -3 if iterations < 100000 else -4 + result = int(round(iterations, round_factor)) + if result > 20000: + result = (result // 5000) * 5000 + return result + + +def pbkdf1(hash_algorithm, password, salt, iterations, key_length): + """ + An implementation of PBKDF1 - should only be used for interop with legacy + systems, not new architectures + + :param hash_algorithm: + The string name of the hash algorithm to use: "md2", "md5", "sha1" + + :param password: + A byte string of the password to use an input to the KDF + + :param salt: + A cryptographic random byte string + + :param iterations: + The numbers of iterations to use when deriving the key + + :param key_length: + The length of the desired key in bytes + + :return: + The derived key as a byte string + """ + + if not isinstance(password, byte_cls): + raise TypeError(pretty_message( + ''' + password must be a byte string, not %s + ''', + (type_name(password)) + )) + + if not isinstance(salt, byte_cls): + raise TypeError(pretty_message( + ''' + salt must be a byte string, not %s + ''', + (type_name(salt)) + )) + + if not isinstance(iterations, int_types): + raise TypeError(pretty_message( + ''' + iterations must be an integer, not %s + ''', + (type_name(iterations)) + )) + + if iterations < 1: + raise ValueError(pretty_message( + ''' + iterations must be greater than 0 - is %s + ''', + repr(iterations) + )) + + if not isinstance(key_length, int_types): + raise TypeError(pretty_message( + ''' + key_length must be an integer, not %s + ''', + (type_name(key_length)) + )) + + if key_length < 1: + raise ValueError(pretty_message( + ''' + key_length must be greater than 0 - is %s + ''', + repr(key_length) + )) + + if hash_algorithm not in set(['md2', 'md5', 'sha1']): + raise ValueError(pretty_message( + ''' + hash_algorithm must be one of "md2", "md5", "sha1", not %s + ''', + repr(hash_algorithm) + )) + + if key_length > 16 and hash_algorithm in set(['md2', 'md5']): + raise ValueError(pretty_message( + ''' + key_length can not be longer than 16 for %s - is %s + ''', + (hash_algorithm, repr(key_length)) + )) + + if key_length > 20 and hash_algorithm == 'sha1': + raise ValueError(pretty_message( + ''' + key_length can not be longer than 20 for sha1 - is %s + ''', + repr(key_length) + )) + + algo = getattr(hashlib, hash_algorithm) + output = algo(password + salt).digest() + for _ in range(2, iterations + 1): + output = algo(output).digest() + + return output[:key_length] diff --git a/tasks/lib/package_control/deps/oscrypto/keys.py b/tasks/lib/package_control/deps/oscrypto/keys.py new file mode 100644 index 0000000..0fd689b --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/keys.py @@ -0,0 +1,24 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from . import backend +from ._asymmetric import parse_certificate, parse_private, parse_public + + +_backend = backend() + + +if _backend == 'mac': + from ._mac.asymmetric import parse_pkcs12 +elif _backend == 'win' or _backend == 'winlegacy': + from ._win.asymmetric import parse_pkcs12 +else: + from ._openssl.asymmetric import parse_pkcs12 + + +__all__ = [ + 'parse_certificate', + 'parse_pkcs12', + 'parse_private', + 'parse_public', +] diff --git a/tasks/lib/package_control/deps/oscrypto/symmetric.py b/tasks/lib/package_control/deps/oscrypto/symmetric.py new file mode 100644 index 0000000..00b03c0 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/symmetric.py @@ -0,0 +1,72 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from . import backend + + +_backend = backend() + + +if _backend == 'mac': + from ._mac.symmetric import ( + aes_cbc_no_padding_decrypt, + aes_cbc_no_padding_encrypt, + aes_cbc_pkcs7_decrypt, + aes_cbc_pkcs7_encrypt, + des_cbc_pkcs5_decrypt, + des_cbc_pkcs5_encrypt, + rc2_cbc_pkcs5_decrypt, + rc2_cbc_pkcs5_encrypt, + rc4_decrypt, + rc4_encrypt, + tripledes_cbc_pkcs5_decrypt, + tripledes_cbc_pkcs5_encrypt, + ) + +elif _backend == 'win' or _backend == 'winlegacy': + from ._win.symmetric import ( + aes_cbc_no_padding_decrypt, + aes_cbc_no_padding_encrypt, + aes_cbc_pkcs7_decrypt, + aes_cbc_pkcs7_encrypt, + des_cbc_pkcs5_decrypt, + des_cbc_pkcs5_encrypt, + rc2_cbc_pkcs5_decrypt, + rc2_cbc_pkcs5_encrypt, + rc4_decrypt, + rc4_encrypt, + tripledes_cbc_pkcs5_decrypt, + tripledes_cbc_pkcs5_encrypt, + ) + +else: + from ._openssl.symmetric import ( + aes_cbc_no_padding_decrypt, + aes_cbc_no_padding_encrypt, + aes_cbc_pkcs7_decrypt, + aes_cbc_pkcs7_encrypt, + des_cbc_pkcs5_decrypt, + des_cbc_pkcs5_encrypt, + rc2_cbc_pkcs5_decrypt, + rc2_cbc_pkcs5_encrypt, + rc4_decrypt, + rc4_encrypt, + tripledes_cbc_pkcs5_decrypt, + tripledes_cbc_pkcs5_encrypt, + ) + + +__all__ = [ + 'aes_cbc_no_padding_decrypt', + 'aes_cbc_no_padding_encrypt', + 'aes_cbc_pkcs7_decrypt', + 'aes_cbc_pkcs7_encrypt', + 'des_cbc_pkcs5_decrypt', + 'des_cbc_pkcs5_encrypt', + 'rc2_cbc_pkcs5_decrypt', + 'rc2_cbc_pkcs5_encrypt', + 'rc4_decrypt', + 'rc4_encrypt', + 'tripledes_cbc_pkcs5_decrypt', + 'tripledes_cbc_pkcs5_encrypt', +] diff --git a/tasks/lib/package_control/deps/oscrypto/tls.py b/tasks/lib/package_control/deps/oscrypto/tls.py new file mode 100644 index 0000000..018b68c --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/tls.py @@ -0,0 +1,32 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +from . import backend + + +_backend = backend() + + +if _backend == 'mac': + from ._mac.tls import ( + TLSSession, + TLSSocket, + ) + +elif _backend == 'win' or _backend == 'winlegacy': + from ._win.tls import ( + TLSSession, + TLSSocket, + ) + +else: + from ._openssl.tls import ( + TLSSession, + TLSSocket, + ) + + +__all__ = [ + 'TLSSession', + 'TLSSocket', +] diff --git a/tasks/lib/package_control/deps/oscrypto/trust_list.py b/tasks/lib/package_control/deps/oscrypto/trust_list.py new file mode 100644 index 0000000..f07dfe4 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/trust_list.py @@ -0,0 +1,347 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import os +import time +import sys +import tempfile +import threading + +from ._asn1 import armor, Certificate +from ._errors import pretty_message +from .errors import CACertsError + +if sys.platform == 'win32': + from ._win.trust_list import extract_from_system, system_path +elif sys.platform == 'darwin': + from ._mac.trust_list import extract_from_system, system_path +else: + from ._linux_bsd.trust_list import extract_from_system, system_path + + +__all__ = [ + 'clear_cache', + 'get_list', + 'get_path', +] + + +path_lock = threading.Lock() +memory_lock = threading.Lock() +_module_values = { + 'last_update': None, + 'certs': None +} + +_oid_map = { + # apple_smime -> email_protection + '1.2.840.113635.100.1.8': set(['1.3.6.1.5.5.7.3.4']), + # apple_code_signing -> code_signing + '1.2.840.113635.100.1.16': set(['1.3.6.1.5.5.7.3.3']), + # apple_time_stamping -> time_stamping + '1.2.840.113635.100.1.20': set(['1.3.6.1.5.5.7.3.8']), + # microsoft_time_stamp_signing -> time_stamping + '1.3.6.1.4.1.311.10.3.2': set(['1.3.6.1.5.5.7.3.8']), + # apple_ssl -> (server_auth, client_auth) + '1.2.840.113635.100.1.3': set([ + '1.3.6.1.5.5.7.3.1', + '1.3.6.1.5.5.7.3.2', + ]), + # apple_eap -> (eap_over_ppp, eap_over_lan) + '1.2.840.113635.100.1.9': set([ + '1.3.6.1.5.5.7.3.13', + '1.3.6.1.5.5.7.3.14', + ]), + # apple_ipsec -> (ipsec_end_system, ipsec_tunnel, ipsec_user, ipsec_ike) + '1.2.840.113635.100.1.11': set([ + '1.3.6.1.5.5.7.3.5', + '1.3.6.1.5.5.7.3.6', + '1.3.6.1.5.5.7.3.7', + '1.3.6.1.5.5.7.3.17', + ]) +} + + +def get_path(temp_dir=None, cache_length=24, cert_callback=None): + """ + Get the filesystem path to a file that contains OpenSSL-compatible CA certs. + + On OS X and Windows, there are extracted from the system certificate store + and cached in a file on the filesystem. This path should not be writable + by other users, otherwise they could inject CA certs into the trust list. + + :param temp_dir: + The temporary directory to cache the CA certs in on OS X and Windows. + Needs to have secure permissions so other users can not modify the + contents. + + :param cache_length: + The number of hours to cache the CA certs on OS X and Windows + + :param cert_callback: + A callback that is called once for each certificate in the trust store. + It should accept two parameters: an asn1crypto.x509.Certificate object, + and a reason. The reason will be None if the certificate is being + exported, otherwise it will be a unicode string of the reason it won't. + This is only called on Windows and OS X when passed to this function. + + :raises: + oscrypto.errors.CACertsError - when an error occurs exporting/locating certs + + :return: + The full filesystem path to a CA certs file + """ + + ca_path, temp = _ca_path(temp_dir) + + # Windows and OS X + if temp and _cached_path_needs_update(ca_path, cache_length): + empty_set = set() + + any_purpose = '2.5.29.37.0' + apple_ssl = '1.2.840.113635.100.1.3' + win_server_auth = '1.3.6.1.5.5.7.3.1' + + with path_lock: + if _cached_path_needs_update(ca_path, cache_length): + with open(ca_path, 'wb') as f: + for cert, trust_oids, reject_oids in extract_from_system(cert_callback, True): + if sys.platform == 'darwin': + if trust_oids != empty_set and any_purpose not in trust_oids \ + and apple_ssl not in trust_oids: + if cert_callback: + cert_callback(Certificate.load(cert), 'implicitly distrusted for TLS') + continue + if reject_oids != empty_set and (apple_ssl in reject_oids + or any_purpose in reject_oids): + if cert_callback: + cert_callback(Certificate.load(cert), 'explicitly distrusted for TLS') + continue + elif sys.platform == 'win32': + if trust_oids != empty_set and any_purpose not in trust_oids \ + and win_server_auth not in trust_oids: + if cert_callback: + cert_callback(Certificate.load(cert), 'implicitly distrusted for TLS') + continue + if reject_oids != empty_set and (win_server_auth in reject_oids + or any_purpose in reject_oids): + if cert_callback: + cert_callback(Certificate.load(cert), 'explicitly distrusted for TLS') + continue + if cert_callback: + cert_callback(Certificate.load(cert), None) + f.write(armor('CERTIFICATE', cert)) + + if not ca_path: + raise CACertsError('No CA certs found') + + return ca_path + + +def get_list(cache_length=24, map_vendor_oids=True, cert_callback=None): + """ + Retrieves (and caches in memory) the list of CA certs from the OS. Includes + trust information from the OS - purposes the certificate should be trusted + or rejected for. + + Trust information is encoded via object identifiers (OIDs) that are sourced + from various RFCs and vendors (Apple and Microsoft). This trust information + augments what is in the certificate itself. Any OID that is in the set of + trusted purposes indicates the certificate has been explicitly trusted for + a purpose beyond the extended key purpose extension. Any OID in the reject + set is a purpose that the certificate should not be trusted for, even if + present in the extended key purpose extension. + + *A list of common trust OIDs can be found as part of the `KeyPurposeId()` + class in the `asn1crypto.x509` module of the `asn1crypto` package.* + + :param cache_length: + The number of hours to cache the CA certs in memory before they are + refreshed + + :param map_vendor_oids: + A bool indicating if the following mapping of OIDs should happen for + trust information from the OS trust list: + - 1.2.840.113635.100.1.3 (apple_ssl) -> 1.3.6.1.5.5.7.3.1 (server_auth) + - 1.2.840.113635.100.1.3 (apple_ssl) -> 1.3.6.1.5.5.7.3.2 (client_auth) + - 1.2.840.113635.100.1.8 (apple_smime) -> 1.3.6.1.5.5.7.3.4 (email_protection) + - 1.2.840.113635.100.1.9 (apple_eap) -> 1.3.6.1.5.5.7.3.13 (eap_over_ppp) + - 1.2.840.113635.100.1.9 (apple_eap) -> 1.3.6.1.5.5.7.3.14 (eap_over_lan) + - 1.2.840.113635.100.1.11 (apple_ipsec) -> 1.3.6.1.5.5.7.3.5 (ipsec_end_system) + - 1.2.840.113635.100.1.11 (apple_ipsec) -> 1.3.6.1.5.5.7.3.6 (ipsec_tunnel) + - 1.2.840.113635.100.1.11 (apple_ipsec) -> 1.3.6.1.5.5.7.3.7 (ipsec_user) + - 1.2.840.113635.100.1.11 (apple_ipsec) -> 1.3.6.1.5.5.7.3.17 (ipsec_ike) + - 1.2.840.113635.100.1.16 (apple_code_signing) -> 1.3.6.1.5.5.7.3.3 (code_signing) + - 1.2.840.113635.100.1.20 (apple_time_stamping) -> 1.3.6.1.5.5.7.3.8 (time_stamping) + - 1.3.6.1.4.1.311.10.3.2 (microsoft_time_stamp_signing) -> 1.3.6.1.5.5.7.3.8 (time_stamping) + + :param cert_callback: + A callback that is called once for each certificate in the trust store. + It should accept two parameters: an asn1crypto.x509.Certificate object, + and a reason. The reason will be None if the certificate is being + exported, otherwise it will be a unicode string of the reason it won't. + + :raises: + oscrypto.errors.CACertsError - when an error occurs exporting/locating certs + + :return: + A (copied) list of 3-element tuples containing CA certs from the OS + trust ilst: + - 0: an asn1crypto.x509.Certificate object + - 1: a set of unicode strings of OIDs of trusted purposes + - 2: a set of unicode strings of OIDs of rejected purposes + """ + + if not _in_memory_up_to_date(cache_length): + with memory_lock: + if not _in_memory_up_to_date(cache_length): + certs = [] + for cert_bytes, trust_oids, reject_oids in extract_from_system(cert_callback): + if map_vendor_oids: + trust_oids = _map_oids(trust_oids) + reject_oids = _map_oids(reject_oids) + certs.append((Certificate.load(cert_bytes), trust_oids, reject_oids)) + _module_values['certs'] = certs + _module_values['last_update'] = time.time() + + return list(_module_values['certs']) + + +def clear_cache(temp_dir=None): + """ + Clears any cached info that was exported from the OS trust store. This will + ensure the latest changes are returned from calls to get_list() and + get_path(), but at the expense of re-exporting and parsing all certificates. + + :param temp_dir: + The temporary directory to cache the CA certs in on OS X and Windows. + Needs to have secure permissions so other users can not modify the + contents. Must be the same value passed to get_path(). + """ + + with memory_lock: + _module_values['last_update'] = None + _module_values['certs'] = None + + ca_path, temp = _ca_path(temp_dir) + if temp: + with path_lock: + if os.path.exists(ca_path): + os.remove(ca_path) + + +def _ca_path(temp_dir=None): + """ + Returns the file path to the CA certs file + + :param temp_dir: + The temporary directory to cache the CA certs in on OS X and Windows. + Needs to have secure permissions so other users can not modify the + contents. + + :return: + A 2-element tuple: + - 0: A unicode string of the file path + - 1: A bool if the file is a temporary file + """ + + ca_path = system_path() + + # Windows and OS X + if ca_path is None: + if temp_dir is None: + temp_dir = tempfile.gettempdir() + + if not os.path.isdir(temp_dir): + raise CACertsError(pretty_message( + ''' + The temp dir specified, "%s", is not a directory + ''', + temp_dir + )) + + ca_path = os.path.join(temp_dir, 'oscrypto-ca-bundle.crt') + return (ca_path, True) + + return (ca_path, False) + + +def _map_oids(oids): + """ + Takes a set of unicode string OIDs and converts vendor-specific OIDs into + generics OIDs from RFCs. + + - 1.2.840.113635.100.1.3 (apple_ssl) -> 1.3.6.1.5.5.7.3.1 (server_auth) + - 1.2.840.113635.100.1.3 (apple_ssl) -> 1.3.6.1.5.5.7.3.2 (client_auth) + - 1.2.840.113635.100.1.8 (apple_smime) -> 1.3.6.1.5.5.7.3.4 (email_protection) + - 1.2.840.113635.100.1.9 (apple_eap) -> 1.3.6.1.5.5.7.3.13 (eap_over_ppp) + - 1.2.840.113635.100.1.9 (apple_eap) -> 1.3.6.1.5.5.7.3.14 (eap_over_lan) + - 1.2.840.113635.100.1.11 (apple_ipsec) -> 1.3.6.1.5.5.7.3.5 (ipsec_end_system) + - 1.2.840.113635.100.1.11 (apple_ipsec) -> 1.3.6.1.5.5.7.3.6 (ipsec_tunnel) + - 1.2.840.113635.100.1.11 (apple_ipsec) -> 1.3.6.1.5.5.7.3.7 (ipsec_user) + - 1.2.840.113635.100.1.11 (apple_ipsec) -> 1.3.6.1.5.5.7.3.17 (ipsec_ike) + - 1.2.840.113635.100.1.16 (apple_code_signing) -> 1.3.6.1.5.5.7.3.3 (code_signing) + - 1.2.840.113635.100.1.20 (apple_time_stamping) -> 1.3.6.1.5.5.7.3.8 (time_stamping) + - 1.3.6.1.4.1.311.10.3.2 (microsoft_time_stamp_signing) -> 1.3.6.1.5.5.7.3.8 (time_stamping) + + :param oids: + A set of unicode strings + + :return: + The original set of OIDs with any mapped OIDs added + """ + + new_oids = set() + for oid in oids: + if oid in _oid_map: + new_oids |= _oid_map[oid] + return oids | new_oids + + +def _cached_path_needs_update(ca_path, cache_length): + """ + Checks to see if a cache file needs to be refreshed + + :param ca_path: + A unicode string of the path to the cache file + + :param cache_length: + An integer representing the number of hours the cache is valid for + + :return: + A boolean - True if the cache needs to be updated, False if the file + is up-to-date + """ + + exists = os.path.exists(ca_path) + if not exists: + return True + + stats = os.stat(ca_path) + + if stats.st_mtime < time.time() - cache_length * 60 * 60: + return True + + if stats.st_size == 0: + return True + + return False + + +def _in_memory_up_to_date(cache_length): + """ + Checks to see if the in-memory cache of certificates is fresh + + :param cache_length: + An integer representing the number of hours the cache is valid for + + :return: + A boolean - True if the cache is up-to-date, False if it needs to be + refreshed + """ + + return ( + _module_values['certs'] and + _module_values['last_update'] and + _module_values['last_update'] > time.time() - (cache_length * 60 * 60) + ) diff --git a/tasks/lib/package_control/deps/oscrypto/util.py b/tasks/lib/package_control/deps/oscrypto/util.py new file mode 100644 index 0000000..f5e4cf5 --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/util.py @@ -0,0 +1,63 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys + +from ._errors import pretty_message +from ._types import type_name, byte_cls + +if sys.platform == 'darwin': + from ._mac.util import rand_bytes +elif sys.platform == 'win32': + from ._win.util import rand_bytes +else: + from ._openssl.util import rand_bytes + + +__all__ = [ + 'constant_compare', + 'rand_bytes', +] + + +def constant_compare(a, b): + """ + Compares two byte strings in constant time to see if they are equal + + :param a: + The first byte string + + :param b: + The second byte string + + :return: + A boolean if the two byte strings are equal + """ + + if not isinstance(a, byte_cls): + raise TypeError(pretty_message( + ''' + a must be a byte string, not %s + ''', + type_name(a) + )) + + if not isinstance(b, byte_cls): + raise TypeError(pretty_message( + ''' + b must be a byte string, not %s + ''', + type_name(b) + )) + + if len(a) != len(b): + return False + + if sys.version_info < (3,): + a = [ord(char) for char in a] + b = [ord(char) for char in b] + + result = 0 + for x, y in zip(a, b): + result |= x ^ y + return result == 0 diff --git a/tasks/lib/package_control/deps/oscrypto/version.py b/tasks/lib/package_control/deps/oscrypto/version.py new file mode 100644 index 0000000..b7c352c --- /dev/null +++ b/tasks/lib/package_control/deps/oscrypto/version.py @@ -0,0 +1,6 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + + +__version__ = '1.3.0' +__version_info__ = (1, 3, 0) diff --git a/tasks/lib/package_control/download_manager.py b/tasks/lib/package_control/download_manager.py new file mode 100644 index 0000000..436dc37 --- /dev/null +++ b/tasks/lib/package_control/download_manager.py @@ -0,0 +1,530 @@ +import os +import re +import socket +import sys +from threading import Lock, Timer +from urllib.parse import urljoin, urlparse + +from . import __version__ +from . import text +from .cache import set_cache, get_cache +from .console_write import console_write +from .show_error import show_error + +from .downloaders import DOWNLOADERS +from .downloaders.binary_not_found_error import BinaryNotFoundError +from .downloaders.downloader_exception import DownloaderException +from .downloaders.oscrypto_downloader_exception import OscryptoDownloaderException +from .downloaders.rate_limit_exception import RateLimitException +from .downloaders.rate_limit_exception import RateLimitSkipException +from .downloaders.urllib_downloader import UrlLibDownloader +from .downloaders.win_downloader_exception import WinDownloaderException +from .http_cache import HttpCache + + +_managers = {} +"""A dict of domains - each points to a list of downloaders""" + +_in_use = 0 +"""How many managers are currently checked out""" + +_lock = Lock() +"""Make sure connection management doesn't run into threading issues""" + +_timer = None +"""A timer used to disconnect all managers after a period of no usage""" + + +def http_get(url, settings, error_message='', prefer_cached=False): + """ + Performs a HTTP GET request using best matching downloader. + + :param url: + The string URL to download + + :param settings: + The dictionary with downloader settings. + + - ``debug`` + - ``downloader_precedence`` + - ``http_basic_auth`` + - ``http_cache`` + - ``http_cache_length`` + - ``http_proxy`` + - ``https_proxy`` + - ``proxy_username`` + - ``proxy_password`` + - ``user_agent`` + - ``timeout`` + + :param error_message: + The error message to include if the download fails + + :param prefer_cached: + If cached version of the URL content is preferred over a new request + + :raises: + DownloaderException: if there was an error downloading the URL + + :return: + The string contents of the URL + """ + + manager = None + result = None + + try: + manager = _grab(url, settings) + result = manager.fetch(url, error_message, prefer_cached) + + finally: + if manager: + _release(url, manager) + + return result + + +def _grab(url, settings): + global _managers, _lock, _in_use, _timer + + with _lock: + if _timer: + _timer.cancel() + _timer = None + + parsed = urlparse(url) + if not parsed or not parsed.hostname: + raise DownloaderException('The URL "%s" is malformed' % url) + hostname = parsed.hostname.lower() + if hostname not in _managers: + _managers[hostname] = [] + + if not _managers[hostname]: + _managers[hostname].append(DownloadManager(settings)) + + _in_use += 1 + + return _managers[hostname].pop() + + +def _release(url, manager): + global _managers, _lock, _in_use, _timer + + with _lock: + parsed = urlparse(url) + if not parsed or not parsed.hostname: + raise DownloaderException('The URL "%s" is malformed' % url) + hostname = parsed.hostname.lower() + + # This means the package was reloaded between _grab and _release, + # so the downloader is using old code and we want to discard it + if hostname not in _managers: + return + + _managers[hostname].insert(0, manager) + + _in_use -= 1 + + if _timer: + _timer.cancel() + _timer = None + + if _in_use == 0: + _timer = Timer(5.0, close_all_connections) + _timer.start() + + +def close_all_connections(): + global _managers, _lock, _in_use, _timer + + with _lock: + if _timer: + _timer.cancel() + _timer = None + + for managers in _managers.values(): + for manager in managers: + manager.close() + _managers = {} + + +def resolve_urls(root_url, uris): + """ + Convert a list of relative uri's to absolute urls/paths. + + :param root_url: + The root url string + + :param uris: + An iteratable of relative uri's to resolve. + + :returns: + A generator of resolved URLs + """ + + scheme_match = re.match(r'(https?:)//', root_url, re.I) + if scheme_match is None: + root_dir = os.path.dirname(root_url) + else: + root_dir = '' + + for url in uris: + if not url: + continue + if url.startswith('//'): + if scheme_match is not None: + url = scheme_match.group(1) + url + else: + url = 'https:' + url + elif url.startswith('/'): + # We don't allow absolute repositories + continue + elif url.startswith('./') or url.startswith('../'): + if root_dir: + url = os.path.normpath(os.path.join(root_dir, url)) + else: + url = urljoin(root_url, url) + yield url + + +def resolve_url(root_url, url): + """ + Convert a list of relative uri's to absolute urls/paths. + + :param root_url: + The root url string + + :param uris: + An iteratable of relative uri's to resolve. + + :returns: + A generator of resolved URLs + """ + + if not url: + return url + + scheme_match = re.match(r'(https?:)//', root_url, re.I) + if scheme_match is None: + root_dir = os.path.dirname(root_url) + else: + root_dir = '' + + if url.startswith('//'): + if scheme_match is not None: + return scheme_match.group(1) + url + else: + return 'https:' + url + + elif url.startswith('./') or url.startswith('../'): + if root_dir: + return os.path.normpath(os.path.join(root_dir, url)) + else: + return urljoin(root_url, url) + + return url + + +def update_url(url, debug): + """ + Takes an old, out-dated URL and updates it. Mostly used with GitHub URLs + since they tend to be constantly evolving their infrastructure. + + :param url: + The URL to update + + :param debug: + If debugging is enabled + + :return: + The updated URL + """ + + if not url: + return url + + original_url = url + url = url.replace('://raw.github.com/', '://raw.githubusercontent.com/') + url = url.replace('://nodeload.github.com/', '://codeload.github.com/') + url = re.sub(r'^(https://codeload\.github\.com/[^/#?]+/[^/#?]+/)zipball(/.*)$', '\\1zip\\2', url) + + # Fix URLs from old versions of Package Control since we are going to + # remove all packages but Package Control from them to force upgrades + if url == 'https://sublime.wbond.net/repositories.json' or url == 'https://sublime.wbond.net/channel.json': + url = 'https://packagecontrol.io/channel_v3.json' + + if debug and url != original_url: + console_write( + ''' + Fixed URL from %s to %s + ''', + (original_url, url) + ) + + return url + + +class DownloadManager: + + def __init__(self, settings): + # Cache the downloader for re-use + self.downloader = None + + keys_to_copy = { + 'debug', + 'downloader_precedence', + 'http_basic_auth', + 'http_proxy', + 'https_proxy', + 'proxy_username', + 'proxy_password', + 'user_agent', + 'timeout', + } + + # Copy required settings to avoid manipulating caller's environment. + # It's needed as e.g. `cache_length` is defined with different meaning in PackageManager's + # settings. Also `cache` object shouldn't be propagated to caller. + self.settings = {key: value for key, value in settings.items() if key in keys_to_copy} + + # add package control version to user agent + user_agent = self.settings.get('user_agent') + if user_agent and '%s' in user_agent: + self.settings['user_agent'] = user_agent % __version__ + + # setup private http cache storage driver + if settings.get('http_cache'): + cache_length = settings.get('http_cache_length', 604800) + self.settings['cache'] = HttpCache(cache_length) + self.settings['cache_length'] = cache_length + + def close(self): + if self.downloader: + self.downloader.close() + self.downloader = None + + def fetch(self, url, error_message, prefer_cached=False): + """ + Downloads a URL and returns the contents + + :param url: + The string URL to download + + :param error_message: + The error message to include if the download fails + + :param prefer_cached: + If cached version of the URL content is preferred over a new request + + :raises: + DownloaderException: if there was an error downloading the URL + + :return: + The string contents of the URL + """ + + is_ssl = re.search('^https://', url) is not None + + url = update_url(url, self.settings.get('debug')) + + # We don't use sublime.platform() here since this is used for + # the crawler on packagecontrol.io also + if sys.platform == 'darwin': + platform = 'osx' + elif sys.platform == 'win32': + platform = 'windows' + else: + platform = 'linux' + + downloader_precedence = self.settings.get( + 'downloader_precedence', + { + "windows": ["wininet", "oscrypto", "urllib"], + "osx": ["urllib", "oscrypto", "curl"], + "linux": ["urllib", "oscrypto", "curl", "wget"] + } + ) + downloader_list = downloader_precedence.get(platform, []) + + if not isinstance(downloader_list, list) or len(downloader_list) == 0: + error_string = text.format( + ''' + No list of preferred downloaders specified in the + "downloader_precedence" setting for the platform "%s" + ''', + platform + ) + show_error(error_string) + raise DownloaderException(error_string) + + # Make sure we have a downloader, and it supports SSL if we need it + if not self.downloader or ( + (is_ssl and not self.downloader.supports_ssl()) + or (not is_ssl and not self.downloader.supports_plaintext())): + + for downloader_name in downloader_list: + + try: + downloader_class = DOWNLOADERS[downloader_name] + if downloader_class is None: + continue + + except KeyError: + error_string = text.format( + ''' + The downloader "%s" from the "downloader_precedence" + setting for the platform "%s" is invalid + ''', + (downloader_name, platform) + ) + show_error(error_string) + raise DownloaderException(error_string) + + try: + downloader = downloader_class(self.settings) + if is_ssl and not downloader.supports_ssl(): + continue + if not is_ssl and not downloader.supports_plaintext(): + continue + self.downloader = downloader + break + + except BinaryNotFoundError: + pass + + if not self.downloader: + error_string = text.format( + ''' + None of the preferred downloaders can download %s. + + This is usually either because the ssl module is unavailable + and/or the command line curl or wget executables could not be + found in the PATH. + + If you customized the "downloader_precedence" setting, please + verify your customization. + ''', + url + ) + show_error(error_string) + raise DownloaderException(error_string.replace('\n\n', ' ')) + + url = url.replace(' ', '%20') + parsed = urlparse(url) + if not parsed or not parsed.hostname: + raise DownloaderException('The URL "%s" is malformed' % url) + hostname = parsed.hostname.lower() + + timeout = self.settings.get('timeout', 3) + + rate_limited_domains = get_cache('rate_limited_domains', []) + + if self.settings.get('debug'): + try: + port = 443 if is_ssl else 80 + ipv6_info = socket.getaddrinfo(hostname, port, socket.AF_INET6) + if ipv6_info: + ipv6 = ipv6_info[0][4][0] + else: + ipv6 = None + except (socket.gaierror): + ipv6 = None + except (TypeError): + ipv6 = None + + try: + ip = socket.gethostbyname(hostname) + except (socket.gaierror) as e: + ip = str(e) + except (TypeError): + ip = None + + console_write( + ''' + Download Debug + URL: %s + Timeout: %s + Resolved IP: %s + ''', + (url, str(timeout), ip) + ) + if ipv6: + console_write( + ' Resolved IPv6: %s', + ipv6, + prefix=False + ) + + if hostname in rate_limited_domains: + exception = RateLimitSkipException(hostname) + if self.settings.get('debug'): + console_write(' %s' % exception, prefix=False) + raise exception + + try: + return self.downloader.download(url, error_message, timeout, 3, prefer_cached) + + except (RateLimitException) as e: + rate_limited_domains.append(hostname) + set_cache( + 'rate_limited_domains', + rate_limited_domains, + self.settings.get('cache_length', 604800) + ) + + console_write( + ''' + %s Skipping all further download requests for this domain. + ''', + str(e) + ) + raise + + except (OscryptoDownloaderException) as e: + console_write( + ''' + Attempting to use Urllib downloader due to Oscrypto error: %s + ''', + str(e) + ) + + self.downloader = UrlLibDownloader(self.settings) + # Try again with the new downloader! + return self.fetch(url, error_message, prefer_cached) + + except (WinDownloaderException) as e: + console_write( + ''' + Attempting to use Urllib downloader due to WinINet error: %s + ''', + str(e) + ) + + # Here we grab the proxy info extracted from WinInet to fill in + # the Package Control settings if those are not present. This should + # hopefully make a seamless fallback for users who run into weird + # windows errors related to network communication. + wininet_proxy = self.downloader.proxy or '' + wininet_proxy_username = self.downloader.proxy_username or '' + wininet_proxy_password = self.downloader.proxy_password or '' + + http_proxy = self.settings.get('http_proxy', '') + https_proxy = self.settings.get('https_proxy', '') + proxy_username = self.settings.get('proxy_username', '') + proxy_password = self.settings.get('proxy_password', '') + + settings = self.settings.copy() + if not http_proxy and wininet_proxy: + settings['http_proxy'] = wininet_proxy + if not https_proxy and wininet_proxy: + settings['https_proxy'] = wininet_proxy + + has_proxy = settings.get('http_proxy') or settings.get('https_proxy') + if has_proxy and not proxy_username and wininet_proxy_username: + settings['proxy_username'] = wininet_proxy_username + if has_proxy and not proxy_password and wininet_proxy_password: + settings['proxy_password'] = wininet_proxy_password + + self.downloader = UrlLibDownloader(settings) + # Try again with the new downloader! + return self.fetch(url, error_message, prefer_cached) diff --git a/tasks/lib/package_control/downloaders/__init__.py b/tasks/lib/package_control/downloaders/__init__.py new file mode 100644 index 0000000..5101fa0 --- /dev/null +++ b/tasks/lib/package_control/downloaders/__init__.py @@ -0,0 +1,44 @@ +import sys + +from ..console_write import console_write + +from .urllib_downloader import UrlLibDownloader +from .curl_downloader import CurlDownloader +from .wget_downloader import WgetDownloader + +DOWNLOADERS = { + 'oscrypto': None, + 'urllib': UrlLibDownloader, + 'curl': CurlDownloader, + 'wget': WgetDownloader +} + +# oscrypto can fail badly on Linux in the Sublime Text 3 environment due to +# trying to mix the statically-linked OpenSSL in plugin_host with the OpenSSL +# loaded from the operating system. On Python 3.8 we dynamically link OpenSSL, +# so it just needs to be configured properly, which is handled in +# oscrypto_downloader.py. +if sys.platform != 'linux' or sys.version_info[:2] != (3, 3) or sys.executable != 'python3': + try: + from .oscrypto_downloader import OscryptoDownloader + DOWNLOADERS['oscrypto'] = OscryptoDownloader + except Exception as e: + console_write( + ''' + OscryptoDownloader not available! %s + ''', + str(e) + ) + +if sys.platform == 'win32': + try: + from .wininet_downloader import WinINetDownloader + DOWNLOADERS['wininet'] = WinINetDownloader + except Exception as e: + DOWNLOADERS['wininet'] = None + console_write( + ''' + WinINetDownloader not available! %s + ''', + str(e) + ) diff --git a/tasks/lib/package_control/downloaders/basic_auth_downloader.py b/tasks/lib/package_control/downloaders/basic_auth_downloader.py new file mode 100644 index 0000000..9150127 --- /dev/null +++ b/tasks/lib/package_control/downloaders/basic_auth_downloader.py @@ -0,0 +1,66 @@ +import base64 + +from urllib.parse import urlparse + + +class BasicAuthDownloader: + + """ + A base for downloaders to add an HTTP basic auth header + """ + + def build_auth_header(self, url): + """ + Constructs an HTTP basic auth header for a URL, if present in + settings + + :param url: + A unicode string of the URL being downloaded + + :return: + A dict with an HTTP header name as the key and the value as the + value. Both are unicode strings. + """ + + auth_string = self.get_auth_string(url) + if not auth_string: + return {} + b64_auth = base64.b64encode(auth_string.encode('utf-8')).decode('utf-8') + return {"Authorization": "Basic %s" % b64_auth} + + def get_auth_string(self, url): + """ + Constructs a string of username:password for use in HTTP basic auth + + :param url: + A unicode string of the URL being downloaded + + :return: + None, or a unicode string of the username:password for the URL + """ + + username, password = self.get_username_password(url) + if username and password: + return "%s:%s" % (username, password) + return None + + def get_username_password(self, url): + """ + Returns a tuple of (username, password) for use in HTTP basic auth + + :param url: + A unicode string of the URL being downloaded + + :return: + A 2-element tuple of either (None, None) or (username, password) + as unicode strings + """ + + domain_name = urlparse(url).netloc + + auth_settings = self.settings.get('http_basic_auth') + if auth_settings and isinstance(auth_settings, dict): + params = auth_settings.get(domain_name) + if params and isinstance(params, (list, tuple)) and len(params) == 2: + return (params[0], params[1]) + return (None, None) diff --git a/tasks/lib/package_control/downloaders/binary_not_found_error.py b/tasks/lib/package_control/downloaders/binary_not_found_error.py new file mode 100644 index 0000000..e93a8f7 --- /dev/null +++ b/tasks/lib/package_control/downloaders/binary_not_found_error.py @@ -0,0 +1,6 @@ +class BinaryNotFoundError(Exception): + + """If a necessary executable is not found in the PATH on the system""" + + def __bytes__(self): + return self.__str__().encode('utf-8') diff --git a/tasks/lib/package_control/downloaders/caching_downloader.py b/tasks/lib/package_control/downloaders/caching_downloader.py new file mode 100644 index 0000000..14191f8 --- /dev/null +++ b/tasks/lib/package_control/downloaders/caching_downloader.py @@ -0,0 +1,227 @@ +import re +import json +import hashlib + +from ..console_write import console_write + + +class CachingDownloader: + + """ + A base downloader that will use a caching backend to cache HTTP requests + and make conditional requests. + """ + + def add_conditional_headers(self, url, headers): + """ + Add `If-Modified-Since` and `If-None-Match` headers to a request if a + cached copy exists + + :param headers: + A dict with the request headers + + :return: + The request headers dict, possibly with new headers added + """ + cache = self.settings.get('cache') + if not cache: + return headers + + info_key = self.generate_key(url, '.info') + info_json = cache.get(info_key) + if not info_json: + return headers + + # Make sure we have the cached content to use if we get a 304 + key = self.generate_key(url) + if not cache.has(key): + return headers + + try: + info = json.loads(info_json.decode('utf-8')) + except ValueError: + return headers + + etag = info.get('etag') + if etag: + headers['If-None-Match'] = etag + + last_modified = info.get('last-modified') + if last_modified: + headers['If-Modified-Since'] = last_modified + + return headers + + def cache_result(self, method, url, status, headers, content): + """ + Processes a request result, either caching the result, or returning + the cached version of the url. + + :param method: + The HTTP method used for the request + + :param url: + The url of the request + + :param status: + The numeric response status of the request + + :param headers: + A dict of reponse headers, with keys being lowercase + + :param content: + The response content + + :return: + The response content + """ + + debug = self.settings.get('debug', False) + cache = self.settings.get('cache') + + if not cache: + if debug: + console_write( + ''' + Skipping cache since there is no cache object + ''' + ) + return content + + if method.lower() != 'get': + if debug: + console_write( + ''' + Skipping cache since the HTTP method != GET + ''' + ) + return content + + status = int(status) + + # Don't do anything unless it was successful or not modified + if status not in (200, 304): + if debug: + console_write( + ''' + Skipping cache since the HTTP status code not one of: 200, 304 + ''' + ) + return content + + key = self.generate_key(url) + + if status == 304: + cached_content = cache.get(key) + if cached_content: + if debug: + console_write( + ''' + Using cached content for %s from %s + ''', + (url, cache.path(key)) + ) + return cached_content + + # If we got a 304, but did not have the cached content + # stop here so we don't cache an empty response + return content + + # If we got here, the status is 200 + + # Respect some basic cache control headers + cache_control = headers.get('cache-control', '') + if cache_control: + fields = re.split(r'\s*,\s*', cache_control) + for field in fields: + if field == 'no-store': + return content + + # Don't ever cache zip/binary files for the sake of hard drive space + if headers.get('content-type') in ('application/zip', 'application/octet-stream'): + if debug: + console_write( + ''' + Skipping cache since the response is a zip file + ''' + ) + return content + + etag = headers.get('etag') + last_modified = headers.get('last-modified') + + if not etag and not last_modified: + return content + + struct = {'etag': etag, 'last-modified': last_modified} + struct_json = json.dumps(struct, indent=4) + + info_key = self.generate_key(url, '.info') + if debug: + console_write( + ''' + Caching %s in %s + ''', + (url, cache.path(key)) + ) + + cache.set(info_key, struct_json.encode('utf-8')) + cache.set(key, content) + + return content + + def generate_key(self, url, suffix=''): + """ + Generates a key to store the cache under + + :param url: + The URL being cached + + :param suffix: + A string to append to the key + + :return: + A string key for the URL + """ + + if isinstance(url, str): + url = url.encode('utf-8') + + key = hashlib.md5(url).hexdigest() + return key + suffix + + def retrieve_cached(self, url): + """ + Tries to return the cached content for a URL + + :param url: + The URL to get the cached content for + + :return: + The cached content + """ + + debug = self.settings.get('debug') + cache = self.settings.get('cache') + + if not cache: + if debug: + console_write( + ''' + Skipping cache since there is no cache object + ''' + ) + return False + + key = self.generate_key(url) + + cached_content = cache.get(key) + if cached_content and debug: + console_write( + ''' + Using cached content for %s from %s + ''', + (url, cache.path(key)) + ) + + return cached_content diff --git a/tasks/lib/package_control/downloaders/cli_downloader.py b/tasks/lib/package_control/downloaders/cli_downloader.py new file mode 100644 index 0000000..31365a0 --- /dev/null +++ b/tasks/lib/package_control/downloaders/cli_downloader.py @@ -0,0 +1,94 @@ +import os +import subprocess + +from ..console_write import console_write +from ..cmd import create_cmd +from .non_clean_exit_error import NonCleanExitError +from .binary_not_found_error import BinaryNotFoundError + + +class CliDownloader: + + """ + Base for downloaders that use a command line program + + :param settings: + A dict of the various Package Control settings. The Sublime Text + Settings API is not used because this code is run in a thread. + """ + + def __init__(self, settings): + self.settings = settings + + def clean_tmp_file(self): + if os.path.exists(self.tmp_file): + os.remove(self.tmp_file) + + def find_binary(self, name): + """ + Finds the given executable name in the system PATH + + :param name: + The exact name of the executable to find + + :return: + The absolute path to the executable + + :raises: + BinaryNotFoundError when the executable can not be found + """ + + dirs = os.environ['PATH'].split(os.pathsep) + if os.name != 'nt': + # This is mostly for OS X, which seems to launch ST with a + # minimal set of environmental variables + dirs.append('/usr/local/bin') + executable = name + else: + executable = name + ".exe" + + for dir_ in dirs: + path = os.path.join(dir_, executable) + if os.path.exists(path): + return path + + raise BinaryNotFoundError('The binary %s could not be located' % executable) + + def execute(self, args): + """ + Runs the executable and args and returns the result + + :param args: + A list of the executable path and all arguments to be passed to it + + :return: + The text output of the executable + + :raises: + NonCleanExitError when the executable exits with an error + """ + + if self.settings.get('debug'): + console_write( + ''' + Trying to execute command %s + ''', + create_cmd(args) + ) + + startupinfo = None + if os.name == 'nt': + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + + proc = subprocess.Popen( + args, startupinfo=startupinfo, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + output, self.stderr = proc.communicate() + + if proc.returncode != 0: + error = NonCleanExitError(proc.returncode) + error.stderr = self.stderr + error.stdout = output + raise error + return output diff --git a/tasks/lib/package_control/downloaders/curl_downloader.py b/tasks/lib/package_control/downloaders/curl_downloader.py new file mode 100644 index 0000000..d0e8c1b --- /dev/null +++ b/tasks/lib/package_control/downloaders/curl_downloader.py @@ -0,0 +1,365 @@ +import tempfile +import re +import os + +from ..ca_certs import get_ca_bundle_path +from ..console_write import console_write +from .cli_downloader import CliDownloader +from .non_clean_exit_error import NonCleanExitError +from .downloader_exception import DownloaderException +from .basic_auth_downloader import BasicAuthDownloader +from .caching_downloader import CachingDownloader +from .decoding_downloader import DecodingDownloader +from .limiting_downloader import LimitingDownloader + + +class CurlDownloader(CliDownloader, DecodingDownloader, LimitingDownloader, CachingDownloader, BasicAuthDownloader): + + """ + A downloader that uses the command line program curl + + :param settings: + A dict of the various Package Control settings. The Sublime Text + Settings API is not used because this code is run in a thread. + + :raises: + BinaryNotFoundError: when curl can not be found + """ + + def __init__(self, settings): + self.settings = settings + self.curl = self.find_binary('curl') + + def close(self): + """ + No-op for compatibility with UrllibDownloader and WinINetDownloader + """ + + pass + + def download(self, url, error_message, timeout, tries, prefer_cached=False): + """ + Downloads a URL and returns the contents + + :param url: + The URL to download + + :param error_message: + A string to include in the console error that is printed + when an error occurs + + :param timeout: + The int number of seconds to set the timeout to + + :param tries: + The int number of times to try and download the URL in the case of + a timeout or HTTP 503 error + + :param prefer_cached: + If a cached version should be returned instead of trying a new request + + :raises: + RateLimitException: when a rate limit is hit + DownloaderException: when any other download error occurs + + :return: + The string contents of the URL + """ + + if prefer_cached: + cached = self.retrieve_cached(url) + if cached: + return cached + + self.tmp_file = tempfile.NamedTemporaryFile().name + command = [ + self.curl, + '--connect-timeout', + str(int(timeout)), + '-sSL', + '--tlsv1', + # We have to capture the headers to check for rate limit info + '--dump-header', + self.tmp_file + ] + + user_agent = self.settings.get('user_agent') + if user_agent: + command.extend(['--user-agent', user_agent]) + + request_headers = self.add_conditional_headers(url, {}) + # Don't be alarmed if the response from the server does not select + # one of these since the server runs a relatively new version of + # OpenSSL which supports compression on the SSL layer, and Apache + # will use that instead of HTTP-level encoding. + request_headers['Accept-Encoding'] = self.supported_encodings() + + auth_string = self.get_auth_string(url) + if auth_string: + command.extend(['-u', auth_string]) + + for name, value in request_headers.items(): + command.extend(['--header', "%s: %s" % (name, value)]) + + secure_url_match = re.match(r'^https://([^/#?]+)', url) + if secure_url_match is not None: + bundle_path = get_ca_bundle_path(self.settings) + command.extend(['--cacert', bundle_path]) + + debug = self.settings.get('debug') + # We always trigger debug output so that we can detect certain errors + command.append('-v') + + http_proxy = self.settings.get('http_proxy') + https_proxy = self.settings.get('https_proxy') + proxy_username = self.settings.get('proxy_username') + proxy_password = self.settings.get('proxy_password') + + if debug: + console_write( + ''' + Curl Debug Proxy + http_proxy: %s + https_proxy: %s + proxy_username: %s + proxy_password: %s + ''', + (http_proxy, https_proxy, proxy_username, proxy_password) + ) + + if http_proxy or https_proxy: + command.append('--proxy-anyauth') + + if proxy_username or proxy_password: + command.extend(['-U', "%s:%s" % (proxy_username, proxy_password)]) + + if http_proxy: + os.putenv('http_proxy', http_proxy) + if https_proxy: + os.putenv('HTTPS_PROXY', https_proxy) + + command.append(url) + + error_string = None + while tries > 0: + tries -= 1 + try: + output = self.execute(command) + + with open(self.tmp_file, 'r') as fobj: + headers_str = fobj.read() + self.clean_tmp_file() + + message = 'OK' + status = 200 + headers = {} + for header in headers_str.splitlines(): + if header[0:5] == 'HTTP/': + message = re.sub(r'^HTTP/\d(?:\.\d)?\s+\d+\s*', '', header) + status = int(re.sub(r'^HTTP/\d(?:\.\d)?\s+(\d+)(\s+.*)?$', '\\1', header)) + continue + if header.strip() == '': + continue + name, value = header.split(':', 1) + name = name.lower() + if name in headers: + headers[name] += ', %s' % value.strip() + else: + headers[name] = value.strip() + + error, debug_sections = self.split_debug(self.stderr.decode('utf-8')) + if debug: + self.print_debug(debug_sections) + + self.handle_rate_limit(headers, url) + + if status not in [200, 304]: + e = NonCleanExitError(22) + e.stderr = "%s %s" % (status, message) + raise e + + encoding = headers.get('content-encoding') + output = self.decode_response(encoding, output) + + output = self.cache_result('get', url, status, headers, output) + + return output + + except (NonCleanExitError) as e: + if hasattr(e.stderr, 'decode'): + e.stderr = e.stderr.decode('utf-8', 'replace') + + # Stderr is used for both the error message and the debug info + # so we need to process it to extract the debug info + e.stderr, debug_sections = self.split_debug(e.stderr) + + if debug: + self.print_debug(debug_sections) + + self.clean_tmp_file() + + download_error = e.stderr.rstrip() + + if e.returncode == 22: + code = re.sub(r'^.*?(\d+)([\w\s]+)?$', '\\1', e.stderr) + if code == '503' and tries != 0: + # GitHub and BitBucket seem to rate limit via 503 + if tries and debug: + console_write( + ''' + Downloading %s was rate limited, trying again + ''', + url + ) + continue + + download_error = 'HTTP error ' + code + + elif e.returncode == 7: + # If the user could not connect, check for ipv6 errors and + # if so, force curl to use ipv4. Apparently some users have + # network configuration where curl will try ipv6 and resolve + # it, but their ISP won't actually route it. + full_debug = "\n".join([section['contents'] for section in debug_sections]) + ipv6_error = re.search( + r'^\s*connect to ([0-9a-f]+(:+[0-9a-f]+)+) port \d+ failed: Network is unreachable', + full_debug, + re.I | re.M + ) + if ipv6_error and tries != 0: + if debug: + console_write( + ''' + Downloading %s failed because the ipv6 address + %s was not reachable, retrying using ipv4 + ''', + (url, ipv6_error.group(1)) + ) + command.insert(1, '-4') + continue + + elif e.returncode == 6: + download_error = 'URL error host not found' + + elif e.returncode == 28: + # GitHub and BitBucket seem to time out a lot + if tries and debug: + console_write( + ''' + Downloading %s timed out, trying again + ''', + url + ) + continue + + error_string = '%s %s downloading %s.' % (error_message, download_error, url) + + break + + raise DownloaderException(error_string) + + def print_debug(self, sections): + """ + Prints out the debug output from split_debug() + + :param sections: + The second element in the tuple that is returned from split_debug() + """ + + for section in sections: + type = section['type'] + indented_contents = section['contents'].replace("\n", "\n ") + console_write( + ''' + Curl HTTP Debug %s + %s + ''', + (type, indented_contents) + ) + + def supports_ssl(self): + """ + Indicates if the object can handle HTTPS requests + + :return: + If the object supports HTTPS requests + """ + + return True + + def supports_plaintext(self): + """ + Indicates if the object can handle non-secure HTTP requests + + :return: + If the object supports non-secure HTTP requests + """ + + return True + + def split_debug(self, string): + """ + Takes debug output from curl and splits it into stderr and + structured debug info + + :param string: + The complete debug output from curl + + :return: + A tuple with [0] stderr output and [1] a list of dict + objects containing the keys "type" and "contents" + """ + + section = 'General' + last_section = None + + stderr = '' + debug_sections = [] + debug_section = '' + + for line in string.splitlines(): + # Placeholder for body of request + if line and line[0:2] == '{ ': + continue + if line and line[0:18] == '} [data not shown]': + continue + + if len(line) > 1: + subtract = 0 + if line[0:2] == '* ': + section = 'General' + subtract = 2 + elif line[0:2] == '> ': + section = 'Write' + subtract = 2 + elif line[0:2] == '< ': + section = 'Read' + subtract = 2 + line = line[subtract:] + + # If the line does not start with "* ", "< ", "> " or " " + # then it is a real stderr message + if subtract == 0 and line[0:2] != ' ': + stderr += line.rstrip() + ' ' + continue + + if line.strip() == '': + continue + + if section != last_section and len(debug_section.rstrip()) > 0: + debug_sections.append({ + 'type': section, + 'contents': debug_section.rstrip() + }) + debug_section = '' + + debug_section += "%s\n" % line + last_section = section + + if len(debug_section.rstrip()) > 0: + debug_sections.append({ + 'type': section, + 'contents': debug_section.rstrip() + }) + + return (stderr.rstrip(), debug_sections) diff --git a/tasks/lib/package_control/downloaders/decoding_downloader.py b/tasks/lib/package_control/downloaders/decoding_downloader.py new file mode 100644 index 0000000..6434c9b --- /dev/null +++ b/tasks/lib/package_control/downloaders/decoding_downloader.py @@ -0,0 +1,58 @@ +import gzip +import zlib +from io import BytesIO + +try: + import bz2 +except (ImportError): + bz2 = None + +from .downloader_exception import DownloaderException + + +class DecodingDownloader: + + """ + A base for downloaders that provides the ability to decode bzip2ed, gzipped + or deflated content. + """ + + def supported_encodings(self): + """ + Determines the supported encodings we can decode + + :return: + A comma-separated string of valid encodings + """ + + encodings = 'gzip,deflate' + if bz2: + encodings = 'bzip2,' + encodings + return encodings + + def decode_response(self, encoding, response): + """ + Decodes the raw response from the web server based on the + Content-Encoding HTTP header + + :param encoding: + The value of the Content-Encoding HTTP header + + :param response: + The raw response from the server + + :return: + The decoded response + """ + + if encoding == 'bzip2': + if bz2: + return bz2.decompress(response) + else: + raise DownloaderException('Received bzip2 file contents, but was unable to import the bz2 module') + elif encoding == 'gzip': + return gzip.GzipFile(fileobj=BytesIO(response)).read() + elif encoding == 'deflate': + decompresser = zlib.decompressobj(-zlib.MAX_WBITS) + return decompresser.decompress(response) + decompresser.flush() + return response diff --git a/tasks/lib/package_control/downloaders/downloader_exception.py b/tasks/lib/package_control/downloaders/downloader_exception.py new file mode 100644 index 0000000..71509be --- /dev/null +++ b/tasks/lib/package_control/downloaders/downloader_exception.py @@ -0,0 +1,6 @@ +class DownloaderException(Exception): + + """If a downloader could not download a URL""" + + def __bytes__(self): + return self.__str__().encode('utf-8') diff --git a/tasks/lib/package_control/downloaders/http_error.py b/tasks/lib/package_control/downloaders/http_error.py new file mode 100644 index 0000000..dbf1383 --- /dev/null +++ b/tasks/lib/package_control/downloaders/http_error.py @@ -0,0 +1,10 @@ +class HttpError(Exception): + + """If a downloader was able to download a URL, but the result was not a 200 or 304""" + + def __init__(self, message, code): + self.code = code + super(HttpError, self).__init__(message) + + def __bytes__(self): + return self.__str__().encode('utf-8') diff --git a/tasks/lib/package_control/downloaders/limiting_downloader.py b/tasks/lib/package_control/downloaders/limiting_downloader.py new file mode 100644 index 0000000..61056b6 --- /dev/null +++ b/tasks/lib/package_control/downloaders/limiting_downloader.py @@ -0,0 +1,32 @@ +from urllib.parse import urlparse + +from .rate_limit_exception import RateLimitException + + +class LimitingDownloader: + + """ + A base for downloaders that checks for rate limiting headers. + """ + + def handle_rate_limit(self, headers, url): + """ + Checks the headers of a response object to make sure we are obeying the + rate limit + + :param headers: + The dict-like object that contains lower-cased headers + + :param url: + The URL that was requested + + :raises: + RateLimitException when the rate limit has been hit + """ + + limit_remaining = headers.get('x-ratelimit-remaining', '1') + limit = headers.get('x-ratelimit-limit', '1') + + if str(limit_remaining) == '0': + hostname = urlparse(url).hostname + raise RateLimitException(hostname, limit) diff --git a/tasks/lib/package_control/downloaders/non_clean_exit_error.py b/tasks/lib/package_control/downloaders/non_clean_exit_error.py new file mode 100644 index 0000000..8c32d6c --- /dev/null +++ b/tasks/lib/package_control/downloaders/non_clean_exit_error.py @@ -0,0 +1,17 @@ +class NonCleanExitError(Exception): + + """ + When an subprocess does not exit cleanly + + :param returncode: + The command line integer return code of the subprocess + """ + + def __init__(self, returncode): + self.returncode = returncode + + def __str__(self): + return str(self.returncode) + + def __bytes__(self): + return self.__str__().encode('utf-8') diff --git a/tasks/lib/package_control/downloaders/non_http_error.py b/tasks/lib/package_control/downloaders/non_http_error.py new file mode 100644 index 0000000..61cf13d --- /dev/null +++ b/tasks/lib/package_control/downloaders/non_http_error.py @@ -0,0 +1,6 @@ +class NonHttpError(Exception): + + """If a downloader had a non-clean exit, but it was not due to an HTTP error""" + + def __bytes__(self): + return self.__str__().encode('utf-8') diff --git a/tasks/lib/package_control/downloaders/oscrypto_downloader.py b/tasks/lib/package_control/downloaders/oscrypto_downloader.py new file mode 100644 index 0000000..9379114 --- /dev/null +++ b/tasks/lib/package_control/downloaders/oscrypto_downloader.py @@ -0,0 +1,773 @@ +# coding: utf-8 + +from __future__ import unicode_literals, division, absolute_import, print_function + +import base64 +import hashlib +import os +import re +import socket +import sys +from urllib.parse import urlparse +from urllib.request import parse_keqv_list, parse_http_list + +from .. import text +from ..ca_certs import get_user_ca_bundle_path +from ..console_write import console_write +from ..deps.asn1crypto.util import OrderedDict +from ..deps.asn1crypto import pem, x509 +from ..deps.oscrypto import use_ctypes, use_openssl +from .downloader_exception import DownloaderException +from .oscrypto_downloader_exception import OscryptoDownloaderException +from .basic_auth_downloader import BasicAuthDownloader +from .caching_downloader import CachingDownloader +from .decoding_downloader import DecodingDownloader +from .limiting_downloader import LimitingDownloader + +use_ctypes() + +# On Linux we need to use the version of OpenSSL included with Sublime Text +# to prevent conflicts between two different versions of OpenSSL being +# dynamically linked. On ST3, we can't use oscrypto for OpenSSL stuff since +# it has OpenSSL statically linked, and we can't dlopen() that. +# ST 4081 broke sys.executable to return "sublime_text", but other 4xxx builds +# will contain "plugin_host". +if sys.version_info[:2] == (3, 8) and sys.platform == 'linux' and ( + 'sublime_text' in sys.executable or + 'plugin_host' in sys.executable): + install_dir = os.path.dirname(sys.executable) + try: + use_openssl( + os.path.join(install_dir, 'libcrypto.so.1.1'), + os.path.join(install_dir, 'libssl.so.1.1') + ) + except RuntimeError: + pass # runtime error may be raised, when reloading modules. + +from ..deps.oscrypto import tls # noqa +from ..deps.oscrypto import errors as oscrypto_errors # noqa + + +class OscryptoDownloader(DecodingDownloader, LimitingDownloader, CachingDownloader, BasicAuthDownloader): + + """ + A downloader that uses the Python oscrypto.tls module + + :param settings: + A dict of the various Package Control settings. The Sublime Text + Settings API is not used because this code is run in a thread. + """ + + def __init__(self, settings): + self.socket = None + self.timeout = None + self.url_info = None + self.proxy_info = None + self.using_proxy = False + self.user_agent = None + self.debug = False + self.settings = settings + + def close(self): + """ + Closes any persistent/open connections + """ + + if not self.socket: + return + self.socket.close() + self.socket = None + self.using_proxy = False + + def download(self, url, error_message, timeout, tries, prefer_cached=False): + """ + Downloads a URL and returns the contents + + Uses the proxy settings from the Package Control.sublime-settings file, + however there seem to be a decent number of proxies that this code + does not work with. Patches welcome! + + :param url: + The URL to download + + :param error_message: + A string to include in the console error that is printed + when an error occurs + + :param timeout: + The int number of seconds to set the timeout to + + :param tries: + The int number of times to try and download the URL in the case of + a timeout or HTTP 503 error + + :param prefer_cached: + If a cached version should be returned instead of trying a new request + + :raises: + RateLimitException: when a rate limit is hit + DownloaderException: when any other download error occurs + + :return: + The string contents of the URL + """ + + if prefer_cached: + cached = self.retrieve_cached(url) + if cached: + return cached + + reused = None + + debug = self.debug + tried = tries + error_string = None + while tries > 0: + tries -= 1 + try: + if reused is None: + reused = self.setup_connection(url, timeout) + else: + self.ensure_connected() + + req_headers = OrderedDict() + req_headers['Host'] = self.url_info[0] + if self.url_info[1] != 443: + req_headers['Host'] += ':%d' % self.url_info[1] + req_headers['Accept-Encoding'] = self.supported_encodings() + req_headers['Connection'] = 'Keep-Alive' + user_agent = self.settings.get('user_agent') + if user_agent: + req_headers["User-Agent"] = user_agent + req_headers = self.add_conditional_headers(url, req_headers) + + req_headers.update(self.build_auth_header(url)) + + request = 'GET ' + url_info = urlparse(url) + if self.using_proxy: + request += url + ' HTTP/1.1' + else: + path = '/' if not url_info.path else url_info.path + if url_info.query: + path += '?' + url_info.query + request += path + ' HTTP/1.1' + self.write_request(request, req_headers) + + response = self.read_headers() + if not response: + self.close() + self.ensure_connected() + if reused: + tries += 1 + reused = False + continue + version, code, message, resp_headers = response + + # Read the body to get any remaining data off the socket + data = self.read_body(code, resp_headers, timeout) + + # Handle cached responses + if code == 304: + return self.cache_result('get', url, code, resp_headers, b'') + + if code == 301 or code == 302: + location = resp_headers.get('location') + if not isinstance(location, str): + raise OscryptoDownloaderException('Missing or duplicate Location HTTP header') + if not re.match(r'https?://', location): + if not location.startswith('/'): + location = os.path.dirname(url_info.path) + location + location = url_info.scheme + '://' + url_info.netloc + location + return self.download(location, error_message, timeout, tried, prefer_cached) + + # Make sure we obey Github's rate limiting headers + self.handle_rate_limit(resp_headers, url) + + # Bitbucket and Github return 503 a decent amount + if code == 503 and tries != 0: + if tries and debug: + console_write( + ''' + Downloading %s was rate limited, trying again + ''', + url + ) + continue + + if code != 200: + error_string = text.format( + ''' + %s HTTP error %s downloading %s. + ''', + (error_message, code, url) + ) + + else: + return self.cache_result('get', url, code, resp_headers, data) + + except (oscrypto_errors.TLSVerificationError) as e: + self.close() + if debug: + self.dump_certificate(e.certificate) + error_string = text.format( + ''' + %s TLS verification error %s downloading %s. + ''', + (error_message, str(e), url) + ) + + except (oscrypto_errors.TLSDisconnectError): + error_string = text.format( + ''' + %s TLS was gracefully closed while downloading %s, trying again. + ''', + (error_message, url) + ) + + self.close() + + continue + + except (oscrypto_errors.TLSError) as e: + self.close() + error_string = text.format( + ''' + %s TLS error %s downloading %s. + ''', + (error_message, str(e), url) + ) + + except (socket.error): + # Handle broken pipes/reset connections by creating a new opener, and + # thus getting new handlers and a new connection + if debug: + console_write( + ''' + Connection went away while trying to download %s, trying again + ''', + url + ) + + self.close() + + continue + + except (OSError) as e: + self.close() + error_string = text.format( + ''' + %s OS error %s downloading %s. + ''', + (error_message, str(e), url) + ) + raise + + break + + if error_string is None: + plural = 's' if tried > 1 else '' + error_string = 'Unable to download %s after %d attempt%s' % (url, tried, plural) + + raise DownloaderException(error_string) + + def setup_connection(self, url, timeout): + """ + :param url: + The URL to download + + :param timeout: + The int number of seconds to set the timeout to + """ + + proxy_username = self.settings.get('proxy_username') + proxy_password = self.settings.get('proxy_password') + + http_proxy = self.settings.get('http_proxy') + https_proxy = self.settings.get('https_proxy') + + proxy_info = None + if https_proxy: + proxy_info = (https_proxy, proxy_username, proxy_password) + + url_info = urlparse(url) + if url_info.scheme == 'http': + raise OscryptoDownloaderException('Can not connect to a non-TLS server') + hostname = url_info.hostname + port = url_info.port + if not port: + port = 443 + + reconnect = False + if self.socket: + if self.url_info != (hostname, port): + reconnect = True + elif self.proxy_info != proxy_info: + reconnect = True + + if reconnect: + self.close() + + if self.socket is None and self.debug: + console_write( + ''' + Oscrypto Debug Proxy + http_proxy: %s + https_proxy: %s + proxy_username: %s + proxy_password: %s + ''', + (http_proxy, https_proxy, proxy_username, proxy_password) + ) + + self.timeout = timeout + self.debug = self.settings.get('debug') + self.user_agent = self.settings.get('user_agent') + self.url_info = (hostname, port) + self.proxy_info = proxy_info + + return self.ensure_connected() + + def ensure_connected(self): + """ + Make sure a valid tls.TLSSocket() is open to the server + """ + + reused = self.setup_socket() + if not reused and self.using_proxy: + self.do_proxy_connect() + return reused + + def setup_socket(self): + """ + Create the oscrypto.tls.TLSSocket() object + """ + + if self.socket: + return True + + extra_trust_roots = [] + user_ca_bundle_path = get_user_ca_bundle_path(self.settings) + if os.path.exists(user_ca_bundle_path): + try: + with open(user_ca_bundle_path, 'rb') as fobj: + file_data = fobj.read() + if len(file_data) > 0: + for type_name, headers, der_bytes in pem.unarmor(file_data, multiple=True): + extra_trust_roots.append(x509.Certificate.load(der_bytes)) + except (ValueError) as e: + console_write( + ''' + Oscrypto Debug General + Error parsing certs file %s: %s + ''', + (user_ca_bundle_path, str(e)) + ) + session = tls.TLSSession(extra_trust_roots=extra_trust_roots) + + if self.proxy_info and self.proxy_info[0]: + proxy_url_info = urlparse(self.proxy_info[0]) + proxy_hostname = proxy_url_info.hostname + proxy_port = proxy_url_info.port + if not proxy_port: + if proxy_url_info.scheme == 'http': + raise OscryptoDownloaderException('Can not connect to a non-TLS proxy') + else: + proxy_port = 443 + host = proxy_hostname + port = proxy_port + self.using_proxy = True + else: + host = self.url_info[0] + port = self.url_info[1] + self.using_proxy = False + if self.debug: + proxy_details = '' if not self.using_proxy else ' (proxying)' + console_write( + ''' + Oscrypto Debug General + Connecting to %s on port %s%s + Using system CA certs plus additional in file at %s + Using hostname "%s" for TLS SNI extension + ''', + (host, port, proxy_details, user_ca_bundle_path, self.url_info[0]) + ) + self.socket = tls.TLSSocket(host, port, timeout=self.timeout, session=session) + if self.debug: + console_write( + ' Successfully negotiated %s with cipher suite %s', + (self.socket.protocol, self.socket.cipher_suite), + prefix=False + ) + console_write( + ' Certificate validated for %s', + host, + prefix=False + ) + self.dump_certificate(self.socket.certificate) + return False + + def write_request(self, request, headers): + """ + :param request: + A unicode string of the first line of the HTTP request + + :param headers: + An OrderedDict of the request headers + """ + + lines = [request] + for header, value in headers.items(): + lines.append('%s: %s' % (header, value)) + + if self.debug: + console_write( + ''' + Oscrypto Debug Write + %s + ''', + '\n '.join(lines) + ) + + lines.extend(['', '']) + + request = '\r\n'.join(lines).encode('iso-8859-1') + self.socket.write(request) + + def read_headers(self): + """ + Reads the HTTP response headers from the socket + + :return: + On error, None, otherwise a 4-element tuple: + 0: A 2-element tuple of integers representing the HTTP version + 1: An integer representing the HTTP response code + 2: A unicode string of the HTTP response code name + 3: An OrderedDict of HTTP headers with lowercase unicode key and unicode values + """ + + version = None + code = None + text = None + headers = OrderedDict() + + data = self.socket.read_until(b'\r\n\r\n') + string = data.decode('iso-8859-1') + if self.debug: + lines = [] + first = True + for line in string.split('\r\n'): + line = line.strip() + if len(line) == 0: + continue + if self.debug: + lines.append(line) + if first: + match = re.match(r'^HTTP/(1\.[01]) +(\d+) +(.*)$', line) + if not match: + if self.debug: + console_write( + ''' + Oscrypto Debug Read + %s + ''', + '\n '.join(lines) + ) + return None + version = tuple(map(int, match.group(1).split('.'))) + code = int(match.group(2)) + text = match.group(3) + first = False + else: + parts = line.split(':', 1) + if len(parts) == 2: + name = parts[0].strip().lower() + value = parts[1].strip() + if name in headers: + headers[name] += ', %s' % value + else: + headers[name] = value + + if self.debug: + console_write( + ''' + Oscrypto Debug Read + %s + ''', + '\n '.join(lines) + ) + + return (version, code, text, headers) + + def parse_content_length(self, headers): + """ + Returns the content-length from a dict of headers + + :return: + An integer of the content length + """ + + content_length = headers.get('content-length') + if isinstance(content_length, str) and len(content_length) > 0: + content_length = int(content_length) + return content_length + + def read_body(self, code, resp_headers, timeout): + """ + Reads the plaintext body of the request + + :param code: + The integer HTTP response code + + :param resp_headers: + A dict of the response headers + + :param timeout: + An integer number of seconds to timeout a read + + :return: + A byte string of the decompressed plain text body + """ + + # Should adhere to https://tools.ietf.org/html/rfc7230#section-3.3.3 + + data = b'' + transfer_encoding = resp_headers.get('transfer-encoding') + if transfer_encoding and transfer_encoding.lower() == 'chunked': + while True: + line = self.socket.read_until(b'\r\n').decode('iso-8859-1').rstrip() + if re.match(r'^[a-fA-F0-9]+$', line): + chunk_length = int(line, 16) + if chunk_length == 0: + break + data += self.socket.read_exactly(chunk_length) + if self.socket.read_exactly(2) != b'\r\n': + raise OscryptoDownloaderException('Unable to parse chunk newline') + else: + self.close() + raise OscryptoDownloaderException('Unable to parse chunk length') + else: + content_length = self.parse_content_length(resp_headers) + if content_length is not None: + if content_length > 0: + data = self.socket.read_exactly(content_length) + elif code == 204 or code == 304 or (code >= 100 and code < 200): + # These HTTP codes are defined to not have a body + pass + elif resp_headers.get('connection', '').lower() == 'keep-alive': + # If the connection is kept-alive, and there is no content-length, + # and not chunked, than the response has an empty body. + pass + else: + # This should only happen if the server is going to close the connection + while self.socket.select_read(timeout=timeout): + data += self.socket.read(8192) + self.close() + + if resp_headers.get('connection', '').lower() == 'close': + self.close() + + encoding = resp_headers.get('content-encoding') + return self.decode_response(encoding, data) + + def dump_certificate(self, cert): + """ + If debugging is enabled, dumps info about the certificate + the server returned + """ + + if self.debug: + sig_algo_names = { + 'rsassa_pkcs1v15': 'RSA PKCS #1 v1.5', + 'dsa': 'DSA', + 'ecdsa': 'ECDSA', + 'rsassa_pss': 'RSA PSS', + } + signature_algo = sig_algo_names[cert.signature_algo] + public_key_algo = cert.public_key.algorithm.upper() + if public_key_algo == 'EC': + curve_info = cert.public_key.curve + if curve_info[0] == 'named': + public_key_algo += ' ' + curve_info[1] + else: + public_key_algo += ' ' + str(cert.public_key.bit_size) + console_write( + ''' + Oscrypto Server TLS Certificate + subject: %s + serial: %s + issuer: %s + expires: %s + valid domains: %s + public key algo: %s + signature algo: %s + sha256 fingerprint: %s + ''', + ( + cert.subject.human_friendly, + cert.serial_number, + cert.issuer.human_friendly, + cert['tbs_certificate']['validity']['not_after'].chosen.native.strftime( + '%Y-%m-%d %H:%M:%S %z' + ).strip(), + ', '.join(cert.valid_domains), + public_key_algo, + signature_algo, + cert.sha256_fingerprint, + ) + ) + + def do_proxy_connect(self, headers=None): + """ + Send the CONNECT request to the proxy server + """ + + req_headers = OrderedDict() + req_headers['Host'] = '%s:%s' % self.url_info + req_headers['User-Agent'] = self.user_agent + req_headers['Accept-Encoding'] = self.supported_encodings() + req_headers['Proxy-Connection'] = 'Keep-Alive' + + self.write_request('CONNECT %s:%d HTTP/1.1' % self.url_info, req_headers) + response = self.read_headers() + if not response: + raise OscryptoDownloaderException('Unable to parse response headers') + version, code, message, resp_headers = response + + close = False + for header in ('connection', 'proxy-connection'): + value = resp_headers.get(header) + if isinstance(value, str) and value.lower() == 'close': + close = True + + if close: + self.socket.close() + self.socket = None + self.setup_socket() + + # According to RFC 7230, there must be no content in the + # response to a CONNECT request, so we don't read anymore + + # Handle proxy auth for SSL connections since regular urllib punts on this + if code == 407 and self.proxy_info[1] and headers is None: + supported_auth_methods = {} + values = resp_headers.get('proxy-authenticate', tuple()) + for value in values: + details = value.split(' ', 1) + supported_auth_methods[details[0].lower()] = details[1] if len(details) > 1 else '' + + req_headers = OrderedDict() + + username = self.proxy_info[1] + password = self.proxy_info[2] + if 'digest' in supported_auth_methods: + response_value = self.build_digest_response( + supported_auth_methods['digest'], username, password) + if response_value: + req_headers['Proxy-Authorization'] = 'Digest %s' % response_value + + elif 'basic' in supported_auth_methods: + response_value = '%s:%s' % (username, password) + response_value = base64.b64encode(response_value.encode('utf-8')).decode('utf-8') + req_headers['Proxy-Authorization'] = 'Basic %s' % response_value.strip() + + return self.do_proxy_connect(req_headers) + + if code != 200: + self.close() + raise OscryptoDownloaderException("Tunnel connection failed: %d %s" % (code, message)) + + def build_digest_response(self, fields, username, password): + """ + Takes a Proxy-Authenticate: Digest header and creates a response + header + + :param fields: + The string portion of the Proxy-Authenticate header after + "Digest " + + :param username: + The username to use for the response + + :param password: + The password to use for the response + + :return: + None if invalid Proxy-Authenticate header, otherwise the + string of fields for the Proxy-Authorization: Digest header + """ + + fields = parse_keqv_list(parse_http_list(fields)) + + realm = fields.get('realm') + nonce = fields.get('nonce') + qop = fields.get('qop') + algorithm = fields.get('algorithm') + if algorithm: + algorithm = algorithm.lower() + opaque = fields.get('opaque') + + if algorithm in ('md5', None): + def md5hash(string): + return hashlib.md5(string).hexdigest() + _hash = md5hash + + elif algorithm == 'sha': + def sha1hash(string): + return hashlib.sha1(string).hexdigest() + _hash = sha1hash + + else: + return None + + host_port = '%s:%s' % self.url_info + + a1 = '%s:%s:%s' % (username, realm, password) + a2 = 'CONNECT:%s' % host_port + ha1 = _hash(a1) + ha2 = _hash(a2) + + if qop is None: + response = _hash('%s:%s:%s' % (ha1, nonce, ha2)) + elif qop == 'auth': + nc = '00000001' + cnonce = _hash(os.urandom(8))[:8] + response = _hash('%s:%s:%s:%s:%s:%s' % (ha1, nonce, nc, cnonce, qop, ha2)) + else: + return None + + resp_fields = OrderedDict() + resp_fields['username'] = username + resp_fields['realm'] = realm + resp_fields['nonce'] = nonce + resp_fields['response'] = response + resp_fields['uri'] = host_port + if algorithm: + resp_fields['algorithm'] = algorithm + if qop == 'auth': + resp_fields['nc'] = nc + resp_fields['cnonce'] = cnonce + resp_fields['qop'] = qop + if opaque: + resp_fields['opaque'] = opaque + + return ', '.join(['%s="%s"' % (field, resp_fields[field]) for field in resp_fields]) + + def supports_ssl(self): + """ + Indicates if the object can handle HTTPS requests + + :return: + If the object supports HTTPS requests + """ + return True + + def supports_plaintext(self): + """ + Indicates if the object can handle non-secure HTTP requests + + :return: + If the object supports non-secure HTTP requests + """ + + return False diff --git a/tasks/lib/package_control/downloaders/oscrypto_downloader_exception.py b/tasks/lib/package_control/downloaders/oscrypto_downloader_exception.py new file mode 100644 index 0000000..43fa1aa --- /dev/null +++ b/tasks/lib/package_control/downloaders/oscrypto_downloader_exception.py @@ -0,0 +1,12 @@ +from .downloader_exception import DownloaderException + + +class OscryptoDownloaderException(DownloaderException): + + """ + If the OscryptoDownloader ran into an error. Most likely a non-HTTPS + connection or non-HTTPS proxy. The means we should retry with the + UrllibDownloader. + """ + + pass diff --git a/tasks/lib/package_control/downloaders/rate_limit_exception.py b/tasks/lib/package_control/downloaders/rate_limit_exception.py new file mode 100644 index 0000000..6d9ce3d --- /dev/null +++ b/tasks/lib/package_control/downloaders/rate_limit_exception.py @@ -0,0 +1,28 @@ +from .downloader_exception import DownloaderException + + +class RateLimitException(DownloaderException): + + """ + An exception for when the rate limit of an API has been exceeded. + """ + + def __init__(self, domain, limit): + self.domain = domain + self.limit = limit + + def __str__(self): + return 'Hit rate limit of %s for %s.' % (self.limit, self.domain) + + +class RateLimitSkipException(DownloaderException): + + """ + An exception for when skipping requests due to rate limit of an API has been exceeded. + """ + + def __init__(self, domain): + self.domain = domain + + def __str__(self): + return 'Skipping %s due to rate limit.' % self.domain diff --git a/tasks/lib/package_control/downloaders/urllib_downloader.py b/tasks/lib/package_control/downloaders/urllib_downloader.py new file mode 100644 index 0000000..65d1f90 --- /dev/null +++ b/tasks/lib/package_control/downloaders/urllib_downloader.py @@ -0,0 +1,323 @@ +import re +import sys +import urllib.request as urllib_compat +from http.client import HTTPException, BadStatusLine +from urllib.request import ( + build_opener, + HTTPPasswordMgrWithDefaultRealm, + ProxyBasicAuthHandler, + ProxyDigestAuthHandler, + ProxyHandler, + Request, +) +from urllib.error import HTTPError, URLError +from socket import error as ConnectionError + +from .. import text +from ..ca_certs import get_ca_bundle_path +from ..console_write import console_write +from ..http.validating_https_handler import ValidatingHTTPSHandler +from ..http.debuggable_http_handler import DebuggableHTTPHandler +from .downloader_exception import DownloaderException +from .basic_auth_downloader import BasicAuthDownloader +from .caching_downloader import CachingDownloader +from .decoding_downloader import DecodingDownloader +from .limiting_downloader import LimitingDownloader + + +class UrlLibDownloader(DecodingDownloader, LimitingDownloader, CachingDownloader, BasicAuthDownloader): + + """ + A downloader that uses the Python urllib module + + :param settings: + A dict of the various Package Control settings. The Sublime Text + Settings API is not used because this code is run in a thread. + """ + + def __init__(self, settings): + self.opener = None + self.settings = settings + + def close(self): + """ + Closes any persistent/open connections + """ + + if not self.opener: + return + handler = self.get_handler() + if handler: + handler.close() + self.opener = None + + def download(self, url, error_message, timeout, tries, prefer_cached=False): + """ + Downloads a URL and returns the contents + + Uses the proxy settings from the Package Control.sublime-settings file, + however there seem to be a decent number of proxies that this code + does not work with. Patches welcome! + + :param url: + The URL to download + + :param error_message: + A string to include in the console error that is printed + when an error occurs + + :param timeout: + The int number of seconds to set the timeout to + + :param tries: + The int number of times to try and download the URL in the case of + a timeout or HTTP 503 error + + :param prefer_cached: + If a cached version should be returned instead of trying a new request + + :raises: + RateLimitException: when a rate limit is hit + DownloaderException: when any other download error occurs + + :return: + The string contents of the URL + """ + + if prefer_cached: + cached = self.retrieve_cached(url) + if cached: + return cached + + self.setup_opener(url, timeout) + + debug = self.settings.get('debug') + tried = tries + error_string = None + while tries > 0: + tries -= 1 + try: + request_headers = { + # Don't be alarmed if the response from the server does not + # select one of these since the server runs a relatively new + # version of OpenSSL which supports compression on the SSL + # layer, and Apache will use that instead of HTTP-level + # encoding. + "Accept-Encoding": self.supported_encodings() + } + user_agent = self.settings.get('user_agent') + if user_agent: + request_headers["User-Agent"] = user_agent + + request_headers.update(self.build_auth_header(url)) + + request_headers = self.add_conditional_headers(url, request_headers) + request = Request(url, headers=request_headers) + http_file = self.opener.open(request, timeout=timeout) + self.handle_rate_limit(http_file.headers, url) + + result = http_file.read() + # Make sure the response is closed so we can re-use the connection + http_file.close() + + encoding = http_file.headers.get('content-encoding') + result = self.decode_response(encoding, result) + + return self.cache_result('get', url, http_file.getcode(), http_file.headers, result) + + except (HTTPException) as e: + # Since we use keep-alives, it is possible the other end closed + # the connection, and we may just need to re-open + if isinstance(e, BadStatusLine): + handler = self.get_handler() + if handler and handler.use_count > 1: + self.close() + self.setup_opener(url, timeout) + tries += 1 + continue + + exception_type = e.__class__.__name__ + error_string = text.format( + ''' + %s HTTP exception %s (%s) downloading %s. + ''', + (error_message, exception_type, str(e), url) + ) + + except (HTTPError) as e: + # Make sure the response is closed so we can re-use the connection + e.read() + e.close() + + # Make sure we obey Github's rate limiting headers + self.handle_rate_limit(e.headers, url) + + # Handle cached responses + if str(e.code) == '304': + return self.cache_result('get', url, int(e.code), e.headers, b'') + + # Bitbucket and Github return 503 a decent amount + if str(e.code) == '503' and tries != 0: + if tries and debug: + console_write( + ''' + Downloading %s was rate limited, trying again + ''', + url + ) + continue + + error_string = text.format( + ''' + %s HTTP error %s downloading %s. + ''', + (error_message, str(e.code), url) + ) + + except (URLError) as e: + + # Bitbucket and Github timeout a decent amount + if str(e.reason) == 'The read operation timed out' \ + or str(e.reason) == 'timed out': + if tries and debug: + console_write( + ''' + Downloading %s timed out, trying again + ''', + url + ) + continue + + error_string = text.format( + ''' + %s URL error %s downloading %s. + ''', + (error_message, str(e.reason), url) + ) + + except (ConnectionError): + # Handle broken pipes/reset connections by creating a new opener, and + # thus getting new handlers and a new connection + if debug: + console_write( + ''' + Connection went away while trying to download %s, trying again + ''', + url + ) + + self.opener = None + self.setup_opener(url, timeout) + + continue + + break + + if error_string is None: + plural = 's' if tried > 1 else '' + error_string = 'Unable to download %s after %d attempt%s' % (url, tried, plural) + + raise DownloaderException(error_string) + + def get_handler(self): + """ + Get the HTTPHandler object for the current connection + """ + + if not self.opener: + return None + + for handler in self.opener.handlers: + if isinstance(handler, ValidatingHTTPSHandler) or isinstance(handler, DebuggableHTTPHandler): + return handler + + def setup_opener(self, url, timeout): + """ + Sets up a urllib OpenerDirector to be used for requests. There is a + fair amount of custom urllib code in Package Control, and part of it + is to handle proxies and keep-alives. Creating an opener the way + below is because the handlers have been customized to send the + "Connection: Keep-Alive" header and hold onto connections so they + can be re-used. + + :param url: + The URL to download + + :param timeout: + The int number of seconds to set the timeout to + """ + + if not self.opener: + http_proxy = self.settings.get('http_proxy') + https_proxy = self.settings.get('https_proxy') + if http_proxy or https_proxy: + proxies = {} + if http_proxy: + proxies['http'] = http_proxy + if https_proxy: + proxies['https'] = https_proxy + proxy_handler = ProxyHandler(proxies) + else: + proxy_handler = ProxyHandler() + + password_manager = HTTPPasswordMgrWithDefaultRealm() + proxy_username = self.settings.get('proxy_username') + proxy_password = self.settings.get('proxy_password') + if proxy_username and proxy_password: + if http_proxy: + password_manager.add_password(None, http_proxy, proxy_username, proxy_password) + if https_proxy: + password_manager.add_password(None, https_proxy, proxy_username, proxy_password) + + handlers = [proxy_handler] + + basic_auth_handler = ProxyBasicAuthHandler(password_manager) + digest_auth_handler = ProxyDigestAuthHandler(password_manager) + handlers.extend([digest_auth_handler, basic_auth_handler]) + + debug = self.settings.get('debug') + + if debug: + console_write( + ''' + Urllib Debug Proxy + http_proxy: %s + https_proxy: %s + proxy_username: %s + proxy_password: %s + ''', + (http_proxy, https_proxy, proxy_username, proxy_password) + ) + + secure_url_match = re.match(r'^https://([^/#?]+)', url) + if secure_url_match is not None: + bundle_path = get_ca_bundle_path(self.settings) + bundle_path = bundle_path.encode(sys.getfilesystemencoding()) + handlers.append(ValidatingHTTPSHandler( + ca_certs=bundle_path, + debug=debug, + passwd=password_manager, + user_agent=self.settings.get('user_agent') + )) + else: + handlers.append(DebuggableHTTPHandler(debug=debug, passwd=password_manager)) + self.opener = build_opener(*handlers) + + def supports_ssl(self): + """ + Indicates if the object can handle HTTPS requests + + :return: + If the object supports HTTPS requests + """ + return 'ssl' in sys.modules and hasattr(urllib_compat, 'HTTPSHandler') + + def supports_plaintext(self): + """ + Indicates if the object can handle non-secure HTTP requests + + :return: + If the object supports non-secure HTTP requests + """ + + return True diff --git a/tasks/lib/package_control/downloaders/wget_downloader.py b/tasks/lib/package_control/downloaders/wget_downloader.py new file mode 100644 index 0000000..d010990 --- /dev/null +++ b/tasks/lib/package_control/downloaders/wget_downloader.py @@ -0,0 +1,394 @@ +import os +import re +import sys +import tempfile + +from ..ca_certs import get_ca_bundle_path +from ..console_write import console_write +from .cli_downloader import CliDownloader +from .non_http_error import NonHttpError +from .non_clean_exit_error import NonCleanExitError +from .downloader_exception import DownloaderException +from .basic_auth_downloader import BasicAuthDownloader +from .caching_downloader import CachingDownloader +from .decoding_downloader import DecodingDownloader +from .limiting_downloader import LimitingDownloader + + +class WgetDownloader(CliDownloader, DecodingDownloader, LimitingDownloader, CachingDownloader, BasicAuthDownloader): + + """ + A downloader that uses the command line program wget + + :param settings: + A dict of the various Package Control settings. The Sublime Text + Settings API is not used because this code is run in a thread. + + :raises: + BinaryNotFoundError: when wget can not be found + """ + + def __init__(self, settings): + self.settings = settings + self.debug = settings.get('debug') + self.wget = self.find_binary('wget') + + def close(self): + """ + No-op for compatibility with UrllibDownloader and WinINetDownloader + """ + + pass + + def download(self, url, error_message, timeout, tries, prefer_cached=False): + """ + Downloads a URL and returns the contents + + :param url: + The URL to download + + :param error_message: + A string to include in the console error that is printed + when an error occurs + + :param timeout: + The int number of seconds to set the timeout to + + :param tries: + The int number of times to try and download the URL in the case of + a timeout or HTTP 503 error + + :param prefer_cached: + If a cached version should be returned instead of trying a new request + + :raises: + RateLimitException: when a rate limit is hit + DownloaderException: when any other download error occurs + + :return: + The string contents of the URL + """ + + if prefer_cached: + cached = self.retrieve_cached(url) + if cached: + return cached + + self.tmp_file = tempfile.NamedTemporaryFile().name + command = [ + self.wget, + '--connect-timeout=' + str(int(timeout)), + '-o', + self.tmp_file, + '-O', + '-', + '--secure-protocol=TLSv1' + ] + + user_agent = self.settings.get('user_agent') + if user_agent: + command.extend(['-U', user_agent]) + + request_headers = { + # Don't be alarmed if the response from the server does not select + # one of these since the server runs a relatively new version of + # OpenSSL which supports compression on the SSL layer, and Apache + # will use that instead of HTTP-level encoding. + 'Accept-Encoding': self.supported_encodings() + } + request_headers = self.add_conditional_headers(url, request_headers) + + username, password = self.get_username_password(url) + if username and password: + command.extend(['--user=%s' % username, '--password=%s' % password]) + + for name, value in request_headers.items(): + command.extend(['--header', "%s: %s" % (name, value)]) + + secure_url_match = re.match(r'^https://([^/#?]+)', url) + if secure_url_match is not None: + bundle_path = get_ca_bundle_path(self.settings) + command.append('--ca-certificate=' + bundle_path) + + command.append('-S') + if self.debug: + command.append('-d') + else: + command.append('-q') + + http_proxy = self.settings.get('http_proxy') + https_proxy = self.settings.get('https_proxy') + proxy_username = self.settings.get('proxy_username') + proxy_password = self.settings.get('proxy_password') + + if proxy_username: + command.append("--proxy-user=%s" % proxy_username) + if proxy_password: + command.append("--proxy-password=%s" % proxy_password) + + if self.debug: + console_write( + ''' + Wget Debug Proxy + http_proxy: %s + https_proxy: %s + proxy_username: %s + proxy_password: %s + ''', + (http_proxy, https_proxy, proxy_username, proxy_password) + ) + + command.append(url) + + if http_proxy: + os.putenv('http_proxy', http_proxy) + if https_proxy: + os.putenv('https_proxy', https_proxy) + + error_string = None + while tries > 0: + tries -= 1 + try: + result = self.execute(command) + + general, headers = self.parse_output(True) + encoding = headers.get('content-encoding') + result = self.decode_response(encoding, result) + + result = self.cache_result('get', url, general['status'], headers, result) + + return result + + except (NonCleanExitError): + + try: + general, headers = self.parse_output(False) + self.handle_rate_limit(headers, url) + + if general['status'] == 304: + return self.cache_result('get', url, general['status'], headers, None) + + if general['status'] == 503 and tries != 0: + # GitHub and BitBucket seem to rate limit via 503 + if tries and self.debug: + console_write( + ''' + Downloading %s was rate limited, trying again + ''', + url + ) + continue + + download_error = 'HTTP error %s' % general['status'] + + except (NonHttpError) as e: + + download_error = str(e) + + # GitHub and BitBucket seem to time out a lot + if download_error.find('timed out') != -1: + if tries and self.debug: + console_write( + ''' + Downloading %s timed out, trying again + ''', + url + ) + continue + + error_string = '%s %s downloading %s.' % (error_message, download_error, url) + + break + + raise DownloaderException(error_string) + + def supports_ssl(self): + """ + Indicates if the object can handle HTTPS requests + + :return: + If the object supports HTTPS requests + """ + + return True + + def supports_plaintext(self): + """ + Indicates if the object can handle non-secure HTTP requests + + :return: + If the object supports non-secure HTTP requests + """ + + return True + + def parse_output(self, clean_run): + """ + Parses the wget output file, prints debug information and returns headers + + :param clean_run: + If wget executed with a successful exit code + + :raises: + NonHttpError - when clean_run is false and an error is detected + + :return: + A tuple of (general, headers) where general is a dict with the keys: + `version` - HTTP version number (string) + `status` - HTTP status code (integer) + `message` - HTTP status message (string) + And headers is a dict with the keys being lower-case version of the + HTTP header names. + """ + + with open(self.tmp_file, 'r', encoding=sys.getdefaultencoding()) as fobj: + output = fobj.read().splitlines() + self.clean_tmp_file() + + debug_missing = False + error = None + header_lines = [] + if self.debug: + section = 'General' + last_section = None + for line in output: + if section == 'General': + if self.skippable_line(line): + continue + + # This handles situations where debug is not compiled in + if line.startswith('HTTP request sent, awaiting response'): + last_section = 'General' + section = 'Read' + debug_missing = True + continue + + # Skip blank lines + if line.strip() == '': + continue + + # Error lines + if line[0:5] == 'wget:': + error = line[5:].strip() + if line[0:7] == 'failed:': + error = line[7:].strip() + + if line == '---request begin---': + section = 'Write' + continue + elif line == '---request end---': + section = 'General' + continue + elif line == '---response begin---': + section = 'Read' + continue + elif line == '---response end---': + section = 'General' + continue + + if section != last_section: + console_write('Wget HTTP Debug %s', section) + + if section == 'Read': + if debug_missing: + if ':' in line: + header_lines.append(line.lstrip()) + else: + header_lines.append(line) + + console_write(' %s', line, prefix=False) + last_section = section + + else: + for line in output: + if self.skippable_line(line): + continue + + # Check the resolving and connecting to lines for errors + if re.match('(Resolving |Connecting to )', line): + failed_match = re.search(' failed: (.*)$', line) + if failed_match: + error = failed_match.group(1).strip() + + # Error lines + if line[0:5] == 'wget:': + error = line[5:].strip() + if line[0:7] == 'failed:': + error = line[7:].strip() + + if line[0:2] == ' ': + header_lines.append(line.lstrip()) + + if not clean_run and error: + raise NonHttpError(error) + + return self.parse_headers(header_lines) + + def skippable_line(self, line): + """ + Determines if a debug line is skippable - usually because of extraneous + or duplicate information. + + :param line: + The debug line to check + + :return: + True if the line is skippable, otherwise None + """ + + # Skip date lines + if re.match(r'--\d{4}-\d{2}-\d{2}', line): + return True + if re.match(r'\d{4}-\d{2}-\d{2}', line): + return True + # Skip HTTP status code lines since we already have that info + if re.match(r'\d{3} ', line): + return True + # Skip Saving to and progress lines + if re.match(r'(Saving to:|\s*\d+K)', line): + return True + # Skip notice about ignoring body on HTTP error + if re.match(r'Skipping \d+ byte', line): + return True + + def parse_headers(self, output): + """ + Parses HTTP headers into two dict objects + + :param output: + An array of header lines + + :return: + A tuple of (general, headers) where general is a dict with the keys: + `version` - HTTP version number (string) + `status` - HTTP status code (integer) + `message` - HTTP status message (string) + And headers is a dict with the keys being lower-case version of the + HTTP header names. + """ + + general = { + 'version': '0.9', + 'status': 200, + 'message': 'OK' + } + headers = {} + for line in output: + # When using the -S option, headers have two spaces before them, + # additionally, valid headers won't have spaces, so this is always + # a safe operation to perform + line = line.lstrip() + if line.find('HTTP/') == 0: + match = re.match(r'HTTP/(\d\.\d)\s+(\d+)(?:\s+(.*))?$', line) + general['version'] = match.group(1) + general['status'] = int(match.group(2)) + general['message'] = match.group(3) or '' + else: + name, value = line.split(':', 1) + name = name.lower() + if name in headers: + headers[name] += ', %s' % value.strip() + else: + headers[name] = value.strip() + + return (general, headers) diff --git a/tasks/lib/package_control/downloaders/win_downloader_exception.py b/tasks/lib/package_control/downloaders/win_downloader_exception.py new file mode 100644 index 0000000..d69af64 --- /dev/null +++ b/tasks/lib/package_control/downloaders/win_downloader_exception.py @@ -0,0 +1,11 @@ +from .downloader_exception import DownloaderException + + +class WinDownloaderException(DownloaderException): + + """ + If the WinInetDownloader ran into a windows-specific error. The means we + should retry with the UrllibDownloader. + """ + + pass diff --git a/tasks/lib/package_control/downloaders/wininet_downloader.py b/tasks/lib/package_control/downloaders/wininet_downloader.py new file mode 100644 index 0000000..da7da77 --- /dev/null +++ b/tasks/lib/package_control/downloaders/wininet_downloader.py @@ -0,0 +1,932 @@ +from ctypes import windll, wintypes +import ctypes +import datetime +# To prevent import errors in thread with datetime +import locale # noqa +import re +import struct +from urllib.parse import urlparse + +from .. import text +from ..console_write import console_write +from .http_error import HttpError +from .non_http_error import NonHttpError +from .downloader_exception import DownloaderException +from .win_downloader_exception import WinDownloaderException +from .basic_auth_downloader import BasicAuthDownloader +from .caching_downloader import CachingDownloader +from .decoding_downloader import DecodingDownloader +from .limiting_downloader import LimitingDownloader + +wininet = windll.wininet + + +class WinINetDownloader(DecodingDownloader, LimitingDownloader, CachingDownloader, BasicAuthDownloader): + + """ + A downloader that uses the Windows WinINet DLL to perform downloads. This + has the benefit of utilizing system-level proxy configuration and CA certs. + + :param settings: + A dict of the various Package Control settings. The Sublime Text + Settings API is not used because this code is run in a thread. + """ + + # General constants + ERROR_INSUFFICIENT_BUFFER = 122 + + # InternetOpen constants + INTERNET_OPEN_TYPE_PRECONFIG = 0 + + # InternetConnect constants + INTERNET_SERVICE_HTTP = 3 + INTERNET_FLAG_EXISTING_CONNECT = 0x20000000 + INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS = 0x00004000 + + # InternetSetOption constants + INTERNET_OPTION_CONNECT_TIMEOUT = 2 + INTERNET_OPTION_SEND_TIMEOUT = 5 + INTERNET_OPTION_RECEIVE_TIMEOUT = 6 + + # InternetQueryOption constants + INTERNET_OPTION_SECURITY_CERTIFICATE_STRUCT = 32 + INTERNET_OPTION_PROXY = 38 + INTERNET_OPTION_PROXY_USERNAME = 43 + INTERNET_OPTION_PROXY_PASSWORD = 44 + INTERNET_OPTION_CONNECTED_STATE = 50 + + # HttpOpenRequest constants + INTERNET_FLAG_KEEP_CONNECTION = 0x00400000 + INTERNET_FLAG_RELOAD = 0x80000000 + INTERNET_FLAG_NO_CACHE_WRITE = 0x04000000 + INTERNET_FLAG_PRAGMA_NOCACHE = 0x00000100 + INTERNET_FLAG_SECURE = 0x00800000 + + # HttpQueryInfo constants + HTTP_QUERY_RAW_HEADERS_CRLF = 22 + + # InternetConnectedState constants + INTERNET_STATE_CONNECTED = 1 + INTERNET_STATE_DISCONNECTED = 2 + INTERNET_STATE_DISCONNECTED_BY_USER = 0x10 + INTERNET_STATE_IDLE = 0x100 + INTERNET_STATE_BUSY = 0x200 + + HTTP_STATUS_MESSAGES = { + 100: "Continue", + 101: "Switching Protocols", + 102: "Processing", + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non-Authoritative Information", + 204: "No Content", + 205: "Reset Content", + 206: "Partial Content", + 207: "Multi-Status", + 208: "Already Reported", + 226: "IM Used", + 300: "Multiple Choices", + 301: "Moved Permanently", + 302: "Found", + 303: "See Other", + 304: "Not Modified", + 305: "Use Proxy", + 306: "Switch Proxy", + 307: "Temporary Redirect", + 308: "Permanent Redirect", + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request Timeout", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Request Entity Too Large", + 414: "Request-URI Too Long", + 415: "Unsupported Media Type", + 416: "Requested Range Not Satisfiable", + 417: "Expectation Failed", + 418: "I'm a teapot", + 419: "Authentication Timeout", + 420: "Enhance Your Calm", + 422: "Unprocessable Entity", + 423: "Locked", + 424: "Failed Dependency", + 425: "Unordered Collection", + 426: "Upgrade Required", + 428: "Precondition Required", + 429: "Too Many Requests", + 431: "Request Header Fields Too Large", + 440: "Login Timeout", + 449: "Retry With", + 450: "Blocked by Windows Parental Controls", + 451: "Redirect", + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Timeout", + 505: "HTTP Version Not Supported", + 506: "Variant Also Negotiates", + 507: "Insufficient Storage", + 508: "Loop Detected", + 509: "Bandwidth Limit Exceeded", + 510: "Not Extended", + 511: "Network Authentication Required", + 520: "Origin Error", + 522: "Connection Timed Out", + 523: "Proxy Declined Request", + 524: "A Timeout Occurred", + 598: "Network Read Timeout Error", + 599: "Network Connect Timeout Error" + } + + def __init__(self, settings): + self.settings = settings + self.debug = settings.get('debug') + self.network_connection = None + self.tcp_connection = None + self.use_count = 0 + self.hostname = None + self.port = None + self.scheme = None + self.was_offline = None + + self.proxy = '' + self.proxy_bypass = '' + self.proxy_username = None + self.proxy_password = None + + def close(self): + """ + Closes any persistent/open connections + """ + + changed_state_back = False + + if self.tcp_connection: + wininet.InternetCloseHandle(self.tcp_connection) + self.tcp_connection = None + + if self.network_connection: + wininet.InternetCloseHandle(self.network_connection) + self.network_connection = None + + if self.was_offline: + dw_connected_state = wintypes.DWORD(self.INTERNET_STATE_DISCONNECTED_BY_USER) + dw_flags = wintypes.DWORD(0) + connected_info = InternetConnectedInfo(dw_connected_state, dw_flags) + wininet.InternetSetOptionA( + None, + self.INTERNET_OPTION_CONNECTED_STATE, + ctypes.byref(connected_info), + ctypes.sizeof(connected_info) + ) + changed_state_back = True + + if self.debug: + s = '' if self.use_count == 1 else 's' + console_write( + ''' + WinINet %s Debug General + Closing connection to %s on port %s after %s request%s + ''', + (self.scheme.upper(), self.hostname, self.port, self.use_count, s) + ) + if changed_state_back: + console_write( + ' Changed Internet Explorer back to Work Offline', + prefix=False + ) + + self.hostname = None + self.port = None + self.scheme = None + self.use_count = 0 + self.was_offline = None + + def download(self, url, error_message, timeout, tries, prefer_cached=False): + """ + Downloads a URL and returns the contents + + :param url: + The URL to download + + :param error_message: + A string to include in the console error that is printed + when an error occurs + + :param timeout: + The int number of seconds to set the timeout to + + :param tries: + The int number of times to try and download the URL in the case of + a timeout or HTTP 503 error + + :param prefer_cached: + If a cached version should be returned instead of trying a new request + + :raises: + RateLimitException: when a rate limit is hit + DownloaderException: when any other download error occurs + WinDownloaderException: when an internal Windows error occurs + + :return: + The string contents of the URL + """ + + if prefer_cached: + cached = self.retrieve_cached(url) + if cached: + return cached + + url_info = urlparse(url) + + if not url_info.port: + port = 443 if url_info.scheme == 'https' else 80 + hostname = url_info.netloc + else: + port = url_info.port + hostname = url_info.hostname + + path = url_info.path + if url_info.params: + path += ';' + url_info.params + if url_info.query: + path += '?' + url_info.query + + username = url_info.username + password = url_info.password + + request_headers = { + 'Accept-Encoding': self.supported_encodings() + } + if not username and not password: + request_headers.update(self.build_auth_header(url)) + + request_headers = self.add_conditional_headers(url, request_headers) + + created_connection = False + # If we switched Internet Explorer out of "Work Offline" mode + changed_to_online = False + + # If the user is requesting a connection to another server, close the connection + if (self.hostname and self.hostname != hostname) or (self.port and self.port != port): + self.close() + + # Reset the error info to a known clean state + ctypes.windll.kernel32.SetLastError(0) + + # Save the internet setup in the class for re-use + if not self.tcp_connection: + created_connection = True + + # Connect to the internet if necessary + state = self.read_option(None, self.INTERNET_OPTION_CONNECTED_STATE) + state = ord(state) + if state & self.INTERNET_STATE_DISCONNECTED or state & self.INTERNET_STATE_DISCONNECTED_BY_USER: + # Track the previous state so we can go back once complete + self.was_offline = True + + dw_connected_state = wintypes.DWORD(self.INTERNET_STATE_CONNECTED) + dw_flags = wintypes.DWORD(0) + connected_info = InternetConnectedInfo(dw_connected_state, dw_flags) + wininet.InternetSetOptionA( + None, + self.INTERNET_OPTION_CONNECTED_STATE, + ctypes.byref(connected_info), + ctypes.sizeof(connected_info) + ) + changed_to_online = True + + self.network_connection = wininet.InternetOpenW( + self.settings.get('user_agent', ''), + self.INTERNET_OPEN_TYPE_PRECONFIG, + None, + None, + 0 + ) + + if not self.network_connection: + error_string = text.format( + ''' + %s %s during network phase of downloading %s. + ''', + (error_message, self.extract_error(), url) + ) + raise WinDownloaderException(error_string) + + win_timeout = wintypes.DWORD(int(timeout) * 1000) + wininet.InternetSetOptionA( + self.network_connection, + self.INTERNET_OPTION_CONNECT_TIMEOUT, + ctypes.byref(win_timeout), + ctypes.sizeof(win_timeout) + ) + wininet.InternetSetOptionA( + self.network_connection, + self.INTERNET_OPTION_SEND_TIMEOUT, + ctypes.byref(win_timeout), + ctypes.sizeof(win_timeout) + ) + wininet.InternetSetOptionA( + self.network_connection, + self.INTERNET_OPTION_RECEIVE_TIMEOUT, + ctypes.byref(win_timeout), + ctypes.sizeof(win_timeout) + ) + + # Don't allow HTTPS sites to redirect to HTTP sites + tcp_flags = self.INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS + # Try to re-use an existing connection to the server + tcp_flags |= self.INTERNET_FLAG_EXISTING_CONNECT + self.tcp_connection = wininet.InternetConnectW( + self.network_connection, + hostname, + port, + username, + password, + self.INTERNET_SERVICE_HTTP, + tcp_flags, + 0 + ) + + if not self.tcp_connection: + error_string = text.format( + ''' + %s %s during connection phase of downloading %s. + ''', + (error_message, self.extract_error(), url) + ) + raise WinDownloaderException(error_string) + + # Normally the proxy info would come from IE, but this allows storing it in + # the Package Control settings file. + proxy_username = self.settings.get('proxy_username') + proxy_password = self.settings.get('proxy_password') + if proxy_username and proxy_password: + username = ctypes.c_wchar_p(proxy_username) + password = ctypes.c_wchar_p(proxy_password) + wininet.InternetSetOptionW( + self.tcp_connection, + self.INTERNET_OPTION_PROXY_USERNAME, + ctypes.cast(username, ctypes.c_void_p), + len(proxy_username) + ) + wininet.InternetSetOptionW( + self.tcp_connection, + self.INTERNET_OPTION_PROXY_PASSWORD, + ctypes.cast(password, ctypes.c_void_p), + len(proxy_password) + ) + + self.hostname = hostname + self.port = port + self.scheme = url_info.scheme + + else: + if self.debug: + console_write( + ''' + WinINet %s Debug General + Re-using connection to %s on port %s for request #%s + ''', + ( + self.scheme.upper(), + self.hostname, + self.port, + self.use_count + ) + ) + + error_string = None + while tries > 0: + tries -= 1 + try: + http_connection = None + + # Keep-alive for better performance + http_flags = self.INTERNET_FLAG_KEEP_CONNECTION + # Prevent caching/retrieving from cache + http_flags |= self.INTERNET_FLAG_RELOAD + http_flags |= self.INTERNET_FLAG_NO_CACHE_WRITE + http_flags |= self.INTERNET_FLAG_PRAGMA_NOCACHE + # Use SSL + if self.scheme == 'https': + http_flags |= self.INTERNET_FLAG_SECURE + + http_connection = wininet.HttpOpenRequestW( + self.tcp_connection, + 'GET', + path, + 'HTTP/1.1', + None, + None, + http_flags, + 0 + ) + if not http_connection: + error_string = text.format( + ''' + %s %s during HTTP connection phase of downloading %s. + ''', + (error_message, self.extract_error(), url) + ) + raise WinDownloaderException(error_string) + + request_header_lines = [] + for header, value in request_headers.items(): + request_header_lines.append("%s: %s" % (header, value)) + request_header_lines = "\r\n".join(request_header_lines) + + success = wininet.HttpSendRequestW( + http_connection, + request_header_lines, + len(request_header_lines), + None, + 0 + ) + + if not success: + error_string = text.format( + ''' + %s %s during HTTP write phase of downloading %s. + ''', + (error_message, self.extract_error(), url) + ) + raise WinDownloaderException(error_string) + + # If we try to query before here, the proxy info will not be available to the first request + self.cache_proxy_info() + if self.debug: + console_write( + ''' + WinINet Debug Proxy + proxy: %s + proxy bypass: %s + proxy username: %s + proxy password: %s + ''', + ( + self.proxy, + self.proxy_bypass, + self.proxy_username, + self.proxy_password + ) + ) + + self.use_count += 1 + + if self.debug and created_connection: + if changed_to_online: + console_write( + ''' + WinINet HTTP Debug General + Internet Explorer was set to Work Offline, temporarily going online + ''' + ) + if self.scheme == 'https': + cert_struct = self.read_option( + http_connection, + self.INTERNET_OPTION_SECURITY_CERTIFICATE_STRUCT + ) + + if cert_struct.lpszIssuerInfo: + issuer_info = cert_struct.lpszIssuerInfo.decode('cp1252') + issuer_parts = issuer_info.split("\r\n") + else: + issuer_parts = ['No issuer info'] + + if cert_struct.lpszSubjectInfo: + subject_info = cert_struct.lpszSubjectInfo.decode('cp1252') + subject_parts = subject_info.split("\r\n") + else: + subject_parts = ["No subject info"] + + common_name = subject_parts[-1] + + if cert_struct.ftStart.dwLowDateTime != 0 and cert_struct.ftStart.dwHighDateTime != 0: + issue_date = self.convert_filetime_to_datetime(cert_struct.ftStart) + issue_date = issue_date.strftime('%a, %d %b %Y %H:%M:%S GMT') + else: + issue_date = "No issue date" + + if cert_struct.ftExpiry.dwLowDateTime != 0 and cert_struct.ftExpiry.dwHighDateTime != 0: + expiration_date = self.convert_filetime_to_datetime(cert_struct.ftExpiry) + expiration_date = expiration_date.strftime('%a, %d %b %Y %H:%M:%S GMT') + else: + expiration_date = "No expiration date" + + console_write( + ''' + WinINet HTTPS Debug General + Server SSL Certificate: + subject: %s + issuer: %s + common name: %s + issue date: %s + expire date: %s + ''', + ( + ', '.join(subject_parts), + ', '.join(issuer_parts), + common_name, + issue_date, + expiration_date + ) + ) + + if self.debug: + # We can't get the real list of headers, so we generate one + # from what we do know + + other_headers = [] + for header, value in request_headers.items(): + other_headers.append('%s: %s' % (header, value)) + indented_headers = '\n '.join(other_headers) + + console_write( + ''' + WinINet %s Debug Write + GET %s HTTP/1.1 + User-Agent: %s + Host: %s + Connection: Keep-Alive + Cache-Control: no-cache + %s + ''', + ( + self.scheme.upper(), + path, + self.settings.get('user_agent'), + hostname, + indented_headers + ) + ) + + header_buffer_size = 8192 + + try_again = True + while try_again: + try_again = False + + to_read_was_read = wintypes.DWORD(header_buffer_size) + headers_buffer = ctypes.create_string_buffer(header_buffer_size) + + success = wininet.HttpQueryInfoA( + http_connection, + self.HTTP_QUERY_RAW_HEADERS_CRLF, + ctypes.byref(headers_buffer), + ctypes.byref(to_read_was_read), + None + ) + if not success: + if ctypes.GetLastError() != self.ERROR_INSUFFICIENT_BUFFER: + error_string = text.format( + ''' + %s %s during header read phase of downloading %s. + ''', + (error_message, self.extract_error(), url) + ) + raise WinDownloaderException(error_string) + # The error was a buffer that was too small, so try again + header_buffer_size = to_read_was_read.value + try_again = True + continue + + headers = b'' + if to_read_was_read.value > 0: + headers += headers_buffer.raw[:to_read_was_read.value] + headers = headers.decode('iso-8859-1').rstrip("\r\n").split("\r\n") + + if self.debug: + indented_headers = '\n '.join(headers) + console_write( + ''' + WinINet %s Debug Read + %s + ''', + (self.scheme.upper(), indented_headers) + ) + + buffer_length = 65536 + output_buffer = ctypes.create_string_buffer(buffer_length) + bytes_read = wintypes.DWORD() + + result = b'' + try_again = True + while try_again: + try_again = False + wininet.InternetReadFile(http_connection, output_buffer, buffer_length, ctypes.byref(bytes_read)) + if bytes_read.value > 0: + result += output_buffer.raw[:bytes_read.value] + try_again = True + + general, headers = self.parse_headers(headers) + self.handle_rate_limit(headers, url) + + if general['status'] == 503 and tries != 0: + # GitHub and BitBucket seem to rate limit via 503 + if tries and self.debug: + console_write( + ''' + Downloading %s was rate limited, trying again + ''', + url + ) + continue + + encoding = headers.get('content-encoding') + result = self.decode_response(encoding, result) + + result = self.cache_result('get', url, general['status'], headers, result) + + if general['status'] not in [200, 304]: + raise HttpError("HTTP error %s" % general['status'], general['status']) + + return result + + except (NonHttpError, HttpError) as e: + + # GitHub and BitBucket seem to time out a lot + if str(e).find('timed out') != -1: + if tries and self.debug: + console_write( + ''' + Downloading %s timed out, trying again + ''', + url + ) + continue + + error_string = text.format( + ''' + %s %s downloading %s. + ''', + (error_message, str(e), url) + ) + + finally: + if http_connection: + wininet.InternetCloseHandle(http_connection) + + break + + raise DownloaderException(error_string) + + def convert_filetime_to_datetime(self, filetime): + """ + Windows returns times as 64-bit unsigned longs that are the number + of hundreds of nanoseconds since Jan 1 1601. This converts it to + a datetime object. + + :param filetime: + A FileTime struct object + + :return: + A (UTC) datetime object + """ + + hundreds_nano_seconds = struct.unpack( + '>Q', + struct.pack('>LL', filetime.dwHighDateTime, filetime.dwLowDateTime) + )[0] + seconds_since_1601 = hundreds_nano_seconds / 10000000 + epoch_seconds = seconds_since_1601 - 11644473600 # Seconds from Jan 1 1601 to Jan 1 1970 + return datetime.datetime.fromtimestamp(epoch_seconds) + + def extract_error(self): + """ + Retrieves and formats an error from WinINet + + :return: + A string with a nice description of the error + """ + + error_num = ctypes.GetLastError() + raw_error_string = ctypes.FormatError(error_num) + + error_string = str(raw_error_string) + + # Try to fill in some known errors + if error_string == "": + error_lookup = { + 12007: 'host not found', + 12029: 'connection refused', + 12057: 'error checking for server certificate revocation', + 12169: 'invalid secure certificate', + 12157: 'secure channel error, server not providing SSL', + 12002: 'operation timed out' + } + if error_num in error_lookup: + error_string = error_lookup[error_num] + + if error_string == "": + return "(errno %s)" % error_num + + error_string = error_string[0].upper() + error_string[1:] + return "%s (errno %s)" % (error_string, error_num) + + def supports_ssl(self): + """ + Indicates if the object can handle HTTPS requests + + :return: + If the object supports HTTPS requests + """ + + return True + + def supports_plaintext(self): + """ + Indicates if the object can handle non-secure HTTP requests + + :return: + If the object supports non-secure HTTP requests + """ + + return True + + def cache_proxy_info(self): + proxy_struct = self.read_option(self.network_connection, self.INTERNET_OPTION_PROXY) + if proxy_struct.lpszProxy: + self.proxy = proxy_struct.lpszProxy.decode('cp1252') + if proxy_struct.lpszProxyBypass: + self.proxy_bypass = proxy_struct.lpszProxyBypass.decode('cp1252') + + # Only try to read proxy username and password if there is a proxy server + # Attempting to prevent https://github.com/wbond/package_control/issues/1122 + if self.proxy: + self.proxy_username = self.read_option(self.tcp_connection, self.INTERNET_OPTION_PROXY_USERNAME) + self.proxy_password = self.read_option(self.tcp_connection, self.INTERNET_OPTION_PROXY_PASSWORD) + else: + self.proxy_username = '' + self.proxy_password = '' + + def read_option(self, handle, option): + """ + Reads information about the internet connection, which may be a string or struct + + :param handle: + The handle to query for the info + + :param option: + The (int) option to get + + :return: + A string, or one of the InternetCertificateInfo or InternetProxyInfo structs + """ + + option_buffer_size = 8192 + try_again = True + + while try_again: + try_again = False + + to_read_was_read = wintypes.DWORD(option_buffer_size) + option_buffer = ctypes.create_string_buffer(option_buffer_size) + ref = ctypes.byref(option_buffer) + + success = wininet.InternetQueryOptionA(handle, option, ref, ctypes.byref(to_read_was_read)) + if not success: + if ctypes.GetLastError() != self.ERROR_INSUFFICIENT_BUFFER: + + # Some users report issues trying to fetch proxy information. + # Rather than bailing on the connection, we just return + # blank info. + if option == self.INTERNET_OPTION_PROXY: + return InternetProxyInfo() + + raise NonHttpError(self.extract_error()) + + # The error was a buffer that was too small, so try again + option_buffer_size = to_read_was_read.value + try_again = True + continue + + if option == self.INTERNET_OPTION_SECURITY_CERTIFICATE_STRUCT: + length = min(len(option_buffer), ctypes.sizeof(InternetCertificateInfo)) + cert_info = InternetCertificateInfo() + ctypes.memmove(ctypes.addressof(cert_info), option_buffer, length) + return cert_info + elif option == self.INTERNET_OPTION_PROXY: + length = min(len(option_buffer), ctypes.sizeof(InternetProxyInfo)) + proxy_info = InternetProxyInfo() + ctypes.memmove(ctypes.addressof(proxy_info), option_buffer, length) + return proxy_info + else: + option = b'' + if to_read_was_read.value > 0: + option += option_buffer.raw[:to_read_was_read.value] + return option.decode('cp1252').rstrip("\x00") + + def parse_headers(self, output): + """ + Parses HTTP headers into two dict objects + + :param output: + An array of header lines + + :return: + A tuple of (general, headers) where general is a dict with the keys: + `version` - HTTP version number (string) + `status` - HTTP status code (integer) + `message` - HTTP status message (string) + And headers is a dict with the keys being lower-case version of the + HTTP header names. + """ + + general = { + 'version': '0.9', + 'status': 200, + 'message': 'OK' + } + headers = {} + for line in output: + line = line.lstrip() + if line.find('HTTP/') == 0: + match = re.match(r'HTTP/(\d\.\d)\s+(\d+)\s+(.*)$', line) + if match: + general['version'] = match.group(1) + general['status'] = int(match.group(2)) + general['message'] = match.group(3) + # The user's proxy is sending bad HTTP headers :-( + else: + match = re.match(r'HTTP/(\d\.\d)\s+(\d+)$', line) + general['version'] = match.group(1) + general['status'] = int(match.group(2)) + # Since the header didn't include the message, use our copy + message = self.HTTP_STATUS_MESSAGES[general['status']] + general['message'] = message + else: + name, value = line.split(':', 1) + name = name.lower() + if name in headers: + headers[name] += ', %s' % value.strip() + else: + headers[name] = value.strip() + + return (general, headers) + + +class FileTime(ctypes.Structure): + + """ + A Windows struct used by InternetCertificateInfo for certificate + date information + """ + + _fields_ = [ + ("dwLowDateTime", wintypes.DWORD), + ("dwHighDateTime", wintypes.DWORD) + ] + + +class InternetCertificateInfo(ctypes.Structure): + + """ + A Windows struct used to store information about an SSL certificate + """ + + _fields_ = [ + ("ftExpiry", FileTime), + ("ftStart", FileTime), + ("lpszSubjectInfo", ctypes.c_char_p), + ("lpszIssuerInfo", ctypes.c_char_p), + ("lpszProtocolName", ctypes.c_char_p), + ("lpszSignatureAlgName", ctypes.c_char_p), + ("lpszEncryptionAlgName", ctypes.c_char_p), + ("dwKeySize", wintypes.DWORD) + ] + + +class InternetProxyInfo(ctypes.Structure): + + """ + A Windows struct usd to store information about the configured proxy server + """ + + _fields_ = [ + ("dwAccessType", wintypes.DWORD), + ("lpszProxy", ctypes.c_char_p), + ("lpszProxyBypass", ctypes.c_char_p) + ] + + +class InternetConnectedInfo(ctypes.Structure): + + """ + A Windows struct usd to store information about the global internet connection state + """ + + _fields_ = [ + ("dwConnectedState", wintypes.DWORD), + ("dwFlags", wintypes.DWORD) + ] diff --git a/tasks/lib/package_control/http/__init__.py b/tasks/lib/package_control/http/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tasks/lib/package_control/http/debuggable_http_connection.py b/tasks/lib/package_control/http/debuggable_http_connection.py new file mode 100644 index 0000000..4ddb147 --- /dev/null +++ b/tasks/lib/package_control/http/debuggable_http_connection.py @@ -0,0 +1,69 @@ +import socket +from http.client import HTTPConnection + +from ..console_write import console_write +from .debuggable_http_response import DebuggableHTTPResponse + + +class DebuggableHTTPConnection(HTTPConnection): + + """ + A custom HTTPConnection that formats debugging info for Sublime Text + """ + + response_class = DebuggableHTTPResponse + _debug_protocol = 'HTTP' + + def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, **kwargs): + self.passwd = kwargs.get('passwd') + + if 'debug' in kwargs and kwargs['debug']: + self.debuglevel = 5 + elif 'debuglevel' in kwargs: + self.debuglevel = kwargs['debuglevel'] + + HTTPConnection.__init__(self, host, port=port, timeout=timeout) + + def connect(self): + if self.debuglevel == -1: + console_write( + ''' + Urllib %s Debug General + Connecting to %s on port %s + ''', + (self._debug_protocol, self.host, self.port) + ) + HTTPConnection.connect(self) + + def send(self, string): + # We have to use a positive debuglevel to get it passed to the + # HTTPResponse object, however we don't want to use it because by + # default debugging prints to the stdout and we can't capture it, so + # we temporarily set it to -1 for the standard httplib code + reset_debug = False + if self.debuglevel == 5: + reset_debug = 5 + self.debuglevel = -1 + HTTPConnection.send(self, string) + if reset_debug or self.debuglevel == -1: + if len(string.strip()) > 0: + unicode_string = string.strip().decode('iso-8859-1') + indented_headers = '\n '.join(unicode_string.splitlines()) + console_write( + ''' + Urllib %s Debug Write + %s + ''', + (self._debug_protocol, indented_headers) + ) + if reset_debug: + self.debuglevel = reset_debug + + def request(self, method, url, body=None, headers={}): + original_headers = headers.copy() + + # By default urllib2 and urllib.request override the Connection header, + # however, it is preferred to be able to re-use it + original_headers['Connection'] = 'Keep-Alive' + + HTTPConnection.request(self, method, url, body, original_headers) diff --git a/tasks/lib/package_control/http/debuggable_http_handler.py b/tasks/lib/package_control/http/debuggable_http_handler.py new file mode 100644 index 0000000..eabf34d --- /dev/null +++ b/tasks/lib/package_control/http/debuggable_http_handler.py @@ -0,0 +1,29 @@ +from urllib.request import HTTPHandler + +from .debuggable_http_connection import DebuggableHTTPConnection +from .persistent_handler import PersistentHandler + + +class DebuggableHTTPHandler(PersistentHandler, HTTPHandler): + + """ + A custom HTTPHandler that formats debugging info for Sublime Text + """ + + def __init__(self, debuglevel=0, debug=False, **kwargs): + # This is a special value that will not trigger the standard debug + # functionality, but custom code where we can format the output + if debug: + self._debuglevel = 5 + else: + self._debuglevel = debuglevel + self.passwd = kwargs.get('passwd') + + def http_open(self, req): + def http_class_wrapper(host, **kwargs): + kwargs['passwd'] = self.passwd + if 'debuglevel' not in kwargs: + kwargs['debuglevel'] = self._debuglevel + return DebuggableHTTPConnection(host, **kwargs) + + return self.do_open(http_class_wrapper, req) diff --git a/tasks/lib/package_control/http/debuggable_http_response.py b/tasks/lib/package_control/http/debuggable_http_response.py new file mode 100644 index 0000000..05fa861 --- /dev/null +++ b/tasks/lib/package_control/http/debuggable_http_response.py @@ -0,0 +1,68 @@ +from http.client import HTTPResponse, IncompleteRead + +from ..console_write import console_write + + +class DebuggableHTTPResponse(HTTPResponse): + + """ + A custom HTTPResponse that formats debugging info for Sublime Text + """ + + _debug_protocol = 'HTTP' + + def __init__(self, sock, debuglevel=0, method=None, **kwargs): + # We have to use a positive debuglevel to get it passed to here, + # however we don't want to use it because by default debugging prints + # to the stdout and we can't capture it, so we use a special -1 value + if debuglevel == 5: + debuglevel = -1 + HTTPResponse.__init__(self, sock, debuglevel=debuglevel, method=method) + + def begin(self): + return_value = HTTPResponse.begin(self) + if self.debuglevel == -1: + # Python 2 + if hasattr(self.msg, 'headers'): + headers = [line.rstrip() for line in self.msg.headers] + # Python 3 + else: + headers = [] + for header in self.msg: + headers.append("%s: %s" % (header, self.msg[header])) + + versions = { + 9: 'HTTP/0.9', + 10: 'HTTP/1.0', + 11: 'HTTP/1.1' + } + status_line = '%s %s %s' % (versions[self.version], str(self.status), self.reason) + headers.insert(0, status_line) + + indented_headers = '\n '.join(headers) + console_write( + ''' + Urllib %s Debug Read + %s + ''', + (self._debug_protocol, indented_headers) + ) + + return return_value + + def is_keep_alive(self): + # Python 2 + if hasattr(self.msg, 'headers'): + connection = self.msg.getheader('connection') + # Python 3 + else: + connection = self.msg['connection'] + if connection and connection.lower() == 'keep-alive': + return True + return False + + def read(self, *args): + try: + return HTTPResponse.read(self, *args) + except (IncompleteRead) as e: + return e.partial diff --git a/tasks/lib/package_control/http/debuggable_https_response.py b/tasks/lib/package_control/http/debuggable_https_response.py new file mode 100644 index 0000000..29fa496 --- /dev/null +++ b/tasks/lib/package_control/http/debuggable_https_response.py @@ -0,0 +1,10 @@ +from .debuggable_http_response import DebuggableHTTPResponse + + +class DebuggableHTTPSResponse(DebuggableHTTPResponse): + + """ + A version of DebuggableHTTPResponse that sets the debug protocol to HTTPS + """ + + _debug_protocol = 'HTTPS' diff --git a/tasks/lib/package_control/http/invalid_certificate_exception.py b/tasks/lib/package_control/http/invalid_certificate_exception.py new file mode 100644 index 0000000..f6f79a7 --- /dev/null +++ b/tasks/lib/package_control/http/invalid_certificate_exception.py @@ -0,0 +1,20 @@ +from http.client import HTTPException +from urllib.error import URLError + + +class InvalidCertificateException(HTTPException, URLError): + + """ + An exception for when an SSL certification is not valid for the URL + it was presented for. + """ + + def __init__(self, host, cert, reason): + self.host = host + self.cert = cert + self.reason = reason.rstrip() + message = 'Host %s returned an invalid certificate (%s) %s' % (self.host, self.reason, self.cert) + HTTPException.__init__(self, message.rstrip()) + + def __bytes__(self): + return self.__str__().encode('utf-8') diff --git a/tasks/lib/package_control/http/persistent_handler.py b/tasks/lib/package_control/http/persistent_handler.py new file mode 100644 index 0000000..6ac0112 --- /dev/null +++ b/tasks/lib/package_control/http/persistent_handler.py @@ -0,0 +1,99 @@ +import socket +from urllib.error import URLError + +from ..console_write import console_write + + +class PersistentHandler: + connection = None + use_count = 0 + + def close(self): + if self.connection: + if self._debuglevel == 5: + s = '' if self.use_count == 1 else 's' + console_write( + ''' + Urllib %s Debug General + Closing connection to %s on port %s after %s request%s + ''', + ( + self.connection._debug_protocol, + self.connection.host, + self.connection.port, + self.use_count, + s + ) + ) + self.connection.close() + self.connection = None + self.use_count = 0 + + def do_open(self, http_class, req): + # Large portions from Python 3.3 Lib/urllib/request.py and + # Python 2.6 Lib/urllib2.py + + host = req.host + + if not host: + raise URLError('no host given') + + if self.connection and self.connection.host != host: + self.close() + + # Re-use the connection if possible + self.use_count += 1 + if not self.connection: + h = http_class(host, timeout=req.timeout) + else: + h = self.connection + if self._debuglevel == 5: + console_write( + ''' + Urllib %s Debug General + Re-using connection to %s on port %s for request #%s + ''', + (h._debug_protocol, h.host, h.port, self.use_count) + ) + + headers = dict(req.unredirected_hdrs) + headers.update(dict((k, v) for k, v in req.headers.items() + if k not in headers)) + headers = dict((name.title(), val) for name, val in headers.items()) + + if req._tunnel_host and not self.connection: + tunnel_headers = {} + proxy_auth_hdr = "Proxy-Authorization" + if proxy_auth_hdr in headers: + tunnel_headers[proxy_auth_hdr] = headers[proxy_auth_hdr] + del headers[proxy_auth_hdr] + + h.set_tunnel(req._tunnel_host, headers=tunnel_headers) + + try: + h.request(req.get_method(), req.selector, req.data, headers) + except socket.error as err: # timeout error + h.close() + raise URLError(err) + else: + r = h.getresponse() + + # Keep the connection around for re-use + if r.is_keep_alive(): + self.connection = h + else: + if self._debuglevel == 5: + s = '' if self.use_count == 1 else 's' + console_write( + ''' + Urllib %s Debug General + Closing connection to %s on port %s after %s request%s + ''', + (h._debug_protocol, h.host, h.port, self.use_count, s) + ) + self.use_count = 0 + self.connection = None + + r.url = req.get_full_url() + r.msg = r.reason + return r diff --git a/tasks/lib/package_control/http/validating_https_connection.py b/tasks/lib/package_control/http/validating_https_connection.py new file mode 100644 index 0000000..8448792 --- /dev/null +++ b/tasks/lib/package_control/http/validating_https_connection.py @@ -0,0 +1,396 @@ +import re +import socket +import base64 +import hashlib +import os +import sys +from http.client import HTTPS_PORT +from urllib.request import parse_keqv_list, parse_http_list + +from ..console_write import console_write +from .debuggable_https_response import DebuggableHTTPSResponse +from .debuggable_http_connection import DebuggableHTTPConnection +from .invalid_certificate_exception import InvalidCertificateException + + +# The following code is wrapped in a try because the Linux versions of Sublime +# Text do not include the ssl module due to the fact that different distros +# have different versions +try: + import ssl + + class ValidatingHTTPSConnection(DebuggableHTTPConnection): + + """ + A custom HTTPConnection class that validates SSL certificates, and + allows proxy authentication for HTTPS connections. + """ + + default_port = HTTPS_PORT + + response_class = DebuggableHTTPSResponse + _debug_protocol = 'HTTPS' + + # The ssl.SSLContext() for the connection - Python 3 only + ctx = None + + def __init__(self, host, port=None, key_file=None, cert_file=None, ca_certs=None, **kwargs): + passed_args = {} + if 'timeout' in kwargs: + passed_args['timeout'] = kwargs['timeout'] + if 'debug' in kwargs: + passed_args['debug'] = kwargs['debug'] + DebuggableHTTPConnection.__init__(self, host, port, **passed_args) + + self.passwd = kwargs.get('passwd') + self.key_file = key_file + self.cert_file = cert_file + self.ca_certs = ca_certs + if 'user_agent' in kwargs: + self.user_agent = kwargs['user_agent'] + if self.ca_certs: + self.cert_reqs = ssl.CERT_REQUIRED + else: + self.cert_reqs = ssl.CERT_NONE + + def get_valid_hosts_for_cert(self, cert): + """ + Returns a list of valid hostnames for an SSL certificate + + :param cert: A dict from SSLSocket.getpeercert() + + :return: An array of hostnames + """ + + if 'subjectAltName' in cert: + return [x[1] for x in cert['subjectAltName'] if x[0].lower() == 'dns'] + else: + return [x[0][1] for x in cert['subject'] if x[0][0].lower() == 'commonname'] + + def validate_cert_host(self, cert, hostname): + """ + Checks if the cert is valid for the hostname + + :param cert: A dict from SSLSocket.getpeercert() + + :param hostname: A string hostname to check + + :return: A boolean if the cert is valid for the hostname + """ + + hosts = self.get_valid_hosts_for_cert(cert) + for host in hosts: + host_re = host.replace('.', r'\.').replace('*', r'[^.]*') + if re.search('^%s$' % (host_re,), hostname, re.I): + return True + return False + + # Compatibility for python 3.3 vs 3.8 + # python 3.8 replaced _set_hostport() by _get_hostport() + if not hasattr(DebuggableHTTPConnection, '_set_hostport'): + + def _set_hostport(self, host, port): + (self.host, self.port) = self._get_hostport(host, port) + self._validate_host(self.host) + + def _tunnel(self): + """ + This custom _tunnel method allows us to read and print the debug + log for the whole response before throwing an error, and adds + support for proxy authentication + """ + + self._proxy_host = self.host + self._proxy_port = self.port + self._set_hostport(self._tunnel_host, self._tunnel_port) + + self._tunnel_headers['Host'] = "%s:%s" % (self.host, self.port) + self._tunnel_headers['User-Agent'] = self.user_agent + self._tunnel_headers['Proxy-Connection'] = 'Keep-Alive' + + request = "CONNECT %s:%d HTTP/1.1\r\n" % (self.host, self.port) + for header, value in self._tunnel_headers.items(): + request += "%s: %s\r\n" % (header, value) + request += "\r\n" + + request = bytes(request, 'iso-8859-1') + + self.send(request) + + response = self.response_class(self.sock, method=self._method) + (version, code, message) = response._read_status() + + status_line = "%s %s %s" % (version, code, message.rstrip()) + headers = [status_line] + + content_length = 0 + close_connection = False + while True: + line = response.fp.readline() + + line = line.decode('iso-8859-1') + + if line == '\r\n': + break + + headers.append(line.rstrip()) + + parts = line.rstrip().split(': ', 1) + name = parts[0].lower() + value = parts[1].lower().strip() + if name == 'content-length': + content_length = int(value) + + if name in ['connection', 'proxy-connection'] and value == 'close': + close_connection = True + + if self.debuglevel in [-1, 5]: + indented_headers = '\n '.join(headers) + console_write( + ''' + Urllib %s Debug Read + %s + ''', + (self._debug_protocol, indented_headers) + ) + + # Handle proxy auth for SSL connections since regular urllib punts on this + if code == 407 and self.passwd and 'Proxy-Authorization' not in self._tunnel_headers: + if content_length: + response._safe_read(content_length) + + supported_auth_methods = {} + for line in headers: + parts = line.split(': ', 1) + if parts[0].lower() != 'proxy-authenticate': + continue + details = parts[1].split(' ', 1) + supported_auth_methods[details[0].lower()] = details[1] if len(details) > 1 else '' + + username, password = self.passwd.find_user_password(None, "%s:%s" % ( + self._proxy_host, self._proxy_port)) + + if 'digest' in supported_auth_methods: + response_value = self.build_digest_response( + supported_auth_methods['digest'], username, password) + if response_value: + self._tunnel_headers['Proxy-Authorization'] = "Digest %s" % response_value + + elif 'basic' in supported_auth_methods: + response_value = "%s:%s" % (username, password) + response_value = base64.b64encode(response_value.encode('utf-8')).decode('utf-8') + self._tunnel_headers['Proxy-Authorization'] = "Basic %s" % response_value.strip() + + if 'Proxy-Authorization' in self._tunnel_headers: + self.host = self._proxy_host + self.port = self._proxy_port + + # If the proxy wanted the connection closed, we need to make a new connection + if close_connection: + self.sock.close() + self.sock = socket.create_connection((self.host, self.port), self.timeout) + + return self._tunnel() + + if code != 200: + self.close() + raise socket.error("Tunnel connection failed: %d %s" % (code, message.strip())) + + def build_digest_response(self, fields, username, password): + """ + Takes a Proxy-Authenticate: Digest header and creates a response + header + + :param fields: + The string portion of the Proxy-Authenticate header after + "Digest " + + :param username: + The username to use for the response + + :param password: + The password to use for the response + + :return: + None if invalid Proxy-Authenticate header, otherwise the + string of fields for the Proxy-Authorization: Digest header + """ + + fields = parse_keqv_list(parse_http_list(fields)) + + realm = fields.get('realm') + nonce = fields.get('nonce') + qop = fields.get('qop') + algorithm = fields.get('algorithm') + if algorithm: + algorithm = algorithm.lower() + opaque = fields.get('opaque') + + if algorithm in ['md5', None]: + def md5hash(string): + return hashlib.md5(string).hexdigest() + hash = md5hash + + elif algorithm == 'sha': + def sha1hash(string): + return hashlib.sha1(string).hexdigest() + hash = sha1hash + + else: + return None + + host_port = "%s:%s" % (self.host, self.port) + + a1 = "%s:%s:%s" % (username, realm, password) + a2 = "CONNECT:%s" % host_port + ha1 = hash(a1) + ha2 = hash(a2) + + if qop is None: + response = hash("%s:%s:%s" % (ha1, nonce, ha2)) + elif qop == 'auth': + nc = '00000001' + cnonce = hash(os.urandom(8))[:8] + response = hash("%s:%s:%s:%s:%s:%s" % (ha1, nonce, nc, cnonce, qop, ha2)) + else: + return None + + response_fields = { + 'username': username, + 'realm': realm, + 'nonce': nonce, + 'response': response, + 'uri': host_port + } + if algorithm: + response_fields['algorithm'] = algorithm + if qop == 'auth': + response_fields['nc'] = nc + response_fields['cnonce'] = cnonce + response_fields['qop'] = qop + if opaque: + response_fields['opaque'] = opaque + + return ', '.join(["%s=\"%s\"" % (field, response_fields[field]) for field in response_fields]) + + def connect(self): + """ + Adds debugging and SSL certification validation + """ + + if self.debuglevel == -1: + console_write( + ''' + Urllib HTTPS Debug General + Connecting to %s on port %s + ''', + (self.host, self.port) + ) + + self.sock = socket.create_connection((self.host, self.port), self.timeout) + if self._tunnel_host: + self._tunnel() + + if self.debuglevel == -1: + console_write( + ''' + Urllib HTTPS Debug General + Upgrading connection to SSL using CA certs file at %s + ''', + self.ca_certs.decode(sys.getfilesystemencoding()) + ) + + hostname = self.host.split(':', 0)[0] + + proto = ssl.PROTOCOL_SSLv23 + if sys.version_info >= (3, 6): + proto = ssl.PROTOCOL_TLS + self.ctx = ssl.SSLContext(proto) + if sys.version_info < (3, 7): + self.ctx.options = ssl.OP_ALL | ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 + else: + self.ctx.minimum_version = ssl.TLSVersion.TLSv1 + self.ctx.verify_mode = self.cert_reqs + self.ctx.load_verify_locations(self.ca_certs) + # We don't call load_cert_chain() with self.key_file and self.cert_file + # since that is for servers, and this code only supports client mode + if self.debuglevel == -1: + console_write( + ''' + Using hostname "%s" for TLS SNI extension + ''', + hostname, + indent=' ', + prefix=False + ) + self.sock = self.ctx.wrap_socket( + self.sock, + server_hostname=hostname + ) + + if self.debuglevel == -1: + cipher_info = self.sock.cipher() + console_write( + ''' + Successfully upgraded connection to %s:%s with SSL + Using %s with cipher %s + ''', + (self.host, self.port, cipher_info[1], cipher_info[0]), + indent=' ', + prefix=False + ) + + # This debugs and validates the SSL certificate + if self.cert_reqs & ssl.CERT_REQUIRED: + cert = self.sock.getpeercert() + + if self.debuglevel == -1: + subjectMap = { + 'organizationName': 'O', + 'commonName': 'CN', + 'organizationalUnitName': 'OU', + 'countryName': 'C', + 'serialNumber': 'serialNumber', + 'commonName': 'CN', + 'localityName': 'L', + 'stateOrProvinceName': 'S', + '1.3.6.1.4.1.311.60.2.1.2': 'incorporationState', + '1.3.6.1.4.1.311.60.2.1.3': 'incorporationCountry' + } + subject_list = list(cert['subject']) + subject_list.reverse() + subject_parts = [] + for pair in subject_list: + if pair[0][0] in subjectMap: + field_name = subjectMap[pair[0][0]] + else: + field_name = pair[0][0] + subject_parts.append(field_name + '=' + pair[0][1]) + + console_write( + ''' + Server SSL certificate: + subject: %s + ''', + ','.join(subject_parts), + indent=' ', + prefix=False + ) + if 'subjectAltName' in cert: + alt_names = [c[1] for c in cert['subjectAltName']] + alt_names = ', '.join(alt_names) + console_write(' subject alt name: %s', alt_names, prefix=False) + if 'notAfter' in cert: + console_write(' expire date: %s', cert['notAfter'], prefix=False) + + if not self.validate_cert_host(cert, hostname): + if self.debuglevel == -1: + console_write(' Certificate INVALID', prefix=False) + + raise InvalidCertificateException(hostname, cert, 'hostname mismatch') + + if self.debuglevel == -1: + console_write(' Certificate validated for %s', hostname, prefix=False) + +except (ImportError): + pass diff --git a/tasks/lib/package_control/http/validating_https_handler.py b/tasks/lib/package_control/http/validating_https_handler.py new file mode 100644 index 0000000..10c50b9 --- /dev/null +++ b/tasks/lib/package_control/http/validating_https_handler.py @@ -0,0 +1,40 @@ +import ssl +from urllib.error import URLError +import urllib.request as urllib_compat + +from .validating_https_connection import ValidatingHTTPSConnection +from .invalid_certificate_exception import InvalidCertificateException +from .persistent_handler import PersistentHandler + + +class ValidatingHTTPSHandler(PersistentHandler, urllib_compat.HTTPSHandler): + + """ + A urllib handler that validates SSL certificates for HTTPS requests + """ + + def __init__(self, **kwargs): + # This is a special value that will not trigger the standard debug + # functionality, but custom code where we can format the output + self._debuglevel = 0 + if 'debug' in kwargs and kwargs['debug']: + self._debuglevel = 5 + elif 'debuglevel' in kwargs: + self._debuglevel = kwargs['debuglevel'] + self._connection_args = kwargs + + def https_open(self, req): + def http_class_wrapper(host, **kwargs): + full_kwargs = dict(self._connection_args) + full_kwargs.update(kwargs) + return ValidatingHTTPSConnection(host, **full_kwargs) + + try: + return self.do_open(http_class_wrapper, req) + except URLError as e: + if type(e.reason) == ssl.SSLError and e.reason.args[0] == 1: + raise InvalidCertificateException(req.host, '', + e.reason.args[1]) + raise + + https_request = urllib_compat.AbstractHTTPHandler.do_request_ diff --git a/tasks/lib/package_control/http_cache.py b/tasks/lib/package_control/http_cache.py new file mode 100644 index 0000000..2611a76 --- /dev/null +++ b/tasks/lib/package_control/http_cache.py @@ -0,0 +1,111 @@ +import os +import time + +from . import sys_path + + +class HttpCache: + + """ + A data store for caching HTTP response data. + """ + + def __init__(self, ttl): + """ + Constructs a new instance. + + :param ttl: + The number of seconds a cache entry should be valid for + """ + self.ttl = int(ttl) + self.base_path = os.path.join(sys_path.pc_cache_dir(), 'http_cache') + os.makedirs(self.base_path, exist_ok=True) + + def __del__(self): + """ + Delete an existing instance. + + Remove outdated cache files, when cache object is deleted. + All files which have been accessed by deleted instance keep untouched. + """ + + if self.ttl > 0: + self.clear(self.ttl) + + def clear(self, ttl): + """ + Removes all cache entries older than the TTL + + :param ttl: + The number of seconds a cache entry should be valid for + """ + + ttl = int(ttl) + + for filename in os.listdir(self.base_path): + path = os.path.join(self.base_path, filename) + # There should not be any folders in the cache dir, but we + # ignore to prevent an exception + if os.path.isdir(path): + continue + mtime = os.stat(path).st_mtime + if mtime < time.time() - ttl: + os.unlink(path) + + def get(self, key): + """ + Returns a cached value + + :param key: + The key to fetch the cache for + + :return: + The (binary) cached value, or False + """ + try: + content = None + cache_file = os.path.join(self.base_path, key) + with open(cache_file, 'rb') as fobj: + content = fobj.read() + + # update filetime to prevent unmodified cache files + # from being deleted, if they are frequently accessed. + now = time.time() + os.utime(cache_file, (now, now)) + + return content + + except FileNotFoundError: + return False + + def has(self, key): + cache_file = os.path.join(self.base_path, key) + return os.path.exists(cache_file) + + def path(self, key): + """ + Returns the filesystem path to the key + + :param key: + The key to get the path for + + :return: + The absolute filesystem path to the cache file + """ + + return os.path.join(self.base_path, key) + + def set(self, key, content): + """ + Saves a value in the cache + + :param key: + The key to save the cache with + + :param content: + The (binary) content to cache + """ + + cache_file = os.path.join(self.base_path, key) + with open(cache_file, 'wb') as f: + f.write(content) diff --git a/tasks/lib/package_control/package_version.py b/tasks/lib/package_control/package_version.py new file mode 100644 index 0000000..5219d1d --- /dev/null +++ b/tasks/lib/package_control/package_version.py @@ -0,0 +1,137 @@ +import re + +from .console_write import console_write +from .pep440 import PEP440Version, PEP440InvalidVersionError + + +class PackageVersion(PEP440Version): + __slots__ = ["_str"] + + _date_time_regex = re.compile(r"^\d{4}\.\d{2}\.\d{2}(?:\.\d{2}\.\d{2}\.\d{2})?$") + + def __init__(self, ver): + """ + Initialize a ``PackageVersion`` instance. + + The initializer acts as compatibility layer to convert legacy version schemes + into a ``PEP440Version``. + + If the version is based on a date, converts to 0.0.1+yyyy.mm.dd.hh.mm.ss. + + :param ver: + A string, dict with 'version' key, or a SemVer object + + :raises: + TypeError, if ver is not a ``str``. + ValueError, if ver is no valid version string + """ + + if not isinstance(ver, str): + raise TypeError("{!r} is not a string".format(ver)) + + # Store original version string with `v` trimmed to maintain backward compatibility + # with regards to not normalize it. + # The one and only use case is to keep existing CI tests working without change. + if ver[0] == 'v': + self._str = ver[1:] + else: + self._str = ver + + # We prepend 0 to all date-based version numbers so that developers + # may switch to explicit versioning from GitHub/GitLab/BitBucket + # versioning based on commit dates. + # + # The resulting semver is alwass 0.0.1 with timestamp being used + # as build number, so any explicitly choosen version (via tags) will + # be greater, once a package moves from branch to tag based releases. + # + # The result looks like: + # 0.0.1+2020.07.15.10.50.38 + match = self._date_time_regex.match(ver) + if match: + ver = "0.0.1+" + ver + + try: + super().__init__(ver) + except PEP440InvalidVersionError: + # maybe semver with incompatible pre-release tag + # if, so treat it as dev build with local version + if "-" in ver: + ver, pre = ver.split("-", 1) + if ver and pre: + super().__init__(ver + "-dev+" + pre) + return + raise + + def __str__(self): + return self._str + + +def version_match_prefix(version, filter_prefix): + """ + Create a SemVer for a given version, if it matches filter_prefix. + + :param version: + The version string to match + + :param filter_prefix: + The prefix to match versions against + + :returns: + SemVer, if version is valid and matches given filter_prefix + None, if version is invalid or doesn't match filter_prefix + """ + + try: + if filter_prefix: + if version.startswith(filter_prefix): + return PackageVersion(version[len(filter_prefix):]) + else: + return PackageVersion(version) + except ValueError: + pass + return None + + +def version_sort(sortable, *fields, **kwargs): + """ + Sorts a list that is a list of versions, or dicts with a 'version' key. + Can also secondly sort by another field. + + :param sortable: + The list to sort + + :param *fields: + If sortable is a list of dicts, perform secondary sort via these fields, + in order + + :param **kwargs: + Keyword args to pass on to sorted() + + :return: + A copy of sortable that is sorted according to SemVer rules + """ + + def _version_sort_key(item): + if isinstance(item, dict): + if "version" not in item: + raise TypeError("%s is not a package or library release" % item) + result = PackageVersion(item["version"]) + if fields: + result = (result,) + for field in fields: + result += (item[field],) + return result + + return PackageVersion(item) + + try: + return sorted(sortable, key=_version_sort_key, **kwargs) + except ValueError as e: + console_write( + """ + Error sorting versions - %s + """, + e, + ) + return [] diff --git a/tasks/lib/package_control/pep440.py b/tasks/lib/package_control/pep440.py new file mode 100644 index 0000000..2f7af33 --- /dev/null +++ b/tasks/lib/package_control/pep440.py @@ -0,0 +1,645 @@ +""" +A PEP440 complient version module for use by Package Control. + +Note: + +This module implements ``PEP440Version`` and ``PEP440VersionSpecifier`` +using independent implementations and regex patterns to parse their string +representation, even though both share a lot. + +The reason for this kind of inlining is targetting best possible performance +for creating and compairing versions, rather than strictly following a +questionable DRY approach. + +Instantiation for each object consists of only 2 main steps: + +1. parse and validate input string using a single regular expression. +2. convert match groups into nested tuple representation, as primary + data storage and comparing key. + +The patterns include additional pre-release tag names +(e.g: ``patch``, ``prerelease``, ``developmment``, ``test``) +to maintain compatibility with various existing packages on packagecontrol.io +""" +import re + +__all__ = [ + "PEP440InvalidVersionError", + "PEP440InvalidVersionSpecifierError", + "PEP440Version", + "PEP440VersionSpecifier", + "check_version" +] + +_local_version_separators = re.compile(r"[-._]") + + +def _norm_tuples(a, b): + """ + Accepts two tuples of PEP440 version numbers and extends them until they + are the same length. This allows for comparisons between them. + + Notes: + + - prerelease segment is padded + - local version don't need padding as shorter sort before longer + + :param a: + A tuple from ``PEP440Version`` + of the format: ``(epoch, release, prerelease, local)`` + + :param b: + A tuple from ``PEP440Version`` + of the format: ``(epoch, release, prerelease, local)`` + + :return: + Two potentially modified tuples, (a, b) + """ + # pad release + ar = a[1] + br = b[1] + + arl = len(ar) + brl = len(br) + + if arl < brl: + while len(ar) < brl: + ar += (0,) + a = a[:1] + (ar,) + a[2:] + + elif arl > brl: + while arl > len(br): + br += (0,) + b = b[:1] + (br,) + b[2:] + + return a, b + + +def _trim_tuples(spec, ver): + """ + Trim version to match specification's length. + + :param spec: + A tuple from ``PEP440VersionSpecifier``, representing a version prefix. + e.g.: ``(epoch, (major [, minor [, micro] ] ) )`` + + :param ver: + A tuple from ``PEP440Version`` + + :returns: + A tuple of prefix and trimmed version. + """ + segs = len(spec[1]) + release = ver[1][:segs] + while len(release) < segs: + release += (0,) + return spec, (ver[0], release) + + +def _version_info(epoch, ver, pre, local, verbose=False): + """ + Create a ``__version_info__`` tuple representation. + + :param epoch: + The epoch + + :param ver: + A tuple of integers representing the version + + :param pre: + A tuple of tuples of integers representing pre-releases + + :param local: + Local version representation. + + :returns: + A tuple of (major, minor, micro, 'pre', 'post', 'dev') + """ + info = ver + + if pre and pre[0][0] != 0: + if verbose: + tag = ("dev", "alpha", "beta", "rc", "", "post") + else: + tag = ("dev", "a", "b", "rc", "", "post") + for t, n in pre: + if t != 0: + info += (tag[t + 4], n) + else: + info += ("final",) + + if local: + info += (".".join(str(n) if n > -1 else s for n, s in local),) + + return info + + +def _version_string(epoch, ver, pre, local, prefix=False, verbose=False): + """ + Create a normalized string representation. + + :param epoch: + The epoch + + :param ver: + A tuple of integers representing the version + + :param pre: + A tuple of tuples of integers representing pre-releases + + :param local: + Local version representation. + + :returns: + String representation of the version. + """ + string = str(epoch) + "!" if epoch else "" + string += ".".join(map(str, ver)) + + if prefix: + return string + ".*" + + if pre and pre[0][0] != 0: + if verbose: + tag = ("-dev{}", "-alpha{}", "-beta{}", "-rc{}", "", "-post{}") + else: + tag = (".dev{}", "a{}", "b{}", "rc{}", "", ".post{}") + for t, n in pre: + if t != 0: + string += tag[t + 4].format(n) + + if local: + string += "+" + ".".join(str(n) if n > -1 else s for n, s in local) + + return string + + +class PEP440InvalidVersionError(ValueError): + pass + + +class PEP440Version: + __slots__ = ["_tup"] + + _regex = re.compile( + r""" + ^\s* + v? + (?:(?P[0-9]+)!)? # epoch + (?P[0-9]+(?:\.[0-9]+)*) # release segment + (?P
                                              # pre-release
+            [-_.]?
+            (?Palpha|a|beta|b|prerelease|preview|pre|c|rc)
+            [-_.]?
+            (?P[0-9]+)?
+        )?
+        (?P                                             # post release
+            (?:-(?P[0-9]+))
+            |
+            (?:
+                [-_.]?
+                (?Ppatch|post|rev|r)
+                [-_.]?
+                (?P[0-9]+)?
+            )
+        )?
+        (?P                                              # dev release
+            [-_.]?
+            (?Pdevelopment|develop|devel|dev)
+            [-_.]?
+            (?P[0-9]+)?
+        )?
+        (?:\+(?P[a-z0-9]+(?:[-_.][a-z0-9]+)*))?        # local version
+        \s*$
+        """,
+        re.VERBOSE,
+    )
+
+    def __init__(self, string):
+        """
+        Constructs a new ``PEP440Version`` instance.
+
+        :param string:
+            An unicode string of the pep44Ăź version.
+        """
+        match = self._regex.match(string.lower())
+        if not match:
+            raise PEP440InvalidVersionError("'{}' is not a valid PEP440 version string".format(string))
+
+        (
+            epoch,
+            release,
+            pre,
+            pre_l,
+            pre_n,
+            post,
+            post_n1,
+            _,
+            post_n2,
+            dev,
+            _,
+            dev_n,
+            local,
+        ) = match.groups()
+
+        epoch = int(epoch or 0)
+        release = tuple(map(int, release.split(".")))
+
+        prerelease = ()
+
+        if pre:
+            if pre_l == "a" or pre_l == "alpha":
+                pre_l = -3
+            elif pre_l == "b" or pre_l == "beta":
+                pre_l = -2
+            else:
+                pre_l = -1
+            prerelease += ((pre_l, int(pre_n or 0)),)
+
+        if post:
+            prerelease += ((1, int(post_n1 or post_n2 or 0)),)
+
+        if dev:
+            prerelease += ((-4, int(dev_n or 0)),)
+
+        while len(prerelease) < 3:
+            prerelease += ((0, 0),)
+
+        tup = ()
+        if local:
+            # Versions with a local segment need that segment parsed to implement
+            # the sorting rules in PEP440.
+            # - Alpha numeric segments sort before numeric segments
+            # - Alpha numeric segments sort lexicographically
+            # - Numeric segments sort numerically
+            # - Shorter versions sort before longer versions when the prefixes
+            #   match exactly
+            for seg in _local_version_separators.split(local):
+                try:
+                    tup += ((int(seg), ""),)
+                except ValueError:
+                    tup += ((-1, seg),)
+
+        local = tup
+
+        self._tup = (epoch, release, prerelease, local)
+
+    def __repr__(self):
+        return "<{0.__class__.__name__}('{0!s}')>".format(self)
+
+    def __str__(self):
+        return self.version_string()
+
+    def __eq__(self, rhs):
+        a, b = _norm_tuples(self._tup, rhs._tup)
+        return a == b
+
+    def __ne__(self, rhs):
+        a, b = _norm_tuples(self._tup, rhs._tup)
+        return a != b
+
+    def __lt__(self, rhs):
+        a, b = _norm_tuples(self._tup, rhs._tup)
+        return a < b
+
+    def __le__(self, rhs):
+        a, b = _norm_tuples(self._tup, rhs._tup)
+        return a <= b
+
+    def __gt__(self, rhs):
+        a, b = _norm_tuples(self._tup, rhs._tup)
+        return a > b
+
+    def __ge__(self, rhs):
+        a, b = _norm_tuples(self._tup, rhs._tup)
+        return a >= b
+
+    def __hash__(self):
+        return hash(self._tup)
+
+    def version_info(self, verbose=False):
+        return _version_info(*self._tup, verbose=verbose)
+
+    def version_string(self, verbose=False):
+        return _version_string(*self._tup, verbose=verbose)
+
+    @property
+    def epoch(self):
+        return self._tup[0]
+
+    @property
+    def release(self):
+        return self._tup[1]
+
+    @property
+    def major(self):
+        try:
+            return self._tup[1][0]
+        except IndexError:
+            return 0
+
+    @property
+    def minor(self):
+        try:
+            return self._tup[1][1]
+        except IndexError:
+            return 0
+
+    @property
+    def micro(self):
+        try:
+            return self._tup[1][2]
+        except IndexError:
+            return 0
+
+    @property
+    def prerelease(self):
+        tup = ()
+        pre = self._tup[2]
+        if pre and pre[0][0] != 0:
+            tag = ("dev", "a", "b", "rc", "", "post")
+            for t, n in pre:
+                if t != 0:
+                    tup += (tag[t + 4], n)
+
+        return tup
+
+    @property
+    def local(self):
+        return ".".join(str(n) if n > -1 else s for n, s in self._tup[3])
+
+    @property
+    def is_final(self):
+        """Version represents a final release."""
+        return self._tup[2][0][0] == 0
+
+    @property
+    def is_dev(self):
+        """Version represents a pre release."""
+        return any(t[0] == -4 for t in self._tup[2])
+
+    @property
+    def is_prerelease(self):
+        """Version represents a pre release."""
+        return self._tup[2][0][0] < 0
+
+    @property
+    def is_postrelease(self):
+        """Version represents a post final release."""
+        return self._tup[2][0][0] > 0
+
+
+class PEP440InvalidVersionSpecifierError(ValueError):
+    pass
+
+
+class PEP440VersionSpecifier:
+    __slots__ = ["_operator", "_prefix", "_prereleases", "_tup"]
+
+    _regex = re.compile(
+        r"""
+        ^\s*
+        (?: (?P===|==|!=|~=|<=?|>=?) \s* )?                 # operator
+        v?
+        (?:(?P[0-9]+)!)?                             # epoch
+        (?P[0-9]+(?:\.[0-9]+)*)                    # release segment
+        (?:
+            \.(?P\*)                              # prefix-release
+            |
+            (?P
                                        # pre-release
+                [-_.]?
+                (?Palpha|a|beta|b|preview|pre|c|rc)
+                [-_.]?
+                (?P[0-9]+)?
+            )?
+            (?P                                       # post release
+                (?:-(?P[0-9]+))
+                |
+                (?:
+                    [-_.]?
+                    (?Ppost|rev|r)
+                    [-_.]?
+                    (?P[0-9]+)?
+                )
+            )?
+            (?P                                        # dev release
+                [-_.]?
+                (?Pdev)
+                [-_.]?
+                (?P[0-9]+)?
+            )?
+            (?:\+(?P[a-z0-9]+(?:[-_.][a-z0-9]+)*))?  # local version
+        )
+        \s*$
+        """,
+        re.VERBOSE,
+    )
+
+    _op_str = ("", "===", "==", "!=", "~=", "<", "<=", ">", ">=")
+
+    OP_ITY = 1
+    OP_EQ = 2
+    OP_NE = 3
+    OP_CPE = 4
+    OP_LT = 5
+    OP_LTE = 6
+    OP_GT = 7
+    OP_GTE = 8
+
+    def __init__(self, string, prereleases=True):
+        """
+        Constructs a new ``PEP440VersionSpecifier`` instance.
+
+        :param string:
+            An unicode string of the pep44Ăź version specifier.
+        """
+        match = self._regex.match(string.lower())
+        if not match:
+            raise PEP440InvalidVersionSpecifierError(
+                "'{}' is not a valid PEP 440 version specifier string".format(string)
+            )
+
+        (
+            op,
+            epoch,
+            release,
+            wildcard,
+            pre,
+            pre_l,
+            pre_n,
+            post,
+            post_n1,
+            _,
+            post_n2,
+            dev,
+            _,
+            dev_n,
+            local,
+        ) = match.groups()
+
+        self._operator = self._op_str.index(op) if op else self.OP_EQ
+        self._prefix = bool(wildcard)
+        self._prereleases = prereleases
+
+        epoch = int(epoch or 0)
+        release = tuple(map(int, release.split(".")))
+
+        if self._prefix:
+            if self._operator not in (self.OP_EQ, self.OP_NE):
+                raise PEP440InvalidVersionSpecifierError(
+                    "'{}' is not a valid PEP 440 version specifier string".format(string)
+                )
+
+            self._tup = (epoch, release)
+            return
+
+        if self._operator == self.OP_CPE and len(release) < 2:
+            raise PEP440InvalidVersionSpecifierError(
+                "'{}' is not a valid PEP 440 version specifier string".format(string)
+            )
+
+        prerelease = ()
+
+        if pre:
+            if pre_l == "a" or pre_l == "alpha":
+                pre_l = -3
+            elif pre_l == "b" or pre_l == "beta":
+                pre_l = -2
+            else:
+                pre_l = -1
+            prerelease += ((pre_l, int(pre_n or 0)),)
+
+        if post:
+            prerelease += ((1, int(post_n1 or post_n2 or 0)),)
+
+        if dev:
+            prerelease += ((-4, int(dev_n or 0)),)
+
+        while len(prerelease) < 3:
+            prerelease += ((0, 0),)
+
+        tup = ()
+        if local:
+            if self._operator not in (self.OP_EQ, self.OP_NE, self.OP_ITY):
+                raise PEP440InvalidVersionSpecifierError(
+                    "'{}' is not a valid PEP 440 version specifier string".format(string)
+                )
+
+            for seg in _local_version_separators.split(local):
+                try:
+                    tup += ((int(seg), ""),)
+                except ValueError:
+                    tup += ((-1, seg),)
+        local = tup
+
+        self._tup = (epoch, release, prerelease, local)
+
+    def __repr__(self):
+        return "<{0.__class__.__name__}('{0!s}')>".format(self)
+
+    def __str__(self):
+        return self._op_str[self._operator] + self.version_string()
+
+    def __contains__(self, version):
+        return self.contains(version)
+
+    def __hash__(self):
+        return hash((self._operator, self._tup))
+
+    def contains(self, version):
+        """
+        Ensures the version matches this specifier
+
+        :param version:
+            A ``PEP440Version`` object to check.
+
+        :return:
+            Returns ``True`` if ``version`` satisfies the ``specifier``.
+        """
+        if not self._prereleases and version.is_prerelease:
+            return False
+
+        if self._prefix:
+            # The specifier is a version prefix (aka. wildcard present).
+            # Trim and normalize version to ( epoch, ( major [, minor [, micro ] ] ) ),
+            # so it matches exactly the specifier's length.
+
+            self_tup, ver_tup = _trim_tuples(self._tup, version._tup)
+
+            if self._operator == self.OP_EQ:
+                return ver_tup == self._tup
+
+            if self._operator == self.OP_NE:
+                return ver_tup != self._tup
+
+        else:
+            if self._operator == self.OP_ITY:
+                return version.version_string(False) == self.version_string(False)
+
+            self_tup, ver_tup = _norm_tuples(self._tup, version._tup)
+
+            if self._operator == self.OP_CPE:
+                # Compatible releases have an equivalent combination of >= and ==.
+                # That is that ~=2.2 is equivalent to >=2.2,==2.*.
+                if ver_tup < self_tup:
+                    return False
+
+                # create prefix specifier with last digit removed.
+                self_tup, ver_tup = _trim_tuples((self._tup[0], self._tup[1][:-1]), version._tup)
+                return ver_tup == self_tup
+
+            if self._operator == self.OP_EQ:
+                return ver_tup == self_tup
+
+            if self._operator == self.OP_NE:
+                return ver_tup != self_tup
+
+            if self._operator == self.OP_GTE:
+                return ver_tup >= self_tup
+
+            if self._operator == self.OP_GT:
+                # TODO:
+                #  - parse local version and include into comparison result
+                #  - drop only invalid local versions
+                return ver_tup[:2] > self_tup[:2]
+
+            if self._operator == self.OP_LTE:
+                return ver_tup <= self_tup
+
+            if self._operator == self.OP_LT:
+                # TODO:
+                #  - parse local version and include into comparison result
+                #  - drop only invalid local versions
+                return ver_tup[:2] < self_tup[:2]
+
+        raise PEP440InvalidVersionSpecifierError(
+            "Invalid PEP 440 version specifier operator: {!r}".format(self._operator)
+        )
+
+    def filter(self, iterable):
+        return filter(self.contains, iterable)
+
+    def version_string(self, verbose=False):
+        return _version_string(*self._tup, prefix=self._prefix, verbose=verbose)
+
+
+def check_version(spec, version, include_prereleases=False):
+    """
+    Check if version satisfies specifications
+
+    :param spec:
+        The pep440 version specifier string.
+
+    :param version:
+        The pep440 version string or ``PEP440Version`` ojbect to check.
+
+    :param include_prereleases:
+        If ``True`` succeed also, if version is a pre-release.
+        If ``False`` (default) succeed only, if version is a final release.
+
+    :returns:
+        Returns ``True`` if ``version`` satisfies the ``specifier``.
+    """
+    if isinstance(version, str):
+        version = PEP440Version(version)
+    return PEP440VersionSpecifier(spec, include_prereleases).contains(version)
diff --git a/tasks/lib/package_control/providers/__init__.py b/tasks/lib/package_control/providers/__init__.py
new file mode 100644
index 0000000..416ccd0
--- /dev/null
+++ b/tasks/lib/package_control/providers/__init__.py
@@ -0,0 +1,20 @@
+from .bitbucket_repository_provider import BitBucketRepositoryProvider
+from .github_repository_provider import GitHubRepositoryProvider
+from .github_user_provider import GitHubUserProvider
+from .gitlab_repository_provider import GitLabRepositoryProvider
+from .gitlab_user_provider import GitLabUserProvider
+from .json_repository_provider import JsonRepositoryProvider
+
+from .channel_provider import ChannelProvider
+
+
+REPOSITORY_PROVIDERS = [
+    BitBucketRepositoryProvider,
+    GitHubRepositoryProvider,
+    GitHubUserProvider,
+    GitLabRepositoryProvider,
+    GitLabUserProvider,
+    JsonRepositoryProvider
+]
+
+CHANNEL_PROVIDERS = [ChannelProvider]
diff --git a/tasks/lib/package_control/providers/base_repository_provider.py b/tasks/lib/package_control/providers/base_repository_provider.py
new file mode 100644
index 0000000..6517534
--- /dev/null
+++ b/tasks/lib/package_control/providers/base_repository_provider.py
@@ -0,0 +1,129 @@
+class BaseRepositoryProvider:
+    """
+    Base repository downloader that fetches package info
+
+    This base class acts as interface to ensure all providers expose the same
+    set of methods. All providers should therefore derive from this base class.
+
+    The structure of the JSON a repository should contain is located in
+    example-packages.json.
+
+    :param repo_url:
+        The URL of the package repository
+
+    :param settings:
+        A dict containing at least the following fields:
+          `cache_length`,
+          `debug`,
+          `timeout`,
+          `user_agent`
+        Optional fields:
+          `http_proxy`,
+          `https_proxy`,
+          `proxy_username`,
+          `proxy_password`,
+          `query_string_params`
+    """
+
+    __slots__ = [
+        'broken_libriaries'
+        'broken_packages',
+        'failed_sources',
+        'libraries',
+        'packages',
+        'repo_url',
+        'settings',
+    ]
+
+    def __init__(self, repo_url, settings):
+        self.broken_libriaries = {}
+        self.broken_packages = {}
+        self.failed_sources = {}
+        self.libraries = None
+        self.packages = None
+        self.repo_url = repo_url
+        self.settings = settings
+
+    @classmethod
+    def match_url(cls, repo_url):
+        """
+        Indicates if this provider can handle the provided repo_url
+        """
+
+        return True
+
+    def prefetch(self):
+        """
+        Go out and perform HTTP operations, caching the result
+        """
+
+        [name for name, info in self.get_packages()]
+
+    def fetch(self):
+        """
+        Retrieves and loads the JSON for other methods to use
+
+        :raises:
+            NotImplementedError: when called
+        """
+
+        raise NotImplementedError()
+
+    def get_broken_libraries(self):
+        """
+        List of library names for libraries that are missing information
+
+        :return:
+            A generator of ("Library Name", Exception()) tuples
+        """
+
+        return self.broken_libriaries.items()
+
+    def get_broken_packages(self):
+        """
+        List of package names for packages that are missing information
+
+        :return:
+            A generator of ("Package Name", Exception()) tuples
+        """
+
+        return self.broken_packages.items()
+
+    def get_failed_sources(self):
+        """
+        List of any URLs that could not be accessed while accessing this repository
+
+        :return:
+            A generator of ("https://example.com", Exception()) tuples
+        """
+
+        return self.failed_sources.items()
+
+    def get_libraries(self, invalid_sources=None):
+        """
+        For API-compatibility with RepositoryProvider
+        """
+
+        return {}.items()
+
+    def get_packages(self, invalid_sources=None):
+        """
+        For API-compatibility with RepositoryProvider
+        """
+
+        return {}.items()
+
+    def get_sources(self):
+        """
+        Return a list of current URLs that are directly referenced by the repo
+
+        :return:
+            A list of URLs
+        """
+
+        return [self.repo_url]
+
+    def get_renamed_packages(self):
+        """For API-compatibility with RepositoryProvider"""
+
+        return {}
diff --git a/tasks/lib/package_control/providers/bitbucket_repository_provider.py b/tasks/lib/package_control/providers/bitbucket_repository_provider.py
new file mode 100644
index 0000000..be1e496
--- /dev/null
+++ b/tasks/lib/package_control/providers/bitbucket_repository_provider.py
@@ -0,0 +1,138 @@
+from ..clients.bitbucket_client import BitBucketClient
+from ..clients.client_exception import ClientException
+from ..downloaders.downloader_exception import DownloaderException
+from .base_repository_provider import BaseRepositoryProvider
+from .provider_exception import (
+    GitProviderDownloadInfoException,
+    GitProviderRepoInfoException,
+    ProviderException,
+)
+
+
+class BitBucketRepositoryProvider(BaseRepositoryProvider):
+    """
+    Allows using a public BitBucket repository as the source for a single package.
+    For legacy purposes, this can also be treated as the source for a Package
+    Control "repository".
+
+    :param repo:
+        The public web URL to the BitBucket repository. Should be in the format
+        `https://bitbucket.org/user/package`.
+
+    :param settings:
+        A dict containing at least the following fields:
+          `cache_length`,
+          `debug`,
+          `timeout`,
+          `user_agent`
+        Optional fields:
+          `http_proxy`,
+          `https_proxy`,
+          `proxy_username`,
+          `proxy_password`,
+          `query_string_params`,
+          `http_basic_auth`
+    """
+
+    @classmethod
+    def match_url(cls, repo_url):
+        """
+        Indicates if this provider can handle the provided repo_url
+
+        :param repo_url:
+            The URL to the repository, in one of the forms:
+                https://bitbucket.org/{user}/{repo}.git
+                https://bitbucket.org/{user}/{repo}
+                https://bitbucket.org/{user}/{repo}/
+                https://bitbucket.org/{user}/{repo}/src/{branch}
+                https://bitbucket.org/{user}/{repo}/src/{branch}/
+
+        :return:
+            True if repo_url matches an supported scheme.
+        """
+        user, repo, _ = BitBucketClient.user_repo_branch(repo_url)
+        return bool(user and repo)
+
+    def get_packages(self, invalid_sources=None):
+        """
+        Uses the BitBucket API to construct necessary info for a package
+
+        :param invalid_sources:
+            A list of URLs that should be ignored
+
+        :return:
+            A generator of
+            (
+                'Package Name',
+                {
+                    'name': name,
+                    'description': description,
+                    'author': author,
+                    'homepage': homepage,
+                    'last_modified': last modified date,
+                    'releases': [
+                        {
+                            'sublime_text': '*',
+                            'platforms': ['*'],
+                            'url': url,
+                            'date': date,
+                            'version': version
+                        }, ...
+                    ],
+                    'previous_names': [],
+                    'labels': [],
+                    'sources': [the repo URL],
+                    'readme': url,
+                    'issues': url,
+                    'donate': url,
+                    'buy': None
+                }
+            )
+            tuples
+        """
+
+        if self.packages is not None:
+            for key, value in self.packages.items():
+                yield (key, value)
+            return
+
+        if invalid_sources is not None and self.repo_url in invalid_sources:
+            return
+
+        client = BitBucketClient(self.settings)
+
+        try:
+            repo_info = client.repo_info(self.repo_url)
+            if not repo_info:
+                raise GitProviderRepoInfoException(self)
+
+            downloads = client.download_info_from_branch(self.repo_url, repo_info['default_branch'])
+            if not downloads:
+                raise GitProviderDownloadInfoException(self)
+
+            for download in downloads:
+                download['sublime_text'] = '*'
+                download['platforms'] = ['*']
+
+            name = repo_info['name']
+            details = {
+                'name': name,
+                'description': repo_info['description'],
+                'homepage': repo_info['homepage'],
+                'author': repo_info['author'],
+                'last_modified': downloads[0].get('date'),
+                'releases': downloads,
+                'previous_names': [],
+                'labels': [],
+                'sources': [self.repo_url],
+                'readme': repo_info['readme'],
+                'issues': repo_info['issues'],
+                'donate': repo_info['donate'],
+                'buy': None
+            }
+            self.packages = {name: details}
+            yield (name, details)
+
+        except (DownloaderException, ClientException, ProviderException) as e:
+            self.failed_sources[self.repo_url] = e
+            self.packages = {}
diff --git a/tasks/lib/package_control/providers/channel_provider.py b/tasks/lib/package_control/providers/channel_provider.py
new file mode 100644
index 0000000..fb26030
--- /dev/null
+++ b/tasks/lib/package_control/providers/channel_provider.py
@@ -0,0 +1,415 @@
+import json
+import os
+import re
+from itertools import chain
+
+from ..console_write import console_write
+from ..download_manager import http_get, resolve_urls, update_url
+from ..package_version import version_sort
+from .provider_exception import ProviderException
+from .schema_version import SchemaVersion
+
+
+class InvalidChannelFileException(ProviderException):
+
+    def __init__(self, channel, reason_message):
+        super().__init__(
+            'Channel %s does not appear to be a valid channel file because'
+            ' %s' % (channel.channel_url, reason_message))
+
+
+class UncachedChannelRepositoryError(ProviderException):
+    pass
+
+
+class ChannelProvider:
+    """
+    Retrieves a channel and provides an API into the information
+
+    The current channel/repository infrastructure caches repository info into
+    the channel to improve the Package Control client performance. This also
+    has the side effect of lessening the load on the GitHub and BitBucket APIs
+    and getting around not-infrequent HTTP 503 errors from those APIs.
+
+    :param channel_url:
+        The URL of the channel
+
+    :param settings:
+        A dict containing at least the following fields:
+          `cache_length`,
+          `debug`,
+          `timeout`,
+          `user_agent`
+        Optional fields:
+          `http_proxy`,
+          `https_proxy`,
+          `proxy_username`,
+          `proxy_password`,
+          `query_string_params`,
+          `http_basic_auth`
+    """
+
+    __slots__ = [
+        'channel_url',
+        'schema_version',
+        'repositories',
+        'libraries_cache',
+        'packages_cache',
+        'settings',
+    ]
+
+    def __init__(self, channel_url, settings):
+        self.channel_url = channel_url
+        self.schema_version = SchemaVersion('4.0.0')
+        self.repositories = None
+        self.libraries_cache = {}
+        self.packages_cache = {}
+        self.settings = settings
+
+    @classmethod
+    def match_url(cls, channel_url):
+        """
+        Indicates if this provider can handle the provided channel_url.
+        """
+
+        return True
+
+    def prefetch(self):
+        """
+        Go out and perform HTTP operations, caching the result
+
+        :raises:
+            ProviderException: when an error occurs trying to open a file
+            DownloaderException: when an error occurs trying to open a URL
+        """
+
+        self.fetch()
+
+    def fetch(self):
+        """
+        Retrieves and loads the JSON for other methods to use
+
+        :raises:
+            InvalidChannelFileException: when parsing or validation file content fails
+            ProviderException: when an error occurs trying to open a file
+            DownloaderException: when an error occurs trying to open a URL
+        """
+
+        if self.repositories is not None:
+            return
+
+        if re.match(r'https?://', self.channel_url, re.I):
+            json_string = http_get(self.channel_url, self.settings, 'Error downloading channel.')
+
+        # All other channels are expected to be filesystem paths
+        else:
+            if not os.path.exists(self.channel_url):
+                raise ProviderException('Error, file %s does not exist' % self.channel_url)
+
+            if self.settings.get('debug'):
+                console_write(
+                    '''
+                    Loading %s as a channel
+                    ''',
+                    self.channel_url
+                )
+
+            # We open as binary so we get bytes like the DownloadManager
+            with open(self.channel_url, 'rb') as f:
+                json_string = f.read()
+
+        try:
+            channel_info = json.loads(json_string.decode('utf-8'))
+        except ValueError:
+            raise InvalidChannelFileException(self, 'parsing JSON failed.')
+
+        try:
+            schema_version = SchemaVersion(channel_info['schema_version'])
+        except KeyError:
+            raise InvalidChannelFileException(self, 'the "schema_version" JSON key is missing.')
+        except ValueError as e:
+            raise InvalidChannelFileException(self, e)
+
+        if 'repositories' not in channel_info:
+            raise InvalidChannelFileException(self, 'the "repositories" JSON key is missing.')
+
+        self.repositories = self._migrate_repositories(channel_info, schema_version)
+        self.packages_cache = self._migrate_packages_cache(channel_info, schema_version)
+        self.libraries_cache = self._migrate_libraries_cache(channel_info, schema_version)
+
+    def get_renamed_packages(self):
+        """
+        :raises:
+            ProviderException: when an error occurs with the channel contents
+            DownloaderException: when an error occurs trying to open a URL
+
+        :return:
+            A dict of the packages that have been renamed
+        """
+
+        self.fetch()
+
+        output = {}
+        for package in chain(*self.packages_cache.values()):
+            previous_names = package.get('previous_names', [])
+            if not isinstance(previous_names, list):
+                previous_names = [previous_names]
+            for previous_name in previous_names:
+                output[previous_name] = package['name']
+
+        return output
+
+    def get_repositories(self):
+        """
+        :raises:
+            ProviderException: when an error occurs with the channel contents
+            DownloaderException: when an error occurs trying to open a URL
+
+        :return:
+            A list of the repository URLs
+        """
+
+        self.fetch()
+
+        return self.repositories
+
+    def get_sources(self):
+        """
+        Return a list of current URLs that are directly referenced by the
+        channel
+
+        :return:
+            A list of URLs and/or file paths
+        """
+
+        return self.get_repositories()
+
+    def get_packages(self, repo_url):
+        """
+        Provides access to the repository info that is cached in a channel
+
+        :param repo_url:
+            The URL of the repository to get the cached info of
+
+        :raises:
+            DownloaderException: when an error occurs trying to open a URL
+            UncachedChannelRepositoryError when no cache entry exists for repo_url
+
+        :return:
+            A generator of
+            (
+                'Package Name',
+                {
+                    'name': name,
+                    'description': description,
+                    'author': author,
+                    'homepage': homepage,
+                    'previous_names': [old_name, ...],
+                    'labels': [label, ...],
+                    'readme': url,
+                    'issues': url,
+                    'donate': url,
+                    'buy': url,
+                    'last_modified': last modified date,
+                    'releases': [
+                        {
+                            'sublime_text': compatible version,
+                            'platforms': [platform name, ...],
+                            'python_versions': ['3.3', '3.8'],
+                            'url': url,
+                            'date': date,
+                            'version': version,
+                            'libraries': [library name, ...]
+                        }, ...
+                    ]
+                }
+            )
+            tuples
+        """
+
+        self.fetch()
+
+        if repo_url not in self.packages_cache:
+            raise UncachedChannelRepositoryError(repo_url)
+
+        for package in self.packages_cache[repo_url]:
+            if package['releases']:
+                yield (package['name'], package)
+
+    def get_libraries(self, repo_url):
+        """
+        Provides access to the library info that is cached in a channel
+
+        :param repo_url:
+            The URL of the repository to get the cached info of
+
+        :raises:
+            DownloaderException: when an error occurs trying to open a URL
+            UncachedChannelRepositoryError when no cache entry exists for repo_url
+
+        :return:
+            A generator of
+            (
+                'Library Name',
+                {
+                    'name': name,
+                    'description': description,
+                    'author': author,
+                    'issues': URL,
+                    'releases': [
+                        {
+                            'sublime_text': compatible version,
+                            'platforms': [platform name, ...],
+                            'python_versions': ['3.3', '3.8'],
+                            'url': url,
+                            'version': version,
+                            'sha256': hex hash
+                        }, ...
+                    ]
+                }
+            )
+            tuples
+        """
+
+        self.fetch()
+
+        if repo_url not in self.libraries_cache:
+            raise UncachedChannelRepositoryError(repo_url)
+
+        for library in self.libraries_cache[repo_url]:
+            if library['releases']:
+                yield (library['name'], library)
+
+    def get_broken_packages(self):
+        """
+        Provide package names without releases.
+
+        :raises:
+            ProviderException: when an error occurs with the channel contents
+            DownloaderException: when an error occurs trying to open a URL
+
+        :return:
+            A generator of 'package names'
+        """
+
+        self.fetch()
+
+        for package in chain(*self.packages_cache.values()):
+            if not package['releases']:
+                yield package['name']
+
+    def get_broken_libraries(self):
+        """
+        Provide library names without releases.
+
+        :raises:
+            ProviderException: when an error occurs with the channel contents
+            DownloaderException: when an error occurs trying to open a URL
+
+        :return:
+            A generator of 'library names'
+        """
+
+        self.fetch()
+
+        for library in chain(*self.libraries_cache.values()):
+            if not library['releases']:
+                yield library['name']
+
+    def _migrate_repositories(self, channel_info, schema_version):
+
+        debug = self.settings.get('debug')
+
+        return [
+            update_url(url, debug)
+            for url in resolve_urls(self.channel_url, channel_info['repositories'])
+        ]
+
+    def _migrate_packages_cache(self, channel_info, schema_version):
+        """
+        Transform input packages cache to scheme version 4.0.0
+
+        :param channel_info:
+            The input channel information of any scheme version
+
+        :param schema_version:
+            The schema version of the input channel information
+
+        :returns:
+            packages_cache object of scheme version 4.0.0
+        """
+
+        debug = self.settings.get('debug')
+
+        package_cache = channel_info.get('packages_cache', {})
+
+        defaults = {
+            'buy': None,
+            'issues': None,
+            'labels': [],
+            'previous_names': [],
+            'readme': None,
+            'donate': None
+        }
+
+        for package in chain(*package_cache.values()):
+
+            for field in defaults:
+                if field not in package:
+                    package[field] = defaults[field]
+
+            # Workaround for packagecontrol.io, which adds `authors` instead of `author`
+            # to cached packages and libraries.
+            if 'authors' in package:
+                package['author'] = package.pop('authors')
+
+            releases = version_sort(package.get('releases', []), 'platforms', reverse=True)
+            package['releases'] = releases
+            package['last_modified'] = releases[0]['date'] if releases else None
+
+            # The 4.0.0 channel schema renamed the `dependencies` key to `libraries`.
+            if schema_version.major < 4:
+                for release in package['releases']:
+                    if 'dependencies' in release:
+                        release['libraries'] = release.pop('dependencies')
+
+        # Fix any out-dated repository URLs in packages cache
+        return {update_url(name, debug): info for name, info in package_cache.items()}
+
+    def _migrate_libraries_cache(self, channel_info, schema_version):
+        """
+        Transform input libraries cache to scheme version 4.0.0
+
+        :param channel_info:
+            The input channel information of any scheme version
+
+        :param schema_version:
+            The schema version of the input channel information
+
+        :returns:
+            libraries_cache object of scheme version 4.0.0
+        """
+
+        debug = self.settings.get('debug')
+
+        if schema_version.major < 4:
+            # The 4.0.0 channel schema renamed the key cached package info was
+            # stored under in order to be more clear to new users.
+            libraries_cache = channel_info.pop('dependencies_cache', {})
+
+            # The 4.0.0 channel scheme drops 'load_order' from each library
+            # and adds a required 'python_versions' list to each release.
+            for library in chain(*libraries_cache.values()):
+                del library['load_order']
+                for release in library['releases']:
+                    release['python_versions'] = ['3.3']
+                library['releases'] = version_sort(library['releases'], 'platforms', reverse=True)
+
+        else:
+            libraries_cache = channel_info.get('libraries_cache', {})
+
+            for library in chain(*libraries_cache.values()):
+                library['releases'] = version_sort(library['releases'], 'platforms', reverse=True)
+
+        # Fix any out-dated repository URLs in libraries cache
+        return {update_url(name, debug): info for name, info in libraries_cache.items()}
diff --git a/tasks/lib/package_control/providers/github_repository_provider.py b/tasks/lib/package_control/providers/github_repository_provider.py
new file mode 100644
index 0000000..c69b6f6
--- /dev/null
+++ b/tasks/lib/package_control/providers/github_repository_provider.py
@@ -0,0 +1,146 @@
+import re
+
+from ..clients.client_exception import ClientException
+from ..clients.github_client import GitHubClient
+from ..downloaders.downloader_exception import DownloaderException
+from .base_repository_provider import BaseRepositoryProvider
+from .provider_exception import (
+    GitProviderDownloadInfoException,
+    GitProviderRepoInfoException,
+    ProviderException,
+)
+
+
+class GitHubRepositoryProvider(BaseRepositoryProvider):
+    """
+    Allows using a public GitHub repository as the source for a single package.
+    For legacy purposes, this can also be treated as the source for a Package
+    Control "repository".
+
+    :param repo_url:
+        The public web URL to the GitHub repository. Should be in the format
+        `https://github.com/user/package` for the master branch, or
+        `https://github.com/user/package/tree/{branch_name}` for any other
+        branch.
+
+    :param settings:
+        A dict containing at least the following fields:
+          `cache_length`,
+          `debug`,
+          `timeout`,
+          `user_agent`
+        Optional fields:
+          `http_proxy`,
+          `https_proxy`,
+          `proxy_username`,
+          `proxy_password`,
+          `query_string_params`,
+          `http_basic_auth`
+    """
+
+    def __init__(self, repo_url, settings):
+        # Clean off the trailing .git to be more forgiving
+        super().__init__(re.sub(r'\.git$', '', repo_url), settings)
+
+    @classmethod
+    def match_url(cls, repo_url):
+        """
+        Indicates if this provider can handle the provided repo_url
+
+        :param repo_url:
+            The URL to the repository, in one of the forms:
+                https://github.com/{user}/{repo}.git
+                https://github.com/{user}/{repo}
+                https://github.com/{user}/{repo}/
+                https://github.com/{user}/{repo}/tree/{branch}
+                https://github.com/{user}/{repo}/tree/{branch}/
+
+        :return:
+            True if repo_url matches an supported scheme.
+        """
+        user, repo, _ = GitHubClient.user_repo_branch(repo_url)
+        return bool(user and repo)
+
+    def get_packages(self, invalid_sources=None):
+        """
+        Uses the GitHub API to construct necessary info for a package
+
+        :param invalid_sources:
+            A list of URLs that should be ignored
+
+        :return:
+            A generator of
+            (
+                'Package Name',
+                {
+                    'name': name,
+                    'description': description,
+                    'author': author,
+                    'homepage': homepage,
+                    'last_modified': last modified date,
+                    'releases': [
+                        {
+                            'sublime_text': '*',
+                            'platforms': ['*'],
+                            'url': url,
+                            'date': date,
+                            'version': version
+                        }, ...
+                    ],
+                    'previous_names': [],
+                    'labels': [],
+                    'sources': [the repo URL],
+                    'readme': url,
+                    'issues': url,
+                    'donate': url,
+                    'buy': None
+                }
+            )
+            tuples
+        """
+
+        if self.packages is not None:
+            for key, value in self.packages.items():
+                yield (key, value)
+            return
+
+        if invalid_sources is not None and self.repo_url in invalid_sources:
+            return
+
+        client = GitHubClient(self.settings)
+
+        try:
+            repo_info = client.repo_info(self.repo_url)
+            if not repo_info:
+                raise GitProviderRepoInfoException(self)
+
+            downloads = client.download_info_from_branch(self.repo_url, repo_info['default_branch'])
+            if not downloads:
+                raise GitProviderDownloadInfoException(self)
+
+            for download in downloads:
+                download['sublime_text'] = '*'
+                download['platforms'] = ['*']
+
+            name = repo_info['name']
+            details = {
+                'name': name,
+                'description': repo_info['description'],
+                'homepage': repo_info['homepage'],
+                'author': repo_info['author'],
+                'last_modified': downloads[0].get('date'),
+                'releases': downloads,
+                'previous_names': [],
+                'labels': [],
+                'sources': [self.repo_url],
+                'readme': repo_info['readme'],
+                'issues': repo_info['issues'],
+                'donate': repo_info['donate'],
+                'buy': None
+            }
+            self.packages = {name: details}
+            yield (name, details)
+
+        except (DownloaderException, ClientException, ProviderException) as e:
+            self.failed_sources[self.repo_url] = e
+            self.packages = {}
diff --git a/tasks/lib/package_control/providers/github_user_provider.py b/tasks/lib/package_control/providers/github_user_provider.py
new file mode 100644
index 0000000..f35e976
--- /dev/null
+++ b/tasks/lib/package_control/providers/github_user_provider.py
@@ -0,0 +1,148 @@
+from ..clients.client_exception import ClientException
+from ..clients.github_client import GitHubClient
+from ..downloaders.downloader_exception import DownloaderException
+from .base_repository_provider import BaseRepositoryProvider
+from .provider_exception import (
+    GitProviderDownloadInfoException,
+    GitProviderUserInfoException,
+    ProviderException,
+)
+
+
+class GitHubUserProvider(BaseRepositoryProvider):
+    """
+    Allows using a GitHub user/organization as the source for multiple packages,
+    or in Package Control terminology, a "repository".
+
+    :param repo_url:
+        The public web URL to the GitHub user/org. Should be in the format
+        `https://github.com/user`.
+
+    :param settings:
+        A dict containing at least the following fields:
+          `cache_length`,
+          `debug`,
+          `timeout`,
+          `user_agent`,
+        Optional fields:
+          `http_proxy`,
+          `https_proxy`,
+          `proxy_username`,
+          `proxy_password`,
+          `query_string_params`,
+          `http_basic_auth`
+    """
+
+    @classmethod
+    def match_url(cls, repo_url):
+        """
+        Indicates if this provider can handle the provided repo_url
+
+        :param repo_url:
+            The URL to the repository, in one of the forms:
+                https://github.com/{user}
+                https://github.com/{user}/
+
+        :return:
+            True if repo_url matches an supported scheme.
+        """
+        user, repo, _ = GitHubClient.user_repo_branch(repo_url)
+        return bool(user and not repo)
+
+    def get_packages(self, invalid_sources=None):
+        """
+        Uses the GitHub API to construct necessary info for all packages
+
+        :param invalid_sources:
+            A list of URLs that should be ignored
+
+        :return:
+            A generator of
+            (
+                'Package Name',
+                {
+                    'name': name,
+                    'description': description,
+                    'author': author,
+                    'homepage': homepage,
+                    'last_modified': last modified date,
+                    'releases': [
+                        {
+                            'sublime_text': '*',
+                            'platforms': ['*'],
+                            'url': url,
+                            'date': date,
+                            'version': version
+                        }, ...
+                    ],
+                    'previous_names': [],
+                    'labels': [],
+                    'sources': [the user URL],
+                    'readme': url,
+                    'issues': url,
+                    'donate': url,
+                    'buy': None
+                }
+            )
+            tuples
+        """
+
+        if self.packages is not None:
+            for key, value in self.packages.items():
+                yield (key, value)
+            return
+
+        if invalid_sources is not None and self.repo_url in invalid_sources:
+            return
+
+        client = GitHubClient(self.settings)
+
+        try:
+            user_repos = client.user_info(self.repo_url)
+            if not user_repos:
+                raise GitProviderUserInfoException(self)
+        except (DownloaderException, ClientException, ProviderException) as e:
+            self.failed_sources[self.repo_url] = e
+            self.packages = {}
+            return
+
+        output = {}
+        for repo_info in user_repos:
+            author = repo_info['author']
+            name = repo_info['name']
+            repo_url = client.repo_url(author, name)
+
+            if invalid_sources is not None and repo_url in invalid_sources:
+                continue
+
+            try:
+                downloads = client.download_info_from_branch(repo_url, repo_info['default_branch'])
+                if not downloads:
+                    raise GitProviderDownloadInfoException(self)
+
+                for download in downloads:
+                    download['sublime_text'] = '*'
+                    download['platforms'] = ['*']
+
+                details = {
+                    'name': name,
+                    'description': repo_info['description'],
+                    'homepage': repo_info['homepage'],
+                    'author': author,
+                    'last_modified': downloads[0].get('date'),
+                    'releases': downloads,
+                    'previous_names': [],
+                    'labels': [],
+                    'sources': [self.repo_url],
+                    'readme': repo_info['readme'],
+                    'issues': repo_info['issues'],
+                    'donate': repo_info['donate'],
+                    'buy': None
+                }
+                output[name] = details
+                yield (name, details)
+
+            except (DownloaderException, ClientException, ProviderException) as e:
+                self.failed_sources[repo_url] = e
+
+        self.packages = output
diff --git a/tasks/lib/package_control/providers/gitlab_repository_provider.py b/tasks/lib/package_control/providers/gitlab_repository_provider.py
new file mode 100644
index 0000000..5872795
--- /dev/null
+++ b/tasks/lib/package_control/providers/gitlab_repository_provider.py
@@ -0,0 +1,146 @@
+import re
+
+from ..clients.client_exception import ClientException
+from ..clients.gitlab_client import GitLabClient
+from ..downloaders.downloader_exception import DownloaderException
+from .base_repository_provider import BaseRepositoryProvider
+from .provider_exception import (
+    GitProviderDownloadInfoException,
+    GitProviderRepoInfoException,
+    ProviderException,
+)
+
+
+class GitLabRepositoryProvider(BaseRepositoryProvider):
+    """
+    Allows using a public GitLab repository as the source for a single package.
+    For legacy purposes, this can also be treated as the source for a Package
+    Control "repository".
+
+    :param repo_url:
+        The public web URL to the GitLab repository. Should be in the format
+        `https://gitlab.com/user/package` for the master branch, or
+        `https://gitlab.com/user/package/-/tree/{branch_name}` for any other
+        branch.
+
+    :param settings:
+        A dict containing at least the following fields:
+          `cache_length`,
+          `debug`,
+          `timeout`,
+          `user_agent`
+        Optional fields:
+          `http_proxy`,
+          `https_proxy`,
+          `proxy_username`,
+          `proxy_password`,
+          `query_string_params`,
+          `http_basic_auth`
+    """
+
+    def __init__(self, repo_url, settings):
+        # Clean off the trailing .git to be more forgiving
+        super().__init__(re.sub(r'\.git$', '', repo_url), settings)
+
+    @classmethod
+    def match_url(cls, repo_url):
+        """
+        Indicates if this provider can handle the provided repo_url
+
+        :param repo_url:
+            The URL to the repository, in one of the forms:
+                https://gitlab.com/{user}/{repo}.git
+                https://gitlab.com/{user}/{repo}
+                https://gitlab.com/{user}/{repo}/
+                https://gitlab.com/{user}/{repo}/-/tree/{branch}
+                https://gitlab.com/{user}/{repo}/-/tree/{branch}/
+
+        :return:
+            True if repo_url matches an supported scheme.
+        """
+        user, repo, _ = GitLabClient.user_repo_branch(repo_url)
+        return bool(user and repo)
+
+    def get_packages(self, invalid_sources=None):
+        """
+        Uses the GitLab API to construct necessary info for a package
+
+        :param invalid_sources:
+            A list of URLs that should be ignored
+
+        :return:
+            A generator of
+            (
+                'Package Name',
+                {
+                    'name': name,
+                    'description': description,
+                    'author': author,
+                    'homepage': homepage,
+                    'last_modified': last modified date,
+                    'releases': [
+                        {
+                            'sublime_text': '*',
+                            'platforms': ['*'],
+                            'url': url,
+                            'date': date,
+                            'version': version
+                        }, ...
+                    ],
+                    'previous_names': [],
+                    'labels': [],
+                    'sources': [the repo URL],
+                    'readme': url,
+                    'issues': url,
+                    'donate': url,
+                    'buy': None
+                }
+            )
+            tuples
+        """
+
+        if self.packages is not None:
+            for key, value in self.packages.items():
+                yield (key, value)
+            return
+
+        if invalid_sources is not None and self.repo_url in invalid_sources:
+            return
+
+        client = GitLabClient(self.settings)
+
+        try:
+            repo_info = client.repo_info(self.repo_url)
+            if not repo_info:
+                raise GitProviderRepoInfoException(self)
+
+            downloads = client.download_info_from_branch(self.repo_url, repo_info['default_branch'])
+            if not downloads:
+                raise GitProviderDownloadInfoException(self)
+
+            for download in downloads:
+                download['sublime_text'] = '*'
+                download['platforms'] = ['*']
+
+            name = repo_info['name']
+            details = {
+                'name': name,
+                'description': repo_info['description'],
+                'homepage': repo_info['homepage'],
+                'author': repo_info['author'],
+                'last_modified': downloads[0].get('date'),
+                'releases': downloads,
+                'previous_names': [],
+                'labels': [],
+                'sources': [self.repo_url],
+                'readme': repo_info['readme'],
+                'issues': repo_info['issues'],
+                'donate': repo_info['donate'],
+                'buy': None
+            }
+            self.packages = {name: details}
+            yield (name, details)
+
+        except (DownloaderException, ClientException, ProviderException) as e:
+            self.failed_sources[self.repo_url] = e
+            self.packages = {}
diff --git a/tasks/lib/package_control/providers/gitlab_user_provider.py b/tasks/lib/package_control/providers/gitlab_user_provider.py
new file mode 100644
index 0000000..61f63f5
--- /dev/null
+++ b/tasks/lib/package_control/providers/gitlab_user_provider.py
@@ -0,0 +1,148 @@
+from ..clients.client_exception import ClientException
+from ..clients.gitlab_client import GitLabClient
+from ..downloaders.downloader_exception import DownloaderException
+from .base_repository_provider import BaseRepositoryProvider
+from .provider_exception import (
+    GitProviderDownloadInfoException,
+    GitProviderUserInfoException,
+    ProviderException,
+)
+
+
+class GitLabUserProvider(BaseRepositoryProvider):
+    """
+    Allows using a GitLab user/organization as the source for multiple packages,
+    or in Package Control terminology, a 'repository'.
+
+    :param repo_url:
+        The public web URL to the GitHub user/org. Should be in the format
+        `https://gitlab.com/user`.
+
+    :param settings:
+        A dict containing at least the following fields:
+          `cache_length`,
+          `debug`,
+          `timeout`,
+          `user_agent`,
+        Optional fields:
+          `http_proxy`,
+          `https_proxy`,
+          `proxy_username`,
+          `proxy_password`,
+          `query_string_params`,
+          `http_basic_auth`
+    """
+
+    @classmethod
+    def match_url(cls, repo_url):
+        """
+        Indicates if this provider can handle the provided repo_url
+
+        :param repo_url:
+            The URL to the repository, in one of the forms:
+                https://gitlab.com/{user}
+                https://gitlab.com/{user}/
+
+        :return:
+            True if repo_url matches an supported scheme.
+        """
+        user, repo, _ = GitLabClient.user_repo_branch(repo_url)
+        return bool(user and not repo)
+
+    def get_packages(self, invalid_sources=None):
+        """
+        Uses the lab API to construct necessary info for all packages
+
+        :param invalid_sources:
+            A list of URLs that should be ignored
+
+        :return:
+            A generator of
+            (
+                'Package Name',
+                {
+                    'name': name,
+                    'description': description,
+                    'author': author,
+                    'homepage': homepage,
+                    'last_modified': last modified date,
+                    'releases': [
+                        {
+                            'sublime_text': '*',
+                            'platforms': ['*'],
+                            'url': url,
+                            'date': date,
+                            'version': version
+                        }, ...
+                    ],
+                    'previous_names': [],
+                    'labels': [],
+                    'sources': [the user URL],
+                    'readme': url,
+                    'issues': url,
+                    'donate': url,
+                    'buy': None
+                }
+            )
+            tuples
+        """
+
+        if self.packages is not None:
+            for key, value in self.packages.items():
+                yield (key, value)
+            return
+
+        if invalid_sources is not None and self.repo_url in invalid_sources:
+            return
+
+        client = GitLabClient(self.settings)
+
+        try:
+            user_repos = client.user_info(self.repo_url)
+            if not user_repos:
+                raise GitProviderUserInfoException(self)
+        except (DownloaderException, ClientException, ProviderException) as e:
+            self.failed_sources[self.repo_url] = e
+            self.packages = {}
+            return
+
+        output = {}
+        for repo_info in user_repos:
+            author = repo_info['author']
+            name = repo_info['name']
+            repo_url = client.repo_url(author, name)
+
+            if invalid_sources is not None and repo_url in invalid_sources:
+                continue
+
+            try:
+                downloads = client.download_info_from_branch(repo_url, repo_info['default_branch'])
+                if not downloads:
+                    raise GitProviderDownloadInfoException(self)
+
+                for download in downloads:
+                    download['sublime_text'] = '*'
+                    download['platforms'] = ['*']
+
+                details = {
+                    'name': name,
+                    'description': repo_info['description'],
+                    'homepage': repo_info['homepage'],
+                    'author': author,
+                    'last_modified': downloads[0].get('date'),
+                    'releases': downloads,
+                    'previous_names': [],
+                    'labels': [],
+                    'sources': [self.repo_url],
+                    'readme': repo_info['readme'],
+                    'issues': repo_info['issues'],
+                    'donate': repo_info['donate'],
+                    'buy': None
+                }
+                output[name] = details
+                yield (name, details)
+
+            except (DownloaderException, ClientException, ProviderException) as e:
+                self.failed_sources[repo_url] = e
+
+        self.packages = output
diff --git a/tasks/lib/package_control/providers/json_repository_provider.py b/tasks/lib/package_control/providers/json_repository_provider.py
new file mode 100644
index 0000000..824218f
--- /dev/null
+++ b/tasks/lib/package_control/providers/json_repository_provider.py
@@ -0,0 +1,931 @@
+import json
+import re
+import os
+from itertools import chain
+from urllib.parse import urlparse
+
+from ..clients.bitbucket_client import BitBucketClient
+from ..clients.client_exception import ClientException
+from ..clients.github_client import GitHubClient
+from ..clients.gitlab_client import GitLabClient
+from ..clients.pypi_client import PyPiClient
+from ..console_write import console_write
+from ..download_manager import http_get, resolve_url, resolve_urls, update_url
+from ..downloaders.downloader_exception import DownloaderException
+from ..package_version import version_sort
+from .base_repository_provider import BaseRepositoryProvider
+from .provider_exception import ProviderException
+from .schema_version import SchemaVersion
+
+try:
+    # running within ST
+    from ..selectors import is_compatible_platform, is_compatible_version
+    IS_ST = True
+except ImportError:
+    # running on CLI or server
+    IS_ST = False
+
+
+class InvalidRepoFileException(ProviderException):
+    def __init__(self, repo, reason_message):
+        super().__init__(
+            'Repository {} does not appear to be a valid repository file because'
+            ' {}'.format(repo.repo_url, reason_message))
+
+
+class InvalidLibraryReleaseKeyError(ProviderException):
+    def __init__(self, repo, name, key):
+        super().__init__(
+            'Invalid or missing release-level key "{}" in library "{}"'
+            ' in repository "{}".'.format(key, name, repo))
+
+
+class InvalidPackageReleaseKeyError(ProviderException):
+    def __init__(self, repo, name, key):
+        super().__init__(
+            'Invalid or missing release-level key "{}" in package "{}"'
+            ' in repository "{}".'.format(key, name, repo))
+
+
+class JsonRepositoryProvider(BaseRepositoryProvider):
+    """
+    Generic repository downloader that fetches package info
+
+    With the current channel/repository architecture where the channel file
+    caches info from all includes repositories, these package providers just
+    serve the purpose of downloading packages not in the default channel.
+
+    The structure of the JSON a repository should contain is located in
+    example-packages.json.
+
+    :param repo_url:
+        The URL of the package repository
+
+    :param settings:
+        A dict containing at least the following fields:
+          `cache_length`,
+          `debug`,
+          `timeout`,
+          `user_agent`
+        Optional fields:
+          `http_proxy`,
+          `https_proxy`,
+          `proxy_username`,
+          `proxy_password`,
+          `query_string_params`,
+          `http_basic_auth`
+    """
+
+    def __init__(self, repo_url, settings):
+        super().__init__(repo_url, settings)
+        self.included_urls = set()
+        self.repo_info = None
+        self.schema_version = None
+
+    def fetch(self):
+        """
+        Retrieves and loads the JSON for other methods to use
+
+        :raises:
+            InvalidChannelFileException: when parsing or validation file content fails
+            ProviderException: when an error occurs trying to open a file
+            DownloaderException: when an error occurs trying to open a URL
+        """
+
+        if self.repo_info is not None:
+            return True
+
+        if self.repo_url in self.failed_sources:
+            return False
+
+        try:
+            self.repo_info = self.fetch_repo(self.repo_url)
+            self.schema_version = self.repo_info['schema_version']
+        except (DownloaderException, ClientException, ProviderException) as e:
+            self.failed_sources[self.repo_url] = e
+            self.libraries = {}
+            self.packages = {}
+            return False
+
+        return True
+
+    def fetch_repo(self, location):
+        """
+        Fetches the contents of a URL of file path
+
+        :param location:
+            The URL or file path
+
+        :raises:
+            ProviderException: when an error occurs trying to open a file
+            DownloaderException: when an error occurs trying to open a URL
+
+        :return:
+            A dict of the parsed JSON
+        """
+
+        # Prevent circular includes
+        if location in self.included_urls:
+            raise ProviderException('Error, repository "%s" already included.' % location)
+
+        self.included_urls.add(location)
+
+        if re.match(r'https?://', location, re.I):
+            json_string = http_get(location, self.settings, 'Error downloading repository.')
+
+        # Anything that is not a URL is expected to be a filesystem path
+        else:
+            if not os.path.exists(location):
+                raise ProviderException('Error, file %s does not exist' % location)
+
+            if self.settings.get('debug'):
+                console_write(
+                    '''
+                    Loading %s as a repository
+                    ''',
+                    location
+                )
+
+            # We open as binary so we get bytes like the DownloadManager
+            with open(location, 'rb') as f:
+                json_string = f.read()
+
+        try:
+            repo_info = json.loads(json_string.decode('utf-8'))
+        except (ValueError):
+            raise InvalidRepoFileException(self, 'parsing JSON failed.')
+
+        try:
+            schema_version = repo_info['schema_version'] = SchemaVersion(repo_info['schema_version'])
+        except KeyError:
+            raise InvalidRepoFileException(
+                self, 'the "schema_version" JSON key is missing.')
+        except ValueError as e:
+            raise InvalidRepoFileException(self, e)
+
+        # Main keys depending on scheme version
+        if schema_version.major < 4:
+            repo_keys = {'packages', 'dependencies', 'includes'}
+        else:
+            repo_keys = {'packages', 'libraries', 'includes'}
+
+        # Check existence of at least one required main key
+        if not set(repo_info.keys()) & repo_keys:
+            raise InvalidRepoFileException(self, 'it doesn\'t look like a repository.')
+
+        # Check type of existing main keys
+        for key in repo_keys:
+            if key in repo_info and not isinstance(repo_info[key], list):
+                raise InvalidRepoFileException(self, 'the "%s" key is not an array.' % key)
+
+        # Migrate dependencies to libraries
+        # The 4.0.0 repository schema renamed dependencies key to libraries.
+        if schema_version.major < 4:
+            repo_info['libraries'] = repo_info.pop('dependencies', [])
+
+        # Allow repositories to include other repositories, recursively
+        includes = repo_info.pop('includes', None)
+        if includes:
+            for include in resolve_urls(self.repo_url, includes):
+                try:
+                    include_info = self.fetch_repo(include)
+                except (DownloaderException, ClientException, ProviderException) as e:
+                    self.failed_sources[include] = e
+                else:
+                    include_version = include_info['schema_version']
+                    if include_version != schema_version:
+                        raise ProviderException(
+                            'Scheme version of included repository %s doesn\'t match its parent.' % include)
+
+                    repo_info['packages'].extend(include_info.get('packages', []))
+                    repo_info['libraries'].extend(include_info.get('libraries', []))
+
+        return repo_info
+
+    def get_libraries(self, invalid_sources=None):
+        """
+        Provides access to the libraries in this repository
+
+        :param invalid_sources:
+            A list of URLs that are permissible to fetch data from
+
+        :return:
+            A generator of
+            (
+                'Library Name',
+                {
+                    'name': name,
+                    'description': description,
+                    'author': author,
+                    'issues': URL,
+                    'releases': [
+                        {
+                            'sublime_text': compatible version,
+                            'platforms': [platform name, ...],
+                            'python_versions': ['3.3', '3.8'],
+                            'url': url,
+                            'version': version,
+                            'sha256': hex hash
+                        }, ...
+                    ],
+                    'sources': [url, ...]
+                }
+            )
+            tuples
+        """
+
+        if self.libraries is not None:
+            for key, value in self.libraries.items():
+                yield (key, value)
+            return
+
+        if invalid_sources is not None and self.repo_url in invalid_sources:
+            return
+
+        if not self.fetch():
+            return
+
+        if not self.repo_info:
+            return
+
+        if self.schema_version.major >= 4:
+            allowed_library_keys = {
+                'name', 'description', 'author', 'homepage', 'issues', 'releases'
+            }
+            allowed_release_keys = {  # todo: remove 'branch'
+                'base', 'version', 'sublime_text', 'platforms', 'python_versions',
+                'branch', 'tags', 'asset', 'url', 'sha256'
+            }
+        else:
+            allowed_library_keys = {
+                'name', 'description', 'author', 'issues', 'load_order', 'releases'
+            }
+            allowed_release_keys = {
+                'base', 'version', 'sublime_text', 'platforms',
+                'branch', 'tags', 'url', 'sha256'
+            }
+
+        copied_library_keys = ('name', 'description', 'author', 'homepage', 'issues')
+        copied_release_keys = ('date', 'version', 'sha256')
+        default_platforms = ['*']
+        default_python_versions = ['3.3']
+        default_sublime_text = '*'
+
+        debug = self.settings.get('debug')
+
+        clients = [
+            Client(self.settings) for Client in (GitHubClient, GitLabClient, BitBucketClient, PyPiClient)
+        ]
+
+        output = {}
+        for library in self.repo_info.get('libraries', []):
+            info = {
+                'releases': [],
+                'sources': [self.repo_url]
+            }
+
+            for field in copied_library_keys:
+                field_value = library.get(field)
+                if field_value:
+                    info[field] = field_value
+
+            if 'name' not in info:
+                self.failed_sources[self.repo_url] = ProviderException(
+                    'No "name" value for one of libraries'
+                    ' in repository "{}".'.format(self.repo_url)
+                )
+                continue
+
+            try:
+                unknown_keys = set(library) - allowed_library_keys
+                if unknown_keys:
+                    raise ProviderException(
+                        'The "{}" key(s) in library "{}" in repository {} are not supported.'.format(
+                            '", "'.join(sorted(unknown_keys)), info['name'],
+                            self.repo_url
+                        )
+                    )
+
+                releases = library.get('releases', [])
+                if releases and not isinstance(releases, list):
+                    raise ProviderException(
+                        'The "releases" value is not an array for library "{}"'
+                        ' in repository {}.'.format(info['name'], self.repo_url)
+                    )
+
+                staged_releases = {}
+
+                for release in releases:
+                    download_info = {}
+
+                    unknown_keys = set(release) - allowed_release_keys
+                    if unknown_keys:
+                        raise ProviderException(
+                            'The "{}" key(s) in one of the releases of library "{}"'
+                            ' in repository {} are not supported.'.format(
+                                '", "'.join(sorted(unknown_keys)), info['name'], self.repo_url
+                            )
+                        )
+
+                    # Validate libraries
+                    # the key can be used to specify dependencies, upstream via repositories
+                    key = 'libraries' if self.schema_version.major >= 4 else 'dependencies'
+                    value = release.get(key, [])
+                    if value:
+                        if not isinstance(value, list):
+                            raise InvalidLibraryReleaseKeyError(self.repo_url, info['name'], key)
+                        download_info['libraries'] = value
+
+                    # Validate supported platforms
+                    key = 'platforms'
+                    value = release.get(key, default_platforms)
+                    if isinstance(value, str):
+                        value = [value]
+                    elif not isinstance(value, list):
+                        raise InvalidLibraryReleaseKeyError(self.repo_url, info['name'], key)
+                    # ignore incompatible release (avoid downloading/evaluating further information)
+                    if IS_ST and not is_compatible_platform(value):
+                        continue
+                    download_info[key] = value
+
+                    # Validate supported python_versions
+                    key = 'python_versions'
+                    value = release.get(key, default_python_versions)
+                    if isinstance(value, str):
+                        value = [value]
+                    elif not isinstance(value, list):
+                        raise InvalidLibraryReleaseKeyError(self.repo_url, info['name'], key)
+                    download_info[key] = value
+
+                    # Validate supported ST version
+                    key = 'sublime_text'
+                    value = release.get(key, default_sublime_text)
+                    if not isinstance(value, str):
+                        raise InvalidLibraryReleaseKeyError(self.repo_url, info['name'], key)
+                    # ignore incompatible release (avoid downloading/evaluating further information)
+                    if IS_ST and not is_compatible_version(value):
+                        continue
+                    download_info[key] = value
+
+                    # Validate url
+                    # if present, it is an explicit or resolved release
+                    url = release.get('url')
+                    if url:
+                        for key in copied_release_keys:
+                            if key in release:
+                                value = release[key]
+                                if not value or not isinstance(value, str):
+                                    raise InvalidLibraryReleaseKeyError(self.repo_url, info['name'], key)
+                                download_info[key] = value
+
+                        if 'version' not in download_info:
+                            raise ProviderException(
+                                'Missing "version" key in release with explicit "url" of library "{}"'
+                                ' in repository "{}".'.format(info['name'], self.repo_url)
+                            )
+
+                        download_info['url'] = update_url(resolve_url(self.repo_url, url), debug)
+                        is_http = urlparse(download_info['url']).scheme == 'http'
+                        if is_http and 'sha256' not in download_info:
+                            raise ProviderException(
+                                'No "sha256" key for the non-secure "url" value in one of the releases'
+                                ' of the library "{}" in repository {}.'.format(info['name'], self.repo_url)
+                            )
+
+                        info['releases'].append(download_info)
+                        continue
+
+                    # Resolve release template using `base` and `branch` or `tags` keys
+
+                    base = release.get('base')
+                    if not base:
+                        raise InvalidLibraryReleaseKeyError(self.repo_url, info['name'], 'base')
+
+                    base_url = resolve_url(self.repo_url, base)
+                    downloads = None
+
+                    # Evaluate and resolve "tags" and "branch" release templates
+                    asset = release.get('asset')
+                    branch = release.get('branch')
+                    tags = release.get('tags')
+                    extra = None if tags is True else tags
+
+                    if asset:
+                        if branch:
+                            raise ProviderException(
+                                'Illegal "asset" key "{}" for branch based release of library "{}"'
+                                ' in repository "{}".'.format(base, info['name'], self.repo_url)
+                            )
+                        # group releases with assets by base_url and tag-prefix
+                        # to prepare gathering download_info with a single API call
+                        staged_releases.setdefault((base_url, extra), []).append((asset, download_info))
+                        continue
+
+                    elif tags:
+                        for client in clients:
+                            downloads = client.download_info_from_tags(base_url, extra)
+                            if downloads is not None:
+                                break
+
+                    elif branch:
+                        for client in clients:
+                            downloads = client.download_info_from_branch(base_url, branch)
+                            if downloads is not None:
+                                break
+                    else:
+                        raise ProviderException(
+                            'Missing "branch", "tags" or "url" key in release of library "{}"'
+                            ' in repository "{}".'.format(info['name'], self.repo_url)
+                        )
+
+                    if downloads is None:
+                        raise ProviderException(
+                            'Invalid "base" value "{}" for one of the releases of library "{}"'
+                            ' in repository "{}".'.format(base, info['name'], self.repo_url)
+                        )
+
+                    if downloads is False:
+                        raise ProviderException(
+                            'No valid semver tags found at "{}" for library "{}"'
+                            ' in repository "{}".'.format(base, info['name'], self.repo_url)
+                        )
+
+                    for download in downloads:
+                        download.update(download_info)
+                        info['releases'].append(download)
+
+                # gather download_info from releases
+                for (base_url, extra), asset_templates in staged_releases.items():
+                    for client in clients:
+                        downloads = client.download_info_from_releases(base_url, asset_templates, extra)
+                        if downloads is not None:
+                            info['releases'].extend(downloads)
+                            break
+
+                # check required library keys
+                for key in ('description', 'author', 'issues'):
+                    if not info.get(key):
+                        raise ProviderException(
+                            'Missing or invalid "{}" key for library "{}"'
+                            ' in repository "{}".'.format(key, info['name'], self.repo_url)
+                        )
+
+                # Empty releases means package is unavailable on current platform or for version of ST
+                if not info['releases']:
+                    continue
+
+                info['releases'] = version_sort(info['releases'], 'platforms', reverse=True)
+
+                output[info['name']] = info
+                yield (info['name'], info)
+
+            except (DownloaderException, ClientException, ProviderException) as e:
+                self.broken_libriaries[info['name']] = e
+
+        self.libraries = output
+
+    def get_packages(self, invalid_sources=None):
+        """
+        Provides access to the packages in this repository
+
+        :param invalid_sources:
+            A list of URLs that are permissible to fetch data from
+
+        :return:
+            A generator of
+            (
+                'Package Name',
+                {
+                    'name': name,
+                    'description': description,
+                    'author': author,
+                    'homepage': homepage,
+                    'previous_names': [old_name, ...],
+                    'labels': [label, ...],
+                    'sources': [url, ...],
+                    'readme': url,
+                    'issues': url,
+                    'donate': url,
+                    'buy': url,
+                    'last_modified': last modified date,
+                    'releases': [
+                        {
+                            'sublime_text': compatible version,
+                            'platforms': [platform name, ...],
+                            'url': url,
+                            'date': date,
+                            'version': version,
+                            'libraries': [library name, ...]
+                        }, ...
+                    ]
+                }
+            )
+            tuples
+        """
+
+        if self.packages is not None:
+            for key, value in self.packages.items():
+                yield (key, value)
+            return
+
+        if invalid_sources is not None and self.repo_url in invalid_sources:
+            return
+
+        if not self.fetch():
+            return
+
+        if not self.repo_info:
+            return
+
+        copied_package_keys = (
+            'name',
+            'description',
+            'author',
+            'last_modified',
+            'previous_names',
+            'labels',
+            'homepage',
+            'readme',
+            'issues',
+            'donate',
+            'buy'
+        )
+        copied_release_keys = ('date', 'version')
+        default_platforms = ['*']
+        default_sublime_text = '*'
+
+        debug = self.settings.get('debug')
+
+        clients = [
+            Client(self.settings) for Client in (GitHubClient, GitLabClient, BitBucketClient)
+        ]
+
+        output = {}
+        for package in self.repo_info.get('packages', []):
+            info = {
+                'releases': [],
+                'sources': [self.repo_url]
+            }
+
+            for field in copied_package_keys:
+                if package.get(field):
+                    info[field] = package.get(field)
+
+            # Try to grab package-level details from GitHub or BitBucket
+            details = package.get('details')
+            if details:
+                details = resolve_url(self.repo_url, details)
+
+                if invalid_sources is not None and details in invalid_sources:
+                    continue
+
+                if details not in info['sources']:
+                    info['sources'].append(details)
+
+                try:
+                    repo_info = None
+
+                    for client in clients:
+                        repo_info = client.repo_info(details)
+                        if repo_info:
+                            break
+                    else:
+                        raise ProviderException(
+                            'Invalid "details" value "{}" for one of the packages'
+                            ' in the repository {}.'.format(details, self.repo_url)
+                        )
+
+                    del repo_info['default_branch']
+
+                    # When grabbing details, prefer explicit field values over the values
+                    # from the GitHub or BitBucket API
+                    info = dict(chain(repo_info.items(), info.items()))
+
+                except (DownloaderException, ClientException, ProviderException) as e:
+                    if 'name' in info:
+                        self.broken_packages[info['name']] = e
+                    self.failed_sources[details] = e
+                    continue
+
+            if 'name' not in info:
+                self.failed_sources[self.repo_url] = ProviderException(
+                    'No "name" value for one of the packages'
+                    ' in the repository {}.'.format(self.repo_url)
+                )
+                continue
+
+            try:
+                if not info.get('author'):
+                    raise ProviderException(
+                        'Missing or invalid "author" key for package "{}"'
+                        ' in repository "{}".'.format(info['name'], self.repo_url)
+                    )
+
+                # evaluate releases
+
+                releases = package.get('releases')
+
+                # If no releases info was specified, also grab the download info from GH or BB
+                if self.schema_version.major == 2 and not releases and details:
+                    releases = [{'details': details}]
+
+                if not releases:
+                    raise ProviderException(
+                        'No "releases" value for the package "{}"'
+                        ' in the repository {}.'.format(info['name'], self.repo_url)
+                    )
+
+                if not isinstance(releases, list):
+                    raise ProviderException(
+                        'The "releases" value is not an array for the package "{}"'
+                        ' in the repository {}.'.format(info['name'], self.repo_url)
+                    )
+
+                staged_releases = {}
+
+                # This allows developers to specify a GH or BB location to get releases from,
+                # especially tags URLs (https://github.com/user/repo/tags or
+                # https://bitbucket.org/user/repo#tags)
+                for release in releases:
+                    download_info = {}
+
+                    # Validate libraries
+                    # the key can be used to specify dependencies, upstream via repositories
+                    key = 'libraries' if self.schema_version.major >= 4 else 'dependencies'
+                    value = release.get(key, [])
+                    if value:
+                        if not isinstance(value, list):
+                            raise InvalidPackageReleaseKeyError(self.repo_url, info['name'], key)
+                        download_info['libraries'] = value
+
+                    # Validate supported platforms
+                    key = 'platforms'
+                    value = release.get(key, default_platforms)
+                    if isinstance(value, str):
+                        value = [value]
+                    elif not isinstance(value, list):
+                        raise InvalidPackageReleaseKeyError(self.repo_url, info['name'], key)
+                    # ignore incompatible release (avoid downloading/evaluating further information)
+                    if IS_ST and not is_compatible_platform(value):
+                        continue
+                    download_info[key] = value
+
+                    # Validate supported python_versions (requires scheme 4.0.0!)
+                    key = 'python_versions'
+                    value = release.get(key)
+                    if value:
+                        # Package releases may optionally contain `python_versions` list to tell
+                        # which python version they are compatibilible with.
+                        # The main purpose is to be able to opt-in unmaintained packages to python 3.8
+                        # if they are known not to cause trouble.
+                        if isinstance(value, str):
+                            value = [value]
+                        elif not isinstance(value, list):
+                            raise InvalidPackageReleaseKeyError(self.repo_url, info['name'], key)
+                        download_info[key] = value
+
+                    if self.schema_version.major >= 3:
+                        # Validate supported ST version
+                        # missing key indicates any ST3+ build is supported
+                        key = 'sublime_text'
+                        value = release.get(key, default_sublime_text)
+                        if not isinstance(value, str):
+                            raise InvalidPackageReleaseKeyError(self.repo_url, info['name'], key)
+                        # ignore incompatible release (avoid downloading/evaluating further information)
+                        if IS_ST and not is_compatible_version(value):
+                            continue
+                        download_info[key] = value
+
+                        # Validate url
+                        # if present, it is an explicit or resolved release
+                        url = release.get('url')
+                        if url:
+                            # Validate date and version
+                            for key in copied_release_keys:
+                                if key in release:
+                                    value = release[key]
+                                    if not value or not isinstance(value, str):
+                                        raise InvalidPackageReleaseKeyError(self.repo_url, info['name'], key)
+                                    download_info[key] = value
+
+                            if 'version' not in download_info:
+                                raise ProviderException(
+                                    'Missing "version" key in release with explicit "url" of package "{}"'
+                                    ' in repository "{}".'.format(info['name'], self.repo_url)
+                                )
+
+                            download_info['url'] = update_url(resolve_url(self.repo_url, url), debug)
+                            info['releases'].append(download_info)
+                            continue
+
+                        # Resolve release template using `base` and `branch` or `tags` keys
+
+                        base = release.get('base')
+                        if not base:
+                            base = details
+                        if not base:
+                            raise ProviderException(
+                                'Missing root-level "details" key, or release-level "base" key'
+                                ' for one of the releases of package "{}"'
+                                ' in repository {}.'.format(info['name'], self.repo_url)
+                            )
+
+                        base_url = resolve_url(self.repo_url, base)
+                        downloads = None
+
+                        asset = release.get('asset')
+                        branch = release.get('branch')
+                        tags = release.get('tags')
+                        extra = None if tags is True else tags
+
+                        if asset:
+                            if branch:
+                                raise ProviderException(
+                                    'Illegal "asset" key "{}" for branch based release of library "{}"'
+                                    ' in repository "{}".'.format(base, info['name'], self.repo_url)
+                                )
+                            # group releases with assets by base_url and tag-prefix
+                            # to prepare gathering download_info with a single API call
+                            staged_releases.setdefault((base_url, extra), []).append((asset, download_info))
+                            continue
+
+                        elif tags:
+                            for client in clients:
+                                downloads = client.download_info_from_tags(base_url, extra)
+                                if downloads is not None:
+                                    break
+
+                        elif branch:
+                            for client in clients:
+                                downloads = client.download_info_from_branch(base_url, branch)
+                                if downloads is not None:
+                                    break
+                        else:
+                            raise ProviderException(
+                                'Missing "branch", "tags" or "url" key in release of package "{}"'
+                                ' in repository "{}".'.format(info['name'], self.repo_url)
+                            )
+
+                        if downloads is None:
+                            raise ProviderException(
+                                'Invalid "base" value "{}" for one of the releases of package "{}"'
+                                ' in repository "{}".'.format(base, info['name'], self.repo_url)
+                            )
+
+                        if downloads is False:
+                            raise ProviderException(
+                                'No valid semver tags found at "{}" for package "{}"'
+                                ' in repository "{}".'.format(base, info['name'], self.repo_url)
+                            )
+
+                        for download in downloads:
+                            download.update(download_info)
+                            info['releases'].append(download)
+
+                    elif self.schema_version.major == 2:
+                        # missing key indicates ST2 release; no longer supported
+                        key = 'sublime_text'
+                        value = release.get(key)
+                        if not value:
+                            continue
+                        if not isinstance(value, str):
+                            raise InvalidPackageReleaseKeyError(self.repo_url, info['name'], key)
+                        # ignore incompatible release (avoid downloading/evaluating further information)
+                        if IS_ST and not is_compatible_version(value):
+                            continue
+                        download_info[key] = value
+
+                        # Validate url
+                        # if present, it is an explicit or resolved release
+                        url = release.get('url')
+                        if url:
+                            for key in copied_release_keys:
+                                if key in release:
+                                    value = release[key]
+                                    if not value or not isinstance(value, str):
+                                        raise InvalidPackageReleaseKeyError(self.repo_url, info['name'], key)
+                                    download_info[key] = value
+
+                            if 'version' not in download_info:
+                                raise ProviderException(
+                                    'Missing "version" key in release with explicit "url" of package "{}"'
+                                    ' in repository "{}".'.format(info['name'], self.repo_url)
+                                )
+
+                            download_info['url'] = update_url(resolve_url(self.repo_url, url), debug)
+                            info['releases'].append(download_info)
+                            continue
+
+                        # Evaluate and resolve "tags" and "branch" release templates
+
+                        download_details = release.get('details')
+                        if not download_details or not isinstance(download_details, str):
+                            raise InvalidPackageReleaseKeyError(self.repo_url, info['name'], 'details')
+
+                        download_details = resolve_url(self.repo_url, release['details'])
+
+                        downloads = None
+
+                        for client in clients:
+                            downloads = client.download_info(download_details)
+                            if downloads is not None:
+                                break
+
+                        if downloads is None:
+                            raise ProviderException(
+                                'Invalid "details" value "{}" for one of the releases of package "{}"'
+                                ' in repository "{}".'.format(download_details, info['name'], self.repo_url)
+                            )
+
+                        if downloads is False:
+                            raise ProviderException(
+                                'No valid semver tags found at "{}" for package "{}"'
+                                ' in repository "{}".'.format(download_details, info['name'], self.repo_url)
+                            )
+
+                        for download in downloads:
+                            download.update(download_info)
+                            info['releases'].append(download)
+
+                # gather download_info from releases
+                for (base_url, extra), asset_templates in staged_releases.items():
+                    for client in clients:
+                        downloads = client.download_info_from_releases(base_url, asset_templates, extra)
+                        if downloads is not None:
+                            info['releases'].extend(downloads)
+                            break
+
+                # Empty releases means package is unavailable on current platform or for version of ST
+                if not info['releases']:
+                    continue
+
+                info['releases'] = version_sort(info['releases'], 'platforms', reverse=True)
+
+                for field in ('previous_names', 'labels'):
+                    if field not in info:
+                        info[field] = []
+
+                if 'readme' in info:
+                    info['readme'] = update_url(resolve_url(self.repo_url, info['readme']), debug)
+
+                for field in ('description', 'readme', 'issues', 'donate', 'buy'):
+                    if field not in info:
+                        info[field] = None
+
+                if 'homepage' not in info:
+                    info['homepage'] = details if details else self.repo_url
+
+                if 'last_modified' not in info:
+                    # Extract a date from the newest release
+                    date = '1970-01-01 00:00:00'
+                    for release in info['releases']:
+                        release_date = release.get('date')
+                        if release_date and isinstance(release_date, str) and release_date > date:
+                            date = release_date
+                    info['last_modified'] = date
+
+                output[info['name']] = info
+                yield (info['name'], info)
+
+            except (DownloaderException, ClientException, ProviderException) as e:
+                self.broken_packages[info['name']] = e
+
+        self.packages = output
+
+    def get_sources(self):
+        """
+        Return a list of current URLs that are directly referenced by the repo
+
+        :return:
+            A list of URLs and/or file paths
+        """
+
+        if not self.fetch():
+            return []
+
+        output = [self.repo_url]
+        for package in self.repo_info['packages']:
+            details = package.get('details')
+            if details:
+                output.append(details)
+        return output
+
+    def get_renamed_packages(self):
+        """:return: A dict of the packages that have been renamed"""
+
+        if not self.fetch():
+            return {}
+
+        output = {}
+        for package in self.repo_info['packages']:
+            if 'previous_names' not in package:
+                continue
+
+            previous_names = package['previous_names']
+            if not isinstance(previous_names, list):
+                previous_names = [previous_names]
+
+            for previous_name in previous_names:
+                output[previous_name] = package['name']
+
+        return output
diff --git a/tasks/lib/package_control/providers/provider_exception.py b/tasks/lib/package_control/providers/provider_exception.py
new file mode 100644
index 0000000..e327f59
--- /dev/null
+++ b/tasks/lib/package_control/providers/provider_exception.py
@@ -0,0 +1,54 @@
+class ProviderException(Exception):
+
+    """If a provider could not return information"""
+
+    def __bytes__(self):
+        return self.__str__().encode('utf-8')
+
+
+class GitProviderUserInfoException(ProviderException):
+    """
+    Exception for signalling user information download error.
+
+    The exception is used to indicate a given URL not being in expected form
+    to be used by given provider to download user info from.
+    """
+
+    def __init__(self, provider):
+        self.provider_name = provider.__class__.__name__
+        self.url = provider.repo_url
+
+    def __str__(self):
+        return '%s unable to fetch user information from "%s".' % (self.provider_name, self.url)
+
+
+class GitProviderRepoInfoException(ProviderException):
+    """
+    Exception for signalling repository information download error.
+
+    The exception is used to indicate a given URL not being in expected form
+    to be used by given provider to download repo info from.
+    """
+
+    def __init__(self, provider):
+        self.provider_name = provider.__class__.__name__
+        self.url = provider.repo_url
+
+    def __str__(self):
+        return '%s unable to fetch repo information from "%s".' % (self.provider_name, self.url)
+
+
+class GitProviderDownloadInfoException(ProviderException):
+    """
+    Exception for signalling download information download error.
+
+    The exception is used to indicate a given URL not being in expected form
+    to be used by given provider to download release information from.
+    """
+
+    def __init__(self, provider, url=None):
+        self.provider_name = provider.__class__.__name__
+        self.url = url or provider.repo_url
+
+    def __str__(self):
+        return '%s unable to fetch download information from "%s".' % (self.provider_name, self.url)
diff --git a/tasks/lib/package_control/providers/schema_version.py b/tasks/lib/package_control/providers/schema_version.py
new file mode 100644
index 0000000..3e5efbb
--- /dev/null
+++ b/tasks/lib/package_control/providers/schema_version.py
@@ -0,0 +1,33 @@
+from ..pep440 import PEP440Version
+
+
+class SchemaVersion(PEP440Version):
+    supported_versions = ('2.0', '3.0.0', '4.0.0')
+
+    def __init__(self, ver):
+        """
+        Custom version string parsing to maintain backward compatibility.
+
+        SemVer needs all of major, minor and patch parts being present in `ver`.
+
+        :param ver:
+            An integer, float or string containing a version string.
+
+        :returns:
+            List of (major, minor, patch)
+        """
+        try:
+            if isinstance(ver, int):
+                ver = float(ver)
+            if isinstance(ver, float):
+                ver = str(ver)
+        except ValueError:
+            raise ValueError('the "schema_version" is not a valid number.')
+
+        if ver not in self.supported_versions:
+            raise ValueError(
+                'the "schema_version" is not recognized. Must be one of: %s or %s.'
+                % (', '.join(self.supported_versions[:-1]), self.supported_versions[-1])
+            )
+
+        super().__init__(ver)
diff --git a/tasks/lib/package_control/readme.md b/tasks/lib/package_control/readme.md
new file mode 100644
index 0000000..23eff38
--- /dev/null
+++ b/tasks/lib/package_control/readme.md
@@ -0,0 +1,60 @@
+# Package Control
+
+The [Sublime Text](http://www.sublimetext.com) package manager. Visit
+[packagecontrol.io](https://packagecontrol.io) for
+[installation instructions](https://packagecontrol.io/installation), a list of
+[available packages](https://packagecontrol.io/browse) and detailed
+[documentation](https://packagecontrol.io/docs).
+
+## License
+
+Package Control is licensed under the MIT license.
+
+All of the source code (except for `package_control/semver.py`), is under the
+license:
+
+```
+Copyright (c) 2011-2020 Will Bond 
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
+
+`package_control/semver.py` is under the license:
+
+```
+Copyright (c) 2013 Zachary King, FichteFoll
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
diff --git a/tasks/lib/package_control/show_error.py b/tasks/lib/package_control/show_error.py
new file mode 100644
index 0000000..9ea608e
--- /dev/null
+++ b/tasks/lib/package_control/show_error.py
@@ -0,0 +1,24 @@
+# Not shared with Package Control
+
+from . import text
+
+
+def show_error(string, params=None, strip=True, indent=None):
+    """
+    Sends an error message to rollbar after running the string through
+    text.format()
+
+    :param string:
+        The error to display
+
+    :param params:
+        Params to interpolate into the string
+
+    :param strip:
+        If the last newline in the string should be removed
+
+    :param indent:
+        If all lines should be indented by a set indent after being dedented
+    """
+
+    print(text.format(string, params, strip=strip, indent=indent))
diff --git a/tasks/lib/package_control/sys_path.py b/tasks/lib/package_control/sys_path.py
new file mode 100644
index 0000000..d182aef
--- /dev/null
+++ b/tasks/lib/package_control/sys_path.py
@@ -0,0 +1,18 @@
+# Not shared with Package Control
+
+import os
+
+__cache_path = os.path.join(os.path.expanduser('~'), '.package_control')
+
+
+def set_cache_dir(cache_path):
+    global __cache_path
+    __cache_path = cache_path
+
+
+def pc_cache_dir():
+    return __cache_path
+
+
+def user_config_dir():
+    return pc_cache_dir()
diff --git a/tasks/lib/package_control/text.py b/tasks/lib/package_control/text.py
new file mode 100644
index 0000000..103c20b
--- /dev/null
+++ b/tasks/lib/package_control/text.py
@@ -0,0 +1,64 @@
+from __future__ import unicode_literals
+
+from textwrap import dedent
+import re
+
+
+def format(string, params=None, strip=True, indent=None):
+    """
+    Takes a multi-line string and does the following:
+
+     - dedents
+     - removes a single leading newline if the second character is not a newline also
+     - converts newlines with text before and after into a single line
+     - removes a single trailing newline if the second-to-laster character is not a newline also
+
+    :param string:
+        The string to format
+
+    :param params:
+        Params to interpolate into the string
+
+    :param strip:
+        If the last newline in the string should be removed
+
+    :param indent:
+        If all lines should be indented by a set indent after being dedented
+
+    :return:
+        The formatted string
+    """
+
+    output = string
+
+    # Only dedent if not a single-line string. This allows for
+    # single-line-formatted string to be printed that include intentional
+    # whitespace.
+    if output.find('\n') != -1:
+        output = dedent(output)
+
+    # If the string starts with just a newline, we want to trim it because
+    # it is a side-effect of the code formatting, but if there are two newlines
+    # then that means we intended there to be newlines at the beginning
+    if output[0] == '\n' and output[1] != '\n':
+        output = output[1:]
+
+    # Unwrap lines, taking into account bulleted lists, ordered lists and
+    # underlines consisting of = signs
+    if output.find('\n') != -1:
+        output = re.sub(r'(?<=\S)\n(?=[^ \n\t\d\*\-=])', ' ', output)
+
+    # By default we want to trim a single trailing newline from a string since
+    # that is likely from the code formatting, but that trimming is prevented
+    # if strip == False, or if there are two trailing newlines, which means we
+    # actually wanted whitespace at the end
+    if output[-1] == '\n' and strip and output[-2] != '\n':
+        output = output[0:-1]
+
+    if params is not None:
+        output = output % params
+
+    if indent is not None:
+        output = indent + output.replace('\n', '\n' + indent)
+
+    return output