├── .ci ├── client-combined.pem.enc └── server-crt.pem ├── .flake8 ├── .github ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── .pre-commit-config.yaml ├── COPYING ├── MANIFEST.in ├── NEWS ├── README.adoc ├── docs ├── Makefile ├── _static │ └── .keep ├── conf.py ├── favicon.ico ├── index.rst └── make.bat ├── examples ├── README.adoc └── yhsmauth_symmetric.py ├── mypy.ini ├── pyproject.toml ├── tests ├── __init__.py ├── conftest.py ├── device │ ├── __init__.py │ ├── conftest.py │ ├── test_aes.py │ ├── test_attestation.py │ ├── test_auth.py │ ├── test_basic.py │ ├── test_delete.py │ ├── test_ec.py │ ├── test_hmac.py │ ├── test_logs.py │ ├── test_opaque.py │ ├── test_otp.py │ ├── test_reset.py │ ├── test_rsa.py │ └── test_wrap.py ├── test_core.py ├── test_defs.py ├── test_objects.py └── test_utils.py └── yubihsm ├── __init__.py ├── backends ├── __init__.py ├── http.py └── usb.py ├── core.py ├── defs.py ├── exceptions.py ├── objects.py ├── py.typed └── utils.py /.ci/client-combined.pem.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yubico/python-yubihsm/463716ed4c2f50cf085f7c318319867d7dc21520/.ci/client-combined.pem.enc -------------------------------------------------------------------------------- /.ci/server-crt.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID/TCCAuWgAwIBAgIUUn0rSVeM6MoylF6Ll6Jy5O95iMwwDQYJKoZIhvcNAQEL 3 | BQAwYzELMAkGA1UEBhMCU0UxEjAQBgNVBAcMCVN0b2NraG9sbTESMBAGA1UECgwJ 4 | WXViaWNvIEFCMSwwKgYDVQQDDCNoc20tY29ubmVjdG9yMDEuc3RobG0uaW4ueXVi 5 | aWNvLm9yZzAgFw0yNTAyMDMxNzQ5MjVaGA8yMTI1MDExMDE3NDkyNVowYzELMAkG 6 | A1UEBhMCU0UxEjAQBgNVBAcMCVN0b2NraG9sbTESMBAGA1UECgwJWXViaWNvIEFC 7 | MSwwKgYDVQQDDCNoc20tY29ubmVjdG9yMDEuc3RobG0uaW4ueXViaWNvLm9yZzCC 8 | ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJXDOCdEJTPn4x0CQbXg12xm 9 | z4yIOAoFhTo40cV+AQnHfaha0Wd7hKTi2r7Lc4ehlv+GBf3fafrNKdzvzIepZMzx 10 | GcVqVpqwdTwdwQS5u0iyPRNqbLQeFEqWD2myYnI6NciTF78GyCwvKc5zsBmue95l 11 | zRooTnr0I0UqBnqWpstmSRD+3uYytr3T0CWbtR2keNSIqKtYErIvc3ahvT6eAywH 12 | Gv7BXpp6Q1htb4eJVIwdB4iyQV4uf283l1rzzWnt4dE5e2IoKvF3eENt/D5YXEuV 13 | alVkr7j/j/56O0cwjGKXdubpn0YUexb7YaWWjmHBzEewk+WDMk7VzliQ2NGajQEC 14 | AwEAAaOBpjCBozAdBgNVHQ4EFgQUOCPzWstJLYd6aptH/QltOrxDzS8wHwYDVR0j 15 | BBgwFoAUOCPzWstJLYd6aptH/QltOrxDzS8wDwYDVR0TAQH/BAUwAwEB/zBQBgNV 16 | HREESTBHgiNoc20tY29ubmVjdG9yMDEuc3RobG0uaW4ueXViaWNvLm9yZ4IgaHNt 17 | LWNvbm5lY3RvcjAxLnN0aGxtLnl1Ymljby5vcmcwDQYJKoZIhvcNAQELBQADggEB 18 | AEydBYgUmKz1Gur9QxCCpPaGmbiMDWb+HpSRP5PsmKiaZc7lwlWEO8buYb/yJhhR 19 | CSBo5Qr9SV1Hdk8ciP0RV69hcIl5IKMShskjjXwcHJjoJ5+6uD+tDfDKOiyqAyeH 20 | 5ttPJEWiyhE+Di5shenxuT9WHMsSMGaeeM91wWAlM0TEwWxYbKb5aMK7v616zTax 21 | 4vKtRh5BkkMDBcpb609DJU3B7SIITXNZzkfPi1WzHRTWNtkP5uSvwuj8EYFDNApd 22 | BijeHm7PXfegboK52AlnN3KPcX1t+xid1DTArVRjOoh21RklQBkY/5Y8zf0fXCr1 23 | Sy+Mzw5uNrrWS6RUzkCEvXo= 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | ignore = E203, W503 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | groups: 8 | github-actions: 9 | patterns: 10 | - "*" 11 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest, windows-latest, macos-latest] 12 | python: ['3.9', '3.10', '3.11', '3.12', '3.13', 'pypy3.9'] 13 | architecture: [x86, x64, arm64] 14 | exclude: 15 | - os: ubuntu-latest 16 | architecture: x86 17 | - os: ubuntu-latest 18 | architecture: arm64 19 | - os: macos-latest 20 | architecture: x86 21 | - os: macos-latest 22 | architecture: x64 23 | - os: windows-latest 24 | architecture: arm64 25 | - os: windows-latest 26 | python: pypy3.9 27 | - os: macos-latest 28 | python: pypy3.9 29 | 30 | name: ${{ matrix.os }} Py ${{ matrix.python }} ${{ matrix.architecture }} 31 | steps: 32 | - uses: actions/checkout@v4 33 | 34 | - name: Set up Python 35 | uses: actions/setup-python@v5 36 | with: 37 | python-version: ${{ matrix.python }} 38 | architecture: ${{ matrix.architecture }} 39 | 40 | - name: Install python dependencies 41 | run: | 42 | python -m pip install --upgrade pip 43 | pip install poetry 44 | poetry install 45 | 46 | - name: Run pre-commit 47 | if: "!startsWith(matrix.python, 'pypy')" 48 | run: | 49 | python -m pip install pre-commit 50 | pre-commit run --all-files --verbose 51 | 52 | - name: Run unit tests 53 | run: poetry run pytest -v -k "not device" 54 | 55 | device_test: 56 | runs-on: ${{ matrix.os }} 57 | 58 | strategy: 59 | matrix: 60 | os: [ubuntu-latest, macos-latest] 61 | 62 | name: Device tests ${{ matrix.os }} 63 | steps: 64 | - uses: actions/checkout@v4 65 | 66 | - name: Set up Python 67 | uses: actions/setup-python@v5 68 | with: 69 | python-version: 3.x 70 | 71 | - name: Install python dependencies 72 | run: | 73 | python -m pip install --upgrade pip 74 | python -m pip install poetry 75 | poetry install -E http 76 | 77 | - name: Set up tunnel 78 | env: 79 | tlspwd: ${{ secrets.TLSPWD }} 80 | run: | 81 | openssl aes-256-cbc -k "$tlspwd" -md sha256 -in ./.ci/client-combined.pem.enc -out ./.ci/client-combined.pem -d 82 | export krnl="$(uname -s | tr '[:upper:]' '[:lower:]')" 83 | wget https://github.com/square/ghostunnel/releases/download/v1.3.1/ghostunnel-v1.3.1-$krnl-amd64-with-pkcs11 -O ghostunnel 84 | chmod +x ./ghostunnel 85 | ./ghostunnel client --listen localhost:12345 --target hsm-connector01.sthlm.in.yubico.org:8443 --keystore ./.ci/client-combined.pem --cacert ./.ci/server-crt.pem 2>/dev/null & 86 | sleep 3 87 | echo "BACKEND=$(curl http://localhost:12345/dispatcher/request)" >> $GITHUB_ENV 88 | 89 | - name: Run device tests 90 | if: ${{ env.BACKEND }} 91 | run: | 92 | echo Using backend $BACKEND 93 | poetry run pytest -v --backend "$BACKEND" tests/device/ 94 | 95 | - name: Release HSM 96 | if: ${{ always() && env.BACKEND }} 97 | run: curl "http://localhost:12345/dispatcher/release?connector=$BACKEND" 98 | 99 | sdist: 100 | runs-on: ubuntu-latest 101 | name: Build Python source .tar.gz 102 | 103 | steps: 104 | - uses: actions/checkout@v4 105 | 106 | - name: Set up Python 107 | uses: actions/setup-python@v5 108 | with: 109 | python-version: 3.x 110 | 111 | - name: Build source package 112 | run: | 113 | python -m pip install --upgrade pip 114 | python -m pip install poetry 115 | poetry build 116 | 117 | - name: Upload source package 118 | uses: actions/upload-artifact@v4 119 | with: 120 | name: python-yubihsm-sdist 121 | path: dist 122 | 123 | docs: 124 | runs-on: ubuntu-latest 125 | name: Build sphinx documentation 126 | 127 | steps: 128 | - uses: actions/checkout@v4 129 | 130 | - name: Set up Python 131 | uses: actions/setup-python@v5 132 | with: 133 | python-version: 3.12 134 | 135 | - name: Install python dependencies 136 | run: | 137 | python -m pip install --upgrade pip 138 | python -m pip install poetry 139 | poetry install -E http -E usb 140 | 141 | - name: Build sphinx documentation 142 | run: poetry run make -C docs/ html 143 | 144 | - name: Upload documentation 145 | uses: actions/upload-artifact@v4 146 | with: 147 | name: python-yubihsm-docs 148 | path: docs/_build/html 149 | 150 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | poetry.lock 2 | build/ 3 | dist/ 4 | venv/ 5 | .DS_Store 6 | .idea/ 7 | .ropeproject/ 8 | *.egg-info/ 9 | *.pyc 10 | .eggs/ 11 | **/_build 12 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/PyCQA/flake8 3 | rev: 7.1.2 4 | hooks: 5 | - id: flake8 6 | - repo: https://github.com/psf/black 7 | rev: 25.1.0 8 | hooks: 9 | - id: black 10 | - repo: https://github.com/PyCQA/bandit 11 | rev: 1.8.3 12 | hooks: 13 | - id: bandit 14 | exclude: ^(tests/|examples/) 15 | - repo: https://github.com/pre-commit/mirrors-mypy 16 | rev: v1.15.0 17 | hooks: 18 | - id: mypy 19 | exclude: ^docs/ # keep in sync with mypy.ini 20 | additional_dependencies: [types-requests] 21 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.adoc 2 | include COPYING 3 | recursive-include test *.py 4 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | * Version 3.1.0 (released 2024-09-09) 2 | ** Support for asymmetric wrap (for FW 2.4+). 3 | ** Support for wrapping ed25519 keys with seed (for FW 2.4+). 4 | ** Deprectaded `get_fips_mode` (use `get_fips_status` instead). 5 | ** Added `py.typed` for type checker compatibility. 6 | 7 | * Version 3.0.0 (released 2023-12-07) 8 | ** NOTE: Backwards incompatible release. 9 | ** Dropped Python 2 support, new minimum requirement: Python 3.8. 10 | ** Added type hints. 11 | ** Bumped minimum supported Cryptography version to 2.6. 12 | ** Dropped yubihsm.eddsa package, in favor of EdDSA support in Cryptography. 13 | ** Dropped custom constants for Brainpool curves, in favor of those in Cryptography. 14 | ** Dropped `.generated`, `.imported`, and `.wrapped` from ORIGIN. 15 | Instead use: `ORIGIN.GENERATED in origin`, etc. 16 | ** Added support for asymmetric authentication. 17 | ** Added support for symmetric encryption (AES). 18 | ** Changes relevant to maintainers: 19 | *** Added mypy to pre-commit checks. 20 | *** Switched build and packaging system to poetry. 21 | *** Switched to using pytest for testing (unittest still used in some places). 22 | 23 | * Version 2.1.2 (released 2022-12-05) 24 | ** Bugfix: Fix broken sign_ssh_certificate command. 25 | 26 | * Version 2.1.1 (released 2022-09-22) 27 | ** Dependency fix: Require Cryptography <38. 28 | 29 | * Version 2.1.0 (released 2021-04-13) 30 | ** Stop using deprecated functions from cryptography.io (prevents warnings). 31 | ** Support Prehashed data when signing. 32 | ** Implement context manager (python with-statement) for YubiHsm and AuthSession. 33 | ** Bugfix: Fix byte-order issue with AEAD nonce ID. 34 | 35 | * Version 2.0.1 (released 2019-06-19) 36 | ** Bugfix: ORIGIN representation was broken, causing get_info() to fail. 37 | ** Bugfix: Algorithm parsing in DeviceInfo fixed. 38 | ** Handing of too large messages improved. 39 | 40 | * Version 2.0.0 (released 2018-11-26) 41 | ** Published under the Apache v2.0 software license. 42 | ** Reworked most library APIs to align with SDK 2.0 changes. 43 | ** Added documentation to all public APIs, with Sphinx generated docs. 44 | 45 | * Version 1.0.0 (released 2017-10-27) 46 | ** First version 47 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | == python-yubihsm 2 | 3 | Python library and tests for the YubiHSM 2. 4 | 5 | The current version (3.0) supports Python 3.9 and later. 6 | 7 | Communicates with the YubiHSM 2 connector daemon, which must already be running. 8 | It can also communicate directly with the YubiHSM 2 via USB (requires libusb). 9 | 10 | === License 11 | 12 | .... 13 | Copyright 2023 Yubico AB 14 | 15 | Licensed under the Apache License, Version 2.0 (the "License"); 16 | you may not use this file except in compliance with the License. 17 | You may obtain a copy of the License at 18 | 19 | http://www.apache.org/licenses/LICENSE-2.0 20 | 21 | Unless required by applicable law or agreed to in writing, software 22 | distributed under the License is distributed on an "AS IS" BASIS, 23 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24 | See the License for the specific language governing permissions and 25 | limitations under the License. 26 | .... 27 | 28 | === Installation 29 | 30 | From PyPI: 31 | 32 | $ pip install yubihsm[http,usb] 33 | 34 | From a source .tar.gz: 35 | 36 | $ pip install yubihsm-.tar.gz[http,usb] 37 | 38 | Omitting a tag from the brackets will install the library without support for 39 | that backend, and will avoid installing unneeded dependencies. 40 | 41 | === Quick reference commands: 42 | [source,python] 43 | ---- 44 | from yubihsm import YubiHsm 45 | from yubihsm.defs import CAPABILITY, ALGORITHM 46 | from yubihsm.objects import AsymmetricKey 47 | 48 | # Connect to the YubiHSM via the connector using the default password: 49 | hsm = YubiHsm.connect('http://localhost:12345') 50 | session = hsm.create_session_derived(1, 'password') 51 | 52 | # Generate a private key on the YubiHSM for creating signatures: 53 | key = AsymmetricKey.generate( # Generate a new key object in the YubiHSM. 54 | session, # Secure YubiHsm session to use. 55 | 0, # Object ID, 0 to get one assigned. 56 | 'My key', # Label for the object. 57 | 1, # Domain(s) for the object. 58 | CAPABILITY.SIGN_ECDSA, # Capabilities for the object, can have multiple. 59 | ALGORITHM.EC_P256 # Algorithm for the key. 60 | ) 61 | 62 | # pub_key is a cryptography.io ec.PublicKey, see https://cryptography.io 63 | pub_key = key.get_public_key() 64 | 65 | # Write the public key to a file: 66 | with open('public_key.pem', 'w') as f: 67 | f.write(pub_key.public_bytes( 68 | encoding=serialization.Encoding.PEM, 69 | format=serialization.PublicFormat.SubjectPublicKeyInfo 70 | )) 71 | 72 | # Sign some data: 73 | signature = key.sign_ecdsa(b'Hello world!') # Create a signature. 74 | 75 | # Clean up: 76 | session.close() 77 | hsm.close() 78 | ---- 79 | 80 | === Development 81 | For development of the library, `poetry` is used. To set up the dev 82 | environment, run this command in the root directory of the repository: 83 | 84 | $ poetry install -E http -E usb 85 | 86 | ==== Pre-commit checks 87 | This project uses https://pre-commit.com to run several checks on the code 88 | prior to committing. To enable the hooks, run this command in the root 89 | directory of the repository: 90 | 91 | $ pre-commit install 92 | 93 | Once the hooks are installed, they will run automatically on any changed files 94 | when committing. To run the hooks against all files in the repository, run: 95 | 96 | $ pre-commit run --all-files 97 | 98 | ==== Running tests 99 | Running the tests require a YubiHSM2 to run against, with the default 100 | authentication key enabled (as is the case after performing a factory reset). 101 | 102 | WARNING: The YubiHSM under test will be factory reset by the tests! 103 | 104 | $ poetry run pytest 105 | 106 | See pytest documentation for instructions on running a specific test. 107 | 108 | By default the tests will connect to a yubihsm-connector running with the 109 | default settings on http://localhost:12345. To change this, use the `--backend` 110 | argument, eg: 111 | 112 | $ poetry run pytest --backend "yhusb://" 113 | 114 | Access to the device requires proper permissions, so either use sudo or setup a 115 | udev rule. Sample udev configuration can be found 116 | link:https://developers.yubico.com/YubiHSM2/Component_Reference/yubihsm-connector/[here]. 117 | 118 | ==== Generating HTML documentation 119 | 120 | To build the HTML documentation, run: 121 | 122 | $ poetry run make -C docs/ html 123 | 124 | The resulting output will be in docs/_build/html/. 125 | 126 | ==== Source releases for distribution 127 | Build a source release: 128 | 129 | $ poetry build 130 | 131 | The resulting .tar.gz and .whl will be created in `dist/`. 132 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = python-yubihsm 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yubico/python-yubihsm/463716ed4c2f50cf085f7c318319867d7dc21520/docs/_static/.keep -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | import re 18 | 19 | sys.path.insert(0, os.path.abspath("../")) 20 | 21 | 22 | def get_version(): 23 | with open("../yubihsm/__init__.py", "r") as f: 24 | match = re.search(r"(?m)^__version__\s*=\s*['\"](.+)['\"]$", f.read()) 25 | return match.group(1) 26 | 27 | 28 | # -- Project information ----------------------------------------------------- 29 | 30 | project = "python-yubihsm" 31 | copyright = "2018, Yubico" 32 | author = "Yubico" 33 | 34 | # The full version, including alpha/beta/rc tags 35 | release = get_version() 36 | 37 | # The short X.Y version 38 | version = ".".join(release.split(".")[:2]) 39 | 40 | # -- General configuration --------------------------------------------------- 41 | 42 | # If your documentation needs a minimal Sphinx version, state it here. 43 | # 44 | # needs_sphinx = '1.0' 45 | 46 | # Add any Sphinx extension module names here, as strings. They can be 47 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 48 | # ones. 49 | extensions = [ 50 | "autoapi.extension", 51 | "sphinx.ext.autodoc.typehints", 52 | "sphinx.ext.doctest", 53 | "sphinx.ext.intersphinx", 54 | "sphinx.ext.viewcode", 55 | ] 56 | 57 | autodoc_typehints = "description" 58 | 59 | # Add any paths that contain templates here, relative to this directory. 60 | templates_path = ["_templates"] 61 | 62 | # The suffix(es) of source filenames. 63 | # You can specify multiple suffix as a list of string: 64 | # 65 | # source_suffix = ['.rst', '.md'] 66 | source_suffix = ".rst" 67 | 68 | # The master toctree document. 69 | master_doc = "index" 70 | 71 | # The language for content autogenerated by Sphinx. Refer to documentation 72 | # for a list of supported languages. 73 | # 74 | # This is also used if you do content translation via gettext catalogs. 75 | # Usually you set "language" from the command line for these cases. 76 | language = "en" 77 | 78 | # List of patterns, relative to source directory, that match files and 79 | # directories to ignore when looking for source files. 80 | # This pattern also affects html_static_path and html_extra_path . 81 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = "sphinx" 85 | 86 | 87 | # -- Options for HTML output ------------------------------------------------- 88 | 89 | # The theme to use for HTML and HTML Help pages. See the documentation for 90 | # a list of builtin themes. 91 | # 92 | html_theme = "sphinx_rtd_theme" 93 | 94 | # Theme options are theme-specific and customize the look and feel of a theme 95 | # further. For a list of options available for each theme, see the 96 | # documentation. 97 | # 98 | # html_theme_options = {} 99 | 100 | html_favicon = "favicon.ico" 101 | 102 | # Add any paths that contain custom static files (such as style sheets) here, 103 | # relative to this directory. They are copied after the builtin static files, 104 | # so a file named "default.css" will overwrite the builtin "default.css". 105 | html_static_path = ["_static"] 106 | 107 | # Don't show a "View page source" link on each page. 108 | html_show_sourcelink = False 109 | 110 | # Custom sidebar templates, must be a dictionary that maps document names 111 | # to template names. 112 | # 113 | # The default sidebars (for documents that don't match any pattern) are 114 | # defined by theme itself. Builtin themes are using these templates by 115 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 116 | # 'searchbox.html']``. 117 | # 118 | # html_sidebars = {} 119 | 120 | 121 | # -- Options for HTMLHelp output --------------------------------------------- 122 | 123 | # Output file base name for HTML help builder. 124 | htmlhelp_basename = "python-yubihsmdoc" 125 | 126 | 127 | # -- Options for LaTeX output ------------------------------------------------ 128 | 129 | latex_elements = { 130 | # The paper size ('letterpaper' or 'a4paper'). 131 | # 132 | # 'papersize': 'letterpaper', 133 | # The font size ('10pt', '11pt' or '12pt'). 134 | # 135 | # 'pointsize': '10pt', 136 | # Additional stuff for the LaTeX preamble. 137 | # 138 | # 'preamble': '', 139 | # Latex figure (float) alignment 140 | # 141 | # 'figure_align': 'htbp', 142 | } 143 | 144 | # Grouping the document tree into LaTeX files. List of tuples 145 | # (source start file, target name, title, 146 | # author, documentclass [howto, manual, or own class]). 147 | latex_documents = [ 148 | ( 149 | master_doc, 150 | "python-yubihsm.tex", 151 | "python-yubihsm Documentation", 152 | "Yubico", 153 | "manual", 154 | ) 155 | ] 156 | 157 | 158 | # -- Options for manual page output ------------------------------------------ 159 | 160 | # One entry per manual page. List of tuples 161 | # (source start file, name, description, authors, manual section). 162 | man_pages = [ 163 | (master_doc, "python-yubihsm", "python-yubihsm Documentation", [author], 1) 164 | ] 165 | 166 | 167 | # -- Options for Texinfo output ---------------------------------------------- 168 | 169 | # Grouping the document tree into Texinfo files. List of tuples 170 | # (source start file, target name, title, author, 171 | # dir menu entry, description, category) 172 | texinfo_documents = [ 173 | ( 174 | master_doc, 175 | "python-yubihsm", 176 | "python-yubihsm Documentation", 177 | author, 178 | "python-yubihsm", 179 | "One line description of project.", 180 | "Miscellaneous", 181 | ) 182 | ] 183 | 184 | 185 | # -- Extension configuration ------------------------------------------------- 186 | 187 | # -- Options for intersphinx extension --------------------------------------- 188 | 189 | # Example configuration for intersphinx: refer to the Python standard library. 190 | intersphinx_mapping = { 191 | "python": ("https://docs.python.org/", None), 192 | "cryptography": ("https://cryptography.io/en/latest/", None), 193 | } 194 | 195 | # Custom config 196 | autoapi_dirs = ["../yubihsm"] 197 | autoapi_options = [ 198 | "members", 199 | "undoc-members", 200 | "show-inheritance", 201 | "show-module-summary", 202 | "imported-members", 203 | ] 204 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yubico/python-yubihsm/463716ed4c2f50cf085f7c318319867d7dc21520/docs/favicon.ico -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. python-yubihsm documentation master file, created by 2 | sphinx-quickstart on Mon Aug 13 15:40:47 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to python-yubihsm's documentation! 7 | ========================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | autoapi/index 14 | 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=python-yubihsm 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /examples/README.adoc: -------------------------------------------------------------------------------- 1 | == Example scripts 2 | The files in this repository are examples of scripts using the `python-yubihsm` 3 | SDK. -------------------------------------------------------------------------------- /examples/yhsmauth_symmetric.py: -------------------------------------------------------------------------------- 1 | """ 2 | This script will import a symmetric YubiHSM Auth credential to a YubiKey 3 | and use that to establish an authenticated session with a YubiHSM device. 4 | 5 | NOTE: The YubiHSM 2 device needs to be configured with an authentication key. 6 | The default authentication key password on KeyID=1 is set to `password`, and this should 7 | be changed or replaced with other authentication keys. This particular script, however, 8 | assumes that the default authentication key is still present on the YubiHSM 2. 9 | 10 | Furthermore, this script requires the `usb` extension of the `python-yubihsm` lib. 11 | This can be installed using `pip install yubihsm[usb]` or `poetry install -E usb`. 12 | 13 | Running this script requires a YubiKey with FW >= 5.4.3. 14 | 15 | Usage: python yhsmauth_symmetric.py 16 | """ 17 | 18 | from yubihsm import YubiHsm 19 | from yubikit.hsmauth import HsmAuthSession, DEFAULT_MANAGEMENT_KEY 20 | from ykman import scripting as s 21 | 22 | # Connect to a YubiKey 23 | yubikey = s.single() 24 | 25 | # Establish a YubiHSM Auth session 26 | hsmauth = HsmAuthSession(yubikey.smart_card()) 27 | 28 | # Connect to a YubiHSM 29 | hsm = YubiHsm.connect("yhusb://") 30 | 31 | # Import a symmetric YubiHSM Auth credential (derived from a password) to YubiKey 32 | # NOTE: the derivation password matches the default authentication 33 | # key password on KeyID=1 in the YubiHSM. 34 | credential = hsmauth.put_credential_derived( 35 | management_key=DEFAULT_MANAGEMENT_KEY, 36 | label="Default credential", 37 | credential_password="1234", 38 | derivation_password="password", 39 | ) 40 | 41 | # Initiate mutual authentication process to YubiHSM 42 | symmetric_auth = hsm.init_session(1) 43 | 44 | # Calculate session keys 45 | session_keys = hsmauth.calculate_session_keys_symmetric( 46 | label=credential.label, context=symmetric_auth.context, credential_password="1234" 47 | ) 48 | 49 | # Authenticate the session 50 | session = symmetric_auth.authenticate(*session_keys) 51 | print("Session authenticated!") 52 | 53 | # Random YubiHSM command over newly authenticated session 54 | objects = session.list_objects() 55 | print("YubiHSM Objects:") 56 | print(objects) 57 | 58 | # Clean up 59 | session.close() 60 | hsm.close() 61 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | files = yubihsm/, tests/ 3 | 4 | [mypy-yubihsm.*] 5 | check_untyped_defs = True 6 | 7 | [mypy-usb.*] 8 | ignore_missing_imports = True 9 | 10 | [mypy-pytest.*] 11 | ignore_missing_imports = True 12 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "yubihsm" 3 | version = "3.1.1.dev0" 4 | description = "Library for communication with a YubiHSM 2 over HTTP or USB." 5 | authors = [{ name = "Dain Nilsson", email = "" }] 6 | readme = "README.adoc" 7 | requires-python = ">=3.9" 8 | license = { file = "COPYING" } 9 | classifiers = [ 10 | "Operating System :: OS Independent", 11 | "Programming Language :: Python", 12 | "Development Status :: 5 - Production/Stable", 13 | "License :: OSI Approved :: Apache Software License", 14 | "Topic :: Security :: Cryptography", 15 | "Topic :: Software Development :: Libraries" 16 | ] 17 | dependencies = ["cryptography (>=2.6, <47)"] 18 | 19 | [project.optional-dependencies] 20 | http = ["requests (>=2.0, <3.0)"] 21 | usb = ["pyusb (>=1.0, <2.0)"] 22 | 23 | [project.urls] 24 | Homepage = "https://developers.yubico.com/YubiHSM2/" 25 | Repository = "https://github.com/Yubico/python-yubihsm" 26 | 27 | [tool.poetry] 28 | include = [ 29 | { path = "COPYING", format = "sdist" }, 30 | { path = "NEWS", format = "sdist" }, 31 | { path = "README.adoc", format = "sdist" }, 32 | "tests/", 33 | ] 34 | 35 | [tool.poetry.group.dev.dependencies] 36 | pytest = "^8.0" 37 | Sphinx = {version = "^8.1", python = ">=3.10"} 38 | sphinx-rtd-theme = {version = "^3.0.1", python = ">=3.10"} 39 | sphinx-autoapi = {version = "^3.3.3", python = ">=3.10"} 40 | 41 | [build-system] 42 | requires = ["poetry-core>=2.0"] 43 | build-backend = "poetry.core.masonry.api" 44 | 45 | [tool.pytest.ini_options] 46 | testpaths = ["tests"] 47 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016-2018 Yubico AB 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 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | def pytest_addoption(parser): 2 | parser.addoption("--backend", action="store", default="http://localhost:12345") 3 | -------------------------------------------------------------------------------- /tests/device/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016-2018 Yubico AB 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 | 16 | DEFAULT_KEY = "password" 17 | -------------------------------------------------------------------------------- /tests/device/conftest.py: -------------------------------------------------------------------------------- 1 | from yubihsm import YubiHsm 2 | from yubihsm.exceptions import YubiHsmDeviceError 3 | from time import sleep 4 | from functools import partial 5 | from . import DEFAULT_KEY 6 | import pytest 7 | from typing import List 8 | 9 | 10 | @pytest.fixture(scope="session") 11 | def connect_hsm(pytestconfig): 12 | backend_uri = pytestconfig.getoption("backend") 13 | return partial(YubiHsm.connect, backend_uri) 14 | 15 | 16 | @pytest.fixture(scope="module") 17 | def hsm(connect_hsm): 18 | with connect_hsm() as hsm: 19 | yield hsm 20 | 21 | 22 | @pytest.fixture(scope="module") 23 | def info(hsm): 24 | return hsm.get_device_info() 25 | 26 | 27 | @pytest.fixture(scope="module") 28 | def session(hsm): 29 | with hsm.create_session_derived(1, DEFAULT_KEY) as session: 30 | yield session 31 | 32 | 33 | _logged_version: List[bool] = [] 34 | 35 | 36 | @pytest.fixture(scope="module", autouse=True) 37 | def _hsm_info(info, session, request): 38 | if not _logged_version: # Run only once 39 | name = "YubiHSM " 40 | try: 41 | session.get_fips_status() 42 | name += "FIPS " 43 | except YubiHsmDeviceError: 44 | pass 45 | name += "v" + (".".join(str(v) for v in info.version)) 46 | 47 | capmanager = request.config.pluginmanager.getplugin("capturemanager") 48 | with capmanager.global_and_fixture_disabled(): 49 | print() 50 | print() 51 | print("ℹ️ Running tests on", name) 52 | print() 53 | _logged_version.append(True) 54 | 55 | 56 | @pytest.fixture(autouse=True, scope="session") 57 | def _reset_hsm(connect_hsm): 58 | with connect_hsm() as hsm: 59 | with hsm.create_session_derived(1, DEFAULT_KEY) as session: 60 | session.reset_device() 61 | sleep(5.0) 62 | -------------------------------------------------------------------------------- /tests/device/test_aes.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016-2018 Yubico AB 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 | from yubihsm.defs import ALGORITHM, CAPABILITY 16 | from yubihsm.objects import SymmetricKey 17 | 18 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes 19 | from typing import Optional 20 | import os 21 | import pytest 22 | 23 | AES_ALGORITHMS = (ALGORITHM.AES128, ALGORITHM.AES192, ALGORITHM.AES256) 24 | AES_CAPABILITIES = ( 25 | CAPABILITY.ENCRYPT_ECB 26 | | CAPABILITY.DECRYPT_ECB 27 | | CAPABILITY.ENCRYPT_CBC 28 | | CAPABILITY.DECRYPT_CBC 29 | ) 30 | 31 | 32 | @pytest.fixture(autouse=True, scope="module") 33 | def prerequisites(info): 34 | if info.version < (2, 3, 0): 35 | pytest.skip("Symmetric keys require YubiHSM 2.3.0") 36 | 37 | 38 | @pytest.fixture(scope="module", params=AES_ALGORITHMS) 39 | def generated_key(session, request): 40 | algorithm = request.param 41 | key = SymmetricKey.generate( 42 | session, 43 | 0, 44 | "Generated AES Key %x" % algorithm, 45 | 0xFFFF, 46 | AES_CAPABILITIES, 47 | algorithm, 48 | ) 49 | yield key 50 | key.delete() 51 | 52 | 53 | @pytest.fixture(scope="module", params=AES_ALGORITHMS) 54 | def imported_key(session, request): 55 | algorithm = request.param 56 | key_to_import = os.urandom(algorithm.to_key_size()) 57 | 58 | key = SymmetricKey.put( 59 | session, 60 | 0, 61 | "Imported AES Key %x" % algorithm, 62 | 0xFFFF, 63 | AES_CAPABILITIES, 64 | algorithm, 65 | key_to_import, 66 | ) 67 | yield key, key_to_import 68 | key.delete() 69 | 70 | 71 | def test_import_invalid_key_size(session): 72 | # Key length must match algorithm 73 | with pytest.raises(ValueError): 74 | SymmetricKey.put( 75 | session, 76 | 0, 77 | "Test PUT invalid key length", 78 | 0xFFFF, 79 | AES_CAPABILITIES, 80 | ALGORITHM.AES128, 81 | os.urandom(24), 82 | ) 83 | 84 | 85 | def test_import_invalid_algorithm(session): 86 | # Algorithm must be AES128, AES192 or AES256 87 | with pytest.raises(ValueError): 88 | SymmetricKey.put( 89 | session, 90 | 0, 91 | "Test PUT invalid algorithm", 92 | 0xFFFF, 93 | AES_CAPABILITIES, 94 | ALGORITHM.AES128_CCM_WRAP, 95 | os.urandom(16), 96 | ) 97 | 98 | 99 | class TestSymmetricECB: 100 | def validate_ecb( 101 | self, pt: bytes, keyobj: SymmetricKey, key: Optional[bytes] = None 102 | ): 103 | ct = keyobj.encrypt_ecb(pt) 104 | if key: 105 | encryptor = Cipher(algorithms.AES(key), modes.ECB()).encryptor() 106 | assert ct == encryptor.update(pt) + encryptor.finalize() 107 | assert pt == keyobj.decrypt_ecb(ct) 108 | 109 | def test_ecb_generated_key(self, generated_key): 110 | pt = os.urandom(256) 111 | self.validate_ecb(pt, generated_key) 112 | 113 | def test_ecb_imported_key(self, imported_key): 114 | pt = os.urandom(256) 115 | self.validate_ecb(pt, *imported_key) 116 | 117 | def test_ecb_large_pt_generated_key(self, generated_key): 118 | pt = os.urandom(4096) 119 | self.validate_ecb(pt, generated_key) 120 | 121 | def test_ecb_large_pt_imported_key(self, imported_key): 122 | pt = os.urandom(4096) 123 | self.validate_ecb(pt, *imported_key) 124 | 125 | 126 | class TestSymmetricCBC: 127 | def validate_cbc( 128 | self, pt: bytes, keyobj: SymmetricKey, key: Optional[bytes] = None 129 | ): 130 | iv = os.urandom(16) 131 | ct = keyobj.encrypt_cbc(iv, pt) 132 | if key: 133 | encryptor = Cipher(algorithms.AES(key), modes.CBC(iv)).encryptor() 134 | assert ct == encryptor.update(pt) + encryptor.finalize() 135 | assert pt == keyobj.decrypt_cbc(iv, ct) 136 | 137 | def test_cbc_generated_key(self, generated_key): 138 | pt = os.urandom(256) 139 | self.validate_cbc(pt, generated_key) 140 | 141 | def test_cbc_imported_key(self, imported_key): 142 | pt = os.urandom(256) 143 | self.validate_cbc(pt, *imported_key) 144 | 145 | def test_cbc_large_pt_generated_key(self, generated_key): 146 | pt = os.urandom(4096) 147 | self.validate_cbc(pt, generated_key) 148 | 149 | def test_cbc_large_pt_imported_key(self, imported_key): 150 | pt = os.urandom(4096) 151 | self.validate_cbc(pt, *imported_key) 152 | -------------------------------------------------------------------------------- /tests/device/test_attestation.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016-2018 Yubico AB 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 | from yubihsm.defs import ALGORITHM, CAPABILITY, FIPS_STATUS, OBJECT 16 | from yubihsm.objects import AsymmetricKey, AttestationExtensions 17 | from yubihsm.exceptions import YubiHsmDeviceError 18 | from cryptography import x509 19 | from cryptography.x509.oid import NameOID 20 | from cryptography.hazmat.backends import default_backend 21 | from cryptography.hazmat.primitives import hashes 22 | from cryptography.hazmat.primitives.asymmetric import ec, rsa, padding 23 | from cryptography.hazmat.primitives.serialization import Encoding 24 | from time import sleep 25 | import datetime 26 | import uuid 27 | import pytest 28 | 29 | 30 | def create_pair(session, algorithm): 31 | if algorithm == ALGORITHM.RSA_2048: 32 | private_key = rsa.generate_private_key(0x10001, 2048, default_backend()) 33 | elif algorithm == ALGORITHM.RSA_3072: 34 | private_key = rsa.generate_private_key(0x10001, 3072, default_backend()) 35 | elif algorithm == ALGORITHM.RSA_4096: 36 | private_key = rsa.generate_private_key(0x10001, 4096, default_backend()) 37 | else: 38 | ec_curve = ALGORITHM.to_curve(algorithm) 39 | private_key = ec.generate_private_key(ec_curve, default_backend()) 40 | 41 | builder = x509.CertificateBuilder() 42 | name = x509.Name( 43 | [x509.NameAttribute(NameOID.COMMON_NAME, "Test Attestation Certificate")] 44 | ) 45 | builder = builder.subject_name(name) 46 | builder = builder.issuer_name(name) 47 | one_day = datetime.timedelta(1, 0, 0) 48 | builder = builder.not_valid_before(datetime.datetime.today() - one_day) 49 | builder = builder.not_valid_after(datetime.datetime.today() + one_day) 50 | builder = builder.serial_number(int(uuid.uuid4())) 51 | builder = builder.public_key(private_key.public_key()) 52 | certificate = builder.sign(private_key, hashes.SHA256(), default_backend()) 53 | 54 | attkey = AsymmetricKey.put( 55 | session, 56 | 0, 57 | "Test Create Pair", 58 | 0xFFFF, 59 | CAPABILITY.SIGN_ATTESTATION_CERTIFICATE, 60 | private_key, 61 | ) 62 | 63 | certobj = attkey.put_certificate( 64 | "Test Create Pair", 0xFFFF, CAPABILITY.NONE, certificate 65 | ) 66 | 67 | assert certificate.public_bytes(Encoding.DER) == certobj.get() 68 | return attkey, certobj, certificate 69 | 70 | 71 | ASYM_ALGOS = [ 72 | ALGORITHM.RSA_2048, 73 | ALGORITHM.RSA_3072, 74 | ALGORITHM.RSA_4096, 75 | ALGORITHM.EC_P256, 76 | ALGORITHM.EC_P384, 77 | ALGORITHM.EC_P521, 78 | ALGORITHM.EC_K256, 79 | ALGORITHM.EC_P224, 80 | ] 81 | 82 | 83 | class TestAttestationAlgorithms: 84 | @pytest.fixture(scope="class", params=ASYM_ALGOS) 85 | def generated_key(self, request, session): 86 | algorithm = request.param 87 | key = AsymmetricKey.generate( 88 | session, 89 | 0, 90 | "Test Attestation %x" % algorithm, 91 | 0xFFFF, 92 | CAPABILITY.NONE, 93 | algorithm, 94 | ) 95 | yield key 96 | key.delete() 97 | 98 | @pytest.mark.parametrize("algorithm", ASYM_ALGOS) 99 | def test_attestation(self, session, generated_key, algorithm, info): 100 | attkey, attcertobj, attcert = create_pair(session, algorithm) 101 | pubkey = attcert.public_key() 102 | 103 | # Verify signatures 104 | cert = generated_key.attest(attkey.id) 105 | data = cert.tbs_certificate_bytes 106 | if isinstance(pubkey, rsa.RSAPublicKey): 107 | pubkey.verify( 108 | cert.signature, 109 | data, 110 | padding.PKCS1v15(), 111 | cert.signature_hash_algorithm, 112 | ) 113 | else: 114 | pubkey.verify(cert.signature, data, ec.ECDSA(cert.signature_hash_algorithm)) 115 | 116 | # Verify certificate extensions 117 | ext = AttestationExtensions.parse(cert) 118 | assert info.version == ext.firmware_version 119 | assert info.serial == ext.serial 120 | 121 | obj = generated_key.get_info() 122 | assert obj.origin == ext.origin 123 | assert obj.domains == ext.domains 124 | assert obj.capabilities == ext.capabilities 125 | assert obj.id == ext.object_id 126 | assert obj.label == ext.label 127 | 128 | # Verify correct public key 129 | assert cert.public_key() == generated_key.get_public_key() 130 | 131 | # Clean up 132 | attkey.delete() 133 | attcertobj.delete() 134 | 135 | 136 | def test_fips_approved_attestation(session, connect_hsm): 137 | try: 138 | session.get_fips_status() 139 | except YubiHsmDeviceError: 140 | pytest.skip("Non-FIPS YubiHSM") 141 | 142 | try: 143 | # Configure into FIPS approved mode 144 | session.reset_device() 145 | sleep(5.0) 146 | hsm = connect_hsm() 147 | new_session = hsm.create_session_derived(1, "password") 148 | new_session.set_fips_mode(True) 149 | assert new_session.get_fips_status() == FIPS_STATUS.PENDING 150 | 151 | # Change the default auth key 152 | authkey = new_session.get_object(1, OBJECT.AUTHENTICATION_KEY) 153 | authkey.change_password("password2") 154 | assert new_session.get_fips_status() == FIPS_STATUS.ON 155 | 156 | # Generate keys 157 | key = AsymmetricKey.generate( 158 | new_session, 159 | 0, 160 | "Test FIPS Attestation", 161 | 0xFFFF, 162 | CAPABILITY.NONE, 163 | ALGORITHM.RSA_2048, 164 | ) 165 | 166 | attkey, attcertobj, attcert = create_pair(new_session, ALGORITHM.RSA_2048) 167 | cert = key.attest(attkey.id) 168 | ext = AttestationExtensions.parse(cert) 169 | assert ext.fips_approved in (True, None) 170 | 171 | finally: 172 | # Reset device to get out of FIPS approved mode 173 | new_session.reset_device() 174 | sleep(5.0) 175 | -------------------------------------------------------------------------------- /tests/device/test_auth.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016-2018 Yubico AB 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 | from yubihsm.defs import COMMAND, CAPABILITY, ERROR 16 | from yubihsm.objects import AuthenticationKey 17 | from yubihsm.exceptions import YubiHsmAuthenticationError, YubiHsmDeviceError 18 | from yubihsm.utils import password_to_key 19 | from cryptography.hazmat.backends import default_backend 20 | from cryptography.hazmat.primitives.asymmetric import ec 21 | from binascii import a2b_hex 22 | import pytest 23 | import os 24 | 25 | 26 | class TestAuthenticationKey: 27 | def test_put_unicode_authkey(self, hsm, session): 28 | # UTF-8 encoded unicode password 29 | password = b"\xf0\x9f\x98\x81\xf0\x9f\x98\x83\xf0\x9f\x98\x84".decode() 30 | 31 | authkey = AuthenticationKey.put_derived( 32 | session, 33 | 0, 34 | "Test PUT authkey", 35 | 1, 36 | CAPABILITY.NONE, 37 | CAPABILITY.NONE, 38 | password, 39 | ) 40 | 41 | with hsm.create_session_derived(authkey.id, password) as session: 42 | message = os.urandom(256) 43 | resp = session.send_secure_cmd(COMMAND.ECHO, message) 44 | 45 | assert resp == message 46 | 47 | authkey.delete() 48 | 49 | 50 | class TestChangeAuthenticationKey: 51 | @pytest.fixture(autouse=True) 52 | def prerequisites(self, info): 53 | if info.version < (2, 1, 0): 54 | pytest.skip("Change authentication key requires 2.1.0") 55 | 56 | def test_change_password(self, hsm, session): 57 | # Create an auth key with the capability to change 58 | authkey = AuthenticationKey.put_derived( 59 | session, 60 | 0, 61 | "Test CHANGE authkey", 62 | 1, 63 | CAPABILITY.CHANGE_AUTHENTICATION_KEY, 64 | CAPABILITY.NONE, 65 | "first_password", 66 | ) 67 | 68 | # Can't change the password of another key 69 | with pytest.raises(YubiHsmDeviceError) as context: 70 | authkey.change_password("second_password") 71 | assert context.value.code == ERROR.INVALID_ID 72 | 73 | # Try again, using the new auth key 74 | with hsm.create_session_derived(authkey.id, "first_password") as session: 75 | authkey.with_session(session).change_password("second_password") 76 | 77 | with pytest.raises(YubiHsmAuthenticationError): 78 | hsm.create_session_derived(authkey.id, "first_password") 79 | 80 | hsm.create_session_derived(authkey.id, "second_password").close() 81 | 82 | authkey.delete() 83 | with pytest.raises(YubiHsmDeviceError) as context: 84 | hsm.create_session_derived(authkey.id, "second_password") 85 | assert context.value.code == ERROR.OBJECT_NOT_FOUND 86 | 87 | def test_change_raw_keys(self, session, hsm): 88 | key_enc = a2b_hex("090b47dbed595654901dee1cc655e420") 89 | key_mac = a2b_hex("592fd483f759e29909a04c4505d2ce0a") 90 | 91 | # Create an auth key with the capability to change 92 | authkey = AuthenticationKey.put( 93 | session, 94 | 0, 95 | "Test CHANGE authkey", 96 | 1, 97 | CAPABILITY.CHANGE_AUTHENTICATION_KEY, 98 | CAPABILITY.NONE, 99 | key_enc, 100 | key_mac, 101 | ) 102 | 103 | with hsm.create_session_derived(authkey.id, "password") as session: 104 | key_enc, key_mac = password_to_key("second_password") 105 | authkey.with_session(session).change_key(key_enc, key_mac) 106 | 107 | with hsm.create_session_derived(authkey.id, "second_password"): 108 | pass 109 | 110 | authkey.delete() 111 | 112 | 113 | class TestSessions: 114 | def test_parallel_sessions(self, session, hsm): 115 | authkey1 = AuthenticationKey.put_derived( 116 | session, 117 | 0, 118 | "Test authkey 1", 119 | 1, 120 | CAPABILITY.NONE, 121 | CAPABILITY.NONE, 122 | "one", 123 | ) 124 | 125 | authkey2 = AuthenticationKey.put_derived( 126 | session, 127 | 0, 128 | "Test authkey 2", 129 | 2, 130 | CAPABILITY.NONE, 131 | CAPABILITY.NONE, 132 | "two", 133 | ) 134 | 135 | authkey3 = AuthenticationKey.put_derived( 136 | session, 137 | 0, 138 | "Test authkey 3", 139 | 1, 140 | CAPABILITY.NONE, 141 | CAPABILITY.NONE, 142 | "three", 143 | ) 144 | 145 | session1 = hsm.create_session_derived(authkey1.id, "one") 146 | session2 = hsm.create_session_derived(authkey2.id, "two") 147 | session3 = hsm.create_session_derived(authkey3.id, "three") 148 | 149 | session2.close() 150 | session1.send_secure_cmd(COMMAND.ECHO, b"hello") 151 | session3.send_secure_cmd(COMMAND.ECHO, b"hi") 152 | 153 | session1.send_secure_cmd(COMMAND.ECHO, b"hello") 154 | session3.send_secure_cmd(COMMAND.ECHO, b"greetings") 155 | session1.close() 156 | 157 | session3.send_secure_cmd(COMMAND.ECHO, b"good bye") 158 | session3.close() 159 | 160 | authkey1.delete() 161 | authkey2.delete() 162 | authkey3.delete() 163 | 164 | 165 | class TestAymmetricAuthenticationKey: 166 | @pytest.fixture(autouse=True) 167 | def prerequisites(self, info): 168 | if info.version < (2, 3, 0): 169 | pytest.skip("Asymmetric authentication requires 2.3.0") 170 | 171 | def test_put_public_key(self, hsm, session): 172 | private_key = ec.generate_private_key(ec.SECP256R1(), backend=default_backend()) 173 | 174 | authkey = AuthenticationKey.put_public_key( 175 | session, 176 | 0, 177 | "Test PUT asym authkey", 178 | 1, 179 | CAPABILITY.NONE, 180 | CAPABILITY.NONE, 181 | private_key.public_key(), 182 | ) 183 | 184 | try: 185 | with hsm.create_session_asymmetric( 186 | authkey.id, private_key 187 | ) as asymmetric_session: 188 | message = os.urandom(256) 189 | resp = asymmetric_session.send_secure_cmd(COMMAND.ECHO, message) 190 | assert message == resp 191 | finally: 192 | authkey.delete() 193 | 194 | def test_change_public_key(self, hsm, session): 195 | first_private_key = ec.generate_private_key( 196 | ec.SECP256R1(), backend=default_backend() 197 | ) 198 | second_private_key = ec.generate_private_key( 199 | ec.SECP256R1(), backend=default_backend() 200 | ) 201 | 202 | authkey = AuthenticationKey.put_public_key( 203 | session, 204 | 0, 205 | "Test PUT asym authkey", 206 | 1, 207 | CAPABILITY.CHANGE_AUTHENTICATION_KEY, 208 | CAPABILITY.NONE, 209 | first_private_key.public_key(), 210 | ) 211 | 212 | with hsm.create_session_asymmetric( 213 | authkey.id, first_private_key 214 | ) as asymmetric_session: 215 | authkey.with_session(asymmetric_session).change_public_key( 216 | second_private_key.public_key() 217 | ) 218 | 219 | try: 220 | with hsm.create_session_asymmetric( 221 | authkey.id, second_private_key 222 | ) as asymmetric_session: 223 | message = os.urandom(256) 224 | resp = asymmetric_session.send_secure_cmd(COMMAND.ECHO, message) 225 | assert message == resp 226 | finally: 227 | authkey.delete() 228 | 229 | def test_cached_device_public_key(self, hsm, session): 230 | private_key = ec.generate_private_key(ec.SECP256R1(), backend=default_backend()) 231 | 232 | authkey = AuthenticationKey.put_public_key( 233 | session, 234 | 0, 235 | "Test PUT asym authkey", 236 | 1, 237 | CAPABILITY.NONE, 238 | CAPABILITY.NONE, 239 | private_key.public_key(), 240 | ) 241 | 242 | right_public_key = hsm.get_device_public_key() 243 | wrong_public_key = ec.generate_private_key( 244 | ec.SECP256R1(), backend=default_backend() 245 | ).public_key() 246 | 247 | with pytest.raises(YubiHsmAuthenticationError): 248 | hsm.create_session_asymmetric(authkey.id, private_key, wrong_public_key) 249 | 250 | try: 251 | hsm.create_session_asymmetric(authkey.id, private_key, right_public_key) 252 | finally: 253 | authkey.delete() 254 | -------------------------------------------------------------------------------- /tests/device/test_basic.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016-2018 Yubico AB 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 | from yubihsm.defs import ALGORITHM, CAPABILITY, OBJECT, COMMAND, ORIGIN, FIPS_STATUS 16 | from yubihsm.objects import ( 17 | YhsmObject, 18 | AsymmetricKey, 19 | HmacKey, 20 | WrapKey, 21 | AuthenticationKey, 22 | ) 23 | from cryptography.hazmat.primitives.asymmetric import ec 24 | from yubihsm.exceptions import YubiHsmInvalidRequestError, YubiHsmDeviceError 25 | from time import sleep 26 | import uuid 27 | import os 28 | import pytest 29 | 30 | 31 | class TestListObjects: 32 | def print_list_objects(self, session): 33 | objlist = session.list_objects() 34 | 35 | for i in range(len(objlist)): 36 | print( 37 | "id: ", 38 | "0x%0.4X" % objlist[i].id, 39 | ",type: ", 40 | objlist[i].object_type.name, 41 | "\t,sequence: ", 42 | objlist[i].sequence, 43 | ) 44 | 45 | objinfo = objlist[1].get_info() 46 | print( 47 | "id: ", 48 | "0x%0.4X" % objinfo.id, 49 | ",type: ", 50 | objinfo.object_type.name, 51 | "\t,sequence: ", 52 | objinfo.sequence, 53 | ",domains: 0x%0.4X" % objinfo.domains, 54 | ",capabilities: 0x%0.8X" % objinfo.capabilities, 55 | ",algorithm: ", 56 | objinfo.algorithm, 57 | ) 58 | 59 | def key_in_list(self, session, keytype, algorithm=None): 60 | dom = None 61 | cap = CAPABILITY.NONE 62 | key_label = "%s%s" % (str(uuid.uuid4()), b"\xf0\x9f\x98\x83".decode()) 63 | 64 | key: YhsmObject 65 | if keytype == OBJECT.ASYMMETRIC_KEY: 66 | dom = 0xFFFF 67 | key = AsymmetricKey.generate(session, 0, key_label, dom, cap, algorithm) 68 | elif keytype == OBJECT.WRAP_KEY: 69 | dom = 0x01 70 | key = WrapKey.generate(session, 0, key_label, dom, cap, algorithm, cap) 71 | elif keytype == OBJECT.HMAC_KEY: 72 | dom = 0x01 73 | key = HmacKey.generate(session, 0, key_label, dom, cap, algorithm) 74 | elif keytype == OBJECT.AUTHENTICATION_KEY: 75 | dom = 0x01 76 | key = AuthenticationKey.put_derived( 77 | session, 78 | 0, 79 | key_label, 80 | dom, 81 | cap, 82 | cap, 83 | "password", 84 | ) 85 | 86 | objlist = session.list_objects(object_id=key.id, object_type=key.object_type) 87 | assert objlist[0].id == key.id 88 | assert objlist[0].object_type == key.object_type 89 | 90 | objinfo = objlist[0].get_info() 91 | assert objinfo.id == key.id 92 | assert objinfo.object_type == key.object_type 93 | assert objinfo.domains == dom 94 | assert objinfo.capabilities == cap 95 | if algorithm: 96 | assert objinfo.algorithm == algorithm 97 | 98 | if key.object_type == OBJECT.AUTHENTICATION_KEY: 99 | assert objinfo.origin == ORIGIN.IMPORTED 100 | else: 101 | assert objinfo.origin == ORIGIN.GENERATED 102 | 103 | assert objinfo.label == key_label 104 | 105 | key.delete() 106 | 107 | def test_keys_in_list(self, session): 108 | self.key_in_list(session, OBJECT.ASYMMETRIC_KEY, ALGORITHM.EC_P256) 109 | self.key_in_list(session, OBJECT.WRAP_KEY, ALGORITHM.AES128_CCM_WRAP) 110 | self.key_in_list(session, OBJECT.HMAC_KEY, ALGORITHM.HMAC_SHA1) 111 | self.key_in_list(session, OBJECT.AUTHENTICATION_KEY) 112 | 113 | def test_list_all_params(self, session): 114 | # TODO: this test should check for presence of some things.. 115 | session.list_objects( 116 | object_id=1, 117 | object_type=OBJECT.HMAC_KEY, 118 | domains=1, 119 | capabilities=CAPABILITY.ALL, 120 | algorithm=ALGORITHM.HMAC_SHA1, 121 | label="foo", 122 | ) 123 | 124 | 125 | class TestVarious: 126 | def test_device_info(self, hsm): 127 | device_info = hsm.get_device_info() 128 | assert len(device_info.version) == 3 129 | assert device_info.serial > 0 130 | assert device_info.log_used > 0 131 | assert device_info.log_size >= device_info.log_used 132 | assert len(device_info.supported_algorithms) >= 47 133 | if device_info.version > (2, 4, 0): 134 | assert isinstance(device_info.part_number, str) 135 | 136 | def test_get_pseudo_random(self, session): 137 | data = session.get_pseudo_random(10) 138 | assert len(data) == 10 139 | data2 = session.get_pseudo_random(10) 140 | assert len(data2) == 10 141 | assert data != data2 142 | 143 | def test_send_too_big(self, hsm, session): 144 | max_msg_size = hsm._msg_buf_size - 1 145 | buf = os.urandom(max_msg_size - 3 + 1) # Message 1 byte too large 146 | with pytest.raises(YubiHsmInvalidRequestError): 147 | hsm.send_cmd(COMMAND.ECHO, buf) 148 | 149 | 150 | class TestDevicePublicKey: 151 | @pytest.fixture(autouse=True) 152 | def prerequisites(self, info): 153 | if info.version < (2, 3, 0): 154 | pytest.skip("Device public keys requires 2.3.0") 155 | 156 | def test_get_device_public_key(self, hsm): 157 | public_key = hsm.get_device_public_key() 158 | assert isinstance(public_key, ec.EllipticCurvePublicKey) 159 | 160 | 161 | class TestEcho: 162 | def plain_echo(self, hsm, echo_len): 163 | echo_buf = os.urandom(echo_len) 164 | 165 | resp = hsm.send_cmd(COMMAND.ECHO, echo_buf) 166 | 167 | assert len(resp) == echo_len 168 | assert resp == echo_buf 169 | 170 | def secure_echo(self, session, echo_len): 171 | echo_buf = os.urandom(echo_len) 172 | 173 | resp = session.send_secure_cmd(COMMAND.ECHO, echo_buf) 174 | assert resp == echo_buf 175 | 176 | def test_plain_echo(self, hsm): 177 | self.plain_echo(hsm, 1024) 178 | 179 | def test_secure_echo(self, session): 180 | self.secure_echo(session, 1024) 181 | 182 | def test_plain_echo_many(self, hsm): 183 | for i in range(1, 256): 184 | self.plain_echo(hsm, i) 185 | 186 | def test_echo_max_size(self, hsm, session): 187 | self.plain_echo(hsm, 2021) 188 | self.secure_echo(session, 2021) 189 | 190 | 191 | class TestFipsOptions: 192 | @pytest.fixture(scope="class", autouse=True) 193 | def session2(self, session, connect_hsm): 194 | try: 195 | session.get_fips_status() 196 | session.reset_device() 197 | sleep(5.0) 198 | hsm = connect_hsm() 199 | new_session = hsm.create_session_derived(1, "password") 200 | yield new_session 201 | new_session.reset_device() 202 | sleep(5.0) 203 | except YubiHsmDeviceError: 204 | pytest.skip("Non-FIPS YubiHSM") 205 | 206 | def test_set_in_fips_mode(self, session2, info): 207 | assert session2.get_fips_status() == FIPS_STATUS.OFF 208 | session2.set_fips_mode(True) 209 | if info.version < (2, 4, 0): 210 | assert session2.get_fips_status() == FIPS_STATUS.ON 211 | else: 212 | assert session2.get_fips_status() == FIPS_STATUS.PENDING 213 | 214 | def test_fips_mode_disables_algorithms(self, session2, info): 215 | session2.set_fips_mode(True) 216 | enabled = session2.get_enabled_algorithms() 217 | if info.version < (2, 4, 0): 218 | assert not any( 219 | enabled[alg] 220 | for alg in ( 221 | ALGORITHM.RSA_PKCS1_SHA1, 222 | ALGORITHM.RSA_PSS_SHA1, 223 | ALGORITHM.EC_ECDSA_SHA1, 224 | ALGORITHM.EC_ED25519, 225 | ) 226 | ) 227 | else: 228 | assert not any( 229 | enabled[alg] 230 | for alg in ( 231 | ALGORITHM.RSA_PKCS1_SHA1, 232 | ALGORITHM.RSA_PSS_SHA1, 233 | ALGORITHM.EC_K256, 234 | ALGORITHM.EC_ECDSA_SHA1, 235 | ALGORITHM.RSA_PKCS1_DECRYPT, 236 | ) 237 | ) 238 | 239 | def test_enabling_algorithms_in_fips_mode(self, session2, info): 240 | session2.set_fips_mode(True) 241 | if info.version < (2, 4, 0): 242 | # For YubiHSM FW < 2.4.0, enabling dissallowed algorithms 243 | # disables FIPS mode. 244 | session2.set_enabled_algorithms( 245 | { 246 | ALGORITHM.RSA_PKCS1_SHA1: True, 247 | } 248 | ) 249 | assert session2.get_fips_status() == FIPS_STATUS.OFF 250 | else: 251 | with pytest.raises(YubiHsmDeviceError): 252 | session2.set_enabled_algorithms({ALGORITHM.RSA_PKCS1_SHA1: True}) 253 | -------------------------------------------------------------------------------- /tests/device/test_delete.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016-2018 Yubico AB 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 | from yubihsm.defs import ALGORITHM, CAPABILITY, ERROR 16 | from yubihsm.objects import ( 17 | AuthenticationKey, 18 | HmacKey, 19 | Opaque, 20 | AsymmetricKey, 21 | OtpAeadKey, 22 | WrapKey, 23 | SymmetricKey, 24 | ) 25 | from yubihsm.exceptions import YubiHsmDeviceError 26 | from cryptography.hazmat.primitives.asymmetric import ec 27 | from cryptography.hazmat.backends import default_backend 28 | import os 29 | import pytest 30 | 31 | 32 | def _set_up_key(hsm, session, capability): 33 | password = session.get_pseudo_random(32).hex() 34 | key = AuthenticationKey.put_derived( 35 | session, 0, "Test Delete authkey", 1, capability, CAPABILITY.NONE, password 36 | ) 37 | session = hsm.create_session_derived(key.id, password) 38 | return key, session 39 | 40 | 41 | def _test_delete(hsm, session, obj, capability): 42 | pos_key, pos_sess = _set_up_key(hsm, session, capability) 43 | neg_key, neg_sess = _set_up_key(hsm, session, CAPABILITY.NONE) 44 | 45 | with pytest.raises(YubiHsmDeviceError) as context: 46 | obj.with_session(neg_sess).delete() 47 | assert context.value.code == ERROR.INSUFFICIENT_PERMISSIONS 48 | 49 | obj.with_session(pos_sess).delete() 50 | 51 | pos_sess.close() 52 | neg_sess.close() 53 | neg_key.delete() 54 | pos_key.delete() 55 | 56 | 57 | def test_opaque(hsm, session): 58 | obj = Opaque.put( 59 | session, 60 | 0, 61 | "Test opaque data", 62 | 1, 63 | CAPABILITY.NONE, 64 | ALGORITHM.OPAQUE_DATA, 65 | b"data", 66 | ) 67 | _test_delete(hsm, session, obj, CAPABILITY.DELETE_OPAQUE) 68 | 69 | 70 | def test_authentication_key(hsm, session): 71 | obj = AuthenticationKey.put_derived( 72 | session, 73 | 0, 74 | "Test delete authkey", 75 | 1, 76 | CAPABILITY.GET_LOG_ENTRIES, 77 | CAPABILITY.NONE, 78 | session.get_pseudo_random(32).hex(), 79 | ) 80 | _test_delete(hsm, session, obj, CAPABILITY.DELETE_AUTHENTICATION_KEY) 81 | 82 | 83 | def test_asymmetric_key(hsm, session): 84 | obj = AsymmetricKey.put( 85 | session, 86 | 0, 87 | "Test delete asym", 88 | 0xFFFF, 89 | CAPABILITY.SIGN_ECDSA, 90 | ec.generate_private_key(ec.SECP384R1(), backend=default_backend()), 91 | ) 92 | _test_delete(hsm, session, obj, CAPABILITY.DELETE_ASYMMETRIC_KEY) 93 | 94 | 95 | def test_wrap_key(hsm, session): 96 | obj = WrapKey.put( 97 | session, 98 | 0, 99 | "Test delete", 100 | 1, 101 | CAPABILITY.IMPORT_WRAPPED, 102 | ALGORITHM.AES192_CCM_WRAP, 103 | CAPABILITY.NONE, 104 | os.urandom(24), 105 | ) 106 | _test_delete(hsm, session, obj, CAPABILITY.DELETE_WRAP_KEY) 107 | 108 | 109 | def test_hmac_key(hsm, session): 110 | obj = HmacKey.put( 111 | session, 0, "Test delete HMAC", 1, CAPABILITY.SIGN_HMAC, bytes(16) 112 | ) 113 | _test_delete(hsm, session, obj, CAPABILITY.DELETE_HMAC_KEY) 114 | 115 | 116 | def test_otp_aead_key(hsm, session): 117 | obj = OtpAeadKey.put( 118 | session, 119 | 0, 120 | "Test delete OTP AEAD", 121 | 1, 122 | CAPABILITY.DECRYPT_OTP, 123 | ALGORITHM.AES256_YUBICO_OTP, 124 | 0x00000001, 125 | os.urandom(32), 126 | ) 127 | _test_delete(hsm, session, obj, CAPABILITY.DELETE_OTP_AEAD_KEY) 128 | 129 | 130 | def test_symmetric_key(hsm, session, info): 131 | if info.version < (2, 3, 0): 132 | pytest.skip("Symmetric keys require YubiHSM 2.3.0") 133 | 134 | obj = SymmetricKey.put( 135 | session, 136 | 0, 137 | "Test delete symmetric", 138 | 0xFFFF, 139 | CAPABILITY.DECRYPT_ECB, 140 | ALGORITHM.AES128, 141 | os.urandom(16), 142 | ) 143 | _test_delete(hsm, session, obj, CAPABILITY.DELETE_SYMMETRIC_KEY) 144 | -------------------------------------------------------------------------------- /tests/device/test_ec.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Copyright 2016-2018 Yubico AB 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from yubihsm.defs import ALGORITHM, CAPABILITY, COMMAND, ERROR 18 | from yubihsm.objects import AsymmetricKey 19 | from yubihsm.exceptions import YubiHsmDeviceError 20 | 21 | from cryptography.hazmat.backends import default_backend 22 | from cryptography.hazmat.primitives import hashes, serialization 23 | from cryptography.hazmat.primitives.asymmetric import ec, ed25519, utils as crypto_utils 24 | from binascii import a2b_hex 25 | from enum import Enum 26 | import os 27 | import struct 28 | import pytest 29 | 30 | 31 | class Mode(Enum): 32 | IMPORT = 0 33 | GENERATE = 1 34 | 35 | def __str__(self): 36 | return self.name 37 | 38 | 39 | ECDSA_CURVES = [ 40 | ec.SECP224R1, 41 | ec.SECP256R1, 42 | ec.SECP256K1, 43 | ec.SECP384R1, 44 | ec.SECP521R1, 45 | ec.BrainpoolP256R1, 46 | ec.BrainpoolP384R1, 47 | ec.BrainpoolP512R1, 48 | ] 49 | 50 | HASHES = [ 51 | hashes.SHA1, 52 | hashes.SHA256, 53 | hashes.SHA384, 54 | hashes.SHA512, 55 | ] 56 | 57 | 58 | @pytest.fixture(params=[Mode.IMPORT, Mode.GENERATE]) 59 | def keypair(request, session, curve): 60 | if request.param == Mode.GENERATE: 61 | asymkey = AsymmetricKey.generate( 62 | session, 63 | 0, 64 | "Generate EC", 65 | 0xFFFF, 66 | CAPABILITY.SIGN_ECDSA | CAPABILITY.DERIVE_ECDH, 67 | ALGORITHM.for_curve(curve()), 68 | ) 69 | public_key = asymkey.get_public_key() 70 | else: 71 | key = ec.generate_private_key(curve(), backend=default_backend()) 72 | asymkey = AsymmetricKey.put( 73 | session, 74 | 0, 75 | "SECP ECDSA Sign Sign", 76 | 0xFFFF, 77 | CAPABILITY.SIGN_ECDSA | CAPABILITY.DERIVE_ECDH, 78 | key, 79 | ) 80 | public_key = key.public_key() 81 | assert public_key.public_bytes( 82 | encoding=serialization.Encoding.PEM, 83 | format=serialization.PublicFormat.SubjectPublicKeyInfo, 84 | ) == asymkey.get_public_key().public_bytes( 85 | encoding=serialization.Encoding.PEM, 86 | format=serialization.PublicFormat.SubjectPublicKeyInfo, 87 | ) 88 | 89 | yield asymkey, public_key 90 | 91 | asymkey.delete() 92 | 93 | 94 | @pytest.mark.parametrize("hashtype", HASHES) 95 | @pytest.mark.parametrize("curve", ECDSA_CURVES) 96 | def test_ecdsa_sign(info, session, keypair, curve, hashtype): 97 | asymkey, public_key = keypair 98 | 99 | data = os.urandom(64) 100 | if info.version < (2, 1, 0): # Manual truncation needed 101 | length = min(curve.key_size // 8, hashtype.digest_size) 102 | resp = asymkey.sign_ecdsa(data, hash=hashtype(), length=length) 103 | else: 104 | resp = asymkey.sign_ecdsa(data, hash=hashtype()) 105 | 106 | public_key.verify(resp, data, ec.ECDSA(hashtype())) 107 | 108 | 109 | @pytest.mark.parametrize("curve", ECDSA_CURVES) 110 | def test_derive_ecdh(session, keypair, curve): 111 | asymkey, public_key = keypair 112 | 113 | ekey = ec.generate_private_key(curve(), backend=default_backend()) 114 | secret = ekey.exchange(ec.ECDH(), public_key) 115 | 116 | resp = asymkey.derive_ecdh(ekey.public_key()) 117 | assert secret == resp 118 | 119 | 120 | def test_bad_ecdh_keys(session): 121 | pubkeys = [ 122 | # this is a public key not on the curve (p256) 123 | "04cdeb39edd03e2b1a11a5e134ec99d5f25f21673d403f3ecb47bd1fa676638958ea58493b8429598c0b49bbb85c3303ddb1553c3b761c2caacca71606ba9ebaca", # noqa E501 124 | # all zeroes public key 125 | "0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", # noqa E501 126 | # all ff public key 127 | "04ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", # noqa E501 128 | ] 129 | 130 | key = AsymmetricKey.generate( 131 | session, 132 | 0, 133 | "badkey ecdh test", 134 | 0xFFFF, 135 | CAPABILITY.DERIVE_ECDH, 136 | ALGORITHM.EC_P256, 137 | ) 138 | keyid = struct.pack("!H", key.id) 139 | for pubkey in pubkeys: 140 | with pytest.raises(YubiHsmDeviceError) as context: 141 | session.send_secure_cmd(COMMAND.DERIVE_ECDH, keyid + a2b_hex(pubkey)) 142 | assert context.value.code == ERROR.INVALID_DATA 143 | key.delete() 144 | 145 | 146 | def test_biased_k(session): 147 | # n is the order of the p256r1 curve. 148 | n = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551 149 | 150 | key = ec.generate_private_key(ec.SECP256R1(), backend=default_backend()) 151 | d = key.private_numbers().private_value 152 | asymkey = AsymmetricKey.put( 153 | session, 0, "Test ECDSA K", 0xFFFF, CAPABILITY.SIGN_ECDSA, key 154 | ) 155 | 156 | data = b"Hello World!" 157 | 158 | digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) 159 | digest.update(data) 160 | h = int.from_bytes(digest.finalize(), "big") 161 | 162 | # The assumption here is that for 1024 runs we should get a distribution 163 | # where each single bit is set between 400 and 1024 - 400 times. 164 | count = 1024 165 | mincount = 400 166 | 167 | bits = [0] * 256 168 | for i in range(0, count): 169 | resp = asymkey.sign_ecdsa(data, hash=hashes.SHA256()) 170 | # Extract random number k from signature: 171 | # k = s^(-1) * (h + d*r) mod n 172 | (r, s) = crypto_utils.decode_dss_signature(resp) 173 | # Fermat's little theorem: a^(p-1) ≡ 1 (mod p), when p is prime. 174 | # s * s^(p-2) ≡ 1 (mod p) 175 | s_inv = pow(s, n - 2, n) 176 | k = s_inv * (h + d * r) % n 177 | for j in range(0, 256): 178 | if (k >> j) & 1: 179 | bits[j] += 1 180 | 181 | for bit in bits: 182 | assert mincount < bit < count - mincount 183 | 184 | asymkey.delete() 185 | 186 | 187 | EDDSA_VECTORS = [ 188 | { 189 | "key": b"\x9d\x61\xb1\x9d\xef\xfd\x5a\x60\xba\x84\x4a\xf4\x92\xec\x2c\xc4\x44\x49\xc5\x69\x7b\x32\x69\x19\x70\x3b\xac\x03\x1c\xae\x7f\x60", # noqa E501 190 | "pubkey": b"\xd7\x5a\x98\x01\x82\xb1\x0a\xb7\xd5\x4b\xfe\xd3\xc9\x64\x07\x3a\x0e\xe1\x72\xf3\xda\xa6\x23\x25\xaf\x02\x1a\x68\xf7\x07\x51\x1a", # noqa E501 191 | "msg": b"", 192 | "sig": b"\xe5\x56\x43\x00\xc3\x60\xac\x72\x90\x86\xe2\xcc\x80\x6e\x82\x8a\x84\x87\x7f\x1e\xb8\xe5\xd9\x74\xd8\x73\xe0\x65\x22\x49\x01\x55\x5f\xb8\x82\x15\x90\xa3\x3b\xac\xc6\x1e\x39\x70\x1c\xf9\xb4\x6b\xd2\x5b\xf5\xf0\x59\x5b\xbe\x24\x65\x51\x41\x43\x8e\x7a\x10\x0b", # noqa E501 193 | }, 194 | { 195 | "key": b"\x4c\xcd\x08\x9b\x28\xff\x96\xda\x9d\xb6\xc3\x46\xec\x11\x4e\x0f\x5b\x8a\x31\x9f\x35\xab\xa6\x24\xda\x8c\xf6\xed\x4f\xb8\xa6\xfb", # noqa E501 196 | "pubkey": b"\x3d\x40\x17\xc3\xe8\x43\x89\x5a\x92\xb7\x0a\xa7\x4d\x1b\x7e\xbc\x9c\x98\x2c\xcf\x2e\xc4\x96\x8c\xc0\xcd\x55\xf1\x2a\xf4\x66\x0c", # noqa E501 197 | "msg": b"\x72", 198 | "sig": b"\x92\xa0\x09\xa9\xf0\xd4\xca\xb8\x72\x0e\x82\x0b\x5f\x64\x25\x40\xa2\xb2\x7b\x54\x16\x50\x3f\x8f\xb3\x76\x22\x23\xeb\xdb\x69\xda\x08\x5a\xc1\xe4\x3e\x15\x99\x6e\x45\x8f\x36\x13\xd0\xf1\x1d\x8c\x38\x7b\x2e\xae\xb4\x30\x2a\xee\xb0\x0d\x29\x16\x12\xbb\x0c\x00", # noqa E501 199 | }, 200 | { 201 | "key": b"\xc5\xaa\x8d\xf4\x3f\x9f\x83\x7b\xed\xb7\x44\x2f\x31\xdc\xb7\xb1\x66\xd3\x85\x35\x07\x6f\x09\x4b\x85\xce\x3a\x2e\x0b\x44\x58\xf7", # noqa E501 202 | "pubkey": b"\xfc\x51\xcd\x8e\x62\x18\xa1\xa3\x8d\xa4\x7e\xd0\x02\x30\xf0\x58\x08\x16\xed\x13\xba\x33\x03\xac\x5d\xeb\x91\x15\x48\x90\x80\x25", # noqa E501 203 | "msg": b"\xaf\x82", 204 | "sig": b"\x62\x91\xd6\x57\xde\xec\x24\x02\x48\x27\xe6\x9c\x3a\xbe\x01\xa3\x0c\xe5\x48\xa2\x84\x74\x3a\x44\x5e\x36\x80\xd7\xdb\x5a\xc3\xac\x18\xff\x9b\x53\x8d\x16\xf2\x90\xae\x67\xf7\x60\x98\x4d\xc6\x59\x4a\x7c\x15\xe9\x71\x6e\xd2\x8d\xc0\x27\xbe\xce\xea\x1e\xc4\x0a", # noqa E501 205 | }, 206 | { 207 | "key": b"\xf5\xe5\x76\x7c\xf1\x53\x31\x95\x17\x63\x0f\x22\x68\x76\xb8\x6c\x81\x60\xcc\x58\x3b\xc0\x13\x74\x4c\x6b\xf2\x55\xf5\xcc\x0e\xe5", # noqa E501 208 | "pubkey": b"\x27\x81\x17\xfc\x14\x4c\x72\x34\x0f\x67\xd0\xf2\x31\x6e\x83\x86\xce\xff\xbf\x2b\x24\x28\xc9\xc5\x1f\xef\x7c\x59\x7f\x1d\x42\x6e", # noqa E501 209 | "msg": b"\x08\xb8\xb2\xb7\x33\x42\x42\x43\x76\x0f\xe4\x26\xa4\xb5\x49\x08\x63\x21\x10\xa6\x6c\x2f\x65\x91\xea\xbd\x33\x45\xe3\xe4\xeb\x98\xfa\x6e\x26\x4b\xf0\x9e\xfe\x12\xee\x50\xf8\xf5\x4e\x9f\x77\xb1\xe3\x55\xf6\xc5\x05\x44\xe2\x3f\xb1\x43\x3d\xdf\x73\xbe\x84\xd8\x79\xde\x7c\x00\x46\xdc\x49\x96\xd9\xe7\x73\xf4\xbc\x9e\xfe\x57\x38\x82\x9a\xdb\x26\xc8\x1b\x37\xc9\x3a\x1b\x27\x0b\x20\x32\x9d\x65\x86\x75\xfc\x6e\xa5\x34\xe0\x81\x0a\x44\x32\x82\x6b\xf5\x8c\x94\x1e\xfb\x65\xd5\x7a\x33\x8b\xbd\x2e\x26\x64\x0f\x89\xff\xbc\x1a\x85\x8e\xfc\xb8\x55\x0e\xe3\xa5\xe1\x99\x8b\xd1\x77\xe9\x3a\x73\x63\xc3\x44\xfe\x6b\x19\x9e\xe5\xd0\x2e\x82\xd5\x22\xc4\xfe\xba\x15\x45\x2f\x80\x28\x8a\x82\x1a\x57\x91\x16\xec\x6d\xad\x2b\x3b\x31\x0d\xa9\x03\x40\x1a\xa6\x21\x00\xab\x5d\x1a\x36\x55\x3e\x06\x20\x3b\x33\x89\x0c\xc9\xb8\x32\xf7\x9e\xf8\x05\x60\xcc\xb9\xa3\x9c\xe7\x67\x96\x7e\xd6\x28\xc6\xad\x57\x3c\xb1\x16\xdb\xef\xef\xd7\x54\x99\xda\x96\xbd\x68\xa8\xa9\x7b\x92\x8a\x8b\xbc\x10\x3b\x66\x21\xfc\xde\x2b\xec\xa1\x23\x1d\x20\x6b\xe6\xcd\x9e\xc7\xaf\xf6\xf6\xc9\x4f\xcd\x72\x04\xed\x34\x55\xc6\x8c\x83\xf4\xa4\x1d\xa4\xaf\x2b\x74\xef\x5c\x53\xf1\xd8\xac\x70\xbd\xcb\x7e\xd1\x85\xce\x81\xbd\x84\x35\x9d\x44\x25\x4d\x95\x62\x9e\x98\x55\xa9\x4a\x7c\x19\x58\xd1\xf8\xad\xa5\xd0\x53\x2e\xd8\xa5\xaa\x3f\xb2\xd1\x7b\xa7\x0e\xb6\x24\x8e\x59\x4e\x1a\x22\x97\xac\xbb\xb3\x9d\x50\x2f\x1a\x8c\x6e\xb6\xf1\xce\x22\xb3\xde\x1a\x1f\x40\xcc\x24\x55\x41\x19\xa8\x31\xa9\xaa\xd6\x07\x9c\xad\x88\x42\x5d\xe6\xbd\xe1\xa9\x18\x7e\xbb\x60\x92\xcf\x67\xbf\x2b\x13\xfd\x65\xf2\x70\x88\xd7\x8b\x7e\x88\x3c\x87\x59\xd2\xc4\xf5\xc6\x5a\xdb\x75\x53\x87\x8a\xd5\x75\xf9\xfa\xd8\x78\xe8\x0a\x0c\x9b\xa6\x3b\xcb\xcc\x27\x32\xe6\x94\x85\xbb\xc9\xc9\x0b\xfb\xd6\x24\x81\xd9\x08\x9b\xec\xcf\x80\xcf\xe2\xdf\x16\xa2\xcf\x65\xbd\x92\xdd\x59\x7b\x07\x07\xe0\x91\x7a\xf4\x8b\xbb\x75\xfe\xd4\x13\xd2\x38\xf5\x55\x5a\x7a\x56\x9d\x80\xc3\x41\x4a\x8d\x08\x59\xdc\x65\xa4\x61\x28\xba\xb2\x7a\xf8\x7a\x71\x31\x4f\x31\x8c\x78\x2b\x23\xeb\xfe\x80\x8b\x82\xb0\xce\x26\x40\x1d\x2e\x22\xf0\x4d\x83\xd1\x25\x5d\xc5\x1a\xdd\xd3\xb7\x5a\x2b\x1a\xe0\x78\x45\x04\xdf\x54\x3a\xf8\x96\x9b\xe3\xea\x70\x82\xff\x7f\xc9\x88\x8c\x14\x4d\xa2\xaf\x58\x42\x9e\xc9\x60\x31\xdb\xca\xd3\xda\xd9\xaf\x0d\xcb\xaa\xaf\x26\x8c\xb8\xfc\xff\xea\xd9\x4f\x3c\x7c\xa4\x95\xe0\x56\xa9\xb4\x7a\xcd\xb7\x51\xfb\x73\xe6\x66\xc6\xc6\x55\xad\xe8\x29\x72\x97\xd0\x7a\xd1\xba\x5e\x43\xf1\xbc\xa3\x23\x01\x65\x13\x39\xe2\x29\x04\xcc\x8c\x42\xf5\x8c\x30\xc0\x4a\xaf\xdb\x03\x8d\xda\x08\x47\xdd\x98\x8d\xcd\xa6\xf3\xbf\xd1\x5c\x4b\x4c\x45\x25\x00\x4a\xa0\x6e\xef\xf8\xca\x61\x78\x3a\xac\xec\x57\xfb\x3d\x1f\x92\xb0\xfe\x2f\xd1\xa8\x5f\x67\x24\x51\x7b\x65\xe6\x14\xad\x68\x08\xd6\xf6\xee\x34\xdf\xf7\x31\x0f\xdc\x82\xae\xbf\xd9\x04\xb0\x1e\x1d\xc5\x4b\x29\x27\x09\x4b\x2d\xb6\x8d\x6f\x90\x3b\x68\x40\x1a\xde\xbf\x5a\x7e\x08\xd7\x8f\xf4\xef\x5d\x63\x65\x3a\x65\x04\x0c\xf9\xbf\xd4\xac\xa7\x98\x4a\x74\xd3\x71\x45\x98\x67\x80\xfc\x0b\x16\xac\x45\x16\x49\xde\x61\x88\xa7\xdb\xdf\x19\x1f\x64\xb5\xfc\x5e\x2a\xb4\x7b\x57\xf7\xf7\x27\x6c\xd4\x19\xc1\x7a\x3c\xa8\xe1\xb9\x39\xae\x49\xe4\x88\xac\xba\x6b\x96\x56\x10\xb5\x48\x01\x09\xc8\xb1\x7b\x80\xe1\xb7\xb7\x50\xdf\xc7\x59\x8d\x5d\x50\x11\xfd\x2d\xcc\x56\x00\xa3\x2e\xf5\xb5\x2a\x1e\xcc\x82\x0e\x30\x8a\xa3\x42\x72\x1a\xac\x09\x43\xbf\x66\x86\xb6\x4b\x25\x79\x37\x65\x04\xcc\xc4\x93\xd9\x7e\x6a\xed\x3f\xb0\xf9\xcd\x71\xa4\x3d\xd4\x97\xf0\x1f\x17\xc0\xe2\xcb\x37\x97\xaa\x2a\x2f\x25\x66\x56\x16\x8e\x6c\x49\x6a\xfc\x5f\xb9\x32\x46\xf6\xb1\x11\x63\x98\xa3\x46\xf1\xa6\x41\xf3\xb0\x41\xe9\x89\xf7\x91\x4f\x90\xcc\x2c\x7f\xff\x35\x78\x76\xe5\x06\xb5\x0d\x33\x4b\xa7\x7c\x22\x5b\xc3\x07\xba\x53\x71\x52\xf3\xf1\x61\x0e\x4e\xaf\xe5\x95\xf6\xd9\xd9\x0d\x11\xfa\xa9\x33\xa1\x5e\xf1\x36\x95\x46\x86\x8a\x7f\x3a\x45\xa9\x67\x68\xd4\x0f\xd9\xd0\x34\x12\xc0\x91\xc6\x31\x5c\xf4\xfd\xe7\xcb\x68\x60\x69\x37\x38\x0d\xb2\xea\xaa\x70\x7b\x4c\x41\x85\xc3\x2e\xdd\xcd\xd3\x06\x70\x5e\x4d\xc1\xff\xc8\x72\xee\xee\x47\x5a\x64\xdf\xac\x86\xab\xa4\x1c\x06\x18\x98\x3f\x87\x41\xc5\xef\x68\xd3\xa1\x01\xe8\xa3\xb8\xca\xc6\x0c\x90\x5c\x15\xfc\x91\x08\x40\xb9\x4c\x00\xa0\xb9\xd0", # noqa E501 210 | "sig": b"\x0a\xab\x4c\x90\x05\x01\xb3\xe2\x4d\x7c\xdf\x46\x63\x32\x6a\x3a\x87\xdf\x5e\x48\x43\xb2\xcb\xdb\x67\xcb\xf6\xe4\x60\xfe\xc3\x50\xaa\x53\x71\xb1\x50\x8f\x9f\x45\x28\xec\xea\x23\xc4\x36\xd9\x4b\x5e\x8f\xcd\x4f\x68\x1e\x30\xa6\xac\x00\xa9\x70\x4a\x18\x8a\x03", # noqa E501 211 | }, 212 | { 213 | "key": b"\x83\x3f\xe6\x24\x09\x23\x7b\x9d\x62\xec\x77\x58\x75\x20\x91\x1e\x9a\x75\x9c\xec\x1d\x19\x75\x5b\x7d\xa9\x01\xb9\x6d\xca\x3d\x42", # noqa E501 214 | "pubkey": b"\xec\x17\x2b\x93\xad\x5e\x56\x3b\xf4\x93\x2c\x70\xe1\x24\x50\x34\xc3\x54\x67\xef\x2e\xfd\x4d\x64\xeb\xf8\x19\x68\x34\x67\xe2\xbf", # noqa E501 215 | "msg": b"\xdd\xaf\x35\xa1\x93\x61\x7a\xba\xcc\x41\x73\x49\xae\x20\x41\x31\x12\xe6\xfa\x4e\x89\xa9\x7e\xa2\x0a\x9e\xee\xe6\x4b\x55\xd3\x9a\x21\x92\x99\x2a\x27\x4f\xc1\xa8\x36\xba\x3c\x23\xa3\xfe\xeb\xbd\x45\x4d\x44\x23\x64\x3c\xe8\x0e\x2a\x9a\xc9\x4f\xa5\x4c\xa4\x9f", # noqa E501 216 | "sig": b"\xdc\x2a\x44\x59\xe7\x36\x96\x33\xa5\x2b\x1b\xf2\x77\x83\x9a\x00\x20\x10\x09\xa3\xef\xbf\x3e\xcb\x69\xbe\xa2\x18\x6c\x26\xb5\x89\x09\x35\x1f\xc9\xac\x90\xb3\xec\xfd\xfb\xc7\xc6\x64\x31\xe0\x30\x3d\xca\x17\x9c\x13\x8a\xc1\x7a\xd9\xbe\xf1\x17\x73\x31\xa7\x04", # noqa E501 217 | }, 218 | ] 219 | 220 | 221 | @pytest.mark.parametrize("vector", EDDSA_VECTORS) 222 | def test_eddsa_vectors(session, vector): 223 | key = ed25519.Ed25519PrivateKey.from_private_bytes(vector["key"]) 224 | k = AsymmetricKey.put( 225 | session, 0, "Test Ed25519", 0xFFFF, CAPABILITY.SIGN_EDDSA, key 226 | ) 227 | assert ( 228 | k.get_public_key().public_bytes( 229 | serialization.Encoding.Raw, serialization.PublicFormat.Raw 230 | ) 231 | == vector["pubkey"] 232 | ) 233 | assert k.sign_eddsa(vector["msg"]) == vector["sig"] 234 | k.delete() 235 | 236 | 237 | @pytest.fixture(params=[Mode.IMPORT, Mode.GENERATE]) 238 | def eddsa_keypair(request, session): 239 | if request.param == Mode.GENERATE: 240 | key = None 241 | asymkey = AsymmetricKey.generate( 242 | session, 243 | 0, 244 | "Generate EC", 245 | 0xFFFF, 246 | CAPABILITY.SIGN_EDDSA, 247 | ALGORITHM.EC_ED25519, 248 | ) 249 | public_key = asymkey.get_public_key() 250 | else: 251 | key = ed25519.Ed25519PrivateKey.generate() 252 | asymkey = AsymmetricKey.put( 253 | session, 0, "Test Ed25519", 0xFFFF, CAPABILITY.SIGN_EDDSA, key 254 | ) 255 | public_key = key.public_key() 256 | assert public_key.public_bytes( 257 | serialization.Encoding.Raw, serialization.PublicFormat.Raw 258 | ) == asymkey.get_public_key().public_bytes( 259 | serialization.Encoding.Raw, serialization.PublicFormat.Raw 260 | ) 261 | 262 | yield asymkey, public_key, key 263 | 264 | asymkey.delete() 265 | 266 | 267 | @pytest.mark.parametrize("length", [128, 129, 2019]) 268 | def test_eddsa_sign(session, eddsa_keypair, length): 269 | asymkey, public_key, private_key = eddsa_keypair 270 | data = os.urandom(length) 271 | sig = asymkey.sign_eddsa(data) 272 | public_key.verify(sig, data) 273 | if private_key: # Imported key, compare to SW signature 274 | assert sig == private_key.sign(data) 275 | -------------------------------------------------------------------------------- /tests/device/test_hmac.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016-2018 Yubico AB 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 | from yubihsm.defs import ALGORITHM, CAPABILITY 16 | from yubihsm.objects import HmacKey 17 | import random 18 | import os 19 | import pytest 20 | 21 | 22 | @pytest.mark.parametrize( 23 | "algorithm, expect_len", 24 | [ 25 | (ALGORITHM.HMAC_SHA1, 20), 26 | (ALGORITHM.HMAC_SHA256, 32), 27 | (ALGORITHM.HMAC_SHA384, 48), 28 | (ALGORITHM.HMAC_SHA512, 64), 29 | ], 30 | ) 31 | def test_generate_hmac(session, algorithm, expect_len): 32 | caps = CAPABILITY.SIGN_HMAC | CAPABILITY.VERIFY_HMAC 33 | hmackey = HmacKey.generate(session, 0, "Generate HMAC", 1, caps, algorithm) 34 | 35 | data = os.urandom(64) 36 | 37 | resp = hmackey.sign_hmac(data) 38 | assert len(resp) == expect_len 39 | assert hmackey.verify_hmac(resp, data) 40 | 41 | resp2 = hmackey.sign_hmac(data) 42 | assert len(resp2) == expect_len 43 | assert resp == resp2 44 | 45 | data = os.urandom(64) 46 | resp2 = hmackey.sign_hmac(data) 47 | assert len(resp2) == expect_len 48 | assert resp != resp2 49 | assert hmackey.verify_hmac(resp2, data) 50 | 51 | hmackey.delete() 52 | 53 | hmackey = HmacKey.generate(session, 0, "Generate HMAC", 1, caps, algorithm) 54 | 55 | resp = hmackey.sign_hmac(data) 56 | assert len(resp) == expect_len 57 | assert resp != resp2 58 | assert hmackey.verify_hmac(resp, data) 59 | 60 | hmackey.delete() 61 | 62 | 63 | @pytest.mark.parametrize( 64 | "vector", 65 | [ 66 | { 67 | "key": b"\x0b" * 20, 68 | "chal": b"Hi There", 69 | "exp_sha1": b"\xb6\x17\x31\x86\x55\x05\x72\x64\xe2\x8b\xc0\xb6\xfb\x37\x8c\x8e\xf1\x46\xbe\x00", # noqa: E501 70 | "exp_sha256": b"\xb0\x34\x4c\x61\xd8\xdb\x38\x53\x5c\xa8\xaf\xce\xaf\x0b\xf1\x2b\x88\x1d\xc2\x00\xc9\x83\x3d\xa7\x26\xe9\x37\x6c\x2e\x32\xcf\xf7", # noqa: E501 71 | "exp_sha512": b"\x87\xaa\x7c\xde\xa5\xef\x61\x9d\x4f\xf0\xb4\x24\x1a\x1d\x6c\xb0\x23\x79\xf4\xe2\xce\x4e\xc2\x78\x7a\xd0\xb3\x05\x45\xe1\x7c\xde\xda\xa8\x33\xb7\xd6\xb8\xa7\x02\x03\x8b\x27\x4e\xae\xa3\xf4\xe4\xbe\x9d\x91\x4e\xeb\x61\xf1\x70\x2e\x69\x6c\x20\x3a\x12\x68\x54", # noqa: E501 72 | "exp_sha384": b"\xaf\xd0\x39\x44\xd8\x48\x95\x62\x6b\x08\x25\xf4\xab\x46\x90\x7f\x15\xf9\xda\xdb\xe4\x10\x1e\xc6\x82\xaa\x03\x4c\x7c\xeb\xc5\x9c\xfa\xea\x9e\xa9\x07\x6e\xde\x7f\x4a\xf1\x52\xe8\xb2\xfa\x9c\xb6", # noqa: E501 73 | }, 74 | { 75 | "key": b"Jefe", 76 | "chal": b"what do ya want for nothing?", 77 | "exp_sha1": b"\xef\xfc\xdf\x6a\xe5\xeb\x2f\xa2\xd2\x74\x16\xd5\xf1\x84\xdf\x9c\x25\x9a\x7c\x79", # noqa: E501 78 | "exp_sha256": b"\x5b\xdc\xc1\x46\xbf\x60\x75\x4e\x6a\x04\x24\x26\x08\x95\x75\xc7\x5a\x00\x3f\x08\x9d\x27\x39\x83\x9d\xec\x58\xb9\x64\xec\x38\x43", # noqa: E501 79 | "exp_sha512": b"\x16\x4b\x7a\x7b\xfc\xf8\x19\xe2\xe3\x95\xfb\xe7\x3b\x56\xe0\xa3\x87\xbd\x64\x22\x2e\x83\x1f\xd6\x10\x27\x0c\xd7\xea\x25\x05\x54\x97\x58\xbf\x75\xc0\x5a\x99\x4a\x6d\x03\x4f\x65\xf8\xf0\xe6\xfd\xca\xea\xb1\xa3\x4d\x4a\x6b\x4b\x63\x6e\x07\x0a\x38\xbc\xe7\x37", # noqa: E501 80 | "exp_sha384": b"\xaf\x45\xd2\xe3\x76\x48\x40\x31\x61\x7f\x78\xd2\xb5\x8a\x6b\x1b\x9c\x7e\xf4\x64\xf5\xa0\x1b\x47\xe4\x2e\xc3\x73\x63\x22\x44\x5e\x8e\x22\x40\xca\x5e\x69\xe2\xc7\x8b\x32\x39\xec\xfa\xb2\x16\x49", # noqa: E501 81 | }, 82 | { 83 | "key": b"\xaa" * 20, 84 | "chal": b"\xdd" * 50, 85 | "exp_sha1": b"\x12\x5d\x73\x42\xb9\xac\x11\xcd\x91\xa3\x9a\xf4\x8a\xa1\x7b\x4f\x63\xf1\x75\xd3", # noqa: E501 86 | "exp_sha256": b"\x77\x3e\xa9\x1e\x36\x80\x0e\x46\x85\x4d\xb8\xeb\xd0\x91\x81\xa7\x29\x59\x09\x8b\x3e\xf8\xc1\x22\xd9\x63\x55\x14\xce\xd5\x65\xfe", # noqa: E501 87 | "exp_sha512": b"\xfa\x73\xb0\x08\x9d\x56\xa2\x84\xef\xb0\xf0\x75\x6c\x89\x0b\xe9\xb1\xb5\xdb\xdd\x8e\xe8\x1a\x36\x55\xf8\x3e\x33\xb2\x27\x9d\x39\xbf\x3e\x84\x82\x79\xa7\x22\xc8\x06\xb4\x85\xa4\x7e\x67\xc8\x07\xb9\x46\xa3\x37\xbe\xe8\x94\x26\x74\x27\x88\x59\xe1\x32\x92\xfb", # noqa: E501 88 | "exp_sha384": b"\x88\x06\x26\x08\xd3\xe6\xad\x8a\x0a\xa2\xac\xe0\x14\xc8\xa8\x6f\x0a\xa6\x35\xd9\x47\xac\x9f\xeb\xe8\x3e\xf4\xe5\x59\x66\x14\x4b\x2a\x5a\xb3\x9d\xc1\x38\x14\xb9\x4e\x3a\xb6\xe1\x01\xa3\x4f\x27", # noqa: E501 89 | }, 90 | { 91 | "key": b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19", # noqa: E501 92 | "chal": b"\xcd" * 50, 93 | "exp_sha1": b"\x4c\x90\x07\xf4\x02\x62\x50\xc6\xbc\x84\x14\xf9\xbf\x50\xc8\x6c\x2d\x72\x35\xda", # noqa: E501 94 | "exp_sha256": b"\x82\x55\x8a\x38\x9a\x44\x3c\x0e\xa4\xcc\x81\x98\x99\xf2\x08\x3a\x85\xf0\xfa\xa3\xe5\x78\xf8\x07\x7a\x2e\x3f\xf4\x67\x29\x66\x5b", # noqa: E501 95 | "exp_sha512": b"\xb0\xba\x46\x56\x37\x45\x8c\x69\x90\xe5\xa8\xc5\xf6\x1d\x4a\xf7\xe5\x76\xd9\x7f\xf9\x4b\x87\x2d\xe7\x6f\x80\x50\x36\x1e\xe3\xdb\xa9\x1c\xa5\xc1\x1a\xa2\x5e\xb4\xd6\x79\x27\x5c\xc5\x78\x80\x63\xa5\xf1\x97\x41\x12\x0c\x4f\x2d\xe2\xad\xeb\xeb\x10\xa2\x98\xdd", # noqa: E501 96 | "exp_sha384": b"\x3e\x8a\x69\xb7\x78\x3c\x25\x85\x19\x33\xab\x62\x90\xaf\x6c\xa7\x7a\x99\x81\x48\x08\x50\x00\x9c\xc5\x57\x7c\x6e\x1f\x57\x3b\x4e\x68\x01\xdd\x23\xc4\xa7\xd6\x79\xcc\xf8\xa3\x86\xc6\x74\xcf\xfb", # noqa: E501 97 | }, 98 | ], 99 | ) 100 | def test_hmac_vectors(session, vector): 101 | key1_id, key2_id, key3_id, key4_id = random.sample(range(1, 0xFFFE), 4) 102 | 103 | caps = CAPABILITY.SIGN_HMAC | CAPABILITY.VERIFY_HMAC 104 | 105 | key1 = HmacKey.put( 106 | session, 107 | key1_id, 108 | "Test HMAC Vectors 0x%04x" % key1_id, 109 | 1, 110 | caps, 111 | vector["key"], 112 | ALGORITHM.HMAC_SHA1, 113 | ) 114 | key2 = HmacKey.put( 115 | session, 116 | key2_id, 117 | "Test HMAC Vectors 0x%04x" % key2_id, 118 | 1, 119 | caps, 120 | vector["key"], 121 | ALGORITHM.HMAC_SHA256, 122 | ) 123 | key3 = HmacKey.put( 124 | session, 125 | key3_id, 126 | "Test HMAC Vectors 0x%04x" % key3_id, 127 | 1, 128 | caps, 129 | vector["key"], 130 | ALGORITHM.HMAC_SHA384, 131 | ) 132 | key4 = HmacKey.put( 133 | session, 134 | key4_id, 135 | "Test HMAC Vectors 0x%04x" % key4_id, 136 | 1, 137 | caps, 138 | vector["key"], 139 | ALGORITHM.HMAC_SHA512, 140 | ) 141 | 142 | assert key1.sign_hmac(vector["chal"]) == vector["exp_sha1"] 143 | assert key2.sign_hmac(vector["chal"]) == vector["exp_sha256"] 144 | assert key3.sign_hmac(vector["chal"]) == vector["exp_sha384"] 145 | assert key4.sign_hmac(vector["chal"]) == vector["exp_sha512"] 146 | assert key1.verify_hmac(vector["exp_sha1"], vector["chal"]) 147 | assert key2.verify_hmac(vector["exp_sha256"], vector["chal"]) 148 | assert key3.verify_hmac(vector["exp_sha384"], vector["chal"]) 149 | assert key4.verify_hmac(vector["exp_sha512"], vector["chal"]) 150 | 151 | key1.delete() 152 | key2.delete() 153 | key3.delete() 154 | key4.delete() 155 | 156 | 157 | @pytest.mark.parametrize( 158 | "vector", 159 | [ 160 | { 161 | "key": b"\x0b" * 65, # Larger than SHA1 block size (64) 162 | "chal": b"\xdd" * 50, 163 | "algorithm": ALGORITHM.HMAC_SHA1, 164 | "exp_sha": b"= 2.3.1 31 | # This ensures test independence across different YubiHSM versions 32 | if info.version >= (2, 3, 1): 33 | ignored_cmds = [ 34 | COMMAND.ERROR, 35 | COMMAND.DEVICE_INFO, 36 | COMMAND.GET_LOG_ENTRIES, 37 | COMMAND.ECHO, 38 | COMMAND.SESSION_MESSAGE, 39 | ] 40 | 41 | if info.version < (2, 4, 0): 42 | ignored_cmds.extend( 43 | [ 44 | COMMAND.PUT_PUBLIC_WRAP_KEY, 45 | COMMAND.WRAP_KEY_RSA, 46 | COMMAND.UNWRAP_KEY_RSA, 47 | COMMAND.EXPORT_WRAPPED_RSA, 48 | COMMAND.IMPORT_WRAPPED_RSA, 49 | ] 50 | ) 51 | 52 | session.set_command_audit( 53 | {cmd: AUDIT.ON for cmd in COMMAND if cmd not in ignored_cmds} 54 | ) 55 | 56 | 57 | def test_get_log_entries(session): 58 | boot, auth, logs = session.get_log_entries() 59 | 60 | last_digest = logs[0].digest 61 | for i in range(1, len(logs)): 62 | digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) 63 | digest.update(logs[i].data) 64 | digest.update(last_digest) 65 | last_digest = digest.finalize()[:16] 66 | assert last_digest == logs[i].digest 67 | 68 | 69 | def test_full_log(session): 70 | hmackey = HmacKey.generate( 71 | session, 72 | 0, 73 | "Test Full Log", 74 | 1, 75 | CAPABILITY.SIGN_HMAC | CAPABILITY.VERIFY_HMAC, 76 | ALGORITHM.HMAC_SHA256, 77 | ) 78 | 79 | for i in range(0, 30): 80 | data = os.urandom(64) 81 | resp = hmackey.sign_hmac(data) 82 | assert len(resp) == 32 83 | assert hmackey.verify_hmac(resp, data) 84 | 85 | boot, auth, logs = session.get_log_entries() 86 | 87 | last_digest = logs[0].digest 88 | for i in range(1, len(logs)): 89 | digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) 90 | digest.update(logs[i].data) 91 | digest.update(last_digest) 92 | last_digest = digest.finalize()[:16] 93 | assert last_digest == logs[i].digest 94 | 95 | hmackey.delete() 96 | 97 | 98 | def test_wrong_chain(session): 99 | hmackey = HmacKey.generate( 100 | session, 101 | 0, 102 | "Test Log hash chain", 103 | 1, 104 | CAPABILITY.SIGN_HMAC | CAPABILITY.VERIFY_HMAC, 105 | ALGORITHM.HMAC_SHA256, 106 | ) 107 | 108 | boot, auth, logs = session.get_log_entries() 109 | last_line = logs.pop() 110 | session.set_log_index(last_line.number) 111 | 112 | hmackey.sign_hmac(b"hello") 113 | hmackey.sign_hmac(b"hello") 114 | hmackey.sign_hmac(b"hello") 115 | 116 | with pytest.raises(ValueError): 117 | session.get_log_entries(logs.pop()) # Wrong number 118 | 119 | wrong_line = replace(last_line, digest=os.urandom(16)) 120 | with pytest.raises(YubiHsmInvalidResponseError): 121 | session.get_log_entries(wrong_line) 122 | 123 | hmackey.delete() 124 | 125 | 126 | def test_forced_log(hsm, session): 127 | boot, auth, logs = session.get_log_entries() 128 | last_line = logs.pop() 129 | session.set_log_index(last_line.number) 130 | session.set_force_audit(AUDIT.ON) 131 | assert session.get_force_audit() == AUDIT.ON 132 | 133 | hmackey = HmacKey.generate( 134 | session, 135 | 0, 136 | "Test Force Log", 137 | 1, 138 | CAPABILITY.SIGN_HMAC | CAPABILITY.VERIFY_HMAC, 139 | ALGORITHM.HMAC_SHA256, 140 | ) 141 | 142 | error = 0 143 | for i in range(0, 32): 144 | try: 145 | data = os.urandom(64) 146 | resp = hmackey.sign_hmac(data) 147 | assert len(resp) == 32 148 | assert hmackey.verify_hmac(resp, data) 149 | except YubiHsmDeviceError as e: 150 | error = e.code 151 | assert error == ERROR.LOG_FULL 152 | device_info = hsm.get_device_info() 153 | assert device_info.log_used == device_info.log_size 154 | 155 | boot, auth, logs = session.get_log_entries(last_line) 156 | last_line = logs.pop() 157 | session.set_log_index(last_line.number) 158 | session.set_force_audit(AUDIT.OFF) 159 | assert session.get_force_audit() == AUDIT.OFF 160 | 161 | for i in range(0, 32): 162 | data = os.urandom(64) 163 | resp = hmackey.sign_hmac(data) 164 | assert len(resp) == 32 165 | assert hmackey.verify_hmac(resp, data) 166 | 167 | hmackey.delete() 168 | 169 | 170 | def test_logs_after_reset(hsm, connect_hsm, session, info): 171 | session.reset_device() 172 | hsm.close() 173 | 174 | time.sleep(5) # Wait for device to reboot 175 | 176 | with connect_hsm() as hsm: 177 | with hsm.create_session_derived(1, DEFAULT_KEY) as session: 178 | boot, auth, logs = session.get_log_entries() 179 | 180 | # Check the version of the YubiHSM and verify the number of log entries. 181 | # YubiHSM versions >= 2.3.1 have command audit logging disabled per default, 182 | # so they will only have two initial logs: the boot and reset line. 183 | # Versions below 2.3.1 are expected to have four log entries: the boot line, 184 | # reset line, create_session and authenticate_session cmd. 185 | if info.version < (2, 3, 1): 186 | assert 4 == len(logs) 187 | else: 188 | assert 2 == len(logs) 189 | 190 | # Reset line 191 | assert logs.pop(0).data == b"\0\1" + b"\xff" * 14 192 | 193 | # Boot line 194 | assert logs.pop(0).data == struct.pack("!HBHHHHBL", 2, 0, 0, 0xFFFF, 0, 0, 0, 0) 195 | 196 | if info.version < (2, 3, 1): 197 | assert logs.pop(0).command == COMMAND.CREATE_SESSION 198 | assert logs.pop(0).command == COMMAND.AUTHENTICATE_SESSION 199 | -------------------------------------------------------------------------------- /tests/device/test_opaque.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016-2018 Yubico AB 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 | from yubihsm.defs import CAPABILITY, ALGORITHM 16 | from yubihsm.objects import Opaque 17 | from yubihsm.exceptions import YubiHsmInvalidRequestError 18 | from cryptography import x509 19 | from cryptography.hazmat.backends import default_backend 20 | from cryptography.hazmat.primitives import hashes 21 | from cryptography.hazmat.primitives.asymmetric import ec 22 | import os 23 | import uuid 24 | import datetime 25 | import pytest 26 | 27 | 28 | def test_put_empty(session): 29 | # Can't put an empty object 30 | with pytest.raises(ValueError): 31 | Opaque.put( 32 | session, 33 | 0, 34 | "Test PUT empty Opaque", 35 | 1, 36 | CAPABILITY.NONE, 37 | ALGORITHM.OPAQUE_DATA, 38 | b"", 39 | ) 40 | 41 | 42 | def test_data(session): 43 | for size in (1, 256, 1234, 1968): 44 | data = os.urandom(size) 45 | 46 | opaque = Opaque.put( 47 | session, 48 | 0, 49 | "Test data Opaque", 50 | 1, 51 | CAPABILITY.NONE, 52 | ALGORITHM.OPAQUE_DATA, 53 | data, 54 | ) 55 | 56 | assert data == opaque.get() 57 | opaque.delete() 58 | 59 | 60 | def test_put_too_big(session): 61 | with pytest.raises(YubiHsmInvalidRequestError): 62 | Opaque.put( 63 | session, 64 | 0, 65 | "Test large Opaque", 66 | 1, 67 | CAPABILITY.NONE, 68 | ALGORITHM.OPAQUE_DATA, 69 | os.urandom(3064), 70 | ) 71 | 72 | # Make sure our session is still working 73 | assert len(session.get_pseudo_random(123)) == 123 74 | 75 | 76 | def test_certificate(session): 77 | private_key = ec.generate_private_key( 78 | ALGORITHM.EC_P256.to_curve(), default_backend() 79 | ) 80 | name = x509.Name( 81 | [x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "Test Certificate")] 82 | ) 83 | one_day = datetime.timedelta(1, 0, 0) 84 | certificate = ( 85 | x509.CertificateBuilder() 86 | .subject_name(name) 87 | .issuer_name(name) 88 | .not_valid_before(datetime.datetime.today() - one_day) 89 | .not_valid_after(datetime.datetime.today() + one_day) 90 | .serial_number(int(uuid.uuid4())) 91 | .public_key(private_key.public_key()) 92 | .sign(private_key, hashes.SHA256(), default_backend()) 93 | ) 94 | 95 | certobj = Opaque.put_certificate( 96 | session, 0, "Test certificate Opaque", 1, CAPABILITY.NONE, certificate 97 | ) 98 | 99 | assert certificate == certobj.get_certificate() 100 | certobj.delete() 101 | -------------------------------------------------------------------------------- /tests/device/test_otp.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016-2018 Yubico AB 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 | from yubihsm.defs import ALGORITHM, CAPABILITY, ERROR 16 | from yubihsm.objects import OtpAeadKey, OtpData 17 | from yubihsm.exceptions import YubiHsmDeviceError 18 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes 19 | from cryptography.hazmat.primitives.ciphers.aead import AESCCM 20 | from cryptography.hazmat.backends import default_backend 21 | from dataclasses import dataclass 22 | import os 23 | import struct 24 | import pytest 25 | from typing import Mapping 26 | 27 | 28 | @dataclass 29 | class OtpTestVector: 30 | key: bytes 31 | id: bytes 32 | otps: Mapping[str, OtpData] 33 | 34 | 35 | # From: https://developers.yubico.com/OTP/Specifications/Test_vectors.html 36 | TEST_VECTORS = [ 37 | OtpTestVector( 38 | key=b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", 39 | id=b"\x01\x02\x03\x04\x05\x06", 40 | otps={ 41 | "dvgtiblfkbgturecfllberrvkinnctnn": OtpData(1, 1, 1, 1), 42 | "rnibcnfhdninbrdebccrndfhjgnhftee": OtpData(1, 2, 1, 1), 43 | "iikkijbdknrrdhfdrjltvgrbkkjblcbh": OtpData(0xFFF, 1, 1, 1), 44 | }, 45 | ), 46 | OtpTestVector( 47 | key=b"\x88\x88\x88\x88\x88\x88\x88\x88\x88\x88\x88\x88\x88\x88\x88\x88", 48 | id=b"\x88\x88\x88\x88\x88\x88", 49 | otps={"dcihgvrhjeucvrinhdfddbjhfjftjdei": OtpData(0x8888, 0x88, 0x88, 0x8888)}, 50 | ), 51 | OtpTestVector( 52 | key=b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 53 | id=b"\x00\x00\x00\x00\x00\x00", 54 | otps={"kkkncjnvcnenkjvjgncjihljiibgbhbh": OtpData(0, 0, 0, 0)}, 55 | ), 56 | OtpTestVector( 57 | key=b"\xc4\x42\x28\x90\x65\x30\x76\xcd\xe7\x3d\x44\x9b\x19\x1b\x41\x6a", 58 | id=b"\x33\xc6\x9e\x7f\x24\x9e", 59 | otps={"iucvrkjiegbhidrcicvlgrcgkgurhjnj": OtpData(1, 0, 0x24, 0x13A7)}, 60 | ), 61 | ] 62 | 63 | 64 | def _crc16(data): 65 | crc = 0xFFFF 66 | for b in bytearray(data): 67 | crc ^= b 68 | for _ in range(8): 69 | j = crc & 1 70 | crc >>= 1 71 | if j: 72 | crc ^= 0x8408 73 | return struct.pack(">\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./" # noqa 37 | _TRANSCEIVE_DEVICE_INFO = b"\x86\x008\x02\x00\x00\x00s4\xbc>\x04\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./" # noqa 38 | 39 | 40 | def get_backend(): 41 | return MagicMock(YhsmBackend) 42 | 43 | 44 | def simple_urandom(length): 45 | """See https://xkcd.com/221/""" 46 | return b"\x00" * length 47 | 48 | 49 | @patch("os.urandom", side_effect=simple_urandom) 50 | def get_mocked_session(patch): 51 | """ 52 | Create a fake session by mocking 53 | the backend.transceive function 54 | """ 55 | mocked_backend = get_backend() 56 | mocked_backend.transceive.side_effect = [ 57 | _TRANSCEIVE_DEVICE_INFO, # get_device_info is called during initialization 58 | b"\x83\x00\x11\x00\x05MV1\xc9\x18o\x802%\xed\x8a2$\xf2\xcf", 59 | b"\x84\x00\x00", 60 | ] 61 | hsm = YubiHsm(backend=mocked_backend) 62 | 63 | auth_key_id = 1 64 | key_enc = b"\t\x0bG\xdb\xedYVT\x90\x1d\xee\x1c\xc6U\xe4 " 65 | key_mac = b"Y/\xd4\x83\xf7Y\xe2\x99\t\xa0LE\x05\xd2\xce\n" 66 | return hsm.create_session(auth_key_id, key_enc, key_mac) 67 | 68 | 69 | class TestDeviceInfo(unittest.TestCase): 70 | """Test the functionality of the DeviceInfo class""" 71 | 72 | def test_class(self): 73 | """ 74 | Full testing of the Device Info class 75 | """ 76 | info = DeviceInfo.parse(_DEVICE_INFO) 77 | self.assertEqual(info.version, (2, 0, 0)) 78 | self.assertEqual(info.serial, 7550317) 79 | self.assertEqual(info.log_size, 62) 80 | self.assertEqual(info.log_used, 62) 81 | self.assertEqual(len(info.supported_algorithms), 47) 82 | 83 | 84 | class TestDeriveFct(unittest.TestCase): 85 | """ 86 | Test the functionality of the private method _derive. If we decide not to 87 | test private functions, this can be removed. 88 | """ 89 | 90 | def test_success(self): 91 | """ 92 | Make sure the function works on a test case that should succeed 93 | """ 94 | context = b"\x0c\xf4\xf5L\xb9\xdfY[" 95 | t = 0x04 96 | key = b"0xff0xff0x120xff0xff0xff0xff0xaf" 97 | rval = _derive(key, t, context) 98 | self.assertEqual(rval, b"W\xa0\x7f1\xb9\x13\xbc\xe5\xff\x066J\x0e9Fz") 99 | 100 | def test_value_error(self): 101 | """ 102 | Make sure the test fails when an unsupported value 103 | """ 104 | context = b"\x0c\xf4\xf5L\xb9\xdfY[" 105 | t = 0x04 106 | key = b"0xff0xff0x120xff0xff0xff0xff0xaf" 107 | self.assertRaises(ValueError, _derive, key, t, context, 0x60) 108 | 109 | 110 | class TestUnpad(unittest.TestCase): 111 | """ 112 | Test the functionality of the private method _unpad. If we decide not to 113 | test private functions, this can be removed. 114 | """ 115 | 116 | def test_invalid_len(self): 117 | """Check if the response length is invalid""" 118 | resp = b"\x02\x7f" 119 | cmd = COMMAND.SIGN_ECDSA 120 | self.assertRaises(YubiHsmInvalidResponseError, _unpad_resp, resp, cmd) 121 | resp = b"\x14\x00\x06\x00|\x00\xff" 122 | self.assertRaises(YubiHsmInvalidResponseError, _unpad_resp, resp, cmd) 123 | 124 | def test_device_error(self): 125 | """Check if the length of the response doesn't match promised length""" 126 | cmd = COMMAND.ERROR 127 | resp = b"\x7f\x00\x01\x00\x01\x00>" 128 | self.assertRaises(YubiHsmDeviceError, _unpad_resp, resp, cmd) 129 | 130 | def test_invalid_rcommand(self): 131 | """Throw error if the response command doesn't match the 132 | command sent | 0x80""" 133 | cmd = COMMAND.AUTHENTICATE_SESSION 134 | resp = struct.pack("!BHHH", cmd - 1, 1, 1, 62) 135 | self.assertRaises(YubiHsmInvalidResponseError, _unpad_resp, resp, cmd) 136 | 137 | def test_success(self): 138 | """Otherwise, succeed""" 139 | cmd = COMMAND.AUTHENTICATE_SESSION 140 | resp = b"\x84\x00\x02\x00\x01\x00\x1d\x00\x04" 141 | rval = _unpad_resp(resp, cmd) 142 | self.assertEqual(rval, b"\x00\x01") 143 | 144 | 145 | class TestLogEntry(unittest.TestCase): 146 | """ 147 | Full testing of the LogEntry class 148 | """ 149 | 150 | def test_construction(self): 151 | """Use classmethod `parse` to construct log entry from data""" 152 | 153 | # Decide on values 154 | vals = 513, 250, 1020, 56, 800, 900, 20, 1023, b"abcdefghiklmnop0" 155 | keys = ( 156 | "number", 157 | "command", 158 | "length", 159 | "session_key", 160 | "target_key", 161 | "second_key", 162 | "result", 163 | "tick", 164 | "digest", 165 | ) 166 | 167 | # Pack it up for parsing 168 | data = struct.pack("!HBHHHHBL16s", *vals) 169 | 170 | # Use the `parse` alternate constructor 171 | log = LogEntry.parse(data) 172 | for key, val in zip(keys, vals): 173 | self.assertEqual(getattr(log, key), val) 174 | 175 | # Make sure __init__ and parse give you the same result 176 | self.assertEqual(LogEntry(**dict(zip(keys, vals))), log) 177 | 178 | 179 | class TestLogCorrect(unittest.TestCase): 180 | """ 181 | Full coverage tests of the log class. Includes construction, checks 182 | for errors, and validation 183 | """ 184 | 185 | FORMAT = "!HBHHHHBL16s" 186 | 187 | # The first 2 entries in the log are provided below, along with a 188 | # version of log 2 with a tampered hash 189 | log1_vals = ( 190 | 2, 191 | 0, 192 | 0, 193 | 65535, 194 | 0, 195 | 0, 196 | 0, 197 | 0, 198 | b"\xf6\x96\x90n[9)\xc6<\xa6\xf1\n\x83\xd2\xa0\xcc", 199 | ) 200 | 201 | log2_vals = ( 202 | 3, 203 | 3, 204 | 10, 205 | 65535, 206 | 1, 207 | 65535, 208 | 131, 209 | 35, 210 | b'"d\xd4Q\xb5\xef\xf5\xdf\xa9LTO3\xb7\x87\xa9', 211 | ) 212 | 213 | # Log 2 is valid, aside from its hash, which doesn't match 214 | log2_badvals = ( 215 | 3, 216 | 3, 217 | 10, 218 | 65535, 219 | 1, 220 | 65535, 221 | 131, 222 | 35, 223 | b'"d\xd4Q\xb5\xef\xf5\xdf\xa9LTO3\xb7\x87\xa8', 224 | ) 225 | 226 | log1 = struct.pack(FORMAT, *log1_vals) 227 | log2 = struct.pack(FORMAT, *log2_vals) 228 | log2_bad = struct.pack(FORMAT, *log2_badvals) 229 | 230 | e1 = LogEntry.parse(log1) 231 | e2 = LogEntry.parse(log2) 232 | e2_bad = LogEntry.parse(log2_bad) 233 | 234 | def test_logvalidation(self): 235 | """Make sure we can validate correct, sequential entries""" 236 | self.assertTrue(self.e2.validate(self.e1)) 237 | 238 | def test_unorderedloginvalid(self): 239 | """Entries can't validate if out of order""" 240 | self.assertRaises(ValueError, self.e1.validate, self.e2) 241 | 242 | def test_tamperedhash(self): 243 | """Bad hashes can be detected""" 244 | self.assertFalse(self.e2_bad.validate(self.e1)) 245 | 246 | def test_data(self): 247 | """Check that the data property works""" 248 | self.assertTrue(self.e1.data == self.log1[:-16]) 249 | 250 | def test_initializer(self): 251 | """ 252 | Check that using the default initializer gives the same 253 | result as using the pack constructor 254 | """ 255 | self.assertTrue(self.e1 == LogEntry(*self.log1_vals)) 256 | 257 | 258 | class TestYubiHsm(unittest.TestCase): 259 | @patch("yubihsm.core.YubiHsm.create_session") 260 | def test_create_session_derived(self, item): 261 | """ 262 | Test if create_session_derived calls create_session correctly 263 | """ 264 | 265 | auth_key_id = 1 266 | password = "password" 267 | expect_enc = b"\t\x0bG\xdb\xedYVT\x90\x1d\xee\x1c\xc6U\xe4 " 268 | expect_mac = b"Y/\xd4\x83\xf7Y\xe2\x99\t\xa0LE\x05\xd2\xce\n" 269 | 270 | # Note: get_device_info gets called during initialization 271 | # which is why we mock the transceive function. 272 | backend = get_backend() 273 | backend.transceive.return_value = _TRANSCEIVE_DEVICE_INFO 274 | 275 | hsm = YubiHsm(backend) 276 | hsm.create_session_derived(auth_key_id, password) 277 | 278 | hsm.create_session.assert_called_once_with(auth_key_id, expect_enc, expect_mac) 279 | 280 | def test_get_device_info_mock_transceive(self): 281 | """ 282 | Test get_device_info function by mocking the transceive function 283 | """ 284 | 285 | backend = get_backend() 286 | backend.transceive.return_value = _TRANSCEIVE_DEVICE_INFO 287 | 288 | hsm = YubiHsm(backend) 289 | 290 | info = hsm.get_device_info() 291 | hsm._backend.transceive.assert_has_calls( 292 | [ 293 | call(b"\x06\x00\x00"), # first call during YubiHSM::__init__ 294 | call(b"\x06\x00\x00"), 295 | ] 296 | ) 297 | 298 | self.assertEqual(info.version, (2, 0, 0)) 299 | self.assertEqual(info.serial, 7550140) 300 | self.assertEqual(info.log_size, 62) 301 | self.assertEqual(info.log_used, 4) 302 | self.assertEqual(len(info.supported_algorithms), 47) 303 | 304 | 305 | class TestAuthsession(unittest.TestCase): 306 | def test_list_objects1(self): 307 | """ 308 | Test the first half of the list_objects function: 309 | We process the input and make our query to the send_cmd 310 | """ 311 | # Create fake session, and mock the return from a call to side_effect 312 | session = MagicMock(AuthSession) 313 | session.send_secure_cmd.side_effect = [b"\x00\x01\x02\x00V7\x03\x00"] 314 | 315 | # Run the function, and make sure the correct call is made to secure_cmd 316 | AuthSession.list_objects( 317 | session, 318 | object_id=2, 319 | object_type=1, 320 | domains=65535, 321 | capabilities=CAPABILITY.ALL, 322 | algorithm=ALGORITHM.HMAC_SHA384, 323 | ) 324 | 325 | # We care only about the value sent; we aren't checking the return value 326 | # from send_secure_cmd 327 | session.send_secure_cmd.assert_called_with( 328 | 72, 329 | b"\x01\x00\x02\x02\x01\x03\xff\xff\x04\x00\xff\xff\xff\xff\xff\xff\xff\x05\x15", # noqa E501 330 | ) 331 | 332 | def test_list_objects2(self): 333 | """ 334 | Test the second half of list_objects(): process the response from the 335 | send_cmd function and return the list of objects 336 | """ 337 | list_objects = AuthSession.list_objects 338 | session = MagicMock(AuthSession) 339 | session.send_secure_cmd.return_value = b"\x00\x01\x02\x00d\x8f\x03\x00\x00\x01\x03\x00\x00\x04\x05\x00\x00\x05\x05\x00\x00\x05\x03\x00" # noqa 340 | 341 | # The input to the below function doesn't matter; 342 | # it's overwritten by the return value listed above 343 | objlist = list_objects(session) 344 | 345 | # Finally, make sure we decode the results correctly 346 | # Not the best way to check, but it is succinct 347 | self.assertEqual( 348 | objlist.__repr__(), 349 | "[AuthenticationKey(id=1), AsymmetricKey(id=25743), AsymmetricKey(id=1), HmacKey(id=4), HmacKey(id=5), AsymmetricKey(id=5)]", # noqa E501 350 | ) 351 | 352 | def test__create_session_patch_transceive(self): 353 | """ 354 | Tests the entire authsession generation codebase, by mocking just 355 | os.urandom and the transceive method. 356 | This test should probably be broken up later. 357 | """ 358 | authsession = get_mocked_session() 359 | # Create session should make two calls to transceive. 360 | # First call was to create session. Second was to authenticate session. 361 | calls = [ 362 | call(b"\x03\x00\n\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"), 363 | call(b"\x04\x00\x11\x00\x1c\xe7U.\x0fyv\xdb\xcc!\x98\xfd\x15\\3Z"), 364 | ] 365 | 366 | authsession._hsm._backend.transceive.assert_has_calls(calls) 367 | 368 | def test_close_session(self): 369 | """ 370 | Test the close session command; only tests output to send_secure_cmd 371 | not to transceive 372 | """ 373 | 374 | session = get_mocked_session() 375 | session.send_secure_cmd = MagicMock(session.send_secure_cmd) 376 | 377 | session.close() 378 | session.send_secure_cmd.assert_called_once_with(COMMAND.CLOSE_SESSION) 379 | 380 | def test_reset(self): 381 | """ 382 | Tests the reset command; only tests output to send_secure_cmd 383 | not to transceive 384 | """ 385 | session = get_mocked_session() 386 | session.send_secure_cmd = MagicMock(session.send_secure_cmd) 387 | session.send_secure_cmd.return_value = b"" 388 | 389 | session.reset_device() 390 | 391 | session.send_secure_cmd.assert_called_with(COMMAND.RESET_DEVICE) 392 | 393 | def test_reset_error(self): 394 | """ 395 | Make sure reset command throws error if nonempty response is returned 396 | """ 397 | 398 | session = get_mocked_session() 399 | session.send_secure_cmd = MagicMock(session.send_secure_cmd) 400 | session.send_secure_cmd.return_value = b"\00" 401 | 402 | self.assertRaises(YubiHsmInvalidResponseError, session.reset_device) 403 | -------------------------------------------------------------------------------- /tests/test_defs.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016-2018 Yubico AB 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 | from yubihsm.defs import ALGORITHM, CAPABILITY 16 | from cryptography.hazmat.primitives.asymmetric import ec 17 | 18 | import pytest 19 | 20 | 21 | @pytest.mark.parametrize( 22 | "algorithm, curve", 23 | [ 24 | (ALGORITHM.EC_P224, ec.SECP224R1), 25 | (ALGORITHM.EC_P256, ec.SECP256R1), 26 | (ALGORITHM.EC_P384, ec.SECP384R1), 27 | (ALGORITHM.EC_P521, ec.SECP521R1), 28 | (ALGORITHM.EC_K256, ec.SECP256K1), 29 | (ALGORITHM.EC_BP256, ec.BrainpoolP256R1), 30 | (ALGORITHM.EC_BP384, ec.BrainpoolP384R1), 31 | (ALGORITHM.EC_BP512, ec.BrainpoolP512R1), 32 | ], 33 | ) 34 | def test_algorithm_to_from_curve(algorithm, curve): 35 | assert isinstance(algorithm.to_curve(), curve) 36 | assert algorithm == ALGORITHM.for_curve(curve()) 37 | 38 | 39 | def test_capability_all_includes_everything(): 40 | assert CAPABILITY.ALL == sum(CAPABILITY) 41 | assert CAPABILITY.NONE == 0 42 | 43 | for c in CAPABILITY: 44 | assert c in CAPABILITY.ALL 45 | assert c not in CAPABILITY.NONE 46 | -------------------------------------------------------------------------------- /tests/test_objects.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016-2018 Yubico AB 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 | from yubihsm.objects import ( 16 | ObjectInfo, 17 | YhsmObject, 18 | Opaque, 19 | AuthenticationKey, 20 | AsymmetricKey, 21 | WrapKey, 22 | PublicWrapKey, 23 | HmacKey, 24 | Template, 25 | OtpAeadKey, 26 | ) 27 | from yubihsm.core import AuthSession 28 | from yubihsm.defs import ORIGIN, ALGORITHM, OBJECT, CAPABILITY 29 | from binascii import a2b_hex 30 | from unittest.mock import MagicMock 31 | from random import randint 32 | 33 | import unittest 34 | 35 | 36 | _DATA = a2b_hex( 37 | "ffffffffffffffff00010028ffff0226000244454641554c5420415554484b4559204348414e47452054484953204153415000c0ffeec0ffee01ffffffffffffffff" # noqa E501 38 | ) 39 | 40 | 41 | class TestObjectInfo(unittest.TestCase): 42 | def test_objectinfo_parsing(self): 43 | info = ObjectInfo.parse(_DATA) 44 | self.assertEqual(info.capabilities, 0xFFFFFFFFFFFFFFFF) 45 | self.assertEqual(info.id, 1) 46 | self.assertEqual(info.size, 40) 47 | self.assertEqual(info.domains, 0xFFFF) 48 | self.assertEqual(info.object_type, OBJECT.AUTHENTICATION_KEY) 49 | self.assertEqual(info.algorithm, ALGORITHM.AES128_YUBICO_AUTHENTICATION) 50 | self.assertEqual(info.sequence, 0) 51 | self.assertEqual(info.origin, ORIGIN.IMPORTED) 52 | self.assertEqual(info.label, "DEFAULT AUTHKEY CHANGE THIS ASAP") 53 | self.assertEqual(info.delegated_capabilities, 0xFFFFFFFFFFFFFFFF) 54 | 55 | def test_non_utf8_label(self): 56 | label = b"\xfe\xed\xfa\xce" * 10 57 | data = bytearray(_DATA) 58 | data[18:58] = label 59 | info = ObjectInfo.parse(bytes(data)) 60 | self.assertEqual(info.label, label) 61 | self.assertIsInstance(info.label, bytes) 62 | 63 | 64 | class TestYhsmObject(unittest.TestCase): 65 | def test_get_info(self): 66 | AuthMock = MagicMock(AuthSession) 67 | AuthMock.send_secure_cmd.return_value = b"\x00\x00\x7f\xff\xff\xff\xff\xff\x00\x05\x01\x00\x00)\x05\x16\x00\x01hmaclabel\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" # noqa E501 68 | 69 | # Create instance from mocked data and set the object type 70 | obj = YhsmObject(session=AuthMock, object_id=5) 71 | obj.object_type = OBJECT.HMAC_KEY 72 | 73 | # The expected ObjectInfo is below 74 | info = ObjectInfo( 75 | capabilities=CAPABILITY(140737488355327), 76 | id=5, 77 | size=256, 78 | domains=41, 79 | object_type=OBJECT.HMAC_KEY, 80 | algorithm=ALGORITHM.HMAC_SHA512, 81 | sequence=0, 82 | origin=ORIGIN.GENERATED, 83 | label="hmaclabel", 84 | delegated_capabilities=0, 85 | ) 86 | 87 | self.assertEqual(info, obj.get_info()) 88 | 89 | def test_delete(self): 90 | AuthMock = MagicMock(AuthSession) 91 | AuthMock.send_secure_cmd.return_value = b"" 92 | obj = YhsmObject(session=AuthMock, object_id=5) 93 | obj.object_type = OBJECT.HMAC_KEY 94 | obj.delete() 95 | 96 | def test__create(self): 97 | # create for every type 98 | items = [ 99 | (OBJECT.OPAQUE, Opaque), 100 | (OBJECT.AUTHENTICATION_KEY, AuthenticationKey), 101 | (OBJECT.ASYMMETRIC_KEY, AsymmetricKey), 102 | (OBJECT.WRAP_KEY, WrapKey), 103 | (OBJECT.PUBLIC_WRAP_KEY, PublicWrapKey), 104 | (OBJECT.HMAC_KEY, HmacKey), 105 | (OBJECT.TEMPLATE, Template), 106 | (OBJECT.OTP_AEAD_KEY, OtpAeadKey), 107 | ] 108 | 109 | AuthMock = MagicMock(AuthSession) 110 | 111 | for obj_type, obj_class in items: 112 | id_num = randint(1, 17) 113 | obj = YhsmObject._create(obj_type, AuthMock, id_num) 114 | self.assertIsInstance(obj, obj_class) 115 | self.assertEqual(obj.id, id_num) 116 | expected_repr = "{class_name}(id={id_num})".format( 117 | class_name=obj_class.__name__, id_num=id_num 118 | ) 119 | self.assertEqual(obj.__repr__(), expected_repr) 120 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Copyright 2016-2018 Yubico AB 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from yubihsm.utils import password_to_key 18 | from binascii import a2b_hex 19 | 20 | import unittest 21 | 22 | 23 | class TestUtils(unittest.TestCase): 24 | def test_password_to_key(self): 25 | self.assertEqual( 26 | ( 27 | a2b_hex("090b47dbed595654901dee1cc655e420"), 28 | a2b_hex("592fd483f759e29909a04c4505d2ce0a"), 29 | ), 30 | password_to_key("password"), 31 | ) 32 | 33 | def test_password_to_key_utf8(self): 34 | self.assertEqual( 35 | ( 36 | a2b_hex("f320972c667ba5cd4d35119a6b0271a1"), 37 | a2b_hex("f10050ca688e5a6ce62b1ffb0f6f6869"), 38 | ), 39 | password_to_key("κόσμε"), 40 | ) 41 | 42 | def test_password_to_key_bytes_fails(self): 43 | with self.assertRaises(AttributeError): 44 | self.assertRaises(AttributeError, password_to_key(b"password")) 45 | 46 | with self.assertRaises(AttributeError): 47 | self.assertRaises( 48 | AttributeError, password_to_key(a2b_hex("cebae1bdb9cf83cebcceb5")) 49 | ) 50 | -------------------------------------------------------------------------------- /yubihsm/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016-2018 Yubico AB 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 | """ 16 | Contains the main YubiHsm class used to connect to a YubiHSM device. 17 | 18 | See :class:`~yubihsm.core.YubiHsm`. 19 | 20 | :Example: 21 | 22 | >>> from yubihsm import YubiHsm 23 | ... hsm = YubiHsm.connect('http://localhost:12345') 24 | ... session = hsm.create_session_derived(1, 'password') 25 | """ 26 | 27 | 28 | from .core import YubiHsm # noqa F401 29 | 30 | 31 | __version__ = "3.1.1.dev0" 32 | -------------------------------------------------------------------------------- /yubihsm/backends/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016-2018 Yubico AB 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 | from urllib import parse 16 | import re 17 | import abc 18 | from typing import Optional 19 | 20 | 21 | class YhsmBackend(abc.ABC): 22 | """Provides low-level communication with a YubiHSM.""" 23 | 24 | @abc.abstractmethod 25 | def transceive(self, msg: bytes) -> bytes: 26 | """Send a verbatim message.""" 27 | 28 | @abc.abstractmethod 29 | def close(self) -> None: 30 | """Closes the connection to the YubiHSM.""" 31 | 32 | 33 | def get_backend(url: Optional[str] = None) -> YhsmBackend: 34 | """Returns a backend suitable for the given URL.""" 35 | url = url or "http://localhost:12345" 36 | parsed = parse.urlparse(url) 37 | 38 | try: 39 | if parsed.scheme == "yhusb": 40 | from .usb import UsbBackend 41 | 42 | serial = re.match(r"serial=(\d+)", parsed.netloc) 43 | if serial: 44 | return UsbBackend(serial=int(serial.group(1)), timeout=600) 45 | elif not parsed.netloc: # On anything else, fall through to error. 46 | return UsbBackend(serial=None, timeout=600) 47 | elif parsed.scheme in ("http", "https"): 48 | from .http import HttpBackend 49 | 50 | return HttpBackend(url, (10, 600)) 51 | except ImportError: 52 | raise ValueError( 53 | 'Unable to initialize backend for scheme "%s", are ' 54 | "required dependencies installed?" % parsed.scheme 55 | ) 56 | 57 | raise ValueError("Invalid YubiHSM backend URL.") 58 | -------------------------------------------------------------------------------- /yubihsm/backends/http.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016-2018 Yubico AB 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 | from . import YhsmBackend 16 | from ..exceptions import YubiHsmConnectionError 17 | from requests.exceptions import RequestException 18 | from urllib import parse 19 | import requests 20 | from typing import Optional, Union, Tuple 21 | 22 | 23 | class HttpBackend(YhsmBackend): 24 | """A backend for communicating with a YubiHSM connector over HTTP.""" 25 | 26 | def __init__( 27 | self, 28 | url: str = "http://localhost:12345", 29 | timeout: Optional[Union[Tuple[int, int], int]] = None, 30 | ): 31 | """Construct a new HttpBackend, connecting to the given URL. 32 | 33 | The URL should be a http(s) URL to a running YubiHSM connector. 34 | By default, the backend will attempt to connect to a connector running 35 | locally, on the default port. 36 | 37 | :param str url: (optional) The URL to connect to. 38 | :param timeout: (optional) A timeout in seconds, or a tuple of two 39 | values to use as connection timeout and request timeout. 40 | :type timeout: int or tuple[int, int] 41 | """ 42 | if not url.endswith("/"): 43 | url = url + "/" 44 | 45 | self._url = parse.urljoin(url, "connector/api") 46 | self._timeout = timeout 47 | 48 | self._session = requests.Session() 49 | self._session.headers.update({"Content-Type": "application/octet-stream"}) 50 | 51 | def transceive(self, msg): 52 | try: 53 | resp = self._session.post(url=self._url, data=msg, timeout=self._timeout) 54 | resp.raise_for_status() 55 | return resp.content 56 | except RequestException as e: 57 | raise YubiHsmConnectionError(e) 58 | 59 | def close(self): 60 | self._session.close() 61 | 62 | def __repr__(self): 63 | return '{0.__class__.__name__}("{0._url}")'.format(self) 64 | -------------------------------------------------------------------------------- /yubihsm/backends/usb.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016-2018 Yubico AB 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 | from . import YhsmBackend 16 | from ..exceptions import YubiHsmConnectionError 17 | import usb.core 18 | import usb.util 19 | from typing import Optional 20 | 21 | 22 | YUBIHSM_VID = 0x1050 23 | YUBIHSM_PID = 0x0030 24 | 25 | 26 | class UsbBackend(YhsmBackend): 27 | """A backend for communicating with a YubiHSM directly over USB.""" 28 | 29 | def __init__(self, serial: Optional[int] = None, timeout: Optional[int] = None): 30 | """Construct a UsbBackend, connected to a YubiHSM via USB. 31 | 32 | :param serial: (optional) The serial number of the YubiHSM to connect to. 33 | :param timeout: (optional) A read/write timeout in seconds. 34 | """ 35 | err = None 36 | for device in usb.core.find( 37 | find_all=True, idVendor=YUBIHSM_VID, idProduct=YUBIHSM_PID 38 | ): 39 | try: 40 | cfg = device.get_active_configuration() 41 | except usb.core.USBError: 42 | cfg = None 43 | 44 | if cfg is None or cfg.bConfigurationValue != 0x01: 45 | try: 46 | device.set_configuration(0x01) 47 | except usb.core.USBError as e: 48 | err = YubiHsmConnectionError(e) 49 | continue 50 | 51 | if serial is None or int(device.serial_number) == serial: 52 | break 53 | 54 | usb.util.dispose_resources(device) 55 | else: 56 | raise err or YubiHsmConnectionError("No YubiHSM found.") 57 | 58 | # Flush any data waiting to be read 59 | try: 60 | device.read(0x81, 0xFFFF, 10) 61 | except usb.core.USBError: 62 | pass # Errors here are expected, and ignored 63 | 64 | self._device = device 65 | 66 | # pyusb expects milliseconds or None if no timeout 67 | self.timeout = None if timeout is None else timeout * 1000 68 | 69 | def transceive(self, msg): 70 | try: 71 | sent = self._device.write(0x01, msg, self.timeout) 72 | if sent != len(msg): 73 | raise YubiHsmConnectionError("Error sending data over USB.") 74 | if sent % 64 == 0: 75 | if self._device.write(0x01, b"", self.timeout) != 0: 76 | raise YubiHsmConnectionError("Error sending data over USB.") 77 | return bytes(bytearray(self._device.read(0x81, 0xFFFF, self.timeout))) 78 | except usb.core.USBError as e: 79 | raise YubiHsmConnectionError(e) 80 | 81 | def close(self): 82 | usb.util.dispose_resources(self._device) 83 | 84 | def __repr__(self): 85 | v_int = self._device.bcdDevice 86 | version = "{}.{}.{}".format((v_int >> 8) & 0xF, (v_int >> 4) & 0xF, v_int & 0xF) 87 | return ( 88 | "{0.__class__.__name__}(" 89 | "version={1}, " 90 | "serial={0._device.serial_number})" 91 | ).format(self, version) 92 | -------------------------------------------------------------------------------- /yubihsm/defs.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016-2018 Yubico AB 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 | """Named constants used in YubiHSM commands.""" 16 | 17 | from cryptography.hazmat.primitives.asymmetric import ec 18 | from cryptography.hazmat.primitives import hashes 19 | from enum import IntEnum, IntFlag, unique 20 | from typing import Tuple 21 | 22 | 23 | Version = Tuple[int, int, int] 24 | 25 | 26 | @unique 27 | class ERROR(IntEnum): 28 | """Error codes returned by the YubiHSM""" 29 | 30 | OK = 0x00 31 | INVALID_COMMAND = 0x01 32 | INVALID_DATA = 0x02 33 | INVALID_SESSION = 0x03 34 | AUTHENTICATION_FAILED = 0x04 35 | SESSIONS_FULL = 0x05 36 | SESSION_FAILED = 0x06 37 | STORAGE_FAILED = 0x07 38 | WRONG_LENGTH = 0x08 39 | INSUFFICIENT_PERMISSIONS = 0x09 40 | LOG_FULL = 0x0A 41 | OBJECT_NOT_FOUND = 0x0B 42 | INVALID_ID = 0x0C 43 | SSH_CA_CONSTRAINT_VIOLATION = 0x0E 44 | INVALID_OTP = 0x0F 45 | DEMO_MODE = 0x10 46 | OBJECT_EXISTS = 0x11 47 | ALGORITHM_DISABLED = 0x12 48 | COMMAND_UNEXECUTED = 0xFF 49 | 50 | def __repr__(self): 51 | return "<%s.%s: %s>" % (self.__class__.__name__, self._name_, hex(self)) 52 | 53 | def __str__(self): 54 | return repr(self) 55 | 56 | 57 | @unique 58 | class COMMAND(IntEnum): 59 | """Commands available to send to the YubiHSM""" 60 | 61 | ECHO = 0x01 62 | CREATE_SESSION = 0x03 63 | AUTHENTICATE_SESSION = 0x04 64 | SESSION_MESSAGE = 0x05 65 | DEVICE_INFO = 0x06 66 | RESET_DEVICE = 0x08 67 | GET_DEVICE_PUBLIC_KEY = 0x0A 68 | CLOSE_SESSION = 0x40 69 | GET_STORAGE_INFO = 0x041 70 | PUT_OPAQUE = 0x42 71 | GET_OPAQUE = 0x43 72 | PUT_AUTHENTICATION_KEY = 0x44 73 | PUT_ASYMMETRIC_KEY = 0x45 74 | GENERATE_ASYMMETRIC_KEY = 0x46 75 | SIGN_PKCS1 = 0x47 76 | LIST_OBJECTS = 0x48 77 | DECRYPT_PKCS1 = 0x49 78 | EXPORT_WRAPPED = 0x4A 79 | IMPORT_WRAPPED = 0x4B 80 | PUT_WRAP_KEY = 0x4C 81 | GET_LOG_ENTRIES = 0x4D 82 | GET_OBJECT_INFO = 0x4E 83 | SET_OPTION = 0x4F 84 | GET_OPTION = 0x50 85 | GET_PSEUDO_RANDOM = 0x51 86 | PUT_HMAC_KEY = 0x52 87 | SIGN_HMAC = 0x53 88 | GET_PUBLIC_KEY = 0x54 89 | SIGN_PSS = 0x55 90 | SIGN_ECDSA = 0x56 91 | DERIVE_ECDH = 0x57 92 | DELETE_OBJECT = 0x58 93 | DECRYPT_OAEP = 0x59 94 | GENERATE_HMAC_KEY = 0x5A 95 | GENERATE_WRAP_KEY = 0x5B 96 | VERIFY_HMAC = 0x5C 97 | SIGN_SSH_CERTIFICATE = 0x5D 98 | PUT_TEMPLATE = 0x5E 99 | GET_TEMPLATE = 0x5F 100 | DECRYPT_OTP = 0x60 101 | CREATE_OTP_AEAD = 0x61 102 | RANDOMIZE_OTP_AEAD = 0x62 103 | REWRAP_OTP_AEAD = 0x63 104 | SIGN_ATTESTATION_CERTIFICATE = 0x64 105 | PUT_OTP_AEAD_KEY = 0x65 106 | GENERATE_OTP_AEAD_KEY = 0x66 107 | SET_LOG_INDEX = 0x67 108 | WRAP_DATA = 0x68 109 | UNWRAP_DATA = 0x69 110 | SIGN_EDDSA = 0x6A 111 | BLINK_DEVICE = 0x6B 112 | CHANGE_AUTHENTICATION_KEY = 0x6C 113 | PUT_SYMMETRIC_KEY = 0x6D 114 | GENERATE_SYMMETRIC_KEY = 0x6E 115 | DECRYPT_ECB = 0x6F 116 | ENCRYPT_ECB = 0x70 117 | DECRYPT_CBC = 0x71 118 | ENCRYPT_CBC = 0x72 119 | PUT_PUBLIC_WRAP_KEY = 0x73 120 | WRAP_KEY_RSA = 0x74 121 | UNWRAP_KEY_RSA = 0x75 122 | EXPORT_WRAPPED_RSA = 0x76 123 | IMPORT_WRAPPED_RSA = 0x77 124 | 125 | ERROR = 0x7F 126 | 127 | def __repr__(self): 128 | return "<%s.%s: %s>" % (self.__class__.__name__, self._name_, hex(self)) 129 | 130 | def __str__(self): 131 | return repr(self) 132 | 133 | 134 | @unique 135 | class ALGORITHM(IntEnum): 136 | """Various algorithm constants""" 137 | 138 | RSA_PKCS1_SHA1 = 1 139 | RSA_PKCS1_SHA256 = 2 140 | RSA_PKCS1_SHA384 = 3 141 | RSA_PKCS1_SHA512 = 4 142 | RSA_PSS_SHA1 = 5 143 | RSA_PSS_SHA256 = 6 144 | RSA_PSS_SHA384 = 7 145 | RSA_PSS_SHA512 = 8 146 | RSA_2048 = 9 147 | RSA_3072 = 10 148 | RSA_4096 = 11 149 | RSA_OAEP_SHA1 = 25 150 | RSA_OAEP_SHA256 = 26 151 | RSA_OAEP_SHA384 = 27 152 | RSA_OAEP_SHA512 = 28 153 | RSA_MGF1_SHA1 = 32 154 | RSA_MGF1_SHA256 = 33 155 | RSA_MGF1_SHA384 = 34 156 | RSA_MGF1_SHA512 = 35 157 | 158 | EC_P256 = 12 159 | EC_P384 = 13 160 | EC_P521 = 14 161 | EC_K256 = 15 162 | EC_BP256 = 16 163 | EC_BP384 = 17 164 | EC_BP512 = 18 165 | 166 | EC_ECDSA_SHA1 = 23 167 | EC_ECDH = 24 168 | 169 | HMAC_SHA1 = 19 170 | HMAC_SHA256 = 20 171 | HMAC_SHA384 = 21 172 | HMAC_SHA512 = 22 173 | 174 | AES128_CCM_WRAP = 29 175 | OPAQUE_DATA = 30 176 | OPAQUE_X509_CERTIFICATE = 31 177 | TEMPLATE_SSH = 36 178 | AES128_YUBICO_OTP = 37 179 | AES128_YUBICO_AUTHENTICATION = 38 180 | AES192_YUBICO_OTP = 39 181 | AES256_YUBICO_OTP = 40 182 | AES192_CCM_WRAP = 41 183 | AES256_CCM_WRAP = 42 184 | EC_ECDSA_SHA256 = 43 185 | EC_ECDSA_SHA384 = 44 186 | EC_ECDSA_SHA512 = 45 187 | EC_ED25519 = 46 188 | EC_P224 = 47 189 | RSA_PKCS1_DECRYPT = 48 190 | EC_P256_YUBICO_AUTHENTICATION = 49 191 | 192 | AES128 = 50 193 | AES192 = 51 194 | AES256 = 52 195 | AES_ECB = 53 196 | AES_CBC = 54 197 | AES_KWP = 55 198 | 199 | def __str__(self): 200 | return repr(self) 201 | 202 | def to_curve(self) -> ec.EllipticCurve: 203 | """Return a Cryptography EC curve instance for a given member. 204 | 205 | :return: The corresponding curve. 206 | :rtype: cryptography.hazmat.primitives.ec. 207 | 208 | :Example: 209 | 210 | >>> isinstance(ALGORITHM.EC_P256.to_curve(), ec.SECP256R1) 211 | True 212 | """ 213 | 214 | return _curve_table[self]() # type: ignore 215 | 216 | @staticmethod 217 | def for_curve(curve: ec.EllipticCurve) -> "ALGORITHM": 218 | """Returns a member corresponding to a Cryptography curve instance. 219 | 220 | :Example: 221 | 222 | >>> ALGORITHM.for_curve(ec.SECP256R1()) == ALGORITHM.EC_P256 223 | True 224 | """ 225 | 226 | curve_type = type(curve) 227 | for key, val in _curve_table.items(): 228 | if val == curve_type: 229 | return key 230 | raise ValueError("Unsupported curve type: %s" % curve.name) 231 | 232 | def to_key_size(self) -> int: 233 | """Return the expected size (in bytes) of a key corresponding to an algorithm. 234 | 235 | :return: The corresponding key size (in bytes) to an algorithm. 236 | 237 | :Example: 238 | 239 | >>> ALGORITHM.AES128.to_key_size() 240 | 16 241 | """ 242 | 243 | return _key_size_table[self] 244 | 245 | def to_hash_algorithm(self) -> hashes.HashAlgorithm: 246 | """Return the cryptography hash algorithm object corresponding to the algorithm. 247 | 248 | :return The corresponding cryptography hash algorithm object. 249 | 250 | :Example: 251 | 252 | >>> ALGORITHM.HMAC_SHA1.to_hash_algorithm() 253 | hashes.SHA1 254 | """ 255 | 256 | return _hash_table[self]() 257 | 258 | 259 | _curve_table = { 260 | ALGORITHM.EC_P224: ec.SECP224R1, 261 | ALGORITHM.EC_P256: ec.SECP256R1, 262 | ALGORITHM.EC_P384: ec.SECP384R1, 263 | ALGORITHM.EC_P521: ec.SECP521R1, 264 | ALGORITHM.EC_K256: ec.SECP256K1, 265 | ALGORITHM.EC_BP256: ec.BrainpoolP256R1, 266 | ALGORITHM.EC_BP384: ec.BrainpoolP384R1, 267 | ALGORITHM.EC_BP512: ec.BrainpoolP512R1, 268 | } 269 | 270 | _key_size_table = { 271 | ALGORITHM.AES128_CCM_WRAP: 16, 272 | ALGORITHM.AES192_CCM_WRAP: 24, 273 | ALGORITHM.AES256_CCM_WRAP: 32, 274 | ALGORITHM.HMAC_SHA1: 64, # Maximum key size 275 | ALGORITHM.HMAC_SHA256: 64, # Maximum key size 276 | ALGORITHM.HMAC_SHA384: 128, # Maximum key size 277 | ALGORITHM.HMAC_SHA512: 128, # Maximum key size 278 | ALGORITHM.AES128_YUBICO_OTP: 16, 279 | ALGORITHM.AES192_YUBICO_OTP: 24, 280 | ALGORITHM.AES256_YUBICO_OTP: 32, 281 | ALGORITHM.AES128: 16, 282 | ALGORITHM.AES192: 24, 283 | ALGORITHM.AES256: 32, 284 | ALGORITHM.RSA_2048: 256, 285 | ALGORITHM.RSA_3072: 384, 286 | ALGORITHM.RSA_4096: 512, 287 | } 288 | 289 | _hash_table = { 290 | ALGORITHM.HMAC_SHA1: hashes.SHA1, 291 | ALGORITHM.HMAC_SHA256: hashes.SHA256, 292 | ALGORITHM.HMAC_SHA384: hashes.SHA384, 293 | ALGORITHM.HMAC_SHA512: hashes.SHA512, 294 | } 295 | 296 | 297 | @unique 298 | class LIST_FILTER(IntEnum): 299 | """Keys for use to filter on in list_objects""" 300 | 301 | ID = 0x01 302 | TYPE = 0x02 303 | DOMAINS = 0x03 304 | CAPABILITIES = 0x04 305 | ALGORITHM = 0x05 306 | LABEL = 0x06 307 | 308 | def __str__(self): 309 | return repr(self) 310 | 311 | 312 | @unique 313 | class OBJECT(IntEnum): 314 | """YubiHSM object types""" 315 | 316 | OPAQUE = 0x01 317 | AUTHENTICATION_KEY = 0x02 318 | ASYMMETRIC_KEY = 0x03 319 | WRAP_KEY = 0x04 320 | HMAC_KEY = 0x05 321 | TEMPLATE = 0x06 322 | OTP_AEAD_KEY = 0x07 323 | SYMMETRIC_KEY = 0x08 324 | PUBLIC_WRAP_KEY = 0x09 325 | 326 | def __repr__(self): 327 | return "<%s.%s: %s>" % (self.__class__.__name__, self._name_, hex(self)) 328 | 329 | def __str__(self): 330 | return repr(self) 331 | 332 | 333 | @unique 334 | class OPTION(IntEnum): 335 | """YubiHSM device options""" 336 | 337 | FORCE_AUDIT = 0x01 338 | COMMAND_AUDIT = 0x03 339 | ALGORITHM_TOGGLE = 0x04 340 | FIPS_MODE = 0x05 341 | 342 | def __repr__(self): 343 | return "<%s.%s: %s>" % (self.__class__.__name__, self._name_, hex(self)) 344 | 345 | def __str__(self): 346 | return repr(self) 347 | 348 | 349 | @unique 350 | class AUDIT(IntEnum): 351 | """Values for audit options""" 352 | 353 | OFF = 0x00 354 | ON = 0x01 355 | FIXED = 0x02 356 | 357 | def __repr__(self): 358 | return "<%s.%s: %s>" % (self.__class__.__name__, self._name_, hex(self)) 359 | 360 | def __str__(self): 361 | return repr(self) 362 | 363 | 364 | @unique 365 | class FIPS_STATUS(IntEnum): 366 | """Values for FIPS status""" 367 | 368 | OFF = 0x00 369 | ON = 0x01 370 | PENDING = 0x03 371 | 372 | def __repr__(self): 373 | return "<%s.%s: %s>" % (self.__class__.__name__, self._name_, hex(self)) 374 | 375 | def __str__(self): 376 | return repr(self) 377 | 378 | 379 | class _enum_prop: 380 | # Static property for use with enums. 381 | def __init__(self, getter): 382 | self.getter = getter 383 | 384 | def __get__(self, instance, cls): 385 | return self.getter(cls) 386 | 387 | 388 | @unique 389 | class CAPABILITY(IntFlag): 390 | """YubiHSM object capability flags""" 391 | 392 | GET_OPAQUE = 1 << 0x00 393 | PUT_OPAQUE = 1 << 0x01 394 | PUT_AUTHENTICATION_KEY = 1 << 0x02 395 | PUT_ASYMMETRIC = 1 << 0x03 396 | GENERATE_ASYMMETRIC_KEY = 1 << 0x04 397 | SIGN_PKCS = 1 << 0x05 398 | SIGN_PSS = 1 << 0x06 399 | SIGN_ECDSA = 1 << 0x07 400 | SIGN_EDDSA = 1 << 0x08 401 | DECRYPT_PKCS = 1 << 0x09 402 | DECRYPT_OAEP = 1 << 0x0A 403 | DERIVE_ECDH = 1 << 0x0B 404 | EXPORT_WRAPPED = 1 << 0x0C 405 | IMPORT_WRAPPED = 1 << 0x0D 406 | PUT_WRAP_KEY = 1 << 0x0E 407 | GENERATE_WRAP_KEY = 1 << 0x0F 408 | EXPORTABLE_UNDER_WRAP = 1 << 0x10 409 | SET_OPTION = 1 << 0x11 410 | GET_OPTION = 1 << 0x12 411 | GET_PSEUDO_RANDOM = 1 << 0x13 412 | PUT_HMAC_KEY = 1 << 0x14 413 | GENERATE_HMAC_KEY = 1 << 0x15 414 | SIGN_HMAC = 1 << 0x16 415 | VERIFY_HMAC = 1 << 0x17 416 | GET_LOG_ENTRIES = 1 << 0x18 417 | SIGN_SSH_CERTIFICATE = 1 << 0x19 418 | GET_TEMPLATE = 1 << 0x1A 419 | PUT_TEMPLATE = 1 << 0x1B 420 | RESET_DEVICE = 1 << 0x1C 421 | DECRYPT_OTP = 1 << 0x1D 422 | CREATE_OTP_AEAD = 1 << 0x1E 423 | RANDOMIZE_OTP_AEAD = 1 << 0x1F 424 | REWRAP_FROM_OTP_AEAD_KEY = 1 << 0x20 425 | REWRAP_TO_OTP_AEAD_KEY = 1 << 0x21 426 | SIGN_ATTESTATION_CERTIFICATE = 1 << 0x22 427 | PUT_OTP_AEAD_KEY = 1 << 0x23 428 | GENERATE_OTP_AEAD_KEY = 1 << 0x24 429 | WRAP_DATA = 1 << 0x25 430 | UNWRAP_DATA = 1 << 0x26 431 | DELETE_OPAQUE = 1 << 0x27 432 | DELETE_AUTHENTICATION_KEY = 1 << 0x28 433 | DELETE_ASYMMETRIC_KEY = 1 << 0x29 434 | DELETE_WRAP_KEY = 1 << 0x2A 435 | DELETE_HMAC_KEY = 1 << 0x2B 436 | DELETE_TEMPLATE = 1 << 0x2C 437 | DELETE_OTP_AEAD_KEY = 1 << 0x2D 438 | CHANGE_AUTHENTICATION_KEY = 1 << 0x2E 439 | PUT_SYMMETRIC_KEY = 1 << 0x2F 440 | GENERATE_SYMMETRIC_KEY = 1 << 0x30 441 | DELETE_SYMMETRIC_KEY = 1 << 0x31 442 | DECRYPT_ECB = 1 << 0x32 443 | ENCRYPT_ECB = 1 << 0x33 444 | DECRYPT_CBC = 1 << 0x34 445 | ENCRYPT_CBC = 1 << 0x35 446 | PUBLIC_WRAP_KEY_WRITE = 1 << 0x36 447 | PUBLIC_WRAP_KEY_DELETE = 1 << 0x37 448 | 449 | def __repr__(self): 450 | return "<%s.%s: %s>" % (self.__class__.__name__, self._name_, hex(self)) 451 | 452 | def __str__(self): 453 | return repr(self) 454 | 455 | @_enum_prop 456 | def NONE(cls) -> "CAPABILITY": 457 | return cls(0) # type: ignore 458 | 459 | @_enum_prop 460 | def ALL(cls) -> "CAPABILITY": 461 | return cls(sum(cls)) # type: ignore 462 | 463 | 464 | class ORIGIN(IntFlag): 465 | GENERATED = 0x01 466 | IMPORTED = 0x02 467 | IMPORTED_WRAPPED = 0x10 # Set in combination with GENERATED/IMPORTED 468 | 469 | def __repr__(self): 470 | return "<%s.%s: %s>" % (self.__class__.__name__, self._name_, hex(self)) 471 | 472 | def __str__(self): 473 | return repr(self) 474 | -------------------------------------------------------------------------------- /yubihsm/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016-2018 Yubico AB 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 | """Exceptions thrown by this library.""" 16 | 17 | from .defs import ERROR 18 | 19 | 20 | class YubiHsmError(Exception): 21 | """Baseclass for YubiHSM errors.""" 22 | 23 | 24 | class YubiHsmConnectionError(YubiHsmError): 25 | """The connection to the YubiHSM failed.""" 26 | 27 | 28 | class YubiHsmDeviceError(YubiHsmError): 29 | """The YubiHSM returned an error code. 30 | 31 | :param int code: The device error code. 32 | """ 33 | 34 | def __init__(self, code: int): 35 | self.code = ERROR(code) 36 | super(YubiHsmDeviceError, self).__init__( 37 | "{0.name} (error code 0x{0.value:02x})".format(self.code) 38 | ) 39 | 40 | 41 | class YubiHsmInvalidRequestError(YubiHsmError): 42 | """The request was not able to be sent to the YubiHSM.""" 43 | 44 | 45 | class YubiHsmInvalidResponseError(YubiHsmError): 46 | """The YubiHSM returned an unexpected response.""" 47 | 48 | 49 | class YubiHsmAuthenticationError(YubiHsmError): 50 | """Authentication failed.""" 51 | -------------------------------------------------------------------------------- /yubihsm/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yubico/python-yubihsm/463716ed4c2f50cf085f7c318319867d7dc21520/yubihsm/py.typed -------------------------------------------------------------------------------- /yubihsm/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016-2018 Yubico AB 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 | """Various utility functions used throughout the library.""" 16 | 17 | from cryptography.hazmat.backends import default_backend 18 | from cryptography.hazmat.primitives import hashes 19 | from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC 20 | from typing import Tuple 21 | 22 | 23 | def password_to_key(password: str) -> Tuple[bytes, bytes]: 24 | """Derive keys for establishing a YubiHSM session from a password. 25 | 26 | :return: A tuple containing the encryption key, and MAC key. 27 | """ 28 | pw_bytes = password.encode() 29 | 30 | key = PBKDF2HMAC( 31 | algorithm=hashes.SHA256(), 32 | length=32, 33 | salt=b"Yubico", 34 | iterations=10000, 35 | backend=default_backend(), 36 | ).derive(pw_bytes) 37 | key_enc, key_mac = key[:16], key[16:] 38 | return key_enc, key_mac 39 | --------------------------------------------------------------------------------