├── test ├── __init__.py └── test_winkerberos.py ├── .github ├── CODEOWNERS ├── reviewers.txt ├── scripts │ └── bump-version.sh ├── dependabot.yml └── workflows │ ├── zizmor.yml │ ├── codeql.yml │ ├── build.yml │ ├── dist.yml │ └── release-python.yml ├── requirements.txt ├── .gitignore ├── MANIFEST.in ├── sbom.json ├── .evergreen ├── assign-pr-reviewer.sh ├── run-tests.sh ├── setup.sh └── config.yml ├── doc ├── winkerberos.rst ├── changelog.rst ├── index.rst ├── make.bat └── conf.py ├── pyproject.toml ├── setup.py ├── src ├── kerberos_sspi.h ├── kerberos_sspi.c └── winkerberos.c ├── .pre-commit-config.yaml ├── README.rst └── LICENSE /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @mongodb/dbx-python 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # No external dependencies 2 | -------------------------------------------------------------------------------- /.github/reviewers.txt: -------------------------------------------------------------------------------- 1 | # List of reviewers for auto-assignment of reviews. 2 | caseyclements 3 | blink1073 4 | Jibola 5 | NoahStapp 6 | -------------------------------------------------------------------------------- /.github/scripts/bump-version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | CURRENT_VERSION=$(python setup.py --version) 5 | sed -i "s/version = \"${CURRENT_VERSION}\"/version = \"$1\"/" pyproject.toml 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *#* 3 | .DS* 4 | *.pyc 5 | *.pyd 6 | build/ 7 | dist/ 8 | doc/_build/ 9 | *.egg 10 | .eggs/ 11 | winkerberos.egg-info/ 12 | .idea 13 | envfile 14 | .venv 15 | signatures 16 | secrets-export.sh 17 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | include requirements.txt 4 | exclude sbom.json 5 | recursive-include doc *.bat 6 | recursive-include doc *.py 7 | recursive-include doc *.rst 8 | recursive-include src *.h 9 | recursive-include test *.py 10 | exclude .pre-commit-config.yaml 11 | prune scripts 12 | prune .evergreen 13 | -------------------------------------------------------------------------------- /sbom.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "timestamp": "2024-05-02T17:34:42.963782+00:00" 4 | }, 5 | "components": [], 6 | "serialNumber": "urn:uuid:d14c9741-5324-4595-9b3e-affab94528f4", 7 | "version": 1, 8 | "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", 9 | "bomFormat": "CycloneDX", 10 | "specVersion": "1.5" 11 | } 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | groups: 9 | actions: 10 | patterns: 11 | - "*" 12 | # Python 13 | - package-ecosystem: "pip" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | -------------------------------------------------------------------------------- /.evergreen/assign-pr-reviewer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | export CONFIG=$PROJECT_DIRECTORY/.github/reviewers.txt 5 | export SCRIPT="$DRIVERS_TOOLS/.evergreen/github_app/assign-reviewer.sh" 6 | # shellcheck disable=SC2154 7 | bash $SCRIPT -p $CONFIG -h ${github_commit} -o "mongodb" -n "winkerberos" 8 | echo '{"results": [{ "status": "PASS", "test_file": "Build", "log_raw": "Test completed" } ]}' > ${PROJECT_DIRECTORY}/test-results.json 9 | -------------------------------------------------------------------------------- /.github/workflows/zizmor.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions Security Analysis with zizmor 🌈 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["**"] 8 | 9 | jobs: 10 | zizmor: 11 | name: zizmor latest via Cargo 12 | runs-on: ubuntu-latest 13 | permissions: 14 | security-events: write 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v6 18 | with: 19 | persist-credentials: false 20 | - name: Run zizmor 🌈 21 | uses: zizmorcore/zizmor-action@cb3d8e846e148d1111d90b03375b9c03deceda37 22 | -------------------------------------------------------------------------------- /.evergreen/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | # Disable xtrace for security reasons (just in case it was accidentally set). 3 | set +x 4 | set -eu 5 | 6 | # Fetch secrets 7 | bash ${DRIVERS_TOOLS}/.evergreen/secrets_handling/setup-secrets.sh drivers/enterprise_auth 8 | source secrets-export.sh 9 | 10 | # Set up env 11 | pushd .. 12 | git clone https://github.com/mongodb/mongo-python-driver 13 | 14 | set -x 15 | "C:/python/Python310/python.exe" -m venv .venv 16 | dos2unix -q .venv/Scripts/activate 17 | . .venv/Scripts/activate 18 | pip install "./mongo-python-driver[test]" 19 | pip install -e ./src 20 | 21 | export CLIENT_PEM="$DRIVERS_TOOLS/.evergreen/x509gen/client.pem" 22 | export CA_PEM="$DRIVERS_TOOLS/.evergreen/x509gen/ca.pem" 23 | export GSSAPI_PASS=${SASL_PASS} 24 | export GSSAPI_CANONICALIZE="true" 25 | export GSSAPI_HOST=${SASL_HOST} 26 | export GSSAPI_PORT=${SASL_PORT} 27 | export GSSAPI_PRINCIPAL=${PRINCIPAL} 28 | pushd ./mongo-python-driver 29 | pytest -W default -m auth 30 | popd 31 | popd 32 | -------------------------------------------------------------------------------- /doc/winkerberos.rst: -------------------------------------------------------------------------------- 1 | :mod:`winkerberos` 2 | ================== 3 | 4 | .. automodule:: winkerberos 5 | :synopsis: A native Kerberos SSPI client implementation. 6 | 7 | .. autofunction:: authGSSClientInit 8 | .. autofunction:: authGSSClientStep 9 | .. autofunction:: authGSSClientResponse 10 | .. autofunction:: authGSSClientResponseConf 11 | .. autofunction:: authGSSClientUserName 12 | .. autofunction:: authGSSClientUnwrap 13 | .. autofunction:: authGSSClientWrap 14 | .. autofunction:: authGSSClientClean 15 | .. autofunction:: channelBindings 16 | .. autofunction:: authGSSServerInit 17 | .. autofunction:: authGSSServerStep 18 | .. autofunction:: authGSSServerResponse 19 | .. autofunction:: authGSSServerUserName 20 | .. autofunction:: authGSSServerClean 21 | .. autoexception:: KrbError 22 | .. autoexception:: GSSError 23 | .. data:: AUTH_GSS_COMPLETE 24 | .. data:: AUTH_GSS_CONTINUE 25 | .. data:: GSS_C_DELEG_FLAG 26 | .. data:: GSS_C_MUTUAL_FLAG 27 | .. data:: GSS_C_REPLAY_FLAG 28 | .. data:: GSS_C_SEQUENCE_FLAG 29 | .. data:: GSS_C_CONF_FLAG 30 | .. data:: GSS_C_INTEG_FLAG 31 | .. data:: GSS_C_AF_UNSPEC 32 | .. data:: GSS_MECH_OID_KRB5 33 | .. data:: GSS_MECH_OID_SPNEGO 34 | .. data:: __version__ 35 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "winkerberos" 7 | version = "0.14.0.dev0" 8 | description = "High level interface to SSPI for Kerberos client auth" 9 | readme = "README.rst" 10 | license = { file = "LICENSE" } 11 | requires-python = ">=3.10" 12 | authors = [ 13 | { name = "Bernie Hackett", email = "bernie@mongodb.com" }, 14 | ] 15 | keywords = [ 16 | "GSSAPI", 17 | "Kerberos", 18 | "SSPI", 19 | ] 20 | classifiers = [ 21 | "Development Status :: 4 - Beta", 22 | "Intended Audience :: Developers", 23 | "License :: OSI Approved :: Apache Software License", 24 | "Operating System :: Microsoft :: Windows", 25 | "Programming Language :: Python :: Implementation :: CPython", 26 | "Programming Language :: Python :: 3", 27 | "Programming Language :: Python :: 3.10", 28 | "Programming Language :: Python :: 3.11", 29 | "Programming Language :: Python :: 3.12", 30 | "Programming Language :: Python :: 3.13", 31 | "Programming Language :: Python :: 3.14", 32 | "Topic :: System :: Systems Administration :: Authentication/Directory", 33 | ] 34 | dependencies = [] 35 | 36 | [project.urls] 37 | Homepage = "https://github.com/mongodb-labs/winkerberos" 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 MongoDB, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import sys 16 | from setuptools import setup, Extension 17 | 18 | 19 | if "MSC" in sys.version: 20 | # msvc: 21 | extra_link_args = [ 22 | "crypt32.lib", 23 | "secur32.lib", 24 | "Shlwapi.lib", 25 | "/NXCOMPAT", 26 | "/DYNAMICBASE", 27 | ] 28 | else: 29 | # mingw: 30 | extra_link_args = ["-lcrypt32", "-lsecur32", "-lshlwapi"] 31 | 32 | 33 | def parse_reqs_file(fname): 34 | with open(fname) as fid: # noqa:PTH123 35 | lines = [li.strip() for li in fid.readlines()] 36 | return [li for li in lines if li and not li.startswith("#")] 37 | 38 | 39 | setup( 40 | install_requires=parse_reqs_file("requirements.txt"), 41 | ext_modules=[ 42 | Extension( 43 | "winkerberos", 44 | extra_link_args=extra_link_args, 45 | sources=["src/winkerberos.c", "src/kerberos_sspi.c"], 46 | ) 47 | ], 48 | ) 49 | -------------------------------------------------------------------------------- /.evergreen/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | set -eux 4 | 5 | # Get the current unique version of this checkout 6 | # shellcheck disable=SC2154 7 | if [ "${is_patch}" = "true" ]; then 8 | # shellcheck disable=SC2154 9 | CURRENT_VERSION=$(git describe)-patch-${version_id} 10 | else 11 | CURRENT_VERSION=latest 12 | fi 13 | 14 | # Python has cygwin path problems on Windows. 15 | DRIVERS_TOOLS="$(dirname "$(pwd)")/drivers-tools" 16 | PROJECT_DIRECTORY="$(pwd)" 17 | 18 | if [ "Windows_NT" = "${OS:-}" ]; then 19 | DRIVERS_TOOLS=$(cygpath -m $DRIVERS_TOOLS) 20 | PROJECT_DIRECTORY=$(cygpath -m $PROJECT_DIRECTORY) 21 | fi 22 | export PROJECT_DIRECTORY 23 | export DRIVERS_TOOLS 24 | 25 | export MONGO_ORCHESTRATION_HOME="$DRIVERS_TOOLS/.evergreen/orchestration" 26 | export MONGODB_BINARIES="$DRIVERS_TOOLS/mongodb/bin" 27 | # shellcheck disable=SC2154 28 | export UPLOAD_BUCKET="${project}" 29 | 30 | cat < expansion.yml 31 | CURRENT_VERSION: "$CURRENT_VERSION" 32 | DRIVERS_TOOLS: "$DRIVERS_TOOLS" 33 | MONGO_ORCHESTRATION_HOME: "$MONGO_ORCHESTRATION_HOME" 34 | MONGODB_BINARIES: "$MONGODB_BINARIES" 35 | UPLOAD_BUCKET: "$UPLOAD_BUCKET" 36 | PROJECT_DIRECTORY: "$PROJECT_DIRECTORY" 37 | EOT 38 | 39 | # Set up drivers-tools with a .env file. 40 | git clone https://github.com/mongodb-labs/drivers-evergreen-tools.git ${DRIVERS_TOOLS} 41 | cat < ${DRIVERS_TOOLS}/.env 42 | CURRENT_VERSION="$CURRENT_VERSION" 43 | DRIVERS_TOOLS="$DRIVERS_TOOLS" 44 | MONGO_ORCHESTRATION_HOME="$MONGO_ORCHESTRATION_HOME" 45 | MONGODB_BINARIES="$MONGODB_BINARIES" 46 | UPLOAD_BUCKET="$UPLOAD_BUCKET" 47 | PROJECT_DIRECTORY="$PROJECT_DIRECTORY" 48 | EOT 49 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master"] 6 | tags: ['*'] 7 | pull_request: 8 | workflow_call: 9 | inputs: 10 | ref: 11 | required: true 12 | type: string 13 | schedule: 14 | - cron: '17 10 * * 2' 15 | 16 | jobs: 17 | analyze: 18 | name: Analyze (${{ matrix.language }}) 19 | runs-on: "windows-latest" 20 | timeout-minutes: 360 21 | permissions: 22 | # required for all workflows 23 | security-events: write 24 | 25 | # required to fetch internal or private CodeQL packs 26 | packages: read 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | include: 32 | # Note: we do NOT include python because this is a pure C extension.exclude: 33 | # See PYTHON-5633. 34 | - language: c-cpp 35 | build-mode: manual 36 | - language: actions 37 | build-mode: none 38 | steps: 39 | - name: Checkout repository 40 | uses: actions/checkout@v6 41 | with: 42 | ref: ${{ inputs.ref }} 43 | persist-credentials: false 44 | - uses: actions/setup-python@v6 45 | with: 46 | # Lowest supported Python 47 | python-version: '3.10' 48 | 49 | # Initializes the CodeQL tools for scanning. 50 | - name: Initialize CodeQL 51 | uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4 52 | with: 53 | languages: ${{ matrix.language }} 54 | build-mode: ${{ matrix.build-mode }} 55 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 56 | queries: security-extended 57 | config: | 58 | paths-ignore: 59 | - 'doc/**' 60 | - 'scripts/**' 61 | - 'test/**' 62 | 63 | - if: matrix.build-mode == 'manual' 64 | run: | 65 | pip install -e . 66 | 67 | - name: Perform CodeQL Analysis 68 | uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4 69 | with: 70 | category: "/language:${{matrix.language}}" 71 | -------------------------------------------------------------------------------- /.evergreen/config.yml: -------------------------------------------------------------------------------- 1 | exec_timeout_secs: 3600 2 | 3 | # Mark a failure as a system/bootstrap failure (purple box) rather then a task 4 | # failure by default. 5 | # Actual testing tasks are marked with `type: test` 6 | command_type: system 7 | 8 | functions: 9 | "setup": 10 | - command: git.get_project 11 | params: 12 | directory: src 13 | - command: subprocess.exec 14 | params: 15 | binary: bash 16 | working_dir: "src" 17 | add_expansions_to_env: true 18 | args: 19 | - ./.evergreen/setup.sh 20 | - command: expansions.update 21 | params: 22 | file: src/expansion.yml 23 | 24 | "bootstrap mongo-orchestration": 25 | - command: subprocess.exec 26 | params: 27 | binary: bash 28 | env: 29 | MONGODB_VERSION: latest 30 | TOPOLOGY: server 31 | args: 32 | - ${DRIVERS_TOOLS}/.evergreen/run-orchestration.sh 33 | 34 | "run unit tests": 35 | - command: ec2.assume_role 36 | params: 37 | role_arn: ${drivers_test_secrets_role} 38 | - command: subprocess.exec 39 | type: test 40 | params: 41 | binary: bash 42 | working_dir: "src" 43 | include_expansions_in_env: [DRIVERS_TOOLS, AWS_SECRET_ACCESS_KEY, AWS_ACCESS_KEY_ID, AWS_SESSION_TOKEN] 44 | args: 45 | - ./.evergreen/run-tests.sh 46 | 47 | "assign pr reviewer": 48 | - command: subprocess.exec 49 | type: test 50 | params: 51 | binary: bash 52 | add_expansions_to_env: true 53 | working_dir: "src" 54 | args: 55 | - ./.evergreen/assign-pr-reviewer.sh 56 | 57 | "teardown": 58 | - command: subprocess.exec 59 | params: 60 | binary: bash 61 | args: 62 | - ${DRIVERS_TOOLS}/.evergreen/teardown.sh 63 | 64 | pre: 65 | - func: setup 66 | - func: bootstrap mongo-orchestration 67 | 68 | post: 69 | - func: teardown 70 | - command: attach.xunit_results 71 | params: 72 | file: "mongo-python-driver/xunit-results/TEST-*.xml" 73 | 74 | tasks: 75 | - name: run-tests 76 | commands: 77 | - func: "run unit tests" 78 | 79 | - name: assign-pr-reviewer 80 | tags: ["pr"] 81 | allowed_requesters: ["patch", "github_pr"] 82 | commands: 83 | - func: "assign pr reviewer" 84 | 85 | buildvariants: 86 | - name: tests 87 | display_name: Run Tests 88 | run_on: windows-64-vsMulti-small 89 | tasks: 90 | - name: run-tests 91 | 92 | - name: rhel8-pr-assign-reviewer 93 | display_name: Assign PR Reviewer 94 | run_on: rhel87-small 95 | tasks: 96 | - name: "assign-pr-reviewer" 97 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | 7 | concurrency: 8 | group: build-${{ github.ref }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | static: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v6 16 | with: 17 | persist-credentials: false 18 | # Lowest supported Python 19 | python-version: '3.10' 20 | - uses: actions/setup-python@v6 21 | - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 22 | with: 23 | extra_args: --all-files --hook-stage manual 24 | - run: | 25 | sudo apt-get install -y cppcheck 26 | - run: | 27 | cppcheck src/*.* 28 | 29 | docs: 30 | runs-on: windows-latest 31 | steps: 32 | - uses: actions/checkout@v6 33 | with: 34 | persist-credentials: false 35 | - uses: actions/setup-python@v6 36 | with: 37 | # Lowest supported Python 38 | python-version: '3.10' 39 | - shell: pwsh 40 | run: | 41 | pip install sphinx 42 | pip install -e . 43 | python -m sphinx -b html -W doc doc/_build 44 | 45 | windows-msys2: 46 | runs-on: windows-latest 47 | strategy: 48 | fail-fast: false 49 | matrix: 50 | msystem: [MINGW64, MINGW32, UCRT64, CLANG64] 51 | name: Build (${{ matrix.msystem }}) 52 | defaults: 53 | run: 54 | shell: msys2 {0} 55 | steps: 56 | - uses: actions/checkout@v6 57 | with: 58 | persist-credentials: false 59 | - uses: msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # v2 60 | with: 61 | msystem: ${{ matrix.msystem }} 62 | update: true 63 | pacboy: cc:p python:p python-setuptools:p python-sphinx:p 64 | - name: Build 65 | run: CC=cc python setup.py build 66 | - name: Check 67 | run: | 68 | CC=cc python setup.py build_ext --inplace 69 | cd src 70 | python -c "import winkerberos;print(winkerberos.__version__)" 71 | windows-msvc: 72 | runs-on: windows-latest 73 | name: Build MSVC 74 | strategy: 75 | matrix: 76 | python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] 77 | fail-fast: false 78 | steps: 79 | - uses: actions/checkout@v6 80 | with: 81 | persist-credentials: false 82 | - name: Setup Python 83 | uses: actions/setup-python@v6 84 | with: 85 | python-version: ${{ matrix.python-version }} 86 | allow-prereleases: true 87 | - name: Check 88 | run: | 89 | pip install -e . 90 | python -c "import winkerberos;print(winkerberos.__version__)" 91 | -------------------------------------------------------------------------------- /.github/workflows/dist.yml: -------------------------------------------------------------------------------- 1 | name: Dist 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | workflow_dispatch: 7 | workflow_call: 8 | inputs: 9 | ref: 10 | required: true 11 | type: string 12 | pull_request: 13 | 14 | concurrency: 15 | group: dist-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | defaults: 19 | run: 20 | shell: bash -eux {0} 21 | 22 | jobs: 23 | build_wheels: 24 | name: "Build Wheels ${{ matrix.buildplat }}" 25 | runs-on: windows-2022 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | buildplat: ["win_amd64", "win32"] 30 | steps: 31 | - uses: actions/checkout@v6 32 | with: 33 | ref: ${{ inputs.ref }} 34 | persist-credentials: false 35 | - name: Build wheels 36 | uses: pypa/cibuildwheel@63fd63b352a9a8bdcc24791c9dbee952ee9a8abc # v3.3.0 37 | env: 38 | CIBW_BUILD: "cp3*-${{ matrix.buildplat }}" 39 | CIBW_PRERELEASE_PYTHONS: "True" 40 | CIBW_TEST_COMMAND: "python -c \"import winkerberos;print(winkerberos.__version__)\"" 41 | 42 | - name: Assert all versions in wheelhouse 43 | run: | 44 | ls wheelhouse/*cp310*.whl 45 | ls wheelhouse/*cp311*.whl 46 | ls wheelhouse/*cp312*.whl 47 | ls wheelhouse/*cp313*.whl 48 | ls wheelhouse/*cp314*.whl 49 | 50 | - uses: actions/upload-artifact@v6 51 | with: 52 | name: wheel-${{ matrix.buildplat }} 53 | path: ./wheelhouse/*.whl 54 | 55 | make_sdist: 56 | name: Make SDist 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: actions/checkout@v6 60 | with: 61 | ref: ${{ inputs.ref }} 62 | persist-credentials: false 63 | 64 | - uses: actions/setup-python@v6 65 | with: 66 | # Build sdist on lowest supported Python 67 | python-version: '3.10' 68 | 69 | - name: Build SDist 70 | run: | 71 | pip install check-manifest build 72 | check-manifest -v 73 | python -m build --sdist . 74 | 75 | - uses: actions/upload-artifact@v6 76 | with: 77 | name: "sdist" 78 | path: dist/*.tar.gz 79 | 80 | collect_dist: 81 | runs-on: ubuntu-latest 82 | needs: [build_wheels, make_sdist] 83 | name: Download Wheels 84 | steps: 85 | - name: Download all workflow run artifacts 86 | uses: actions/download-artifact@v7 87 | - name: Flatten directory 88 | working-directory: . 89 | run: | 90 | find . -mindepth 2 -type f -exec mv {} . \; 91 | find . -type d -empty -delete 92 | - uses: actions/upload-artifact@v6 93 | with: 94 | name: all-dist-${{ github.run_id }} 95 | path: "./*" 96 | -------------------------------------------------------------------------------- /src/kerberos_sspi.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 MongoDB, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #define SECURITY_WIN32 1 /* Required for SSPI */ 18 | #define PY_SSIZE_T_CLEAN 19 | 20 | #include "Python.h" 21 | #include 22 | #include 23 | 24 | #define AUTH_GSS_ERROR -1 25 | #define AUTH_GSS_COMPLETE 1 26 | #define AUTH_GSS_CONTINUE 0 27 | 28 | #define GSS_MECH_OID_KRB5 L"Kerberos" 29 | #define GSS_MECH_OID_SPNEGO L"Negotiate" 30 | 31 | typedef struct { 32 | CredHandle cred; 33 | CtxtHandle ctx; 34 | WCHAR* spn; 35 | SEC_CHAR* response; 36 | SEC_CHAR* username; 37 | ULONG flags; 38 | UCHAR haveCred; 39 | UCHAR haveCtx; 40 | ULONG qop; 41 | } sspi_client_state; 42 | 43 | typedef struct { 44 | CredHandle cred; 45 | CtxtHandle ctx; 46 | WCHAR* spn; 47 | SEC_CHAR* response; 48 | SEC_CHAR* username; 49 | ULONG flags; 50 | UCHAR haveCred; 51 | UCHAR haveCtx; 52 | ULONG qop; 53 | } sspi_server_state; 54 | 55 | VOID set_gsserror(DWORD errCode, const SEC_CHAR* msg); 56 | VOID destroy_sspi_client_state(sspi_client_state* state); 57 | VOID destroy_sspi_server_state(sspi_server_state* state); 58 | INT auth_sspi_client_init(WCHAR* service, 59 | ULONG flags, 60 | WCHAR* user, 61 | ULONG ulen, 62 | WCHAR* domain, 63 | ULONG dlen, 64 | WCHAR* password, 65 | ULONG plen, 66 | WCHAR* mechoid, 67 | sspi_client_state* state); 68 | INT auth_sspi_server_init(WCHAR* service, sspi_server_state* state); 69 | INT auth_sspi_client_step(sspi_client_state* state, 70 | SEC_CHAR* challenge, 71 | SecPkgContext_Bindings* sec_pkg_context_bindings); 72 | INT auth_sspi_server_step(sspi_server_state* state, SEC_CHAR* challenge); 73 | INT auth_sspi_client_unwrap(sspi_client_state* state, SEC_CHAR* challenge); 74 | INT auth_sspi_client_wrap(sspi_client_state* state, 75 | SEC_CHAR* data, 76 | SEC_CHAR* user, 77 | ULONG ulen, 78 | INT protect); 79 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.4.0 5 | hooks: 6 | - id: check-added-large-files 7 | - id: check-case-conflict 8 | - id: check-toml 9 | - id: check-json 10 | - id: check-yaml 11 | exclude: template.yaml 12 | - id: debug-statements 13 | - id: end-of-file-fixer 14 | exclude: WHEEL 15 | exclude_types: [json] 16 | - id: forbid-new-submodules 17 | - id: trailing-whitespace 18 | exclude: .patch 19 | exclude_types: [json] 20 | 21 | - repo: https://github.com/astral-sh/ruff-pre-commit 22 | # Ruff version. 23 | rev: v0.1.3 24 | hooks: 25 | - id: ruff 26 | args: ["--fix", "--show-fixes"] 27 | - id: ruff-format 28 | 29 | - repo: https://github.com/adamchainz/blacken-docs 30 | rev: "1.14.0" 31 | hooks: 32 | - id: blacken-docs 33 | additional_dependencies: 34 | - black==22.3.0 35 | 36 | - repo: https://github.com/pre-commit/pygrep-hooks 37 | rev: "v1.10.0" 38 | hooks: 39 | - id: rst-backticks 40 | - id: rst-directive-colons 41 | - id: rst-inline-touching-normal 42 | 43 | # We use the Python version instead of the original version which seems to require Docker 44 | # https://github.com/koalaman/shellcheck-precommit 45 | - repo: https://github.com/shellcheck-py/shellcheck-py 46 | rev: v0.9.0.6 47 | hooks: 48 | - id: shellcheck 49 | name: shellcheck-warning 50 | args: ["--severity=warning"] 51 | stages: [manual] 52 | 53 | - repo: https://github.com/shellcheck-py/shellcheck-py 54 | rev: v0.9.0.6 55 | hooks: 56 | - id: shellcheck 57 | name: shellcheck 58 | args: ["--severity=error"] 59 | 60 | - repo: https://github.com/PyCQA/doc8 61 | rev: v1.1.1 62 | hooks: 63 | - id: doc8 64 | args: ["--ignore=D001"] # ignore line length 65 | stages: [manual] 66 | 67 | - repo: https://github.com/sirosen/check-jsonschema 68 | rev: 0.29.0 69 | hooks: 70 | - id: check-github-workflows 71 | - id: check-github-actions 72 | - id: check-dependabot 73 | 74 | - repo: https://github.com/ariebovenberg/slotscheck 75 | rev: v0.17.0 76 | hooks: 77 | - id: slotscheck 78 | files: \.py$ 79 | exclude: "^(test|doc)/" 80 | stages: [manual] 81 | args: ["--no-strict-imports"] 82 | 83 | - repo: https://github.com/codespell-project/codespell 84 | rev: "v2.2.6" 85 | hooks: 86 | - id: codespell 87 | # Examples of errors or updates to justify the exceptions: 88 | # - test/test_on_demand_csfle.py:44: FLE ==> FILE 89 | # - test/test_bson.py:1043: fo ==> of, for, to, do, go 90 | # - test/bson_corpus/decimal128-4.json:98: Infinit ==> Infinite 91 | # - test/test_bson.py:267: isnt ==> isn't 92 | # - test/versioned-api/crud-api-version-1-strict.json:514: nin ==> inn, min, bin, nine 93 | # - test/test_client.py:188: te ==> the, be, we, to 94 | args: ["-L", "fle,fo,infinit,isnt,nin,te"] 95 | -------------------------------------------------------------------------------- /doc/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | Changes in Version 0.13.0 5 | ------------------------- 6 | 7 | - Add support for Python 3.14. 8 | - Drop support for Python 3.9. 9 | 10 | 11 | Changes in Version 0.12.2 12 | ------------------------- 13 | 14 | - Add Python 3.13 wheels. 15 | 16 | 17 | Changes in Version 0.12.0 18 | ------------------------- 19 | 20 | - Add support for Python 3.13. 21 | 22 | 23 | Changes in Version 0.11.0 24 | ------------------------- 25 | 26 | - Drop support for Python version 3.7. 27 | - Add Secure Software Development Life Cycle automation to release process. 28 | GitHub Releases now include a Software Bill of Materials, and signature 29 | files corresponding to the distribution files released on PyPI. 30 | 31 | Changes in Version 0.10.0 32 | ------------------------- 33 | 34 | - Add support for Python 3.12. Drop support for Python versions 2.7, 3.5, and 3.6. 35 | 36 | 37 | Changes in Version 0.9.1 38 | ------------------------ 39 | 40 | - Add support for Python 3.11. 41 | 42 | 43 | Changes in Version 0.9.0 44 | ------------------------ 45 | 46 | - Fix build in mingw-w64: ``MINGW_CHOST`` is specific to msys2 build and 47 | packaging environment. Instead, check ``sys.version`` which is available in 48 | any python env. Also remove ``SecPkgContext_Bindings`` definition which has 49 | been added in ``mingw-w64`` headers. 50 | - Allow ``channel_bindings=None`` for ``authGSSClientStep``. 51 | - WinKerberos now requires Python 2.7 or 3.5+. 52 | 53 | Changes in Version 0.8.0 54 | ------------------------ 55 | 56 | - WinKerberos now builds under MSYS2 using mingw-w64. Note 57 | that you can't use this support to build with python.org 58 | provided Pythons and mingw-w64. See ``_ 59 | for a related discussion. Thanks go to Antoine Martin for the patch. 60 | - Experimental server side API. Thanks go to Kacper Bostrom for the patch. 61 | 62 | *Backward Breaking Changes* 63 | 64 | - ``authGSSClientUsername`` has been renamed 65 | :func:`winkerberos.authGSSClientUserName` to match ccs-pykerberos. 66 | - WinKerberos no longer supports Python 2.6 or Python 3.3. 67 | 68 | Changes in Version 0.7.0 69 | ------------------------ 70 | 71 | - Added optional support for passing in Channel Binding Tokens (RFC 5929) into 72 | :func:`winkerberos.authGSSClientStep`. The binding token structure can be 73 | built using :func:`winkerberos.channelBindings` (see the example 74 | for more details). Thanks go to Jordan Borean for the patch. 75 | 76 | Changes in Version 0.6.0 77 | ------------------------ 78 | 79 | - Added the ``mech_oid`` parameter to :func:`~winkerberos.authGSSClientInit`. 80 | Thanks go to Alexey Veklov for the patch. 81 | 82 | Changes in Version 0.5.0 83 | ------------------------ 84 | 85 | - Added :func:`~winkerberos.authGSSClientResponseConf` and the ``protect`` 86 | parameter to :func:`~winkerberos.authGSSClientWrap`. 87 | - Fixed support for the ``principal`` parameter of 88 | :func:`~winkerberos.authGSSClientInit`, which had no effect in previous 89 | versions. 90 | - Deprecated the :func:`~winkerberos.authGSSClientInit` parameters ``user``, 91 | ``domain``, and ``password``. 92 | - Various improvements to Sphinx documentation builds. 93 | 94 | Changes in Version 0.4.0 95 | ------------------------ 96 | 97 | - Added :exc:`~winkerberos.GSSError`, inheriting from 98 | :exc:`~winkerberos.KrbError`, for compatibility with pykerberos. WinKerberos 99 | now raises GSSError instead of KrbError. This change is backward compatible 100 | for all existing applications. 101 | 102 | Changes in Version 0.3.0 103 | ------------------------ 104 | 105 | - Switched to InitializeSecurityContextW to better support unicode 106 | service principal names. 107 | 108 | Changes in Version 0.2.0 109 | ------------------------ 110 | 111 | - The ``password`` parameter of :func:`~winkerberos.authGSSClientInit` can be a 112 | :class:`bytearray` or any other 8-bit string type that implements the buffer 113 | interface. 114 | - Fixed an issue where :func:`~winkerberos.authGSSClientUsername` could raise 115 | :exc:`UnicodeDecodeError`. 116 | 117 | Changes in Version 0.1.0 118 | ------------------------ 119 | 120 | This was the initial release of WinKerberos. 121 | -------------------------------------------------------------------------------- /.github/workflows/release-python.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | following_version: 7 | description: "The post (dev) version to set" 8 | required: false 9 | dry_run: 10 | description: "Dry Run?" 11 | default: false 12 | type: boolean 13 | schedule: 14 | - cron: '30 5 * * *' 15 | 16 | env: 17 | # Changes per repo 18 | PRODUCT_NAME: WinKerberos 19 | # Changes per branch 20 | EVERGREEN_PROJECT: winkerberos 21 | # Constant 22 | # inputs will be empty on a scheduled run. so, we only set dry_run 23 | # to 'false' when the input is set to 'false'. 24 | DRY_RUN: ${{ ! contains(inputs.dry_run, 'false') }} 25 | FOLLOWING_VERSION: ${{ inputs.following_version || '' }} 26 | VERSION: ${{ github.event_name == 'schedule' && '10.10.10.10' || '' }} 27 | 28 | defaults: 29 | run: 30 | shell: bash -eux {0} 31 | 32 | jobs: 33 | pre-publish: 34 | environment: release 35 | runs-on: ubuntu-latest 36 | if: github.repository_owner == 'mongodb' || github.event_name == 'workflow_dispatch' 37 | permissions: 38 | id-token: write 39 | contents: write 40 | outputs: 41 | version: ${{ steps.pre-publish.outputs.version }} 42 | steps: 43 | - uses: mongodb-labs/drivers-github-tools/secure-checkout@v3 44 | with: 45 | app_id: ${{ vars.APP_ID }} 46 | private_key: ${{ secrets.APP_PRIVATE_KEY }} 47 | - uses: mongodb-labs/drivers-github-tools/setup@v3 48 | with: 49 | aws_role_arn: ${{ secrets.AWS_ROLE_ARN }} 50 | aws_region_name: ${{ vars.AWS_REGION_NAME }} 51 | aws_secret_id: ${{ secrets.AWS_SECRET_ID }} 52 | - uses: mongodb-labs/drivers-github-tools/python/pre-publish@v3 53 | id: pre-publish 54 | with: 55 | version: ${{ env.VERSION }} 56 | version_bump_script: ./.github/scripts/bump-version.sh 57 | dry_run: ${{ env.DRY_RUN }} 58 | 59 | build-dist: 60 | needs: [pre-publish] 61 | uses: ./.github/workflows/dist.yml 62 | with: 63 | ref: ${{ needs.pre-publish.outputs.version }} 64 | 65 | static-scan: 66 | needs: [pre-publish] 67 | uses: ./.github/workflows/codeql.yml 68 | with: 69 | ref: ${{ needs.pre-publish.outputs.version }} 70 | 71 | publish: 72 | needs: [build-dist, static-scan] 73 | name: Upload release to PyPI 74 | runs-on: ubuntu-latest 75 | environment: release 76 | permissions: 77 | id-token: write 78 | steps: 79 | - name: Download all the dists 80 | uses: actions/download-artifact@v7 81 | with: 82 | name: all-dist-${{ github.run_id }} 83 | path: dist/ 84 | - name: Publish package distributions to TestPyPI 85 | uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1 86 | with: 87 | repository-url: https://test.pypi.org/legacy/ 88 | skip-existing: true 89 | attestations: ${{ env.DRY_RUN }} 90 | - name: Publish package distributions to PyPI 91 | if: startsWith(env.DRY_RUN, 'false') 92 | uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1 93 | 94 | post-publish: 95 | needs: [publish] 96 | runs-on: ubuntu-latest 97 | environment: release 98 | permissions: 99 | id-token: write 100 | contents: write 101 | attestations: write 102 | security-events: write 103 | steps: 104 | - uses: mongodb-labs/drivers-github-tools/secure-checkout@v3 105 | with: 106 | app_id: ${{ vars.APP_ID }} 107 | private_key: ${{ secrets.APP_PRIVATE_KEY }} 108 | - uses: mongodb-labs/drivers-github-tools/setup@v3 109 | with: 110 | aws_role_arn: ${{ secrets.AWS_ROLE_ARN }} 111 | aws_region_name: ${{ vars.AWS_REGION_NAME }} 112 | aws_secret_id: ${{ secrets.AWS_SECRET_ID }} 113 | - uses: mongodb-labs/drivers-github-tools/python/post-publish@v3 114 | with: 115 | version: ${{ env.VERSION }} 116 | version_bump_script: ./.github/scripts/bump-version.sh 117 | following_version: ${{ env.FOLLOWING_VERSION }} 118 | product_name: ${{ env.PRODUCT_NAME }} 119 | evergreen_project: ${{ env.EVERGREEN_PROJECT }} 120 | token: ${{ github.token }} 121 | dry_run: ${{ env.DRY_RUN }} 122 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | WinKerberos 3 | =========== 4 | :Info: See `github `_ for the latest source. 5 | :Author: Bernie Hackett 6 | 7 | About 8 | ===== 9 | 10 | A native Kerberos client implementation for Python on Windows. This module 11 | mimics the API of `pykerberos `_ to 12 | implement Kerberos authentication with Microsoft's Security Support Provider 13 | Interface (SSPI). It supports Python 3.10+. 14 | 15 | Installation 16 | ============ 17 | 18 | WinKerberos is in the `Python Package Index (pypi) 19 | `_. Use `pip 20 | `_ to install it:: 21 | 22 | python -m pip install winkerberos 23 | 24 | WinKerberos requires Windows 7 / Windows Server 2008 R2 or newer. 25 | 26 | Building and installing from source 27 | =================================== 28 | 29 | You must have the correct version of VC++ installed for your version of 30 | Python: 31 | 32 | - Python 3.10+ - Visual Studio 2015+ (Any version) 33 | 34 | Once you have the required compiler installed, run the following command from 35 | the root directory of the WinKerberos source:: 36 | 37 | pip install . 38 | 39 | Building HTML documentation 40 | =========================== 41 | 42 | First install `Sphinx `_:: 43 | 44 | python -m pip install Sphinx 45 | 46 | Then run the following command from the root directory of the WinKerberos 47 | source:: 48 | 49 | pip install -e . 50 | python -m sphinx -b html doc doc/_build 51 | 52 | 53 | Examples 54 | ======== 55 | 56 | This is a simplified example of a complete authentication session 57 | following RFC-4752, section 3.1: 58 | 59 | .. code-block:: python 60 | 61 | import winkerberos as kerberos 62 | 63 | 64 | def send_response_and_receive_challenge(response): 65 | # Your server communication code here... 66 | pass 67 | 68 | 69 | def authenticate_kerberos(service, user, channel_bindings=None): 70 | # Initialize the context object with a service principal. 71 | status, ctx = kerberos.authGSSClientInit(service) 72 | 73 | # GSSAPI is a "client goes first" SASL mechanism. Send the 74 | # first "response" to the server and receive its first 75 | # challenge. 76 | if channel_bindings is not None: 77 | status = kerberos.authGSSClientStep(ctx, "", channel_bindings=channel_bindings) 78 | else: 79 | status = kerberos.authGSSClientStep(ctx, "") 80 | response = kerberos.authGSSClientResponse(ctx) 81 | challenge = send_response_and_receive_challenge(response) 82 | 83 | # Keep processing challenges and sending responses until 84 | # authGSSClientStep reports AUTH_GSS_COMPLETE. 85 | while status == kerberos.AUTH_GSS_CONTINUE: 86 | if channel_bindings is not None: 87 | status = kerberos.authGSSClientStep( 88 | ctx, challenge, channel_bindings=channel_bindings 89 | ) 90 | else: 91 | status = kerberos.authGSSClientStep(ctx, challenge) 92 | 93 | response = kerberos.authGSSClientResponse(ctx) or "" 94 | challenge = send_response_and_receive_challenge(response) 95 | 96 | # Decrypt the server's last challenge 97 | kerberos.authGSSClientUnwrap(ctx, challenge) 98 | data = kerberos.authGSSClientResponse(ctx) 99 | # Encrypt a response including the user principal to authorize. 100 | kerberos.authGSSClientWrap(ctx, data, user) 101 | response = kerberos.authGSSClientResponse(ctx) 102 | 103 | # Complete authentication. 104 | send_response_and_receive_challenge(response) 105 | 106 | Channel bindings can be generated with help from the cryptography_ module. See 107 | ``_ for the rules regarding 108 | hash algorithm choice: 109 | 110 | .. code-block:: python 111 | 112 | from cryptography import x509 113 | from cryptography.hazmat.backends import default_backend 114 | from cryptography.hazmat.primitives import hashes 115 | 116 | 117 | def channel_bindings(ssl_socket): 118 | server_certificate = ssl_socket.getpeercert(True) 119 | cert = x509.load_der_x509_certificate(server_certificate, default_backend()) 120 | hash_algorithm = cert.signature_hash_algorithm 121 | if hash_algorithm.name in ("md5", "sha1"): 122 | digest = hashes.Hash(hashes.SHA256(), default_backend()) 123 | else: 124 | digest = hashes.Hash(hash_algorithm, default_backend()) 125 | digest.update(server_certificate) 126 | application_data = b"tls-server-end-point:" + digest.finalize() 127 | return kerberos.channelBindings(application_data=application_data) 128 | 129 | 130 | .. _cryptography: https://pypi.python.org/pypi/cryptography 131 | 132 | Viewing API Documentation without Sphinx 133 | ======================================== 134 | 135 | Use the help function in the python interactive shell: 136 | 137 | .. code-block:: pycon 138 | 139 | >>> import winkerberos 140 | >>> help(winkerberos) 141 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | WinKerberos |release| 2 | ===================== 3 | 4 | About 5 | ----- 6 | **WinKerberos** is a native Kerberos client implementation for Python on 7 | Windows. It mimics the client API of `pykerberos 8 | `_ to implement Kerberos 9 | authentication with Microsoft's Security Support Provider Interface (SSPI). The 10 | source is `available on github `_. 11 | 12 | Documentation 13 | ------------- 14 | 15 | :doc:`winkerberos` 16 | Full API documentation for the winkerberos module. 17 | 18 | Installation 19 | ------------ 20 | 21 | WinKerberos is in the `Python Package Index (pypi) 22 | `_. Use `pip 23 | `_ to install it:: 24 | 25 | python -m pip install winkerberos 26 | 27 | WinKerberos requires Windows 7 / Windows Server 2008 R2 or newer. 28 | 29 | Building and installing from source 30 | ----------------------------------- 31 | 32 | You must have the correct version of VC++ installed for your version of 33 | Python: 34 | 35 | - Python 3.10+ - Visual Studio 2015+ (Any version) 36 | 37 | Once you have the required compiler installed, just run the following command:: 38 | 39 | python -m pip install . 40 | 41 | Examples 42 | -------- 43 | 44 | This is a simplified example of a complete authentication session 45 | following RFC-4752, section 3.1: 46 | 47 | .. code-block:: python 48 | 49 | import winkerberos as kerberos 50 | 51 | 52 | def send_response_and_receive_challenge(response): 53 | # Your server communication code here... 54 | pass 55 | 56 | 57 | def authenticate_kerberos(service, user, channel_bindings=None): 58 | # Initialize the context object with a service principal. 59 | status, ctx = kerberos.authGSSClientInit(service) 60 | 61 | # GSSAPI is a "client goes first" SASL mechanism. Send the 62 | # first "response" to the server and receive its first 63 | # challenge. 64 | if channel_bindings is not None: 65 | status = kerberos.authGSSClientStep(ctx, "", channel_bindings=channel_bindings) 66 | else: 67 | status = kerberos.authGSSClientStep(ctx, "") 68 | response = kerberos.authGSSClientResponse(ctx) 69 | challenge = send_response_and_receive_challenge(response) 70 | 71 | # Keep processing challenges and sending responses until 72 | # authGSSClientStep reports AUTH_GSS_COMPLETE. 73 | while status == kerberos.AUTH_GSS_CONTINUE: 74 | if channel_bindings is not None: 75 | status = kerberos.authGSSClientStep( 76 | ctx, "", channel_bindings=channel_bindings 77 | ) 78 | else: 79 | status = kerberos.authGSSClientStep(ctx, "") 80 | 81 | response = kerberos.authGSSClientResponse(ctx) or "" 82 | challenge = send_response_and_receive_challenge(response) 83 | 84 | # Decrypt the server's last challenge 85 | kerberos.authGSSClientUnwrap(ctx, challenge) 86 | data = kerberos.authGSSClientResponse(ctx) 87 | # Encrypt a response including the user principal to authorize. 88 | kerberos.authGSSClientWrap(ctx, data, user) 89 | response = kerberos.authGSSClientResponse(ctx) 90 | 91 | # Complete authentication. 92 | send_response_and_receive_challenge(response) 93 | 94 | Channel bindings can be generated with help from the cryptography_ module. See 95 | ``_ for the rules regarding 96 | hash algorithm choice: 97 | 98 | .. code-block:: python 99 | 100 | from cryptography import x509 101 | from cryptography.hazmat.backends import default_backend 102 | from cryptography.hazmat.primitives import hashes 103 | 104 | 105 | def channel_bindings(ssl_socket): 106 | server_certificate = ssl_socket.getpeercert(True) 107 | cert = x509.load_der_x509_certificate(server_certificate, default_backend()) 108 | hash_algorithm = cert.signature_hash_algorithm 109 | if hash_algorithm.name in ("md5", "sha1"): 110 | digest = hashes.Hash(hashes.SHA256(), default_backend()) 111 | else: 112 | digest = hashes.Hash(hash_algorithm, default_backend()) 113 | digest.update(server_certificate) 114 | application_data = b"tls-server-end-point:" + digest.finalize() 115 | return kerberos.channelBindings(application_data=application_data) 116 | 117 | 118 | .. _cryptography: https://pypi.python.org/pypi/cryptography 119 | 120 | Issues 121 | ------ 122 | All issues should be reported (and can be tracked / voted for / 123 | commented on) on the `github issues tracker 124 | `_. 125 | 126 | Contributing 127 | ------------ 128 | To contribute, fork the project on 129 | `github `_ and send a 130 | pull request. 131 | 132 | Changes 133 | ------- 134 | See the :doc:`changelog` for a full list of changes to WinKerberos. 135 | 136 | About This Documentation 137 | ------------------------ 138 | This documentation is generated using the `Sphinx 139 | `_ documentation generator. The source files 140 | for the documentation are located in the *doc/* directory of the 141 | **WinKerberos** distribution. To generate the docs locally install Sphinx:: 142 | 143 | python -m pip install Sphinx 144 | 145 | Then run the following command from the root directory of the **WinKerberos** 146 | source:: 147 | 148 | python setup.py doc 149 | 150 | Indices and tables 151 | ------------------ 152 | 153 | * :ref:`genindex` 154 | * :ref:`modindex` 155 | * :ref:`search` 156 | 157 | .. toctree:: 158 | :hidden: 159 | 160 | changelog 161 | winkerberos 162 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 1>NUL 2>NUL 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\WinKerberos.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\WinKerberos.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # WinKerberos documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Mar 21 13:26:40 2016. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # Put the built extension on sys.path. 19 | sys.path.append(os.path.abspath(os.path.join(os.getcwd(), os.pardir))) 20 | 21 | import winkerberos # noqa: E402 22 | 23 | # If extensions (or modules to document with autodoc) are in another directory, 24 | # add these directories to sys.path here. If the directory is relative to the 25 | # documentation root, use os.path.abspath to make it absolute, like shown here. 26 | # sys.path.insert(0, os.path.abspath('.')) 27 | 28 | # -- General configuration ------------------------------------------------ 29 | 30 | # If your documentation needs a minimal Sphinx version, state it here. 31 | # needs_sphinx = '1.0' 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 35 | # ones. 36 | extensions = [ 37 | "sphinx.ext.autodoc", 38 | ] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ["_templates"] 42 | 43 | # The suffix(es) of source filenames. 44 | # You can specify multiple suffix as a list of string: 45 | # source_suffix = ['.rst', '.md'] 46 | source_suffix = ".rst" 47 | 48 | # The encoding of source files. 49 | # source_encoding = 'utf-8-sig' 50 | 51 | # The master toctree document. 52 | master_doc = "index" 53 | 54 | # General information about the project. 55 | project = "WinKerberos" 56 | copyright = "2016, MongoDB, Inc" 57 | author = "Bernie Hackett" 58 | 59 | # The version info for the project you're documenting, acts as replacement for 60 | # |version| and |release|, also used in various other places throughout the 61 | # built documents. 62 | # 63 | # The short X.Y version. 64 | version = winkerberos.__version__ 65 | # The full version, including alpha/beta/rc tags. 66 | release = winkerberos.__version__ 67 | 68 | # The language for content autogenerated by Sphinx. Refer to documentation 69 | # for a list of supported languages. 70 | # 71 | # This is also used if you do content translation via gettext catalogs. 72 | # Usually you set "language" from the command line for these cases. 73 | language = "en" 74 | 75 | # There are two options for replacing |today|: either, you set today to some 76 | # non-false value, then it is used: 77 | # today = '' 78 | # Else, today_fmt is used as the format for a strftime call. 79 | # today_fmt = '%B %d, %Y' 80 | 81 | # List of patterns, relative to source directory, that match files and 82 | # directories to ignore when looking for source files. 83 | exclude_patterns = ["_build"] 84 | 85 | # The reST default role (used for this markup: `text`) to use for all 86 | # documents. 87 | # default_role = None 88 | 89 | # If true, '()' will be appended to :func: etc. cross-reference text. 90 | # add_function_parentheses = True 91 | 92 | # If true, the current module name will be prepended to all description 93 | # unit titles (such as .. function::). 94 | # add_module_names = True 95 | 96 | # If true, sectionauthor and moduleauthor directives will be shown in the 97 | # output. They are ignored by default. 98 | # show_authors = False 99 | 100 | # The name of the Pygments (syntax highlighting) style to use. 101 | pygments_style = "sphinx" 102 | 103 | # A list of ignored prefixes for module index sorting. 104 | # modindex_common_prefix = [] 105 | 106 | # If true, keep warnings as "system message" paragraphs in the built documents. 107 | # keep_warnings = False 108 | 109 | # If true, `todo` and `todoList` produce output, else they produce nothing. 110 | todo_include_todos = False 111 | 112 | 113 | # -- Options for HTML output ---------------------------------------------- 114 | 115 | # The theme to use for HTML and HTML Help pages. See the documentation for 116 | # a list of builtin themes. 117 | html_theme = "nature" 118 | 119 | # Theme options are theme-specific and customize the look and feel of a theme 120 | # further. For a list of options available for each theme, see the 121 | # documentation. 122 | # html_theme_options = {} 123 | 124 | # Add any paths that contain custom themes here, relative to this directory. 125 | # html_theme_path = [] 126 | 127 | # The name for this set of Sphinx documents. If None, it defaults to 128 | # " v documentation". 129 | # html_title = None 130 | 131 | # A shorter title for the navigation bar. Default is the same as html_title. 132 | # html_short_title = None 133 | 134 | # The name of an image file (relative to this directory) to place at the top 135 | # of the sidebar. 136 | # html_logo = None 137 | 138 | # The name of an image file (relative to this directory) to use as a favicon of 139 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 140 | # pixels large. 141 | # html_favicon = None 142 | 143 | # Add any paths that contain custom static files (such as style sheets) here, 144 | # relative to this directory. They are copied after the builtin static files, 145 | # so a file named "default.css" will overwrite the builtin "default.css". 146 | # html_static_path = ['_static'] 147 | 148 | # Add any extra paths that contain custom files (such as robots.txt or 149 | # .htaccess) here, relative to this directory. These files are copied 150 | # directly to the root of the documentation. 151 | # html_extra_path = [] 152 | 153 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 154 | # using the given strftime format. 155 | # html_last_updated_fmt = '%b %d, %Y' 156 | 157 | # If true, SmartyPants will be used to convert quotes and dashes to 158 | # typographically correct entities. 159 | # html_use_smartypants = True 160 | 161 | # Custom sidebar templates, maps document names to template names. 162 | # html_sidebars = {} 163 | 164 | # Additional templates that should be rendered to pages, maps page names to 165 | # template names. 166 | # html_additional_pages = {} 167 | 168 | # If false, no module index is generated. 169 | # html_domain_indices = True 170 | 171 | # If false, no index is generated. 172 | # html_use_index = True 173 | 174 | # If true, the index is split into individual pages for each letter. 175 | # html_split_index = False 176 | 177 | # If true, links to the reST sources are added to the pages. 178 | # html_show_sourcelink = True 179 | 180 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 181 | # html_show_sphinx = True 182 | 183 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 184 | # html_show_copyright = True 185 | 186 | # If true, an OpenSearch description file will be output, and all pages will 187 | # contain a tag referring to it. The value of this option must be the 188 | # base URL from which the finished HTML is served. 189 | # html_use_opensearch = '' 190 | 191 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 192 | # html_file_suffix = None 193 | 194 | # Language to be used for generating the HTML full-text search index. 195 | # Sphinx supports the following languages: 196 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 197 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 198 | # html_search_language = 'en' 199 | 200 | # A dictionary with options for the search language support, empty by default. 201 | # Now only 'ja' uses this config value 202 | # html_search_options = {'type': 'default'} 203 | 204 | # The name of a javascript file (relative to the configuration directory) that 205 | # implements a search results scorer. If empty, the default will be used. 206 | # html_search_scorer = 'scorer.js' 207 | 208 | # Output file base name for HTML help builder. 209 | htmlhelp_basename = "WinKerberosdoc" 210 | 211 | # -- Options for LaTeX output --------------------------------------------- 212 | 213 | latex_elements = { 214 | # The paper size ('letterpaper' or 'a4paper'). 215 | #'papersize': 'letterpaper', 216 | # The font size ('10pt', '11pt' or '12pt'). 217 | #'pointsize': '10pt', 218 | # Additional stuff for the LaTeX preamble. 219 | #'preamble': '', 220 | # Latex figure (float) alignment 221 | #'figure_align': 'htbp', 222 | } 223 | 224 | # Grouping the document tree into LaTeX files. List of tuples 225 | # (source start file, target name, title, 226 | # author, documentclass [howto, manual, or own class]). 227 | latex_documents = [ 228 | ( 229 | master_doc, 230 | "WinKerberos.tex", 231 | "WinKerberos Documentation", 232 | "Bernie Hackett", 233 | "manual", 234 | ), 235 | ] 236 | 237 | # The name of an image file (relative to this directory) to place at the top of 238 | # the title page. 239 | # latex_logo = None 240 | 241 | # For "manual" documents, if this is true, then toplevel headings are parts, 242 | # not chapters. 243 | # latex_use_parts = False 244 | 245 | # If true, show page references after internal links. 246 | # latex_show_pagerefs = False 247 | 248 | # If true, show URL addresses after external links. 249 | # latex_show_urls = False 250 | 251 | # Documents to append as an appendix to all manuals. 252 | # latex_appendices = [] 253 | 254 | # If false, no module index is generated. 255 | # latex_domain_indices = True 256 | 257 | 258 | # -- Options for manual page output --------------------------------------- 259 | 260 | # One entry per manual page. List of tuples 261 | # (source start file, name, description, authors, manual section). 262 | # man_pages = [ 263 | # (master_doc, 'winkerberos', u'WinKerberos Documentation', 264 | # [author], 1) 265 | # ] 266 | 267 | # If true, show URL addresses after external links. 268 | # man_show_urls = False 269 | 270 | 271 | # -- Options for Texinfo output ------------------------------------------- 272 | 273 | # Grouping the document tree into Texinfo files. List of tuples 274 | # (source start file, target name, title, author, 275 | # dir menu entry, description, category) 276 | # texinfo_documents = [ 277 | # (master_doc, 'WinKerberos', u'WinKerberos Documentation', 278 | # author, 'WinKerberos', 'One line description of project.', 279 | # 'Miscellaneous'), 280 | # ] 281 | 282 | # Documents to append as an appendix to all manuals. 283 | # texinfo_appendices = [] 284 | 285 | # If false, no module index is generated. 286 | # texinfo_domain_indices = True 287 | 288 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 289 | # texinfo_show_urls = 'footnote' 290 | 291 | # If true, do not generate a @detailmenu in the "Top" node's menu. 292 | # texinfo_no_detailmenu = False 293 | -------------------------------------------------------------------------------- /test/test_winkerberos.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 MongoDB, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import array 16 | import base64 17 | import mmap 18 | import os 19 | import sys 20 | import unittest 21 | 22 | sys.path[0:0] = [""] 23 | 24 | import winkerberos as kerberos # noqa: E402 25 | 26 | _HAVE_PYMONGO = True 27 | try: 28 | from pymongo import MongoClient 29 | from pymongo.errors import ConnectionFailure 30 | except ImportError: 31 | _HAVE_PYMONGO = False 32 | 33 | _HOST = os.environ.get("MONGODB_HOST", "localhost") 34 | _PORT = int(os.environ.get("MONGODB_PORT", 27017)) 35 | _SPN = os.environ.get("KERBEROS_SERVICE") 36 | _PRINCIPAL = os.environ.get("KERBEROS_PRINCIPAL") 37 | _UPN = os.environ.get("KERBEROS_UPN") 38 | _USER = os.environ.get("KERBEROS_USER") 39 | _DOMAIN = os.environ.get("KERBEROS_DOMAIN") 40 | _PASSWORD = os.environ.get("KERBEROS_PASSWORD") 41 | 42 | 43 | class TestWinKerberos(unittest.TestCase): 44 | @classmethod 45 | def setUpClass(cls): 46 | if not _HAVE_PYMONGO: 47 | raise unittest.SkipTest("Could not import pymongo") 48 | if _SPN is None: 49 | raise unittest.SkipTest("KERBEROS_SERVICE is required") 50 | cls.client = MongoClient(_HOST, _PORT, connect=False, maxPoolSize=1) 51 | cls.db = cls.client["$external"] 52 | try: 53 | cls.client.admin.command("ismaster") 54 | except ConnectionFailure: 55 | raise unittest.SkipTest("Could not connection to MongoDB") 56 | 57 | def authenticate( 58 | self, 59 | service=_SPN, 60 | principal=None, 61 | flags=kerberos.GSS_C_MUTUAL_FLAG, 62 | user=_USER, 63 | domain=_DOMAIN, 64 | password=_PASSWORD, 65 | mech_oid=kerberos.GSS_MECH_OID_KRB5, 66 | upn=_UPN, 67 | protect=0, 68 | ): 69 | res, ctx = kerberos.authGSSClientInit( 70 | service, principal, flags, user, domain, password, mech_oid 71 | ) 72 | res = kerberos.authGSSClientStep(ctx, "") 73 | payload = kerberos.authGSSClientResponse(ctx) 74 | response = self.db.command("saslStart", mechanism="GSSAPI", payload=payload) 75 | while res == kerberos.AUTH_GSS_CONTINUE: 76 | res = kerberos.authGSSClientStep(ctx, response["payload"]) 77 | payload = kerberos.authGSSClientResponse(ctx) or "" 78 | response = self.db.command( 79 | "saslContinue", 80 | conversationId=response["conversationId"], 81 | payload=payload, 82 | ) 83 | kerberos.authGSSClientUnwrap(ctx, response["payload"]) 84 | kerberos.authGSSClientWrap( 85 | ctx, kerberos.authGSSClientResponse(ctx), upn, protect 86 | ) 87 | response = self.db.command( 88 | "saslContinue", 89 | conversationId=response["conversationId"], 90 | payload=kerberos.authGSSClientResponse(ctx), 91 | ) 92 | self.assertTrue(response["done"]) 93 | 94 | def test_authenticate(self): 95 | res, ctx = kerberos.authGSSClientInit( 96 | _SPN, None, kerberos.GSS_C_MUTUAL_FLAG, _USER, _DOMAIN, _PASSWORD 97 | ) 98 | self.assertEqual(res, kerberos.AUTH_GSS_COMPLETE) 99 | 100 | res = kerberos.authGSSClientStep(ctx, "", channel_bindings=None) 101 | self.assertEqual(res, kerberos.AUTH_GSS_CONTINUE) 102 | 103 | payload = kerberos.authGSSClientResponse(ctx) 104 | self.assertIsInstance(payload, str) 105 | 106 | response = self.db.command("saslStart", mechanism="GSSAPI", payload=payload) 107 | while res == kerberos.AUTH_GSS_CONTINUE: 108 | res = kerberos.authGSSClientStep(ctx, response["payload"]) 109 | payload = kerberos.authGSSClientResponse(ctx) or "" 110 | response = self.db.command( 111 | "saslContinue", 112 | conversationId=response["conversationId"], 113 | payload=payload, 114 | ) 115 | 116 | res = kerberos.authGSSClientUnwrap(ctx, response["payload"]) 117 | self.assertEqual(res, 1) 118 | 119 | unwrapped = kerberos.authGSSClientResponse(ctx) 120 | self.assertIsInstance(unwrapped, str) 121 | self.assertIsInstance(kerberos.authGSSClientResponseConf(ctx), int) 122 | 123 | # RFC-4752 124 | challenge_bytes = base64.standard_b64decode(unwrapped) 125 | self.assertEqual(4, len(challenge_bytes)) 126 | 127 | # Manually create an authorization message and encrypt it. This 128 | # is the "no security layer" message as detailed in RFC-4752, 129 | # section 3.1, final paragraph. This is also the message created 130 | # by calling authGSSClientWrap with the "user" option. 131 | msg = base64.standard_b64encode( 132 | b"\x01\x00\x00\x00" + _UPN.encode("utf8") 133 | ).decode("utf8") 134 | res = kerberos.authGSSClientWrap(ctx, msg) 135 | self.assertEqual(res, 1) 136 | 137 | custom = kerberos.authGSSClientResponse(ctx) 138 | self.assertIsInstance(custom, str) 139 | 140 | # Wrap using unwrapped and user principal. 141 | res = kerberos.authGSSClientWrap(ctx, unwrapped, _UPN) 142 | self.assertEqual(res, 1) 143 | 144 | wrapped = kerberos.authGSSClientResponse(ctx) 145 | self.assertIsInstance(wrapped, str) 146 | 147 | # Actually complete authentication, using our custom message. 148 | response = self.db.command( 149 | "saslContinue", conversationId=response["conversationId"], payload=custom 150 | ) 151 | self.assertTrue(response["done"]) 152 | 153 | self.assertIsInstance(kerberos.authGSSClientUserName(ctx), str) 154 | 155 | def test_uninitialized_context(self): 156 | res, ctx = kerberos.authGSSClientInit( 157 | _SPN, None, kerberos.GSS_C_MUTUAL_FLAG, _USER, _DOMAIN, _PASSWORD 158 | ) 159 | self.assertEqual(res, kerberos.AUTH_GSS_COMPLETE) 160 | 161 | self.assertIsNone(kerberos.authGSSClientResponse(ctx)) 162 | self.assertIsNone(kerberos.authGSSClientUserName(ctx)) 163 | self.assertRaises( 164 | kerberos.GSSError, kerberos.authGSSClientUnwrap, ctx, "foobar" 165 | ) 166 | self.assertRaises(kerberos.GSSError, kerberos.authGSSClientWrap, ctx, "foobar") 167 | 168 | def test_arg_parsing(self): 169 | self.assertRaises(TypeError, kerberos.authGSSClientInit, None) 170 | self.assertRaises( 171 | TypeError, kerberos.authGSSClientInit, "foo", "foo", 0, bytearray() 172 | ) 173 | self.assertRaises( 174 | TypeError, kerberos.authGSSClientInit, "foo", "foo", 0, "foo", bytearray() 175 | ) 176 | self.assertRaises( 177 | TypeError, kerberos.authGSSClientInit, "foo", "foo", 0, "foo", "foo", {} 178 | ) 179 | 180 | self.assertRaises(ValueError, kerberos.authGSSClientInit, "foo", "fo\0") 181 | self.assertRaises( 182 | ValueError, kerberos.authGSSClientInit, "foo", "foo", 0, "f0\0" 183 | ) 184 | self.assertRaises( 185 | ValueError, kerberos.authGSSClientInit, "foo", "foo", 0, "foo", "fo\0" 186 | ) 187 | self.assertRaises( 188 | ValueError, 189 | kerberos.authGSSClientInit, 190 | "foo", 191 | "foo", 192 | 0, 193 | "foo", 194 | "foo", 195 | "fo\0", 196 | ) 197 | 198 | self.assertRaises( 199 | TypeError, kerberos.authGSSClientInit, "foo", "foo", 0, b"foo" 200 | ) 201 | self.assertRaises( 202 | TypeError, kerberos.authGSSClientInit, "foo", "foo", 0, "foo", b"foo" 203 | ) 204 | 205 | def test_password_buffer(self): 206 | password = bytearray(_PASSWORD, "utf8") 207 | try: 208 | self.authenticate(password=password) 209 | except kerberos.GSSError as exc: 210 | self.fail("Failed bytearray: {}".format(str(exc))) 211 | 212 | try: 213 | self.authenticate(password=memoryview(password)) 214 | except kerberos.GSSError as exc: 215 | self.fail("Failed memoryview: {}".format(str(exc))) 216 | 217 | mm = mmap.mmap(-1, len(password)) 218 | mm.write(_PASSWORD.encode("utf8")) 219 | mm.seek(0) 220 | try: 221 | self.authenticate(password=mm) 222 | except kerberos.GSSError as exc: 223 | self.fail("Failed map.map: {}".format(str(exc))) 224 | 225 | # Note that only ascii and utf8 strings are supported, so 226 | # 'u' with a unicode object won't work. Unicode objects 227 | # must be encoded utf8 first. 228 | try: 229 | self.authenticate(password=array.array("b", password)) 230 | except kerberos.GSSError as exc: 231 | self.fail("Failed array.array: {}".format(str(exc))) 232 | 233 | def test_principal(self): 234 | if _PRINCIPAL is None: 235 | raise unittest.SkipTest("Must set KERBEROS_PRINCIPAL to test") 236 | try: 237 | self.authenticate( 238 | principal=_PRINCIPAL, user=None, domain=None, password=None 239 | ) 240 | except kerberos.GSSError as exc: 241 | self.fail("Failed testing principal: {}".format(str(exc))) 242 | 243 | encoded = bytearray(_PRINCIPAL, "utf8") 244 | # No error. 245 | self.authenticate(principal=encoded, user=None, domain=None, password=None) 246 | 247 | # No error. For backward compatibility, the user parameter takes 248 | # precedence. 249 | self.authenticate(principal="somebogus@user:pass") 250 | 251 | # Again, the user parameter takes precedence. 252 | self.assertRaises( 253 | kerberos.GSSError, 254 | self.authenticate, 255 | principal=_PRINCIPAL, 256 | user="somebogus", 257 | domain="user", 258 | password="pass", 259 | ) 260 | 261 | def test_confidentiality(self): 262 | # No error. 263 | self.authenticate( 264 | flags=kerberos.GSS_C_MUTUAL_FLAG | kerberos.GSS_C_CONF_FLAG, protect=1 265 | ) 266 | self.assertRaises( 267 | kerberos.GSSError, 268 | self.authenticate, 269 | flags=kerberos.GSS_C_MUTUAL_FLAG, 270 | protect=1, 271 | ) 272 | 273 | def test_mech_oid(self): 274 | # No error. 275 | self.authenticate(mech_oid=kerberos.GSS_MECH_OID_KRB5) 276 | # No error here either, since the two sides 277 | # negotiate kerberos automatically. 278 | self.authenticate(mech_oid=kerberos.GSS_MECH_OID_SPNEGO) 279 | 280 | def test_exception_hierarchy(self): 281 | self.assertIsInstance(kerberos.KrbError(), Exception) 282 | self.assertIsInstance(kerberos.GSSError(), kerberos.KrbError) 283 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/kerberos_sspi.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 MongoDB, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include "kerberos_sspi.h" 18 | 19 | extern PyObject* GSSError; 20 | 21 | VOID 22 | destroy_sspi_client_state(sspi_client_state* state) { 23 | if (state->haveCtx) { 24 | DeleteSecurityContext(&state->ctx); 25 | state->haveCtx = 0; 26 | } 27 | if (state->haveCred) { 28 | FreeCredentialsHandle(&state->cred); 29 | state->haveCred = 0; 30 | } 31 | if (state->spn != NULL) { 32 | free(state->spn); 33 | state->spn = NULL; 34 | } 35 | if (state->response != NULL) { 36 | free(state->response); 37 | state->response = NULL; 38 | } 39 | if (state->username != NULL) { 40 | free(state->username); 41 | state->username = NULL; 42 | } 43 | } 44 | 45 | VOID 46 | destroy_sspi_server_state(sspi_server_state* state) { 47 | if (state->haveCtx) { 48 | DeleteSecurityContext(&state->ctx); 49 | state->haveCtx = 0; 50 | } 51 | if (state->haveCred) { 52 | FreeCredentialsHandle(&state->cred); 53 | state->haveCred = 0; 54 | } 55 | if (state->spn != NULL) { 56 | free(state->spn); 57 | state->spn = NULL; 58 | } 59 | if (state->response != NULL) { 60 | free(state->response); 61 | state->response = NULL; 62 | } 63 | if (state->username != NULL) { 64 | free(state->username); 65 | state->username = NULL; 66 | } 67 | } 68 | 69 | VOID 70 | set_gsserror(DWORD errCode, const SEC_CHAR* msg) { 71 | SEC_CHAR* err = NULL; 72 | DWORD status; 73 | DWORD flags = (FORMAT_MESSAGE_ALLOCATE_BUFFER | 74 | FORMAT_MESSAGE_FROM_SYSTEM | 75 | FORMAT_MESSAGE_IGNORE_INSERTS); 76 | status = FormatMessageA(flags, 77 | NULL, 78 | errCode, 79 | MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), 80 | (LPTSTR)&err, 81 | 0, 82 | NULL); 83 | if (status) { 84 | PyErr_Format(GSSError, "SSPI: %s: %s", msg, err); 85 | LocalFree(err); 86 | } else { 87 | PyErr_Format(GSSError, "SSPI: %s", msg); 88 | } 89 | } 90 | 91 | static SEC_CHAR* 92 | base64_encode(const SEC_CHAR* value, DWORD vlen) { 93 | SEC_CHAR* out = NULL; 94 | DWORD len; 95 | /* Get the correct size for the out buffer. */ 96 | if (CryptBinaryToStringA((BYTE*)value, 97 | vlen, 98 | CRYPT_STRING_BASE64|CRYPT_STRING_NOCRLF, 99 | NULL, 100 | &len)) { 101 | out = (SEC_CHAR*)malloc(sizeof(SEC_CHAR) * len); 102 | if (out) { 103 | /* Encode to the out buffer. */ 104 | if (CryptBinaryToStringA((BYTE*)value, 105 | vlen, 106 | CRYPT_STRING_BASE64|CRYPT_STRING_NOCRLF, 107 | out, 108 | &len)) { 109 | return out; 110 | } else { 111 | free(out); 112 | } 113 | } 114 | } 115 | PyErr_Format(GSSError, "CryptBinaryToString failed."); 116 | return NULL; 117 | } 118 | 119 | static SEC_CHAR* 120 | base64_decode(const SEC_CHAR* value, DWORD* rlen) { 121 | SEC_CHAR* out = NULL; 122 | /* Get the correct size for the out buffer. */ 123 | if (CryptStringToBinaryA(value, 124 | 0, 125 | CRYPT_STRING_BASE64, 126 | NULL, 127 | rlen, 128 | NULL, 129 | NULL)) { 130 | out = (SEC_CHAR*)malloc(sizeof(SEC_CHAR) * *rlen); 131 | if (out) { 132 | /* Decode to the out buffer. */ 133 | if (CryptStringToBinaryA(value, 134 | 0, 135 | CRYPT_STRING_BASE64, 136 | (BYTE*)out, 137 | rlen, 138 | NULL, 139 | NULL)) { 140 | return out; 141 | } else { 142 | free(out); 143 | } 144 | } 145 | } 146 | PyErr_Format(GSSError, "CryptStringToBinary failed."); 147 | return NULL; 148 | } 149 | 150 | static CHAR* 151 | wide_to_utf8(WCHAR* value) { 152 | CHAR* out; 153 | INT len = WideCharToMultiByte(CP_UTF8, 154 | 0, 155 | value, 156 | -1, 157 | NULL, 158 | 0, 159 | NULL, 160 | NULL); 161 | if (len) { 162 | out = (CHAR*)malloc(sizeof(CHAR) * len); 163 | if (!out) { 164 | PyErr_SetNone(PyExc_MemoryError); 165 | return NULL; 166 | } else { 167 | if (WideCharToMultiByte(CP_UTF8, 168 | 0, 169 | value, 170 | -1, 171 | out, 172 | len, 173 | NULL, 174 | NULL)) { 175 | return out; 176 | } else { 177 | free(out); 178 | } 179 | } 180 | } 181 | set_gsserror(GetLastError(), "WideCharToMultiByte"); 182 | return NULL; 183 | } 184 | 185 | static VOID 186 | set_uninitialized_context(VOID) { 187 | PyErr_SetString(GSSError, 188 | "Uninitialized security context. You must use " 189 | "authGSSClientStep to initialize the security " 190 | "context before calling this function."); 191 | } 192 | 193 | INT 194 | auth_sspi_client_init(WCHAR* service, 195 | ULONG flags, 196 | WCHAR* user, 197 | ULONG ulen, 198 | WCHAR* domain, 199 | ULONG dlen, 200 | WCHAR* password, 201 | ULONG plen, 202 | WCHAR* mechoid, 203 | sspi_client_state* state) { 204 | SECURITY_STATUS status; 205 | SEC_WINNT_AUTH_IDENTITY_W authIdentity; 206 | TimeStamp ignored; 207 | 208 | state->response = NULL; 209 | state->username = NULL; 210 | state->qop = SECQOP_WRAP_NO_ENCRYPT; 211 | state->flags = flags; 212 | state->haveCred = 0; 213 | state->haveCtx = 0; 214 | state->spn = _wcsdup(service); 215 | if (state->spn == NULL) { 216 | PyErr_SetNone(PyExc_MemoryError); 217 | return AUTH_GSS_ERROR; 218 | } 219 | /* Convert RFC-2078 format to SPN */ 220 | if (!wcschr(state->spn, L'/')) { 221 | WCHAR* ptr = wcschr(state->spn, L'@'); 222 | if (ptr) { 223 | *ptr = L'/'; 224 | } 225 | } 226 | 227 | if (user) { 228 | authIdentity.User = user; 229 | authIdentity.UserLength = ulen; 230 | authIdentity.Domain = domain; 231 | authIdentity.DomainLength = dlen; 232 | authIdentity.Password = password; 233 | authIdentity.PasswordLength = plen; 234 | authIdentity.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE; 235 | } 236 | 237 | /* Note that the first parameter, pszPrincipal, appears to be 238 | * completely ignored in the Kerberos SSP. For more details see 239 | * https://github.com/mongodb-labs/winkerberos/issues/11. 240 | * */ 241 | status = AcquireCredentialsHandleW(/* Principal */ 242 | NULL, 243 | /* Security package name */ 244 | mechoid, 245 | /* Credentials Use */ 246 | SECPKG_CRED_OUTBOUND, 247 | /* LogonID (We don't use this) */ 248 | NULL, 249 | /* AuthData */ 250 | user ? &authIdentity : NULL, 251 | /* Always NULL */ 252 | NULL, 253 | /* Always NULL */ 254 | NULL, 255 | /* CredHandle */ 256 | &state->cred, 257 | /* Expiry (Required but unused by us) */ 258 | &ignored); 259 | if (status != SEC_E_OK) { 260 | set_gsserror(status, "AcquireCredentialsHandle"); 261 | return AUTH_GSS_ERROR; 262 | } 263 | state->haveCred = 1; 264 | return AUTH_GSS_COMPLETE; 265 | } 266 | 267 | INT 268 | auth_sspi_server_init(WCHAR* service, sspi_server_state* state) { 269 | SECURITY_STATUS status; 270 | TimeStamp ignored; 271 | 272 | state->response = NULL; 273 | state->username = NULL; 274 | state->qop = SECQOP_WRAP_NO_ENCRYPT; 275 | state->haveCred = 0; 276 | state->haveCtx = 0; 277 | state->spn = _wcsdup(service); 278 | if (state->spn == NULL) { 279 | PyErr_SetNone(PyExc_MemoryError); 280 | return AUTH_GSS_ERROR; 281 | } 282 | /* Convert RFC-2078 format to SPN */ 283 | if (!wcschr(state->spn, L'/')) { 284 | WCHAR* ptr = wcschr(state->spn, L'@'); 285 | if (ptr) { 286 | *ptr = L'/'; 287 | } 288 | } 289 | 290 | /* Note that the first parameter, pszPrincipal, appears to be 291 | * completely ignored in the Kerberos SSP. For more details see 292 | * https://github.com/mongodb-labs/winkerberos/issues/11. 293 | * */ 294 | status = AcquireCredentialsHandleW(/* Principal */ 295 | NULL, 296 | /* Security package name */ 297 | L"Negotiate", 298 | /* Credentials Use */ 299 | SECPKG_CRED_INBOUND, 300 | /* LogonID (We don't use this) */ 301 | NULL, 302 | /* AuthData */ 303 | NULL, 304 | /* Always NULL */ 305 | NULL, 306 | /* Always NULL */ 307 | NULL, 308 | /* CredHandle */ 309 | &state->cred, 310 | /* Expiry (Required but unused by us) */ 311 | &ignored); 312 | if (status != SEC_E_OK) { 313 | set_gsserror(status, "AcquireCredentialsHandle"); 314 | return AUTH_GSS_ERROR; 315 | } 316 | state->haveCred = 1; 317 | return AUTH_GSS_COMPLETE; 318 | } 319 | 320 | INT 321 | auth_sspi_client_step(sspi_client_state* state, SEC_CHAR* challenge, SecPkgContext_Bindings* sec_pkg_context_bindings) { 322 | SecBufferDesc inbuf; 323 | SecBuffer inBufs[2]; 324 | SecBufferDesc outbuf; 325 | SecBuffer outBufs[1]; 326 | ULONG ignored; 327 | SECURITY_STATUS status = AUTH_GSS_CONTINUE; 328 | DWORD len; 329 | BOOL haveToken = FALSE; 330 | INT tokenBufferIndex = 0; 331 | 332 | if (state->response != NULL) { 333 | free(state->response); 334 | state->response = NULL; 335 | } 336 | 337 | inbuf.ulVersion = SECBUFFER_VERSION; 338 | inbuf.pBuffers = inBufs; 339 | inbuf.cBuffers = 0; 340 | 341 | if (sec_pkg_context_bindings != NULL) { 342 | inBufs[inbuf.cBuffers].BufferType = SECBUFFER_CHANNEL_BINDINGS; 343 | inBufs[inbuf.cBuffers].pvBuffer = sec_pkg_context_bindings->Bindings; 344 | inBufs[inbuf.cBuffers].cbBuffer = sec_pkg_context_bindings->BindingsLength; 345 | inbuf.cBuffers++; 346 | } 347 | 348 | tokenBufferIndex = inbuf.cBuffers; 349 | if (state->haveCtx) { 350 | haveToken = TRUE; 351 | inBufs[tokenBufferIndex].BufferType = SECBUFFER_TOKEN; 352 | inBufs[tokenBufferIndex].pvBuffer = base64_decode(challenge, &len); 353 | if (!inBufs[tokenBufferIndex].pvBuffer) { 354 | return AUTH_GSS_ERROR; 355 | } 356 | inBufs[tokenBufferIndex].cbBuffer = len; 357 | inbuf.cBuffers++; 358 | } 359 | 360 | outbuf.ulVersion = SECBUFFER_VERSION; 361 | outbuf.cBuffers = 1; 362 | outbuf.pBuffers = outBufs; 363 | outBufs[0].pvBuffer = NULL; 364 | outBufs[0].cbBuffer = 0; 365 | outBufs[0].BufferType = SECBUFFER_TOKEN; 366 | 367 | Py_BEGIN_ALLOW_THREADS 368 | status = InitializeSecurityContextW(/* CredHandle */ 369 | &state->cred, 370 | /* CtxtHandle (NULL on first call) */ 371 | state->haveCtx ? &state->ctx : NULL, 372 | /* Service Principal Name */ 373 | state->spn, 374 | /* Flags */ 375 | ISC_REQ_ALLOCATE_MEMORY | state->flags, 376 | /* Always 0 */ 377 | 0, 378 | /* Target data representation */ 379 | SECURITY_NETWORK_DREP, 380 | /* Challenge (Set to NULL if no buffers are set) */ 381 | inbuf.cBuffers > 0 ? &inbuf : NULL, 382 | /* Always 0 */ 383 | 0, 384 | /* CtxtHandle (Set on first call) */ 385 | &state->ctx, 386 | /* Output */ 387 | &outbuf, 388 | /* Context attributes */ 389 | &ignored, 390 | /* Expiry (We don't use this) */ 391 | NULL); 392 | Py_END_ALLOW_THREADS 393 | if (status != SEC_E_OK && status != SEC_I_CONTINUE_NEEDED) { 394 | set_gsserror(status, "InitializeSecurityContext"); 395 | status = AUTH_GSS_ERROR; 396 | goto done; 397 | } 398 | state->haveCtx = 1; 399 | if (outBufs[0].cbBuffer) { 400 | state->response = base64_encode(outBufs[0].pvBuffer, 401 | outBufs[0].cbBuffer); 402 | if (!state->response) { 403 | status = AUTH_GSS_ERROR; 404 | goto done; 405 | } 406 | } 407 | if (status == SEC_E_OK) { 408 | /* Get authenticated username. */ 409 | SecPkgContext_NamesW names; 410 | status = QueryContextAttributesW( 411 | &state->ctx, SECPKG_ATTR_NAMES, &names); 412 | if (status != SEC_E_OK) { 413 | set_gsserror(status, "QueryContextAttributesW"); 414 | status = AUTH_GSS_ERROR; 415 | goto done; 416 | } 417 | state->username = wide_to_utf8(names.sUserName); 418 | if (state->username == NULL) { 419 | FreeContextBuffer(names.sUserName); 420 | status = AUTH_GSS_ERROR; 421 | goto done; 422 | } 423 | FreeContextBuffer(names.sUserName); 424 | status = AUTH_GSS_COMPLETE; 425 | } else { 426 | status = AUTH_GSS_CONTINUE; 427 | } 428 | done: 429 | if (haveToken) { 430 | free(inBufs[tokenBufferIndex].pvBuffer); 431 | } 432 | if (outBufs[0].pvBuffer) { 433 | FreeContextBuffer(outBufs[0].pvBuffer); 434 | } 435 | return status; 436 | } 437 | 438 | INT 439 | auth_sspi_server_step(sspi_server_state* state, SEC_CHAR* challenge) { 440 | SecBufferDesc inbuf; 441 | SecBuffer inBufs[1]; 442 | SecBufferDesc outbuf; 443 | SecBuffer outBufs[1]; 444 | ULONG ignored; 445 | SECURITY_STATUS status = AUTH_GSS_CONTINUE; 446 | DWORD len; 447 | 448 | if (state->response != NULL) { 449 | free(state->response); 450 | state->response = NULL; 451 | } 452 | 453 | inbuf.ulVersion = SECBUFFER_VERSION; 454 | inbuf.pBuffers = inBufs; 455 | inbuf.cBuffers = 1; 456 | inBufs[0].BufferType = SECBUFFER_TOKEN; 457 | inBufs[0].pvBuffer = base64_decode(challenge, &len); 458 | if (!inBufs[0].pvBuffer) { 459 | return AUTH_GSS_ERROR; 460 | } 461 | inBufs[0].cbBuffer = len; 462 | 463 | outbuf.ulVersion = SECBUFFER_VERSION; 464 | outbuf.cBuffers = 1; 465 | outbuf.pBuffers = outBufs; 466 | outBufs[0].pvBuffer = NULL; 467 | outBufs[0].cbBuffer = 0; 468 | outBufs[0].BufferType = SECBUFFER_TOKEN; 469 | 470 | Py_BEGIN_ALLOW_THREADS 471 | status = AcceptSecurityContext(/* CredHandle */ 472 | &state->cred, 473 | /* CtxtHandle (NULL on first call) */ 474 | state->haveCtx ? &state->ctx : NULL, 475 | /* Buff */ 476 | &inbuf, 477 | /* Flags */ 478 | ASC_REQ_ALLOCATE_MEMORY, 479 | /* Target data representation */ 480 | SECURITY_NETWORK_DREP, 481 | /* CtxtHandle (Set on first call) */ 482 | &state->ctx, 483 | /* Output */ 484 | &outbuf, 485 | /* Context attributes */ 486 | &ignored, 487 | /* Expiry (We don't use this) */ 488 | NULL); 489 | Py_END_ALLOW_THREADS 490 | if (status != SEC_E_OK && status != SEC_I_CONTINUE_NEEDED) { 491 | set_gsserror(status, "AcceptSecurityContext"); 492 | status = AUTH_GSS_ERROR; 493 | goto done; 494 | } 495 | state->haveCtx = 1; 496 | if (outBufs[0].cbBuffer) { 497 | state->response = base64_encode(outBufs[0].pvBuffer, 498 | outBufs[0].cbBuffer); 499 | if (!state->response) { 500 | status = AUTH_GSS_ERROR; 501 | goto done; 502 | } 503 | } 504 | if (status == SEC_E_OK) { 505 | /* Get authenticated username. */ 506 | SecPkgContext_NamesW names; 507 | status = QueryContextAttributesW( 508 | &state->ctx, SECPKG_ATTR_NAMES, &names); 509 | if (status != SEC_E_OK) { 510 | set_gsserror(status, "QueryContextAttributesW"); 511 | status = AUTH_GSS_ERROR; 512 | goto done; 513 | } 514 | state->username = wide_to_utf8(names.sUserName); 515 | if (state->username == NULL) { 516 | FreeContextBuffer(names.sUserName); 517 | status = AUTH_GSS_ERROR; 518 | goto done; 519 | } 520 | FreeContextBuffer(names.sUserName); 521 | status = AUTH_GSS_COMPLETE; 522 | } 523 | else { 524 | status = AUTH_GSS_CONTINUE; 525 | } 526 | done: 527 | if (inBufs[0].pvBuffer) { 528 | free(inBufs[0].pvBuffer); 529 | } 530 | if (outBufs[0].pvBuffer) { 531 | FreeContextBuffer(outBufs[0].pvBuffer); 532 | } 533 | return status; 534 | } 535 | 536 | INT 537 | auth_sspi_client_unwrap(sspi_client_state* state, SEC_CHAR* challenge) { 538 | SECURITY_STATUS status; 539 | DWORD len; 540 | SecBuffer wrapBufs[2]; 541 | SecBufferDesc wrapBufDesc; 542 | wrapBufDesc.ulVersion = SECBUFFER_VERSION; 543 | wrapBufDesc.cBuffers = 2; 544 | wrapBufDesc.pBuffers = wrapBufs; 545 | 546 | if (state->response != NULL) { 547 | free(state->response); 548 | state->response = NULL; 549 | state->qop = SECQOP_WRAP_NO_ENCRYPT; 550 | } 551 | 552 | if (!state->haveCtx) { 553 | set_uninitialized_context(); 554 | return AUTH_GSS_ERROR; 555 | } 556 | 557 | wrapBufs[0].pvBuffer = base64_decode(challenge, &len); 558 | if (!wrapBufs[0].pvBuffer) { 559 | return AUTH_GSS_ERROR; 560 | } 561 | wrapBufs[0].cbBuffer = len; 562 | wrapBufs[0].BufferType = SECBUFFER_STREAM; 563 | 564 | wrapBufs[1].pvBuffer = NULL; 565 | wrapBufs[1].cbBuffer = 0; 566 | wrapBufs[1].BufferType = SECBUFFER_DATA; 567 | 568 | status = DecryptMessage(&state->ctx, &wrapBufDesc, 0, &state->qop); 569 | if (status == SEC_E_OK) { 570 | status = AUTH_GSS_COMPLETE; 571 | } else { 572 | set_gsserror(status, "DecryptMessage"); 573 | status = AUTH_GSS_ERROR; 574 | goto done; 575 | } 576 | if (wrapBufs[1].cbBuffer) { 577 | state->response = base64_encode(wrapBufs[1].pvBuffer, 578 | wrapBufs[1].cbBuffer); 579 | if (!state->response) { 580 | status = AUTH_GSS_ERROR; 581 | } 582 | } 583 | done: 584 | if (wrapBufs[0].pvBuffer) { 585 | free(wrapBufs[0].pvBuffer); 586 | } 587 | return status; 588 | } 589 | 590 | INT 591 | auth_sspi_client_wrap(sspi_client_state* state, 592 | SEC_CHAR* data, 593 | SEC_CHAR* user, 594 | ULONG ulen, 595 | INT protect) { 596 | SECURITY_STATUS status; 597 | SecPkgContext_Sizes sizes; 598 | SecBuffer wrapBufs[3]; 599 | SecBufferDesc wrapBufDesc; 600 | SEC_CHAR* decodedData = NULL; 601 | SEC_CHAR* inbuf; 602 | SIZE_T inbufSize; 603 | SEC_CHAR* outbuf; 604 | DWORD outbufSize; 605 | SEC_CHAR* plaintextMessage; 606 | ULONG plaintextMessageSize; 607 | 608 | if (state->response != NULL) { 609 | free(state->response); 610 | state->response = NULL; 611 | } 612 | 613 | if (!state->haveCtx) { 614 | set_uninitialized_context(); 615 | return AUTH_GSS_ERROR; 616 | } 617 | 618 | status = QueryContextAttributes(&state->ctx, SECPKG_ATTR_SIZES, &sizes); 619 | if (status != SEC_E_OK) { 620 | set_gsserror(status, "QueryContextAttributes"); 621 | return AUTH_GSS_ERROR; 622 | } 623 | 624 | if (user) { 625 | /* Length of user + 4 bytes for security layer (see below). */ 626 | plaintextMessageSize = ulen + 4; 627 | } else { 628 | decodedData = base64_decode(data, &plaintextMessageSize); 629 | if (!decodedData) { 630 | return AUTH_GSS_ERROR; 631 | } 632 | } 633 | 634 | inbufSize = 635 | sizes.cbSecurityTrailer + plaintextMessageSize + sizes.cbBlockSize; 636 | inbuf = (SEC_CHAR*)malloc(inbufSize); 637 | if (inbuf == NULL) { 638 | free(decodedData); 639 | PyErr_SetNone(PyExc_MemoryError); 640 | return AUTH_GSS_ERROR; 641 | } 642 | 643 | plaintextMessage = inbuf + sizes.cbSecurityTrailer; 644 | if (user) { 645 | /* Authenticate the provided user. Unlike pykerberos, we don't 646 | * need any information from "data" to do that. 647 | * */ 648 | plaintextMessage[0] = 1; /* No security layer */ 649 | plaintextMessage[1] = 0; 650 | plaintextMessage[2] = 0; 651 | plaintextMessage[3] = 0; 652 | memcpy_s( 653 | plaintextMessage + 4, 654 | inbufSize - sizes.cbSecurityTrailer - 4, 655 | user, 656 | strlen(user)); 657 | } else { 658 | /* No user provided. Just rewrap data. */ 659 | memcpy_s( 660 | plaintextMessage, 661 | inbufSize - sizes.cbSecurityTrailer, 662 | decodedData, 663 | plaintextMessageSize); 664 | free(decodedData); 665 | } 666 | 667 | wrapBufDesc.cBuffers = 3; 668 | wrapBufDesc.pBuffers = wrapBufs; 669 | wrapBufDesc.ulVersion = SECBUFFER_VERSION; 670 | 671 | wrapBufs[0].cbBuffer = sizes.cbSecurityTrailer; 672 | wrapBufs[0].BufferType = SECBUFFER_TOKEN; 673 | wrapBufs[0].pvBuffer = inbuf; 674 | 675 | wrapBufs[1].cbBuffer = (ULONG)plaintextMessageSize; 676 | wrapBufs[1].BufferType = SECBUFFER_DATA; 677 | wrapBufs[1].pvBuffer = inbuf + sizes.cbSecurityTrailer; 678 | 679 | wrapBufs[2].cbBuffer = sizes.cbBlockSize; 680 | wrapBufs[2].BufferType = SECBUFFER_PADDING; 681 | wrapBufs[2].pvBuffer = 682 | inbuf + (sizes.cbSecurityTrailer + plaintextMessageSize); 683 | 684 | status = EncryptMessage( 685 | &state->ctx, 686 | protect ? 0 : SECQOP_WRAP_NO_ENCRYPT, 687 | &wrapBufDesc, 688 | 0); 689 | if (status != SEC_E_OK) { 690 | free(inbuf); 691 | set_gsserror(status, "EncryptMessage"); 692 | return AUTH_GSS_ERROR; 693 | } 694 | 695 | outbufSize = 696 | wrapBufs[0].cbBuffer + wrapBufs[1].cbBuffer + wrapBufs[2].cbBuffer; 697 | outbuf = (SEC_CHAR*)malloc(sizeof(SEC_CHAR) * outbufSize); 698 | if (outbuf == NULL) { 699 | free(inbuf); 700 | PyErr_SetNone(PyExc_MemoryError); 701 | return AUTH_GSS_ERROR; 702 | } 703 | memcpy_s(outbuf, 704 | outbufSize, 705 | wrapBufs[0].pvBuffer, 706 | wrapBufs[0].cbBuffer); 707 | memcpy_s(outbuf + wrapBufs[0].cbBuffer, 708 | outbufSize - wrapBufs[0].cbBuffer, 709 | wrapBufs[1].pvBuffer, 710 | wrapBufs[1].cbBuffer); 711 | memcpy_s(outbuf + wrapBufs[0].cbBuffer + wrapBufs[1].cbBuffer, 712 | outbufSize - wrapBufs[0].cbBuffer - wrapBufs[1].cbBuffer, 713 | wrapBufs[2].pvBuffer, 714 | wrapBufs[2].cbBuffer); 715 | state->response = base64_encode(outbuf, outbufSize); 716 | if (!state->response) { 717 | status = AUTH_GSS_ERROR; 718 | } else { 719 | status = AUTH_GSS_COMPLETE; 720 | } 721 | free(inbuf); 722 | free(outbuf); 723 | return status; 724 | } 725 | -------------------------------------------------------------------------------- /src/winkerberos.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 MongoDB, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include "kerberos_sspi.h" 18 | 19 | #include 20 | 21 | #define PyInt_FromLong PyLong_FromLong 22 | #define PyString_FromString PyUnicode_FromString 23 | 24 | #define MECH_OID_KRB5_CAPSULE_NAME "winkerberos.GSS_MECH_OID_KRB5" 25 | #define MECH_OID_SPNEGO_CAPSULE_NAME "winkerberos.GSS_MECH_OID_SPNEGO" 26 | #define CLIENT_CTX_CAPSULE_NAME "GSSAPIClientContext" 27 | #define SERVER_CTX_CAPSULE_NAME "GSSAPIServerContext" 28 | #define CHANNEL_BINDINGS_CTX_CAPSULE_NAME "GSSAPIChannelBindingsContext" 29 | 30 | PyDoc_STRVAR(winkerberos_documentation, 31 | "A native Kerberos SSPI client implementation.\n" 32 | "\n" 33 | "This module mimics the client API of pykerberos to implement\n" 34 | "Kerberos SSPI authentication on Microsoft Windows."); 35 | 36 | PyObject* KrbError; 37 | /* Note - also defined extern in kerberos_sspi.c */ 38 | PyObject* GSSError; 39 | 40 | static BOOL 41 | _string_too_long(const SEC_CHAR* key, SIZE_T len) { 42 | if (len > ULONG_MAX) { 43 | PyErr_Format(PyExc_ValueError, "%s too large", key); 44 | return TRUE; 45 | } 46 | return FALSE; 47 | } 48 | 49 | static BOOL 50 | _py_buffer_to_wchar(PyObject* obj, WCHAR** out, Py_ssize_t* outlen) { 51 | Py_buffer view; 52 | WCHAR* outbuf; 53 | INT result_len; 54 | BOOL result = FALSE; 55 | if (PyObject_GetBuffer(obj, &view, PyBUF_SIMPLE) == -1) { 56 | return FALSE; 57 | } 58 | if (!PyBuffer_IsContiguous(&view, 'C')) { 59 | PyErr_SetString(PyExc_ValueError, 60 | "must be a contiguous buffer"); 61 | goto done; 62 | } 63 | if (!view.buf || view.len < 0) { 64 | PyErr_SetString(PyExc_ValueError, "invalid buffer"); 65 | goto done; 66 | } 67 | if (view.itemsize != 1) { 68 | PyErr_SetString(PyExc_ValueError, 69 | "buffer data must be ascii or utf8"); 70 | goto done; 71 | } 72 | if (view.len > INT_MAX) { 73 | /* MultiByteToWideChar expects length as a signed int. */ 74 | PyErr_SetString(PyExc_ValueError, "buffer too large"); 75 | goto done; 76 | } 77 | outbuf = (WCHAR*)malloc(sizeof(WCHAR) * (view.len + 1)); 78 | if (!outbuf) { 79 | PyErr_SetNone(PyExc_MemoryError); 80 | goto done; 81 | } 82 | result_len = MultiByteToWideChar( 83 | CP_UTF8, 0, (CHAR*)view.buf, (INT)view.len, outbuf, (INT)view.len); 84 | if (!result_len) { 85 | set_gsserror(GetLastError(), "MultiByteToWideChar failed"); 86 | free(outbuf); 87 | goto done; 88 | } 89 | outbuf[result_len] = L'\0'; 90 | *outlen = (Py_ssize_t)result_len; 91 | *out = outbuf; 92 | result = TRUE; 93 | done: 94 | PyBuffer_Release(&view); 95 | return result; 96 | } 97 | 98 | static BOOL 99 | _py_unicode_to_wchar(PyObject* obj, WCHAR** out, Py_ssize_t* outlen) { 100 | Py_ssize_t res; 101 | Py_ssize_t len = PyUnicode_GET_LENGTH(obj); 102 | WCHAR* buf = (WCHAR*)malloc(sizeof(WCHAR) * (len + 1)); 103 | if (!buf) { 104 | PyErr_SetNone(PyExc_MemoryError); 105 | return FALSE; 106 | } 107 | #if PY_VERSION_HEX < 0x03020000 108 | res = PyUnicode_AsWideChar((PyUnicodeObject*)obj, buf, len); 109 | #else 110 | res = PyUnicode_AsWideChar(obj, buf, len); 111 | #endif 112 | if (res == -1) { 113 | goto fail; 114 | } 115 | buf[len] = L'\0'; 116 | if (wcslen(buf) != (size_t)len) { 117 | PyErr_SetString(PyExc_ValueError, "embedded null character"); 118 | goto fail; 119 | } 120 | *out = buf; 121 | *outlen = len; 122 | return TRUE; 123 | 124 | fail: 125 | free(buf); 126 | return FALSE; 127 | } 128 | 129 | static BOOL 130 | BufferObject_AsWCHAR(PyObject* arg, WCHAR** out, Py_ssize_t* outlen) { 131 | if (arg == Py_None) { 132 | *out = NULL; 133 | *outlen = 0; 134 | return TRUE; 135 | } else if (PyUnicode_Check(arg)){ 136 | return _py_unicode_to_wchar(arg, out, outlen); 137 | } else { 138 | return _py_buffer_to_wchar(arg, out, outlen); 139 | } 140 | } 141 | 142 | static BOOL 143 | StringObject_AsWCHAR(PyObject* arg, 144 | INT argnum, 145 | BOOL allow_none, 146 | WCHAR** out, 147 | Py_ssize_t* outlen) { 148 | if (arg == Py_None && allow_none) { 149 | *out = NULL; 150 | *outlen = 0; 151 | return TRUE; 152 | #if PY_MAJOR_VERSION < 3 153 | } else if (PyString_Check(arg)) { 154 | BOOL result; 155 | PyObject* localobj = PyUnicode_FromEncodedObject(arg, NULL, "strict"); 156 | if (!localobj) { 157 | return FALSE; 158 | } 159 | result = _py_unicode_to_wchar(localobj, out, outlen); 160 | Py_DECREF(localobj); 161 | return result; 162 | #endif 163 | } else if (PyUnicode_Check(arg)){ 164 | return _py_unicode_to_wchar(arg, out, outlen); 165 | } else { 166 | PyErr_Format( 167 | PyExc_TypeError, 168 | #if PY_MAJOR_VERSION < 3 169 | "argument %d must be string%s, not %s", 170 | #else 171 | "argument %d must be str%s, not %s", 172 | #endif 173 | argnum, 174 | allow_none ? " or None" : "", 175 | (arg == Py_None) ? "None" : arg->ob_type->tp_name); 176 | return FALSE; 177 | } 178 | } 179 | 180 | static VOID 181 | destroy_sspi_client(PyObject* obj) { 182 | sspi_client_state* state = PyCapsule_GetPointer(obj, PyCapsule_GetName(obj)); 183 | if (state) { 184 | destroy_sspi_client_state(state); 185 | free(state); 186 | } 187 | } 188 | 189 | static VOID 190 | destroy_sspi_server(PyObject* obj) { 191 | sspi_server_state* state = PyCapsule_GetPointer(obj, PyCapsule_GetName(obj)); 192 | if (state) { 193 | destroy_sspi_server_state(state); 194 | free(state); 195 | } 196 | } 197 | 198 | PyDoc_STRVAR(sspi_client_init_doc, 199 | "authGSSClientInit(service, principal=None, gssflags=" 200 | "GSS_C_MUTUAL_FLAG|GSS_C_SEQUENCE_FLAG, user=None, domain=None," 201 | " password=None, mech_oid=GSS_MECH_OID_KRB5)\n" 202 | "\n" 203 | "Initializes a context for Kerberos SSPI client side authentication with\n" 204 | "the given service principal.\n" 205 | "\n" 206 | "The following flags are available (with SSPI mapping)::\n" 207 | "\n" 208 | " GSS_C_DELEG_FLAG (ISC_REQ_DELEG)\n" 209 | " GSS_C_MUTUAL_FLAG (ISC_REQ_MUTUAL_AUTH)\n" 210 | " GSS_C_REPLAY_FLAG (ISC_REQ_REPLAY_DETECT)\n" 211 | " GSS_C_SEQUENCE_FLAG (ISC_REQ_SEQUENCE_DETECT)\n" 212 | " GSS_C_CONF_FLAG (ISC_REQ_CONFIDENTIALITY)\n" 213 | " GSS_C_INTEG_FLAG (ISC_REQ_INTEGRITY)\n" 214 | "\n" 215 | "The following flags are *not* available as they have no mapping in SSPI::\n" 216 | "\n" 217 | " GSS_C_ANON_FLAG\n" 218 | " GSS_C_PROT_READY_FLAG\n" 219 | " GSS_C_TRANS_FLAG\n" 220 | "\n" 221 | ":Parameters:\n" 222 | " - `service`: A string containing the service principal in RFC-2078 format\n" 223 | " (``service@hostname``) or SPN format (``service/hostname`` or\n" 224 | " ``service/hostname@REALM``).\n" 225 | " - `principal`: An optional string containing the user principal name in\n" 226 | " the format ``user@realm``. Can be unicode (str in python 3.x) or any 8 \n" 227 | " bit string type that implements the buffer interface. A password can \n" 228 | " be provided using the format ``user@realm:password``. The principal \n" 229 | " and password can be percent encoded if either might include the ``:`` \n" 230 | " character::\n" 231 | "\n" 232 | " try:\n" 233 | " # Python 3.x\n" 234 | " from urllib.parse import quote\n" 235 | " except ImportError:\n" 236 | " # Python 2.x\n" 237 | " from urllib import quote\n" 238 | " principal = '%s:%s' % (\n" 239 | " quote(user_principal), quote(password))\n" 240 | "\n" 241 | " If the `user` parameter is provided `principal` is ignored.\n" 242 | " - `gssflags`: An optional integer used to set GSS flags. Defaults to\n" 243 | " GSS_C_MUTUAL_FLAG|GSS_C_SEQUENCE_FLAG.\n" 244 | " - `user` (DEPRECATED): An optional string that contains the name of the \n" 245 | " user whose credentials should be used for authentication.\n" 246 | " - `domain` (DEPRECATED): An optional string that contains the domain or \n" 247 | " workgroup name for `user`.\n" 248 | " - `password` (DEPRECATED): An optional string that contains the password \n" 249 | " for `user` in `domain`. Can be unicode (str in python 3.x) or any 8 \n" 250 | " bit string type that implements the buffer interface.\n" 251 | " - `mech_oid`: Optional GSS mech OID. Defaults to GSS_MECH_OID_KRB5.\n" 252 | " Another possible value is GSS_MECH_OID_SPNEGO." 253 | "\n" 254 | ":Returns: A tuple of (result, context) where result is\n" 255 | " :data:`AUTH_GSS_COMPLETE` and context is an opaque value passed\n" 256 | " in subsequent function calls.\n" 257 | "\n" 258 | ".. versionchanged:: 0.5.0\n" 259 | " The `principal` parameter actually works now. Deprecated the `user`,\n" 260 | " `domain`, and `password` parameters.\n" 261 | ".. versionchanged:: 0.6.0\n" 262 | " Added support for the `mech_oid` parameter.\n"); 263 | 264 | static PyObject* 265 | sspi_client_init(PyObject* self, PyObject* args, PyObject* kw) { 266 | sspi_client_state* state; 267 | PyObject* pyctx = NULL; 268 | PyObject* serviceobj; 269 | PyObject* principalobj = Py_None; 270 | LONG flags = ISC_REQ_MUTUAL_AUTH | ISC_REQ_SEQUENCE_DETECT; 271 | PyObject* userobj = Py_None; 272 | PyObject* domainobj = Py_None; 273 | PyObject* passwordobj = Py_None; 274 | PyObject* mechoidobj = Py_None; 275 | WCHAR *service = NULL, *principal = NULL; 276 | WCHAR *user = NULL, *domain = NULL, *password = NULL; 277 | Py_ssize_t slen, len, ulen, dlen, plen = 0; 278 | WCHAR *mechoid = GSS_MECH_OID_KRB5; 279 | PyObject* resultobj = NULL; 280 | INT result = 0; 281 | static SEC_CHAR* keywords[] = { 282 | "service", "principal", "gssflags", "user", "domain", "password", "mech_oid", NULL}; 283 | 284 | if (!PyArg_ParseTupleAndKeywords(args, 285 | kw, 286 | "O|OlOOOO", 287 | keywords, 288 | &serviceobj, 289 | &principalobj, 290 | &flags, 291 | &userobj, 292 | &domainobj, 293 | &passwordobj, 294 | &mechoidobj)) { 295 | return NULL; 296 | } 297 | if (flags < 0) { 298 | PyErr_SetString(PyExc_ValueError, "gss_flags must be >= 0"); 299 | return NULL; 300 | } 301 | 302 | if (!StringObject_AsWCHAR(serviceobj, 1, FALSE, &service, &slen) || 303 | !BufferObject_AsWCHAR(principalobj, &principal, &len) || 304 | !StringObject_AsWCHAR(userobj, 4, TRUE, &user, &ulen) || 305 | !StringObject_AsWCHAR(domainobj, 5, TRUE, &domain, &dlen) || 306 | !BufferObject_AsWCHAR(passwordobj, &password, &plen) || 307 | _string_too_long("user", (SIZE_T)ulen) || 308 | _string_too_long("domain", (SIZE_T)dlen) || 309 | _string_too_long("password", (SIZE_T)plen)) { 310 | goto done; 311 | } 312 | 313 | /* Prefer (user, domain, password) for backward compatibility. */ 314 | if (!user && principal) { 315 | HRESULT res; 316 | /* Use (user, domain, password) or principal, not a mix of both. */ 317 | free(domain); 318 | domain = NULL; 319 | if (password) { 320 | SecureZeroMemory(password, sizeof(WCHAR) * plen); 321 | free(password); 322 | password = NULL; 323 | } 324 | /* Support password as part of the principal parameter. */ 325 | if (wcschr(principal, L':')) { 326 | WCHAR* current; 327 | WCHAR* next; 328 | current = wcstok_s(principal, L":", &next); 329 | if (!current) { 330 | goto memoryerror; 331 | } 332 | user = _wcsdup(current); 333 | if (!user) { 334 | goto memoryerror; 335 | } 336 | current = wcstok_s(NULL, L":", &next); 337 | if (!current) { 338 | goto memoryerror; 339 | } 340 | password = _wcsdup(current); 341 | if (!password) { 342 | goto memoryerror; 343 | } 344 | } else { 345 | user = _wcsdup(principal); 346 | if (!user) { 347 | goto memoryerror; 348 | } 349 | } 350 | /* Support user principal or password including the : character. */ 351 | res = UrlUnescapeW(user, NULL, NULL, URL_UNESCAPE_INPLACE); 352 | if (res != S_OK) { 353 | set_gsserror(res, "UrlUnescapeW"); 354 | goto done; 355 | } 356 | if (password) { 357 | res = UrlUnescapeW(password, NULL, NULL, URL_UNESCAPE_INPLACE); 358 | if (res != S_OK) { 359 | set_gsserror(res, "UrlUnescapeW"); 360 | goto done; 361 | } 362 | plen = wcslen(password); 363 | } 364 | ulen = wcslen(user); 365 | } 366 | 367 | if (mechoidobj != Py_None) { 368 | if (!PyCapsule_CheckExact(mechoidobj)) { 369 | PyErr_SetString(PyExc_TypeError, "Invalid type for mech_oid"); 370 | goto done; 371 | } 372 | /* TODO: check that PyCapsule_GetName returns one of the two valid names. */ 373 | mechoid = (WCHAR*)PyCapsule_GetPointer(mechoidobj, PyCapsule_GetName(mechoidobj)); 374 | if (mechoid == NULL) { 375 | PyErr_SetString(PyExc_TypeError, "Invalid value for mech_oid"); 376 | goto done; 377 | } 378 | } 379 | 380 | state = (sspi_client_state*)malloc(sizeof(sspi_client_state)); 381 | if (state == NULL) { 382 | goto memoryerror; 383 | } 384 | 385 | pyctx = PyCapsule_New( 386 | state, CLIENT_CTX_CAPSULE_NAME, &destroy_sspi_client); 387 | if (pyctx == NULL) { 388 | free(state); 389 | goto done; 390 | } 391 | 392 | result = auth_sspi_client_init( 393 | service, (ULONG)flags, 394 | user, (ULONG)ulen, domain, (ULONG)dlen, password, (ULONG)plen, mechoid, state); 395 | if (result == AUTH_GSS_ERROR) { 396 | Py_DECREF(pyctx); 397 | goto done; 398 | } 399 | 400 | resultobj = Py_BuildValue("(iN)", result, pyctx); 401 | goto done; 402 | 403 | memoryerror: 404 | PyErr_SetNone(PyExc_MemoryError); 405 | 406 | done: 407 | free(service); 408 | /* The principal parameter can include a password. */ 409 | if (principal) { 410 | SecureZeroMemory(principal, sizeof(WCHAR) * len); 411 | free(principal); 412 | } 413 | free(user); 414 | free(domain); 415 | if (password) { 416 | SecureZeroMemory(password, sizeof(WCHAR) * plen); 417 | free(password); 418 | } 419 | return resultobj; 420 | } 421 | 422 | PyDoc_STRVAR(sspi_server_init_doc, 423 | "authGSSServerInit(service)\n" 424 | "\n" 425 | "Initializes a context for Kerberos SSPI server side authentication with\n" 426 | "the given service principal.\n" 427 | "\n" 428 | ":Parameters:\n" 429 | " - `service`: A string containing the service principal in RFC-2078 format\n" 430 | " (``service@hostname``) or SPN format (``service/hostname`` or\n" 431 | " ``service/hostname@REALM``).\n" 432 | "\n" 433 | ":Returns: A tuple of (result, context) where result is\n" 434 | " :data:`AUTH_GSS_COMPLETE` and context is an opaque value passed\n" 435 | " in subsequent function calls.\n" 436 | "\n" 437 | ".. versionadded:: 0.8.0"); 438 | 439 | static PyObject* 440 | sspi_server_init(PyObject* self, PyObject* args) { 441 | sspi_server_state* state; 442 | PyObject* pyctx = NULL; 443 | PyObject* serviceobj; 444 | WCHAR *service = NULL; 445 | Py_ssize_t slen = 0; 446 | PyObject* resultobj = NULL; 447 | INT result = 0; 448 | 449 | if (!PyArg_ParseTuple(args, "O", &serviceobj)) { 450 | return NULL; 451 | } 452 | 453 | if (!StringObject_AsWCHAR(serviceobj, 1, FALSE, &service, &slen)) { 454 | goto done; 455 | } 456 | 457 | state = (sspi_server_state*)malloc(sizeof(sspi_server_state)); 458 | if (state == NULL) { 459 | goto memoryerror; 460 | } 461 | 462 | pyctx = PyCapsule_New( 463 | state, SERVER_CTX_CAPSULE_NAME, &destroy_sspi_server); 464 | if (pyctx == NULL) { 465 | free(state); 466 | goto done; 467 | } 468 | 469 | result = auth_sspi_server_init(service, state); 470 | if (result == AUTH_GSS_ERROR) { 471 | Py_DECREF(pyctx); 472 | goto done; 473 | } 474 | 475 | resultobj = Py_BuildValue("(iN)", result, pyctx); 476 | goto done; 477 | 478 | memoryerror: 479 | PyErr_SetNone(PyExc_MemoryError); 480 | 481 | done: 482 | free(service); 483 | return resultobj; 484 | } 485 | 486 | PyDoc_STRVAR(sspi_client_clean_doc, 487 | "authGSSClientClean(context)\n" 488 | "\n" 489 | "Destroys the client context. This function is provided for API\n" 490 | "compatibility with pykerberos but does nothing. The context object\n" 491 | "destroys itself when it is reclaimed.\n" 492 | "\n" 493 | ":Parameters:\n" 494 | " - `context`: The context object returned by :func:`authGSSClientInit`.\n" 495 | "\n" 496 | ":Returns: :data:`AUTH_GSS_COMPLETE`"); 497 | 498 | static PyObject* 499 | sspi_client_clean(PyObject* self, PyObject* args) { 500 | /* Do nothing. For compatibility with pykerberos only. */ 501 | return Py_BuildValue("i", AUTH_GSS_COMPLETE); 502 | } 503 | 504 | PyDoc_STRVAR(sspi_server_clean_doc, 505 | "authGSSServerClean(context)\n" 506 | "\n" 507 | "Destroys the server context. This function is provided for API\n" 508 | "compatibility with pykerberos but does nothing. The context object\n" 509 | "destroys itself when it is reclaimed.\n" 510 | "\n" 511 | ":Parameters:\n" 512 | " - `context`: The context object returned by :func:`authGSSServerInit`.\n" 513 | "\n" 514 | ":Returns: :data:`AUTH_GSS_COMPLETE`\n" 515 | "\n" 516 | ".. versionadded:: 0.8.0"); 517 | 518 | static PyObject* 519 | sspi_server_clean(PyObject* self, PyObject* args) { 520 | /* Do nothing. For compatibility with pykerberos only. */ 521 | return Py_BuildValue("i", AUTH_GSS_COMPLETE); 522 | } 523 | 524 | void destruct_channel_bindings_struct(PyObject* o) { 525 | SecPkgContext_Bindings* context_bindings = PyCapsule_GetPointer(o, PyCapsule_GetName(o)); 526 | if (context_bindings != NULL) { 527 | free(context_bindings->Bindings); 528 | free(context_bindings); 529 | } 530 | } 531 | 532 | PyDoc_STRVAR(sspi_channel_bindings_doc, 533 | "channelBindings(**kwargs)\n" 534 | "\n" 535 | "Builds a `SecPkgContext_Bindings` struct and returns an opaque pointer to\n" 536 | "it. The return value can be passed to :func:`authGSSClientStep` using the\n" 537 | "``channel_bindings`` keyword argument. The SecPkgContext_Bindings object\n" 538 | "destroys itself when it is reclaimed. While the parameters supported by\n" 539 | "this method match the structure of the GSSAPI `gss_channel_bindings_t\n" 540 | "`_,\n" 541 | "Windows only supports the ``application_data`` parameter for now. The other\n" 542 | "parameters are kept for compatibility with `ccs-pykerberos\n" 543 | "`_ and future compatibility.\n" 544 | "\n" 545 | "If using an endpoint over TLS like IIS with Extended Protection or WinRM\n" 546 | "with the CbtHardeningLevel set to Strict then you can use this method to\n" 547 | "build the channel bindings structure required for a successful\n" 548 | "authentication. Following `RFC5929 - Channel Bindings for TLS\n" 549 | "`_, pass the value\n" 550 | "\"tls-server-end-point:{cert-hash}\" as the ``application_data`` argument.\n" 551 | "\n" 552 | "When using requests and cryptography you can access the server's\n" 553 | "certificate and create the channel bindings like this::\n" 554 | "\n" 555 | " from cryptography import x509\n" 556 | " from cryptography.hazmat.backends import default_backend\n" 557 | " from cryptography.hazmat.primitives import hashes\n" 558 | "\n" 559 | " if sys.version_info > (3, 0):\n" 560 | " socket = response.raw._fp.fp.raw._sock\n" 561 | " else:\n" 562 | " socket = response.raw._fp.fp._sock\n" 563 | " server_certificate = socket.getpeercert(True)\n" 564 | " cert = x509.load_der_x509_certificate(server_certificate, default_backend())\n" 565 | " hash_algorithm = cert.signature_hash_algorithm\n" 566 | " if hash_algorithm.name in ('md5', 'sha1'):\n" 567 | " digest = hashes.Hash(hashes.SHA256(), default_backend())\n" 568 | " else:\n" 569 | " digest = hashes.Hash(hash_algorithm, default_backend())\n" 570 | " digest.update(server_certificate)\n" 571 | " application_data = b'tls-server-end-point:' + digest.finalize()\n" 572 | " channel_bindings = winkerberos.channelBindings(application_data=application_data)\n" 573 | "\n" 574 | "See ``_ for the rules\n" 575 | "regarding hash algorithm choice.\n" 576 | "\n" 577 | ":Parameters:\n" 578 | " - `initiator_addrtype`: Optional int specifying the initiator address type.\n" 579 | " Defaults to :data:`GSS_C_AF_UNSPEC`.\n" 580 | " - `initiator_address`: Optional byte string containing the initiator address.\n" 581 | " - `acceptor_addrtype`: Optional int specifying the acceptor address type.\n" 582 | " Defaults to :data:`GSS_C_AF_UNSPEC`.\n" 583 | " - `acceptor_address`: Optional byte string containing the acceptor address.\n" 584 | " - `application_data`: Optional byte string containing the application data.\n" 585 | " An example of this would be 'tls-server-end-point:{cert-hash}' where\n" 586 | " {cert-hash} is the hash of the server's certificate.\n" 587 | "\n" 588 | ":Returns: An opaque value to be passed to the ``channel_bindings`` parameter of\n" 589 | " :func:`authGSSClientStep`\n" 590 | "\n" 591 | ".. versionadded:: 0.7.0"); 592 | 593 | static PyObject* 594 | sspi_channel_bindings(PyObject* self, PyObject* args, PyObject* keywds) { 595 | static char* kwlist[] = {"initiator_addrtype", "initiator_address", "acceptor_addrtype", 596 | "acceptor_address", "application_data", NULL}; 597 | 598 | SEC_CHANNEL_BINDINGS* channel_bindings; 599 | SecPkgContext_Bindings* context_bindings; 600 | PyObject* pychan_bindings = NULL; 601 | 602 | unsigned long initiator_addrtype = 0; 603 | unsigned long acceptor_addrtype = 0; 604 | Py_ssize_t initiator_length = 0; 605 | Py_ssize_t acceptor_length = 0; 606 | Py_ssize_t application_length = 0; 607 | char* initiator_address = NULL; 608 | char* acceptor_address = NULL; 609 | char* application_data = NULL; 610 | 611 | unsigned long offset = 0; 612 | unsigned long data_length = 0; 613 | unsigned char* offset_p = NULL; 614 | 615 | if (!PyArg_ParseTupleAndKeywords(args, keywds, "|ks#ks#s#", kwlist, 616 | &initiator_addrtype, &initiator_address, &initiator_length, 617 | &acceptor_addrtype, &acceptor_address, &acceptor_length, 618 | &application_data, &application_length)) { 619 | return NULL; 620 | } 621 | 622 | if (_string_too_long("initiator_address", initiator_length)) { 623 | return NULL; 624 | } 625 | if (_string_too_long("acceptor_address", acceptor_length)) { 626 | return NULL; 627 | } 628 | if (_string_too_long("application_data", application_length)) { 629 | return NULL; 630 | } 631 | 632 | data_length = (unsigned long)(initiator_length + acceptor_length + application_length); 633 | offset = (unsigned long)sizeof(SEC_CHANNEL_BINDINGS); 634 | 635 | context_bindings = (SecPkgContext_Bindings *)malloc(sizeof(SecPkgContext_Bindings)); 636 | if (context_bindings == NULL) { 637 | PyErr_SetString(PyExc_MemoryError, "Failed to allocate a memory block for SecPkgContext_Bindings"); 638 | return NULL; 639 | } 640 | context_bindings->BindingsLength = (unsigned long)sizeof(SEC_CHANNEL_BINDINGS) + data_length; 641 | 642 | channel_bindings = (SEC_CHANNEL_BINDINGS*)malloc(context_bindings->BindingsLength); 643 | if (channel_bindings == NULL) { 644 | free(context_bindings); 645 | PyErr_SetString(PyExc_MemoryError, "Failed to allocate a memory block for SEC_CHANNEL_BINDINGS"); 646 | return NULL; 647 | } 648 | context_bindings->Bindings = channel_bindings; 649 | 650 | channel_bindings->dwInitiatorAddrType = initiator_addrtype; 651 | channel_bindings->cbInitiatorLength = (unsigned long)initiator_length; 652 | if (initiator_address != NULL) { 653 | channel_bindings->dwInitiatorOffset = offset; 654 | 655 | offset_p = &((unsigned char*)channel_bindings)[offset]; 656 | memcpy_s(&offset_p[0], data_length, initiator_address, initiator_length); 657 | offset = offset + (unsigned long)initiator_length; 658 | } else { 659 | channel_bindings->dwInitiatorOffset = 0; 660 | } 661 | 662 | channel_bindings->dwAcceptorAddrType = acceptor_addrtype; 663 | channel_bindings->cbAcceptorLength = (unsigned long)acceptor_length; 664 | if (acceptor_address != NULL) { 665 | channel_bindings->dwAcceptorOffset = offset; 666 | 667 | offset_p = &((unsigned char*)channel_bindings)[offset]; 668 | memcpy_s(&offset_p[0], 669 | data_length - initiator_length, 670 | acceptor_address, 671 | acceptor_length); 672 | offset = offset + (unsigned long)acceptor_length; 673 | } else { 674 | channel_bindings->dwAcceptorOffset = 0; 675 | } 676 | 677 | channel_bindings->cbApplicationDataLength = (unsigned long)application_length; 678 | if (application_data != NULL) { 679 | channel_bindings->dwApplicationDataOffset = offset; 680 | offset_p = &((unsigned char*) channel_bindings)[offset]; 681 | memcpy_s(&offset_p[0], 682 | data_length - initiator_length - acceptor_length, 683 | application_data, 684 | application_length); 685 | } else { 686 | channel_bindings->dwApplicationDataOffset = 0; 687 | } 688 | 689 | pychan_bindings = PyCapsule_New( 690 | context_bindings, CHANNEL_BINDINGS_CTX_CAPSULE_NAME, &destruct_channel_bindings_struct); 691 | if (pychan_bindings == NULL) { 692 | free(channel_bindings); 693 | free(context_bindings); 694 | return NULL; 695 | } 696 | 697 | return Py_BuildValue("N", pychan_bindings); 698 | } 699 | 700 | PyDoc_STRVAR(sspi_client_step_doc, 701 | "authGSSClientStep(context, challenge, **kwargs)\n" 702 | "\n" 703 | "Executes a single Kerberos SSPI client step using the supplied server " 704 | "challenge.\n" 705 | "\n" 706 | ":Parameters:\n" 707 | " - `context`: The context object returned by :func:`authGSSClientInit`.\n" 708 | " - `challenge`: A string containing the base64 encoded server challenge.\n" 709 | " Ignored for the first step (pass the empty string).\n" 710 | " - `channel_bindings`: Optional SecPkgContext_Bindings structure\n" 711 | " returned by :func:`channelBindings`. This is used to bind\n" 712 | " the kerberos token with the TLS channel.\n" 713 | "\n" 714 | ":Returns: :data:`AUTH_GSS_CONTINUE` or :data:`AUTH_GSS_COMPLETE`"); 715 | 716 | static PyObject* 717 | sspi_client_step(PyObject* self, PyObject* args, PyObject* keywds) { 718 | sspi_client_state* state; 719 | PyObject* pyctx; 720 | SEC_CHAR* challenge = NULL; 721 | INT result = 0; 722 | PyObject* pychan_bindings = NULL; 723 | SecPkgContext_Bindings* sec_pkg_context_bindings = NULL; 724 | static char *kwlist[] = {"state", "challenge", "channel_bindings", NULL}; 725 | 726 | if (!PyArg_ParseTupleAndKeywords(args, keywds, "Os|O", kwlist, &pyctx, &challenge, &pychan_bindings)) { 727 | return NULL; 728 | } 729 | 730 | if (_string_too_long("challenge", strlen(challenge))) { 731 | return NULL; 732 | } 733 | 734 | if (!PyCapsule_CheckExact(pyctx)) { 735 | PyErr_SetString(PyExc_TypeError, "Expected a context object"); 736 | return NULL; 737 | } 738 | 739 | state = (sspi_client_state*)PyCapsule_GetPointer( 740 | pyctx, CLIENT_CTX_CAPSULE_NAME); 741 | if (state == NULL) { 742 | return NULL; 743 | } 744 | 745 | if (pychan_bindings != NULL && pychan_bindings != Py_None) { 746 | if (!PyCapsule_CheckExact(pychan_bindings)) { 747 | PyErr_SetString(PyExc_TypeError, "Expected a channel bindings object"); 748 | return NULL; 749 | } 750 | sec_pkg_context_bindings = (SecPkgContext_Bindings *)PyCapsule_GetPointer( 751 | pychan_bindings, CHANNEL_BINDINGS_CTX_CAPSULE_NAME); 752 | if (sec_pkg_context_bindings == NULL) { 753 | return NULL; 754 | } 755 | } 756 | 757 | result = auth_sspi_client_step(state, challenge, sec_pkg_context_bindings); 758 | if (result == AUTH_GSS_ERROR) { 759 | return NULL; 760 | } 761 | 762 | return Py_BuildValue("i", result); 763 | } 764 | 765 | PyDoc_STRVAR(sspi_server_step_doc, 766 | "authGSSServerStep(context, challenge)\n" 767 | "\n" 768 | "Executes a single Kerberos SSPI server step using the supplied client data.\n" 769 | "\n" 770 | ":Parameters:\n" 771 | " - `context`: The context object returned by :func:`authGSSServerInit`.\n" 772 | " - `challenge`: A string containing the base64 encoded client data.\n" 773 | "\n" 774 | ":Returns: :data:`AUTH_GSS_CONTINUE` or :data:`AUTH_GSS_COMPLETE`\n" 775 | "\n" 776 | ".. versionadded:: 0.8.0"); 777 | 778 | static PyObject* 779 | sspi_server_step(PyObject* self, PyObject* args) { 780 | sspi_server_state* state; 781 | PyObject* pyctx; 782 | SEC_CHAR* challenge = NULL; 783 | INT result = 0; 784 | 785 | if (!PyArg_ParseTuple(args, "Os", &pyctx, &challenge)) { 786 | return NULL; 787 | } 788 | 789 | if (_string_too_long("challenge", strlen(challenge))) { 790 | return NULL; 791 | } 792 | 793 | if (!PyCapsule_CheckExact(pyctx)) { 794 | PyErr_SetString(PyExc_TypeError, "Expected a context object"); 795 | return NULL; 796 | } 797 | 798 | state = (sspi_server_state*)PyCapsule_GetPointer( 799 | pyctx, SERVER_CTX_CAPSULE_NAME); 800 | if (state == NULL) { 801 | return NULL; 802 | } 803 | 804 | result = auth_sspi_server_step(state, challenge); 805 | if (result == AUTH_GSS_ERROR) { 806 | return NULL; 807 | } 808 | 809 | return Py_BuildValue("i", result); 810 | } 811 | 812 | PyDoc_STRVAR(sspi_client_response_doc, 813 | "authGSSClientResponse(context)\n" 814 | "\n" 815 | "Get the response to the last successful client operation.\n" 816 | "\n" 817 | ":Parameters:\n" 818 | " - `context`: The context object returned by :func:`authGSSClientInit`.\n" 819 | "\n" 820 | ":Returns: A base64 encoded string to return to the server."); 821 | 822 | static PyObject* 823 | sspi_client_response(PyObject* self, PyObject* args) { 824 | sspi_client_state* state; 825 | PyObject* pyctx; 826 | 827 | if (!PyArg_ParseTuple(args, "O", &pyctx)) { 828 | return NULL; 829 | } 830 | 831 | if (!PyCapsule_CheckExact(pyctx)) { 832 | PyErr_SetString(PyExc_TypeError, "Expected a context object"); 833 | return NULL; 834 | } 835 | 836 | state = (sspi_client_state*)PyCapsule_GetPointer( 837 | pyctx, CLIENT_CTX_CAPSULE_NAME); 838 | if (state == NULL) { 839 | return NULL; 840 | } 841 | 842 | return Py_BuildValue("s", state->response); 843 | } 844 | 845 | PyDoc_STRVAR(sspi_server_response_doc, 846 | "authGSSServerResponse(context)\n" 847 | "\n" 848 | "Get the response to the last successful server operation.\n" 849 | "\n" 850 | ":Parameters:\n" 851 | " - `context`: The context object returned by :func:`authGSSServerInit`.\n" 852 | "\n" 853 | ":Returns: A base64 encoded string to be sent to the client.\n" 854 | "\n" 855 | ".. versionadded:: 0.8.0"); 856 | 857 | static PyObject* 858 | sspi_server_response(PyObject* self, PyObject* args) { 859 | sspi_server_state* state; 860 | PyObject* pyctx; 861 | 862 | if (!PyArg_ParseTuple(args, "O", &pyctx)) { 863 | return NULL; 864 | } 865 | 866 | if (!PyCapsule_CheckExact(pyctx)) { 867 | PyErr_SetString(PyExc_TypeError, "Expected a context object"); 868 | return NULL; 869 | } 870 | 871 | state = (sspi_server_state*)PyCapsule_GetPointer( 872 | pyctx, SERVER_CTX_CAPSULE_NAME); 873 | if (state == NULL) { 874 | return NULL; 875 | } 876 | 877 | return Py_BuildValue("s", state->response); 878 | } 879 | 880 | PyDoc_STRVAR(sspi_client_response_conf_doc, 881 | "authGSSClientResponseConf(context)\n" 882 | "\n" 883 | "Determine whether confidentiality was enabled in the previously unwrapped\n" 884 | "buffer.\n" 885 | "\n" 886 | ":Parameters:\n" 887 | " - `context`: The context object returned by :func:`authGSSClientInit`.\n" 888 | "\n" 889 | ":Returns: 1 if confidentiality was enabled in the previously unwrapped\n" 890 | " buffer, 0 otherwise.\n" 891 | "\n" 892 | ".. versionadded:: 0.5.0"); 893 | 894 | static PyObject* 895 | sspi_client_response_conf(PyObject* self, PyObject* args) { 896 | sspi_client_state* state; 897 | PyObject* pyctx; 898 | 899 | if (!PyArg_ParseTuple(args, "O", &pyctx)) { 900 | return NULL; 901 | } 902 | 903 | if (!PyCapsule_CheckExact(pyctx)) { 904 | PyErr_SetString(PyExc_TypeError, "Expected a context object"); 905 | return NULL; 906 | } 907 | 908 | state = (sspi_client_state*)PyCapsule_GetPointer( 909 | pyctx, CLIENT_CTX_CAPSULE_NAME); 910 | if (state == NULL) { 911 | return NULL; 912 | } 913 | 914 | return Py_BuildValue("i", state->qop != SECQOP_WRAP_NO_ENCRYPT); 915 | } 916 | 917 | PyDoc_STRVAR(sspi_client_username_doc, 918 | "authGSSClientUserName(context)\n" 919 | "\n" 920 | "Get the user name of the authenticated principal. Will only succeed after\n" 921 | "authentication is complete.\n" 922 | "\n" 923 | ":Parameters:\n" 924 | " - `context`: The context object returned by :func:`authGSSClientInit`.\n" 925 | "\n" 926 | ":Returns: A string containing the username."); 927 | 928 | static PyObject* 929 | sspi_client_username(PyObject* self, PyObject* args) { 930 | sspi_client_state* state; 931 | PyObject* pyctx; 932 | 933 | if (!PyArg_ParseTuple(args, "O", &pyctx)) { 934 | return NULL; 935 | } 936 | 937 | if (!PyCapsule_CheckExact(pyctx)) { 938 | PyErr_SetString(PyExc_TypeError, "Expected a context object"); 939 | return NULL; 940 | } 941 | 942 | state = (sspi_client_state*)PyCapsule_GetPointer( 943 | pyctx, CLIENT_CTX_CAPSULE_NAME); 944 | if (state == NULL) { 945 | return NULL; 946 | } 947 | 948 | return Py_BuildValue("s", state->username); 949 | } 950 | 951 | PyDoc_STRVAR(sspi_server_username_doc, 952 | "authGSSServerUserName(context)\n" 953 | "\n" 954 | "Get the user name of the primcipal trying to authenticate to the server.\n" 955 | "Will only succeed after :func:`authGSSServerStep` returns a complete or\n" 956 | "continue response.\n" 957 | "\n" 958 | ":Parameters:\n" 959 | " - `context`: The context object returned by :func:`authGSSServerInit`.\n" 960 | "\n" 961 | ":Returns: A string containing the username.\n" 962 | "\n" 963 | ".. versionadded:: 0.8.0"); 964 | 965 | static PyObject* 966 | sspi_server_username(PyObject* self, PyObject* args) { 967 | sspi_server_state* state; 968 | PyObject* pyctx; 969 | 970 | if (!PyArg_ParseTuple(args, "O", &pyctx)) { 971 | return NULL; 972 | } 973 | 974 | if (!PyCapsule_CheckExact(pyctx)) { 975 | PyErr_SetString(PyExc_TypeError, "Expected a context object"); 976 | return NULL; 977 | } 978 | 979 | state = (sspi_server_state*)PyCapsule_GetPointer( 980 | pyctx, SERVER_CTX_CAPSULE_NAME); 981 | if (state == NULL) { 982 | return NULL; 983 | } 984 | 985 | return Py_BuildValue("s", state->username); 986 | } 987 | 988 | PyDoc_STRVAR(sspi_client_unwrap_doc, 989 | "authGSSClientUnwrap(context, challenge)\n" 990 | "\n" 991 | "Execute the client side DecryptMessage (GSSAPI Unwrap) operation.\n" 992 | "\n" 993 | ":Parameters:\n" 994 | " - `context`: The context object returned by :func:`authGSSClientInit`.\n" 995 | " - `challenge`: A string containing the base64 encoded server\n" 996 | " challenge.\n" 997 | "\n" 998 | ":Returns: :data:`AUTH_GSS_COMPLETE`"); 999 | 1000 | static PyObject* 1001 | sspi_client_unwrap(PyObject* self, PyObject* args) { 1002 | sspi_client_state* state; 1003 | PyObject* pyctx; 1004 | SEC_CHAR* challenge; 1005 | INT result; 1006 | 1007 | if (!PyArg_ParseTuple(args, "Os", &pyctx, &challenge)) { 1008 | return NULL; 1009 | } 1010 | 1011 | if (_string_too_long("challenge", strlen(challenge))) { 1012 | return NULL; 1013 | } 1014 | 1015 | if (!PyCapsule_CheckExact(pyctx)) { 1016 | PyErr_SetString(PyExc_TypeError, "Expected a context object"); 1017 | return NULL; 1018 | } 1019 | 1020 | state = (sspi_client_state*)PyCapsule_GetPointer( 1021 | pyctx, CLIENT_CTX_CAPSULE_NAME); 1022 | if (state == NULL) { 1023 | return NULL; 1024 | } 1025 | 1026 | result = auth_sspi_client_unwrap(state, challenge); 1027 | if (result == AUTH_GSS_ERROR) { 1028 | return NULL; 1029 | } 1030 | 1031 | return Py_BuildValue("i", result); 1032 | } 1033 | 1034 | PyDoc_STRVAR(sspi_client_wrap_doc, 1035 | "authGSSClientWrap(context, data, user=None, protect=0)\n" 1036 | "\n" 1037 | "Execute the client side EncryptMessage (GSSAPI Wrap) operation.\n" 1038 | "\n" 1039 | ":Parameters:\n" 1040 | " - `context`: The context object returned by :func:`authGSSClientInit`.\n" 1041 | " - `data`: If `user` is not None, this should be the result of calling\n" 1042 | " :func:`authGSSClientResponse` after :func:`authGSSClientUnwrap`.\n" 1043 | " If `user` is None, this should be a base64 encoded authorization\n" 1044 | " message as specified in Section 3.1 of RFC-4752.\n" 1045 | " - `user`: An optional string containing the user principal to authorize.\n" 1046 | " - `protect`: If 0 (the default), then just provide integrity protection.\n" 1047 | " If 1, then provide confidentiality as well (requires passing\n" 1048 | " GSS_C_CONF_FLAG to gssflags in :func:`authGSSClientInit`).\n" 1049 | "\n" 1050 | ":Returns: :data:`AUTH_GSS_COMPLETE`\n" 1051 | "\n" 1052 | ".. versionchanged:: 0.5.0\n" 1053 | " Added the `protect` parameter."); 1054 | 1055 | static PyObject* 1056 | sspi_client_wrap(PyObject* self, PyObject* args) { 1057 | sspi_client_state* state; 1058 | PyObject* pyctx; 1059 | SEC_CHAR* data; 1060 | SEC_CHAR* user = NULL; 1061 | SIZE_T ulen = 0; 1062 | INT protect = 0; 1063 | INT result; 1064 | 1065 | if (!PyArg_ParseTuple(args, "Os|zi", &pyctx, &data, &user, &protect)) { 1066 | return NULL; 1067 | } 1068 | if (user) { 1069 | ulen = strlen(user); 1070 | } 1071 | 1072 | if (_string_too_long("data", strlen(data)) || 1073 | /* Length of user + 4 bytes for security options. */ 1074 | _string_too_long("user", ulen + 4)) { 1075 | return NULL; 1076 | } 1077 | 1078 | if (!PyCapsule_CheckExact(pyctx)) { 1079 | PyErr_SetString(PyExc_TypeError, "Expected a context object"); 1080 | return NULL; 1081 | } 1082 | 1083 | state = (sspi_client_state*)PyCapsule_GetPointer( 1084 | pyctx, CLIENT_CTX_CAPSULE_NAME); 1085 | if (state == NULL) { 1086 | return NULL; 1087 | } 1088 | 1089 | result = auth_sspi_client_wrap(state, data, user, (ULONG)ulen, protect); 1090 | if (result == AUTH_GSS_ERROR) { 1091 | return NULL; 1092 | } 1093 | 1094 | return Py_BuildValue("i", result); 1095 | } 1096 | 1097 | static PyMethodDef WinKerberosClientMethods[] = { 1098 | {"authGSSClientInit", (PyCFunction)sspi_client_init, 1099 | METH_VARARGS | METH_KEYWORDS, sspi_client_init_doc}, 1100 | { "authGSSServerInit", sspi_server_init, 1101 | METH_VARARGS, sspi_server_init_doc}, 1102 | {"authGSSClientClean", sspi_client_clean, 1103 | METH_VARARGS, sspi_client_clean_doc}, 1104 | { "authGSSServerClean", sspi_server_clean, 1105 | METH_VARARGS, sspi_server_clean_doc}, 1106 | {"channelBindings", (PyCFunction)sspi_channel_bindings, 1107 | METH_VARARGS | METH_KEYWORDS, sspi_channel_bindings_doc}, 1108 | {"authGSSClientStep", (PyCFunction)sspi_client_step, 1109 | METH_VARARGS | METH_KEYWORDS, sspi_client_step_doc}, 1110 | { "authGSSServerStep", sspi_server_step, 1111 | METH_VARARGS, sspi_server_step_doc}, 1112 | {"authGSSClientResponse", sspi_client_response, 1113 | METH_VARARGS, sspi_client_response_doc}, 1114 | { "authGSSServerResponse", sspi_server_response, 1115 | METH_VARARGS, sspi_server_response_doc}, 1116 | {"authGSSClientResponseConf", sspi_client_response_conf, 1117 | METH_VARARGS, sspi_client_response_conf_doc}, 1118 | {"authGSSClientUserName", sspi_client_username, 1119 | METH_VARARGS, sspi_client_username_doc}, 1120 | { "authGSSServerUserName", sspi_server_username, 1121 | METH_VARARGS, sspi_server_username_doc}, 1122 | {"authGSSClientUnwrap", sspi_client_unwrap, 1123 | METH_VARARGS, sspi_client_unwrap_doc}, 1124 | {"authGSSClientWrap", sspi_client_wrap, 1125 | METH_VARARGS, sspi_client_wrap_doc}, 1126 | {NULL, NULL, 0, NULL} 1127 | }; 1128 | 1129 | #if PY_MAJOR_VERSION >= 3 1130 | #define INITERROR return NULL 1131 | 1132 | static struct PyModuleDef moduledef = { 1133 | PyModuleDef_HEAD_INIT, 1134 | "winkerberos", 1135 | winkerberos_documentation, 1136 | -1, 1137 | WinKerberosClientMethods, 1138 | NULL, 1139 | NULL, 1140 | NULL, 1141 | NULL, 1142 | }; 1143 | 1144 | PyMODINIT_FUNC 1145 | PyInit_winkerberos(VOID) 1146 | #else 1147 | #define INITERROR return 1148 | PyMODINIT_FUNC 1149 | initwinkerberos(VOID) 1150 | #endif 1151 | { 1152 | #if PY_MAJOR_VERSION >= 3 1153 | PyObject* module = PyModule_Create(&moduledef); 1154 | #else 1155 | PyObject* module = Py_InitModule3( 1156 | "winkerberos", 1157 | WinKerberosClientMethods, 1158 | winkerberos_documentation); 1159 | #endif 1160 | if (module == NULL) { 1161 | INITERROR; 1162 | } 1163 | 1164 | KrbError = PyErr_NewException( 1165 | "winkerberos.KrbError", NULL, NULL); 1166 | if (KrbError == NULL) { 1167 | Py_DECREF(module); 1168 | INITERROR; 1169 | } 1170 | Py_INCREF(KrbError); 1171 | 1172 | GSSError = PyErr_NewException( 1173 | "winkerberos.GSSError", KrbError, NULL); 1174 | if (GSSError == NULL) { 1175 | Py_DECREF(KrbError); 1176 | Py_DECREF(module); 1177 | INITERROR; 1178 | } 1179 | Py_INCREF(GSSError); 1180 | 1181 | if (PyModule_AddObject(module, 1182 | "KrbError", 1183 | KrbError) || 1184 | PyModule_AddObject(module, 1185 | "GSSError", 1186 | GSSError) || 1187 | PyModule_AddObject(module, 1188 | "AUTH_GSS_COMPLETE", 1189 | PyInt_FromLong(AUTH_GSS_COMPLETE)) || 1190 | PyModule_AddObject(module, 1191 | "AUTH_GSS_CONTINUE", 1192 | PyInt_FromLong(AUTH_GSS_CONTINUE)) || 1193 | PyModule_AddObject(module, 1194 | "GSS_C_DELEG_FLAG", 1195 | PyInt_FromLong(ISC_REQ_DELEGATE)) || 1196 | PyModule_AddObject(module, 1197 | "GSS_C_MUTUAL_FLAG", 1198 | PyInt_FromLong(ISC_REQ_MUTUAL_AUTH)) || 1199 | PyModule_AddObject(module, 1200 | "GSS_C_REPLAY_FLAG", 1201 | PyInt_FromLong(ISC_REQ_REPLAY_DETECT)) || 1202 | PyModule_AddObject(module, 1203 | "GSS_C_SEQUENCE_FLAG", 1204 | PyInt_FromLong(ISC_REQ_SEQUENCE_DETECT)) || 1205 | PyModule_AddObject(module, 1206 | "GSS_C_CONF_FLAG", 1207 | PyInt_FromLong(ISC_REQ_CONFIDENTIALITY)) || 1208 | PyModule_AddObject(module, 1209 | "GSS_C_INTEG_FLAG", 1210 | PyInt_FromLong(ISC_REQ_INTEGRITY)) || 1211 | PyModule_AddObject(module, 1212 | "GSS_C_AF_UNSPEC", 1213 | PyInt_FromLong(0)) || 1214 | PyModule_AddObject(module, 1215 | "GSS_MECH_OID_KRB5", 1216 | PyCapsule_New(GSS_MECH_OID_KRB5, MECH_OID_KRB5_CAPSULE_NAME, NULL)) || 1217 | PyModule_AddObject(module, 1218 | "GSS_MECH_OID_SPNEGO", 1219 | PyCapsule_New(GSS_MECH_OID_SPNEGO, MECH_OID_SPNEGO_CAPSULE_NAME, NULL)) || 1220 | PyModule_AddObject(module, 1221 | "__version__", 1222 | PyString_FromString("0.9.0.dev0"))) { 1223 | Py_DECREF(GSSError); 1224 | Py_DECREF(KrbError); 1225 | Py_DECREF(module); 1226 | INITERROR; 1227 | } 1228 | 1229 | #if PY_MAJOR_VERSION >= 3 1230 | return module; 1231 | #endif 1232 | } 1233 | --------------------------------------------------------------------------------